@sema-agent/server 7.4.0 → 7.5.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 +43 -0
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +108 -11
- package/dist/approval-reconciler.js +146 -19
- package/dist/boot/coordinators.js +10 -2
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -2
- package/dist/boot/resolve-spec.js +132 -32
- package/dist/boot/runner-deps.d.ts +23 -1
- package/dist/boot/runner-deps.js +8 -11
- package/dist/boot/workflow-orchestration.d.ts +8 -3
- package/dist/boot/workflow-orchestration.js +23 -1
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-types.d.ts +24 -8
- package/dist/config.d.ts +6 -1
- package/dist/config.js +56 -11
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +6 -1
- package/dist/fleet/fleet-bus.js +25 -3
- package/dist/governance-ask-marks.d.ts +31 -0
- package/dist/governance-ask-marks.js +122 -0
- package/dist/hooks/hook-runner.d.ts +28 -0
- package/dist/hooks/hook-runner.js +149 -25
- package/dist/http/routes/diagnostics.js +10 -5
- package/dist/http/routes/memory-policy.d.ts +2 -1
- package/dist/http/routes/memory-policy.js +77 -13
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +59 -22
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +23 -12
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/main.js +17 -5
- package/dist/observability/fail-open.d.ts +13 -2
- package/dist/observability/fail-open.js +15 -4
- package/dist/observability/prompt-manifest.d.ts +5 -1
- package/dist/orchestration/workflow-notify-journal.d.ts +57 -1
- package/dist/orchestration/workflow-notify-journal.js +137 -32
- package/dist/parked-decide.js +9 -4
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +27 -5
- package/dist/plugins/approval-ask-store-sql.js +9 -2
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +62 -6
- package/dist/plugins/checkpoint-store-sql.js +71 -11
- package/dist/plugins/local-checkpoint-store.d.ts +20 -1
- package/dist/plugins/local-checkpoint-store.js +19 -0
- package/dist/plugins/mailbox-store-sql.d.ts +4 -10
- package/dist/plugins/mailbox-store-sql.js +57 -4
- package/dist/runs.d.ts +8 -0
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- package/dist/task-settings.d.ts +3 -9
- package/dist/task-settings.js +16 -13
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +80 -23
- package/dist/trace/core-keyset-guard.d.ts +17 -3
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -10
- package/dist/boot/lexical-path-env.js +0 -88
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/finance/cost-taxonomy.d.ts +0 -34
- package/dist/finance/cost-taxonomy.js +0 -26
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -151
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
- package/dist/sema-registry.d.ts +0 -41
- package/dist/sema-registry.js +0 -40
|
@@ -99,8 +99,15 @@ function pendingActionToolCallId(blob) {
|
|
|
99
99
|
}
|
|
100
100
|
if (typeof parsed !== "object" || parsed === null)
|
|
101
101
|
return { readable: false };
|
|
102
|
+
// 🔴 `sourceTaskId` 是 checkpoint 的**顶层**字段(不在 pendingAction 里),没有对应的列 ⇒ 只能从
|
|
103
|
+
// blob 读。它是和解三元组的第一维(#168 件1);形不合(有这个键但不是字符串)= 结构漂移,按本函数
|
|
104
|
+
// 一贯的收窄纪律判 `readable: false`,不静默折成缺席。
|
|
105
|
+
const rawSource = "sourceTaskId" in parsed ? parsed.sourceTaskId : null;
|
|
106
|
+
if (rawSource !== null && rawSource !== undefined && typeof rawSource !== "string")
|
|
107
|
+
return { readable: false };
|
|
108
|
+
const sourceTaskId = typeof rawSource === "string" ? rawSource : null;
|
|
102
109
|
if (!("pendingAction" in parsed))
|
|
103
|
-
return { readable: true, toolCallId: null, boundInputHash: null, kind: null }; // 无 pendingAction 的 park:确定不匹配
|
|
110
|
+
return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceTaskId }; // 无 pendingAction 的 park:确定不匹配
|
|
104
111
|
const pa = parsed.pendingAction;
|
|
105
112
|
if (typeof pa !== "object" || pa === null)
|
|
106
113
|
return { readable: false };
|
|
@@ -108,7 +115,7 @@ function pendingActionToolCallId(blob) {
|
|
|
108
115
|
if (kind !== null && typeof kind !== "string")
|
|
109
116
|
return { readable: false };
|
|
110
117
|
if (!("toolCallId" in pa))
|
|
111
|
-
return { readable: true, toolCallId: null, boundInputHash: null, kind }; // plan_review / task_done / resource_limit 腿
|
|
118
|
+
return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceTaskId }; // plan_review / task_done / resource_limit 腿
|
|
112
119
|
const id = pa.toolCallId;
|
|
113
120
|
if (typeof id !== "string")
|
|
114
121
|
return { readable: false };
|
|
@@ -122,7 +129,7 @@ function pendingActionToolCallId(blob) {
|
|
|
122
129
|
const bih = "boundInputHash" in pa ? pa.boundInputHash : null;
|
|
123
130
|
if (bih !== null && typeof bih !== "string")
|
|
124
131
|
return { readable: false };
|
|
125
|
-
return { readable: true, toolCallId: id, boundInputHash: bih, kind };
|
|
132
|
+
return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceTaskId };
|
|
126
133
|
}
|
|
127
134
|
const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
|
|
128
135
|
const PG_UNIQUE_VIOLATION = "23505";
|
|
@@ -228,6 +235,39 @@ const STEER_CAS_ATTEMPTS = 8;
|
|
|
228
235
|
export class SqlCheckpointStore {
|
|
229
236
|
db;
|
|
230
237
|
logger;
|
|
238
|
+
/**
|
|
239
|
+
* `CheckpointStore.durability` 声明(#167 欠账,#168 件5)—— 行落在 MySQL-protocol / PostgreSQL 的
|
|
240
|
+
* `checkpoint` 表里,进程重启、副本轮换、整机重建都不丢 ⇒ `"durable"`,如实。
|
|
241
|
+
*
|
|
242
|
+
* 🔴 为什么这一格空着是有代价的:core 的 `resolveDeclaredDurability` 把**缺席**折成 `"process-local"`
|
|
243
|
+
* (它不能替一个没表态的店猜),于是静态装配面对每一个真持久部署都读出 `process_local`,
|
|
244
|
+
* `GET /v1/diagnostics/wiring` 的 park 车道读数与启动自检的那条警告都因此不可信 —— 而 park 正是流内
|
|
245
|
+
* 审批协议的降级目的地,「重启后还赎不赎得回」是运维必须能一眼看见的事。声明是店自己的责任,不是
|
|
246
|
+
* 消费侧靠 backend.kind 猜出来的。
|
|
247
|
+
*/
|
|
248
|
+
durability = "durable";
|
|
249
|
+
/**
|
|
250
|
+
* `CheckpointStore.fidelity` 声明(core 5.17.0 [3052] 提货批 #172)——**如实按介质判**:本店把整个
|
|
251
|
+
* checkpoint 经 {@link SqlCheckpointStore.json} 编码进一个 JSON 列(TiDB 逐字文本 / PG lossless
|
|
252
|
+
* `::jsonb` 协议信封),读侧 `parseJson` 还原 ⇒ 能扛过 round-trip 的只有 JSON 值域,`"json"`。
|
|
253
|
+
*
|
|
254
|
+
* 🔴 为什么必须显式写、哪怕缺席也折向 json:core 的 `resolveDeclaredFidelity` 对缺席是 fail-closed
|
|
255
|
+
* (读 json),所以沉默不会立刻错——但沉默**表达不出**「我核对过我的介质就是这个宽度」。park 铸行
|
|
256
|
+
* 的 args / preview / 风险描述 / `boundInputHash` 全部从这一格算出的投影铸;哪天这四行编码里任何
|
|
257
|
+
* 一处改了介质(换存储格式、换列类型),声明在场才有东西可以红,沉默那格只会安静地按错宽度铸出
|
|
258
|
+
* 一份「审批人看到的 ≠ 盘上躺着的 ≠ resume 执行的」。同 `durability` 的 #168 件5 教训:表态是店
|
|
259
|
+
* 自己的责任,不是消费侧靠 backend.kind 猜。
|
|
260
|
+
*
|
|
261
|
+
* ⚠️ **已知残余(不是遮掩,是这两个词表达不了的那一格)**:PG 臂比 `"json"` 字面**略窄** ——
|
|
262
|
+
* {@link pgProtocolJsonStringify} 对含 NUL / lone surrogate 的串 fail-loud 拒绝(R4-H1 有意裁定:
|
|
263
|
+
* 复核面必须与真执行的 args 在 NUL 那一位上一致,「悄悄清洗再存」是不可接受的那一支),而 core 的
|
|
264
|
+
* json 宽度收下这些码位。`StoreFidelity` 的闭集只有 `"structured-clone" | "json"`,没有第三个词能说
|
|
265
|
+
* 「json 减去本介质存不下的码位」——声明 `"structured-clone"` 是大得多的谎,所以 `"json"` 仍是两者
|
|
266
|
+
* 里唯一诚实的选择。后果有界且 fail-closed:core 5.17.0 起 park 铸行失败会把 cause 带到 gate、追加
|
|
267
|
+
* 到 fallback 的 deny 上,这条 args 退回**同步门**由人判(不静默漏批、不挂死)。边界钉在
|
|
268
|
+
* `wiring-governance-operator.test.ts` 的 #172 组;已上报上游求一个能表达该宽度的词。
|
|
269
|
+
*/
|
|
270
|
+
fidelity = "json";
|
|
231
271
|
constructor(db, logger) {
|
|
232
272
|
this.db = db;
|
|
233
273
|
this.logger = logger;
|
|
@@ -236,8 +276,16 @@ export class SqlCheckpointStore {
|
|
|
236
276
|
q(tidb, pg) {
|
|
237
277
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
238
278
|
}
|
|
239
|
-
/** JSON column binding
|
|
240
|
-
*
|
|
279
|
+
/** JSON column binding. TiDB: plain `JSON.stringify`, stored verbatim. PG: `pgProtocolJsonStringify` —
|
|
280
|
+
* ordinary `JSON.stringify` that **refuses** (throws `PgUnstorableError`) when the value carries a code
|
|
281
|
+
* point PG cannot hold (NUL / lone surrogate); the `::jsonb` at the call site is just the bind cast, not
|
|
282
|
+
* an escaping layer.
|
|
283
|
+
*
|
|
284
|
+
* "LOSSLESS" in R4-H1 means exactly **"never silently lossy"**, not "encodes everything": the contrast is
|
|
285
|
+
* with `pgSanitizeText`, the lossy U+FFFD-scarring path used for CONTENT faces. An approval row is not a
|
|
286
|
+
* content face — the operator's review surface has to agree with the executed args AT the NUL position, so
|
|
287
|
+
* scrubbing the byte and storing the scrubbed row is the unacceptable arm; refusing loudly is the chosen one.
|
|
288
|
+
* That refusal is why {@link SqlCheckpointStore.fidelity} carries a documented residual (see it). */
|
|
241
289
|
json(value, label) {
|
|
242
290
|
return dialectProtocolJsonEncoder(this.db.dialect)(value, label);
|
|
243
291
|
}
|
|
@@ -300,8 +348,12 @@ export class SqlCheckpointStore {
|
|
|
300
348
|
// design/80 D-1: surface the reopen-by-reason + OCC fields off the AUTHORITATIVE columns (the blob is the
|
|
301
349
|
// suspend-time snapshot; resolve/reopen mutate only the columns). Core's resume reads these to validate an
|
|
302
350
|
// env_failed re-resume against the persisted winner, and to require the `rev` it observed is still live (OCC).
|
|
303
|
-
// resolvedOutcome is DERIVED from the stored full outcome
|
|
304
|
-
//
|
|
351
|
+
// resolvedOutcome is DERIVED from the stored full outcome by core's `winnerFromOutcome`, so it is preserved
|
|
352
|
+
// across a reopen for free (reopen leaves `outcome` untouched). Do NOT read a key list into this line: the
|
|
353
|
+
// derived shape is core's and it GROWS (`answer` since 5.7.0, `reason` since 5.16.0 — the operator's deny
|
|
354
|
+
// note IS persisted; an older copy of this comment enumerated only {boundCallId,decision,updatedInput?} and
|
|
355
|
+
// read as if it weren't). We store the whole outcome blob and round-trip it, so additive keys ride for free
|
|
356
|
+
// — `ResolvedOutcome` in core's `checkpoint-store.d.ts` is the single owner of that list.
|
|
305
357
|
const outcomeRaw = parseJson(r.outcome);
|
|
306
358
|
if (outcomeRaw)
|
|
307
359
|
cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
|
|
@@ -595,9 +647,14 @@ export class SqlCheckpointStore {
|
|
|
595
647
|
* 扫描(§8 C-6)。什么算读不出:blob 的 `version` 超出本 build 支持(`get()` 那条前向兼容门在这里
|
|
596
648
|
* 不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
|
|
597
649
|
*
|
|
598
|
-
* 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId`
|
|
599
|
-
*
|
|
600
|
-
*
|
|
650
|
+
* 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,SQL 谓词
|
|
651
|
+
* 先按它(或 NULL)收窄;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)靠解 blob 补判——解得出
|
|
652
|
+
* 且相等才算候选,解不出就标 `unparseable`。
|
|
653
|
+
* 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
|
|
654
|
+
* blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
|
|
655
|
+
* 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
|
|
656
|
+
* 的 resume 坐标,而 PARKED 不可回滚);(b) `#168` 之后 blob 顶层的 `sourceTaskId` 是和解三元组的第一
|
|
657
|
+
* 维,列命中行结构上也必须解 blob 才拿得到它。
|
|
601
658
|
*/
|
|
602
659
|
async findCheckpointCandidatesForAsk(scope, sessionId, toolCallId, sinceMs) {
|
|
603
660
|
const { rows } = await this.db.query(this.q("SELECT token, status, created_at, tool_call_id, bound_input_hash, version, checkpoint FROM checkpoint " +
|
|
@@ -605,11 +662,14 @@ export class SqlCheckpointStore {
|
|
|
605
662
|
"WHERE scope=$1 AND session_id=$2 AND created_at>=$3 AND (tool_call_id=$4 OR tool_call_id IS NULL) ORDER BY created_at ASC"), [scope, sessionId, sinceMs, toolCallId]);
|
|
606
663
|
const out = [];
|
|
607
664
|
for (const r of rows) {
|
|
665
|
+
// `sourceTaskId: null` 是**所有 unparseable 臂的共同底**(读不出的行不许带出一个可用于身份比对的
|
|
666
|
+
// 值);健康臂在下面用 blob 解出来的值覆盖它。
|
|
608
667
|
const base = {
|
|
609
668
|
token: String(r.token),
|
|
610
669
|
status: String(r.status),
|
|
611
670
|
createdAtMs: Number(r.created_at),
|
|
612
671
|
boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
|
|
672
|
+
sourceTaskId: null,
|
|
613
673
|
};
|
|
614
674
|
// 🔴 前向兼容门必须在**任何**命中路径之前(codex 复审 F3,2026-08-06 真缺陷):版本超前的行是
|
|
615
675
|
// `get()` 明确拒读的行,而 `tool_call_id` 列命中与否跟能不能读懂 blob 毫无关系。若让列的快路径
|
|
@@ -652,7 +712,7 @@ export class SqlCheckpointStore {
|
|
|
652
712
|
// 列在场时以列为准(权威投影);列为 NULL 的旧行回落用 blob 解出来的值。
|
|
653
713
|
const effective = col ?? derived.toolCallId;
|
|
654
714
|
if (effective === toolCallId)
|
|
655
|
-
out.push({ ...base, boundInputHash: effectiveHash, boundCallId: effective });
|
|
715
|
+
out.push({ ...base, boundInputHash: effectiveHash, sourceTaskId: derived.sourceTaskId, boundCallId: effective });
|
|
656
716
|
// 解得出、但不是这只 ask 的 callId(或这条 park 本来就没有工具动作)⇒ **确定**不是候选:
|
|
657
717
|
// 既不返回也不标坏行(标坏行会让收敛器把一条明确的「不匹配」当成「不确定」)。
|
|
658
718
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "@sema-agent/core";
|
|
1
|
+
import { type Checkpoint, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ReopenReason, type ResolveExpectation, type ResumeOutcome, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
|
|
2
2
|
import { type PendingCheckpoint } from "./checkpoint-store-sql.js";
|
|
3
3
|
export interface LocalCheckpointStoreOptions {
|
|
4
4
|
/** taskId join for the pending card (the local FileRunStore's `getActiveTaskId`). Absent ⇒ `taskId: null`. */
|
|
@@ -8,6 +8,25 @@ export interface LocalCheckpointStoreOptions {
|
|
|
8
8
|
ownerOf?: (sessionId: string) => Promise<string | null | undefined>;
|
|
9
9
|
}
|
|
10
10
|
export declare class LocalCheckpointStore {
|
|
11
|
+
/**
|
|
12
|
+
* `CheckpointStore.durability` 声明(#167 欠账,#168 件5)——**如实按内核判**:本类的内核是 core 的
|
|
13
|
+
* `FileCheckpointStore`(crash-safe ledger + snapshot,自身声明 `"durable"`),本地 scope 注册表也落
|
|
14
|
+
* 在同一个盘上目录里 ⇒ 进程重启后 park 全部还在、还赎得回,`"durable"` 是诚实读数而不是抬举。
|
|
15
|
+
*
|
|
16
|
+
* 它与「session store 的 local 形声明 durable 就拒启」不矛盾:那条判据管的是**声明 vs 介质**,而这里
|
|
17
|
+
* 的介质就是盘。真正的 process-local 形(`InMemoryCheckpointStore`)由 core 自己声明 `"process-local"`。
|
|
18
|
+
*/
|
|
19
|
+
readonly durability: StoreDurability;
|
|
20
|
+
/**
|
|
21
|
+
* `CheckpointStore.fidelity` 声明(core 5.17.0 [3052] 提货批 #172)——**如实按内核判**,与 durability
|
|
22
|
+
* 同一条判据:本类的读写内核是 core 的 `FileCheckpointStore`,它自己声明 `"json"`(盘上是 JSON 行),
|
|
23
|
+
* 包装层只加 scope/session 索引与 ctx 附属文件,不改这条 round-trip 的宽度 ⇒ 抄内核的读数是诚实的。
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ 包装类**不继承**被包装者的声明(本类是委派不是子类),所以这一格必须自己写。它与内核那一格
|
|
26
|
+
* 是否仍然一致由测试盯着(`wiring-governance-operator.test.ts` 的 #172 组直接读内核那一格对表)——
|
|
27
|
+
* 内核哪天改宽度,包装层的声明会被那条钉当场揪出来,而不是靠人记得同步。
|
|
28
|
+
*/
|
|
29
|
+
readonly fidelity: StoreFidelity;
|
|
11
30
|
private readonly inner;
|
|
12
31
|
private readonly scopesPath;
|
|
13
32
|
private readonly sessionTokensPath;
|
|
@@ -34,6 +34,25 @@ function ctxFileOf(sessionId) {
|
|
|
34
34
|
return `${sanitizePathComponent(sessionId).slice(0, 48)}-${createHash("sha256").update(sessionId).digest("hex").slice(0, 12)}.json`;
|
|
35
35
|
}
|
|
36
36
|
export class LocalCheckpointStore {
|
|
37
|
+
/**
|
|
38
|
+
* `CheckpointStore.durability` 声明(#167 欠账,#168 件5)——**如实按内核判**:本类的内核是 core 的
|
|
39
|
+
* `FileCheckpointStore`(crash-safe ledger + snapshot,自身声明 `"durable"`),本地 scope 注册表也落
|
|
40
|
+
* 在同一个盘上目录里 ⇒ 进程重启后 park 全部还在、还赎得回,`"durable"` 是诚实读数而不是抬举。
|
|
41
|
+
*
|
|
42
|
+
* 它与「session store 的 local 形声明 durable 就拒启」不矛盾:那条判据管的是**声明 vs 介质**,而这里
|
|
43
|
+
* 的介质就是盘。真正的 process-local 形(`InMemoryCheckpointStore`)由 core 自己声明 `"process-local"`。
|
|
44
|
+
*/
|
|
45
|
+
durability = "durable";
|
|
46
|
+
/**
|
|
47
|
+
* `CheckpointStore.fidelity` 声明(core 5.17.0 [3052] 提货批 #172)——**如实按内核判**,与 durability
|
|
48
|
+
* 同一条判据:本类的读写内核是 core 的 `FileCheckpointStore`,它自己声明 `"json"`(盘上是 JSON 行),
|
|
49
|
+
* 包装层只加 scope/session 索引与 ctx 附属文件,不改这条 round-trip 的宽度 ⇒ 抄内核的读数是诚实的。
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ 包装类**不继承**被包装者的声明(本类是委派不是子类),所以这一格必须自己写。它与内核那一格
|
|
52
|
+
* 是否仍然一致由测试盯着(`wiring-governance-operator.test.ts` 的 #172 组直接读内核那一格对表)——
|
|
53
|
+
* 内核哪天改宽度,包装层的声明会被那条钉当场揪出来,而不是靠人记得同步。
|
|
54
|
+
*/
|
|
55
|
+
fidelity = "json";
|
|
37
56
|
inner;
|
|
38
57
|
scopesPath;
|
|
39
58
|
sessionTokensPath;
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
39
39
|
import type { Pool as PgPool } from "pg";
|
|
40
40
|
import type { MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
41
|
+
type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
|
|
41
42
|
export declare const MAILBOX_TABLE = "mailboxes";
|
|
42
43
|
export declare const MAILBOX_MSG_TABLE = "mailbox_messages";
|
|
43
44
|
export declare function ensureTiDBMailboxSchema(pool: MySqlPool): Promise<void>;
|
|
@@ -46,11 +47,7 @@ export declare class TiDBMailboxStore implements MailboxStore {
|
|
|
46
47
|
private readonly pool;
|
|
47
48
|
constructor(pool: MySqlPool);
|
|
48
49
|
private tx;
|
|
49
|
-
append(scope: string, handle: string, msg:
|
|
50
|
-
from?: string;
|
|
51
|
-
content: string;
|
|
52
|
-
sentAt: number;
|
|
53
|
-
}): Promise<number>;
|
|
50
|
+
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
54
51
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
55
52
|
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
|
|
56
53
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
@@ -66,11 +63,7 @@ export declare class PgMailboxStore implements MailboxStore {
|
|
|
66
63
|
private tx;
|
|
67
64
|
/** 身份键拒绝式([1439] 协议纪律):scope/handle/owner/from 清洗形变 = 路由/attribution 错位。 */
|
|
68
65
|
private assertIdentity;
|
|
69
|
-
append(scope: string, handle: string, msg:
|
|
70
|
-
from?: string;
|
|
71
|
-
content: string;
|
|
72
|
-
sentAt: number;
|
|
73
|
-
}): Promise<number>;
|
|
66
|
+
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
74
67
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
75
68
|
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
|
|
76
69
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
@@ -80,4 +73,5 @@ export declare class PgMailboxStore implements MailboxStore {
|
|
|
80
73
|
maxAgeMs?: number;
|
|
81
74
|
}): Promise<number>;
|
|
82
75
|
}
|
|
76
|
+
export {};
|
|
83
77
|
//# sourceMappingURL=mailbox-store-sql.d.ts.map
|
|
@@ -25,12 +25,58 @@ function assertKeyBytes(what, v) {
|
|
|
25
25
|
function bufToStr(v) {
|
|
26
26
|
return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
|
|
27
27
|
}
|
|
28
|
+
/** `hopChain` 的 JSON 文本列上界(TiDB `TEXT` = 65535 字节)。core 侧链本就有界
|
|
29
|
+
* (`PEER_ADMISSION_DEFAULTS.maxChainLength` = 28,`PEER_HOP_CHAIN_WINDOW` = 32,元素是 peerAxisToken
|
|
30
|
+
* 短串)⇒ 正常形离这个数几个数量级远;守卫存在的理由是**关掉 MySQL 的静默截断类**(非严格模式下
|
|
31
|
+
* 超长 TEXT 写入只发 warning),截断后的 JSON 文本读侧 parse 失败 = 整条链静默变缺席,而缺席在
|
|
32
|
+
* mailbox 上是有语义的(见下 encodeHopChain 注)。同 assertKeyBytes 精神:写入面 fail-loud。 */
|
|
33
|
+
const HOP_CHAIN_MAX_BYTES = 60_000;
|
|
34
|
+
/**
|
|
35
|
+
* `hopChain` 存取(core 5.17.0 design/176)。**JSON 文本列**,不是清洗面也不是身份键面:
|
|
36
|
+
*
|
|
37
|
+
* - 为什么不清洗:链元素参与 core 的 `hop_loop` **等值比对**。内容面的 lossy 清洗(pgSanitizeText 的
|
|
38
|
+
* U+FFFD 标记)会让两个不同 token 塌成同一个(误报环路)或让一个 token 变形(漏判环路)——两个
|
|
39
|
+
* 方向都是把准入判决改错,与 `content` 那种「读者读到近似文本仍可用」的容忍面性质不同。
|
|
40
|
+
* - 为什么也不需要拒绝:`JSON.stringify` 是 **well-formed**(ES2019)——NUL 转义成 `\u0000`、lone
|
|
41
|
+
* surrogate 转义成 `\udXXX`,产物恒是 ASCII 可存字节。于是 PG 的 unstorable 面根本不会被触及,
|
|
42
|
+
* 两方言都拿到逐字 round-trip,不必像 `from`/`scope`/`handle` 那样走拒绝式。
|
|
43
|
+
* - **缺席 ≠ 空数组**:core 明示 mailbox 的 `hopChain` 在场性本身携带语义(「经 sema 准入门录入」vs
|
|
44
|
+
* 「外部/存量写入」,是整份契约里唯一允许从在场性推断的例外)。所以 NULL 读回 = 不铸键,`[]` 读回
|
|
45
|
+
* = 铸一个空数组键,两者绝不互相折叠。
|
|
46
|
+
*/
|
|
47
|
+
function encodeHopChain(hopChain) {
|
|
48
|
+
if (hopChain === undefined)
|
|
49
|
+
return null;
|
|
50
|
+
const text = JSON.stringify(hopChain);
|
|
51
|
+
if (Buffer.byteLength(text, "utf8") > HOP_CHAIN_MAX_BYTES) {
|
|
52
|
+
throw new Error(`MailboxStore: hopChain exceeds the ${HOP_CHAIN_MAX_BYTES}-byte column (refusing — a truncated chain would silently read back as ABSENT, which mailbox treats as "not gate-admitted")`);
|
|
53
|
+
}
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
/** 读侧:NULL/缺席 ⇒ undefined(不铸键);坏行(非 JSON / 非字符串数组)⇒ 同样 undefined —— 一条读不
|
|
57
|
+
* 懂的链只能诚实降级成「没有链」,绝不半解析出一条会被拿去做环路比对的残链。 */
|
|
58
|
+
function decodeHopChain(raw) {
|
|
59
|
+
if (raw === null || raw === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(bufToStr(raw));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(parsed) || parsed.some((x) => typeof x !== "string"))
|
|
69
|
+
return undefined;
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
28
72
|
function rowToMessage(r) {
|
|
73
|
+
const hopChain = decodeHopChain(r.hop_chain);
|
|
29
74
|
return {
|
|
30
75
|
seq: Number(r.seq),
|
|
31
76
|
...(r.from_id !== null && r.from_id !== undefined ? { from: bufToStr(r.from_id) } : {}),
|
|
32
77
|
content: String(r.content),
|
|
33
78
|
sentAt: Number(r.sent_at_ms),
|
|
79
|
+
...(hopChain !== undefined ? { hopChain } : {}),
|
|
34
80
|
};
|
|
35
81
|
}
|
|
36
82
|
export async function ensureTiDBMailboxSchema(pool) {
|
|
@@ -53,6 +99,10 @@ export async function ensureTiDBMailboxSchema(pool) {
|
|
|
53
99
|
from_id VARBINARY(255) NULL,
|
|
54
100
|
content LONGTEXT NOT NULL,
|
|
55
101
|
sent_at_ms BIGINT NOT NULL,
|
|
102
|
+
-- hop_chain(core 5.17.0 design/176):peer 准入的 hop 链,JSON 文本;NULL = 缺席(**有语义**,
|
|
103
|
+
-- 见 encodeHopChain 头注:缺席 vs 空数组绝不互折)。TEXT 而非 LONGTEXT——链上界在 core(28 跳,
|
|
104
|
+
-- 短 token),写入面另有 HOP_CHAIN_MAX_BYTES fail-loud 守卫关掉静默截断类。
|
|
105
|
+
hop_chain TEXT NULL,
|
|
56
106
|
PRIMARY KEY (scope_key, handle, seq),
|
|
57
107
|
KEY idx_mbm_sent (scope_key, handle, sent_at_ms)
|
|
58
108
|
) COLLATE utf8mb4_bin`);
|
|
@@ -75,6 +125,7 @@ export async function ensurePgMailboxSchema(q) {
|
|
|
75
125
|
from_id VARCHAR(255) COLLATE "C",
|
|
76
126
|
content TEXT NOT NULL,
|
|
77
127
|
sent_at_ms BIGINT NOT NULL,
|
|
128
|
+
hop_chain TEXT, -- core 5.17.0 design/176(TiDB twin 同注:NULL = 缺席且有语义)
|
|
78
129
|
PRIMARY KEY (scope_key, handle, seq)
|
|
79
130
|
)`);
|
|
80
131
|
await q(`CREATE INDEX IF NOT EXISTS idx_mbm_sent ON ${MAILBOX_MSG_TABLE} (scope_key, handle, sent_at_ms)`);
|
|
@@ -106,6 +157,7 @@ export class TiDBMailboxStore {
|
|
|
106
157
|
assertIdentityChars("handle", handle);
|
|
107
158
|
assertKeyBytes("scope", scope);
|
|
108
159
|
assertKeyBytes("handle", handle);
|
|
160
|
+
const hopChain = encodeHopChain(msg.hopChain); // 事务外先编码:超界 fail-loud 必须在建盒/分配 seq 之前
|
|
109
161
|
// ensure-first 形:先幂等 INSERT IGNORE 建盒(next_seq=1,首条/drop 后重建同臂)再 FOR UPDATE——
|
|
110
162
|
// 「无行 FOR UPDATE + INSERT」的并发死锁/dup 撞车形整个消掉;seq 分配与 append 同事务(定谳 2)。
|
|
111
163
|
return this.tx(async (c) => {
|
|
@@ -119,7 +171,7 @@ export class TiDBMailboxStore {
|
|
|
119
171
|
}
|
|
120
172
|
const seq = Number(rows[0].next_seq);
|
|
121
173
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET next_seq = ? WHERE scope_key = ? AND handle = ?`, [seq + 1, scope, handle]);
|
|
122
|
-
await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms) VALUES (?, ?, ?, ?, ?, ?)`, [scope, handle, seq, msg.from ?? null, msg.content, msg.sentAt]);
|
|
174
|
+
await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain) VALUES (?, ?, ?, ?, ?, ?, ?)`, [scope, handle, seq, msg.from ?? null, msg.content, msg.sentAt, hopChain]);
|
|
123
175
|
return seq;
|
|
124
176
|
});
|
|
125
177
|
}
|
|
@@ -139,7 +191,7 @@ export class TiDBMailboxStore {
|
|
|
139
191
|
}
|
|
140
192
|
// FOR UPDATE = 当前读:TiDB 悲观事务的普通 SELECT 按 start_ts 快照(task-list twin 同案)——
|
|
141
193
|
// 锁到 box 行后消息集必须读「现在」,否则 drop+重建窗内老消息混进新盒 lease(maxSeq 错位)。
|
|
142
|
-
const [msgs] = (await c.query(`SELECT seq, from_id, content, sent_at_ms FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq FOR UPDATE`, [scope, handle]));
|
|
194
|
+
const [msgs] = (await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq FOR UPDATE`, [scope, handle]));
|
|
143
195
|
if (msgs.length === 0)
|
|
144
196
|
return null; // 定谳 5:空盒 claim = null(box 行在也 null)
|
|
145
197
|
const maxSeq = Number(msgs[msgs.length - 1].seq);
|
|
@@ -241,6 +293,7 @@ export class PgMailboxStore {
|
|
|
241
293
|
const content = pgSanitizeText(msg.content); // 内容面 lossy(U+FFFD 标记,与 PG 转录 payload 同族)
|
|
242
294
|
assertKeyBytes("scope", scope);
|
|
243
295
|
assertKeyBytes("handle", handle);
|
|
296
|
+
const hopChain = encodeHopChain(msg.hopChain); // TiDB 同注:事务外先编码,超界 fail-loud 不留半个盒
|
|
244
297
|
// ensure-first 形(TiDB 同注):幂等建盒再 FOR UPDATE,死锁/dup 撞车形消。
|
|
245
298
|
return this.tx(async (c) => {
|
|
246
299
|
await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES ($1, $2, $3, 1) ON CONFLICT (scope_key, handle) DO NOTHING`, [
|
|
@@ -260,7 +313,7 @@ export class PgMailboxStore {
|
|
|
260
313
|
}
|
|
261
314
|
const seq = Number(rows[0].next_seq);
|
|
262
315
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET next_seq = $1 WHERE scope_key = $2 AND handle = $3`, [seq + 1, scope, handle]);
|
|
263
|
-
await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms) VALUES ($1, $2, $3, $4, $5, $6)`, [scope, handle, seq, msg.from ?? null, content, msg.sentAt]);
|
|
316
|
+
await c.query(`INSERT INTO ${MAILBOX_MSG_TABLE} (scope_key, handle, seq, from_id, content, sent_at_ms, hop_chain) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [scope, handle, seq, msg.from ?? null, content, msg.sentAt, hopChain]);
|
|
264
317
|
return seq;
|
|
265
318
|
});
|
|
266
319
|
}
|
|
@@ -273,7 +326,7 @@ export class PgMailboxStore {
|
|
|
273
326
|
const box = boxes[0];
|
|
274
327
|
if (box.lease_owner !== null && Number(box.lease_expires_at_ms) > now && String(box.lease_owner) !== owner)
|
|
275
328
|
return null;
|
|
276
|
-
const { rows: msgs } = await c.query(`SELECT seq, from_id, content, sent_at_ms FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq FOR UPDATE`, [scope, handle]);
|
|
329
|
+
const { rows: msgs } = await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 ORDER BY seq FOR UPDATE`, [scope, handle]);
|
|
277
330
|
if (msgs.length === 0)
|
|
278
331
|
return null;
|
|
279
332
|
const maxSeq = Number(msgs[msgs.length - 1].seq);
|
package/dist/runs.d.ts
CHANGED
|
@@ -80,6 +80,14 @@ export declare function taskHandleStop(registry: typeof defaultTaskRegistry, han
|
|
|
80
80
|
content: string;
|
|
81
81
|
details: unknown;
|
|
82
82
|
}>;
|
|
83
|
+
/**
|
|
84
|
+
* Attribute this signal's abort as a USER stop for the owner's task-scoped children.
|
|
85
|
+
*
|
|
86
|
+
* 🔴 已经 abort 的信号在这里**立刻**记账,不是静默无操作(#168 件3 的配套)。调用点在 #168 件3 之后
|
|
87
|
+
* 一律挪到了「run 认领成功之后」—— 认领是一次 await,断连完全可能落在它之前。`addEventListener` 对一个
|
|
88
|
+
* 已 fire 的信号永不回调,那样一次真实的人为断连会被吞成「无归因」(子代 settle 时记成 system)。
|
|
89
|
+
* 语义上这两种时序是同一件事:本请求赢下了这条 session,而这条 session 的连接是被人断掉的。
|
|
90
|
+
*/
|
|
83
91
|
export declare function markChildrenStoppedByUserOnAbort(signal: AbortSignal, taskId: string, principal: string | null | undefined): void;
|
|
84
92
|
/**
|
|
85
93
|
* A session CAS conflict is swallowed by core into a failed `TaskResult` (it is not thrown from
|
package/dist/runs.js
CHANGED
|
@@ -111,15 +111,28 @@ export async function taskHandleStop(registry, handle, access, agentStore) {
|
|
|
111
111
|
registry.markStopSource(handle, "user");
|
|
112
112
|
return registry.stopTask(handle, access);
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Attribute this signal's abort as a USER stop for the owner's task-scoped children.
|
|
116
|
+
*
|
|
117
|
+
* 🔴 已经 abort 的信号在这里**立刻**记账,不是静默无操作(#168 件3 的配套)。调用点在 #168 件3 之后
|
|
118
|
+
* 一律挪到了「run 认领成功之后」—— 认领是一次 await,断连完全可能落在它之前。`addEventListener` 对一个
|
|
119
|
+
* 已 fire 的信号永不回调,那样一次真实的人为断连会被吞成「无归因」(子代 settle 时记成 system)。
|
|
120
|
+
* 语义上这两种时序是同一件事:本请求赢下了这条 session,而这条 session 的连接是被人断掉的。
|
|
121
|
+
*/
|
|
114
122
|
export function markChildrenStoppedByUserOnAbort(signal, taskId, principal) {
|
|
115
|
-
|
|
123
|
+
const mark = () => {
|
|
116
124
|
try {
|
|
117
125
|
defaultTaskRegistry.markStopSourceForOwner({ owner: taskId, scope: principal ?? "default" }, "user", { skipSessionScoped: true });
|
|
118
126
|
}
|
|
119
127
|
catch {
|
|
120
128
|
/* attribution is advisory — the cancel itself must never fail on it */
|
|
121
129
|
}
|
|
122
|
-
}
|
|
130
|
+
};
|
|
131
|
+
if (signal.aborted) {
|
|
132
|
+
mark();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
signal.addEventListener("abort", mark, { once: true });
|
|
123
136
|
}
|
|
124
137
|
/**
|
|
125
138
|
* A session CAS conflict is swallowed by core into a failed `TaskResult` (it is not thrown from
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* name). Real isolation is the `executionEnv` sandbox. Same caveat core documents on `createCoarseCommandNamePolicy`.
|
|
25
25
|
*/
|
|
26
26
|
import { type TaskSpec, type ToolPolicy } from "@sema-agent/core";
|
|
27
|
+
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
27
28
|
/** A per-command governance rule (registry `runtime.commandPolicy[]`). */
|
|
28
29
|
export type CommandRule = {
|
|
29
30
|
command: string;
|
|
@@ -106,10 +107,27 @@ export declare function autonomyOverrides(autonomy: Autonomy | undefined): Parti
|
|
|
106
107
|
* ⇒ 旋钮静默失效(clay 实机 Monitor 案「门根本没铸」);bypassPermissions 一句话掀掉部署闸;非 host lane
|
|
107
108
|
* fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。rank 合成:
|
|
108
109
|
* autonomy 派生值与旋钮取大(ask→"always" 压过 "classify"),再对 base 只升不降(防 tightenTaskSpec throw)。
|
|
110
|
+
*
|
|
111
|
+
* #177([2951] issue #29,**邻仓 test AI 的 46 格真机矩阵**实证——不是本仓的钉,别到本仓文件里数 46;
|
|
112
|
+
* 本仓的类级门是 test/operator-knob-client-posture-matrix.test.ts 的 旋钮×表态×lane 矩阵):
|
|
113
|
+
* `SENSITIVE_WRITE_PATTERNS` 是 #153 那次搬家漏下的兄弟旋钮——同一个病灶原样复发在 sensitive-path
|
|
114
|
+
* DENY 腿上(唯一合成点在 deriveSettingsPolicy 的 fsWriteGate 闭包,bypassPermissions/键缺席/settings
|
|
115
|
+
* 整缺席三形整条腿不建)。同方修:调用方(resolve-spec)把 operator 守卫集预铸成 `sensitivePathPolicy`
|
|
116
|
+
* 从这里**无条件施加**——它只在守卫段上产 deny(core 语义 deny/"safety"),对其余目标返回的是
|
|
117
|
+
* `action:"allow"`(**不是**「无意见」这种第三态:core 的 ToolPolicy 没有弃权值。它在
|
|
118
|
+
* `combinePolicies` 的 deny/ask 优先折叠里不构成一票,所以在**折叠语境**下等价于弃权;若日后有人把
|
|
119
|
+
* 这条策略单独当成唯一 policy 用,那个 allow 就是真放行——别照抄这句话去别的语境)。于是对 bypass
|
|
120
|
+
* 只挡守卫段、不给普通目标加 ask 门([820] 表第四行「bypass=不加门非开门」保持逐字)。策略在这里
|
|
121
|
+
* 只组合不构造:裁决 env 的 lane 分形(host 真 fs / 沙箱 deferred 代理)是 resolve-spec 的属主知识,
|
|
122
|
+
* governance 层不重复它。
|
|
109
123
|
*/
|
|
110
124
|
export declare function applyRuntimeGovernance(base: TaskSpec, governance: {
|
|
111
125
|
autonomy?: Autonomy;
|
|
112
126
|
commandPolicy?: CommandRule[];
|
|
113
127
|
manualModeShellGate?: "always" | "classify";
|
|
128
|
+
sensitivePathPolicy?: ToolPolicy;
|
|
129
|
+
/** [2942]/[2943] `governanceForced` 的标记表(缺省 = **当前 run 腿的 ALS 表**,见
|
|
130
|
+
* `governance-ask-marks.ts` 顶注「作用域」段)。注入口只为测试与将来的多实例形。 */
|
|
131
|
+
askMarks?: GovernanceAskMarks;
|
|
114
132
|
}): TaskSpec;
|
|
115
133
|
//# sourceMappingURL=runtime-governance.d.ts.map
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* name). Real isolation is the `executionEnv` sandbox. Same caveat core documents on `createCoarseCommandNamePolicy`.
|
|
25
25
|
*/
|
|
26
26
|
import { combinePolicies, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, } from "@sema-agent/core";
|
|
27
|
+
import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
|
|
27
28
|
/** True when `name` names a DELEGATION tool — the subagent tool(CC-187 canonical `"Agent"`,core
|
|
28
29
|
* `DEFAULT_SUBAGENT_TOOL_NAME`)。5.0.0 RB-476:折叠面退役,RAW 比对 core 单源常量——旧名("Task")
|
|
29
30
|
* 在 core roster 层响亮 miss,不再需要归一;单源常量本身就防了当年 1.202 改名时裸字符串失配的
|
|
@@ -178,7 +179,14 @@ export function compileCommandPolicy(rules) {
|
|
|
178
179
|
policies.push(createCoarseCommandNamePolicy({ allow, deny, defaultAction: "ask" }));
|
|
179
180
|
}
|
|
180
181
|
else {
|
|
181
|
-
// Blocklist mode: only `deny` blocks; per-command `ask` via the ask-list; unlisted
|
|
182
|
+
// Blocklist mode: only `deny` blocks; per-command `ask` via the ask-list; an unlisted **simple** command
|
|
183
|
+
// passes through.
|
|
184
|
+
// ⚠️ #184([3038]/[3047] 双探针实证):「unlisted pass through」只对**单纯命令**成立。带 shell 运算符的
|
|
185
|
+
// 复合命令(`a && b`、管道、`;`)`parseLeadingCommandName` 判 reject,而 `createCoarseCommandNamePolicy`
|
|
186
|
+
// 的 `defaultAction` **缺省就是 `"ask"`**(core tool-policy.js:`opts.defaultAction ?? "ask"`)—— 于是它们
|
|
187
|
+
// 在 deny 门上折 **ask**,不是放行(无 command 串的调用同理)。这与 allowlist 模式那句「fall to
|
|
188
|
+
// defaultAction」是同一个兜底,只是这里没显式传参所以容易读漏。上方 docblock 的「Un-parseable bypass
|
|
189
|
+
// commands fail toward `ask` in both the deny gate and the ask-list」说的就是这一条。零行为码改动。
|
|
182
190
|
// (NOTE: never pass `allow: []` to the coarse helper — an empty array is truthy there and would turn it
|
|
183
191
|
// into an allowlist that asks on EVERYTHING. Omit allow entirely instead.)
|
|
184
192
|
if (deny.length > 0)
|
|
@@ -211,6 +219,56 @@ export function autonomyOverrides(autonomy) {
|
|
|
211
219
|
return {};
|
|
212
220
|
}
|
|
213
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* [2942]/[2943] `governanceForced` 的**写侧** —— 把内层策略(治理层自己合成的那些)产的 `ask` 记进标记表。
|
|
224
|
+
*
|
|
225
|
+
* 决议本身**逐字透传**:本包装器只观察,不改 action / message / updatedInput / decisionReason,也不吞异常
|
|
226
|
+
* (内层抛就照抛,fail-closed 的属主仍是内层)。同步内层保持同步返回(不无谓地把一条同步策略异步化)。
|
|
227
|
+
*
|
|
228
|
+
* 为什么标在这一层而不是让 `decisionReason` 上 wire:core 的 ask 铸造点不透传 `PermissionResult`
|
|
229
|
+
* (侦察结论逐字见 `governance-ask-marks.ts` 顶注),这条缝在当前 core 上根本不存在。
|
|
230
|
+
*/
|
|
231
|
+
function createGovernanceAskMarkingPolicy(inner, marksOf) {
|
|
232
|
+
// 🔴 codex 交叉复审 round2 [medium](验真):**如实覆写**,不是只加不减 —— core 的
|
|
233
|
+
// `recheckApprovedEdit` 拿同一个 `toolCallId` 重跑策略链(人编辑 args 之后),「原始 args 治理层要问 /
|
|
234
|
+
// 编辑后治理层放行、由别的策略提第二问」会让第二张卡挂陈旧治理标。理由全文见 GovernanceAskMarks.setMarked。
|
|
235
|
+
const note = (d, req) => {
|
|
236
|
+
// 表在**裁决那一刻**解析(不是构造时):策略闭包铸于 resolveSpec,真正的作用域要到 run 腿跑起来才在。
|
|
237
|
+
marksOf()?.setMarked(req.toolCallId, d.action === "ask");
|
|
238
|
+
return d;
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
check(req, signal) {
|
|
242
|
+
const decision = inner.check(req, signal);
|
|
243
|
+
return decision instanceof Promise ? decision.then((d) => note(d, req)) : note(decision, req);
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* [2942]/[2943] shellGate 腿的写侧 —— **纯观察器**(恒 `allow` = `combinePolicies` 折叠里的弃权,见
|
|
249
|
+
* `applyRuntimeGovernance` 顶注对 sensitivePathPolicy 那段 allow 语义的同款说明)。
|
|
250
|
+
*
|
|
251
|
+
* 只在治理层把 shellGate 抬到 **`"always"`** 时装:该档下 core 对**每一次** shell 调用都要人批
|
|
252
|
+
* (`prepare-task.js` 把 Bash/Monitor 的 irreversibility tier 置 `"always"`),所以「这是一次 shell 调用」
|
|
253
|
+
* 与「这只 ask 是治理层门出来的」等价——推断是紧的,零错标。
|
|
254
|
+
*
|
|
255
|
+
* 🔴 `"classify"` 档**故意不装**:那一档由 core 的分类器逐调用裁决,一次 shell ask 可能来自分类器
|
|
256
|
+
* (治理),也可能来自 `APPROVAL_REQUIRE` 这类别的门(非治理),在 `AskRequest` 上无从分辨。按本信号
|
|
257
|
+
* 「只做正向断言」的语义,分不清就**让键缺席**,不去猜(`governance-ask-marks.ts` 语义 1)。
|
|
258
|
+
*/
|
|
259
|
+
function createGovernanceShellGateMarkPolicy(marksOf, tools = COARSE_SHELL_TOOLS) {
|
|
260
|
+
const shellTools = new Set(tools);
|
|
261
|
+
return {
|
|
262
|
+
check(req) {
|
|
263
|
+
// 只对 shell 工具**置位**,非 shell 一格不碰(那格的属主是上面的 ask 观察器)。本策略在合成里
|
|
264
|
+
// 恒排在 ask 观察器**之后**(见 applyRuntimeGovernance 的 combinePolicies 顺序):`always` 档下
|
|
265
|
+
// 每一次 shell 调用都被治理层门住,所以它对 shell 那一格的裁定压过「治理策略这次没产 ask」。
|
|
266
|
+
if (shellTools.has(req.toolName))
|
|
267
|
+
marksOf()?.setMarked(req.toolCallId, true);
|
|
268
|
+
return { action: "allow" };
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
214
272
|
/** core `tighten-task-spec` 的 SHELL_GATE_RANK 镜像(单一属主:#153 搬家后本文件是 server 侧唯一副本;
|
|
215
273
|
* task-settings 的旧副本随 settings 折叠臂一并拆除)。tightenTaskSpec 的契约是「override 自身必须收紧」
|
|
216
274
|
* 而非「取两者较严」——低于 base 的 override 直接 throw,所以施加前要按 rank 只升不降。 */
|
|
@@ -229,12 +287,34 @@ const SHELL_GATE_RANK = { off: 0, classify: 1, always: 2 };
|
|
|
229
287
|
* ⇒ 旋钮静默失效(clay 实机 Monitor 案「门根本没铸」);bypassPermissions 一句话掀掉部署闸;非 host lane
|
|
230
288
|
* fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。rank 合成:
|
|
231
289
|
* autonomy 派生值与旋钮取大(ask→"always" 压过 "classify"),再对 base 只升不降(防 tightenTaskSpec throw)。
|
|
290
|
+
*
|
|
291
|
+
* #177([2951] issue #29,**邻仓 test AI 的 46 格真机矩阵**实证——不是本仓的钉,别到本仓文件里数 46;
|
|
292
|
+
* 本仓的类级门是 test/operator-knob-client-posture-matrix.test.ts 的 旋钮×表态×lane 矩阵):
|
|
293
|
+
* `SENSITIVE_WRITE_PATTERNS` 是 #153 那次搬家漏下的兄弟旋钮——同一个病灶原样复发在 sensitive-path
|
|
294
|
+
* DENY 腿上(唯一合成点在 deriveSettingsPolicy 的 fsWriteGate 闭包,bypassPermissions/键缺席/settings
|
|
295
|
+
* 整缺席三形整条腿不建)。同方修:调用方(resolve-spec)把 operator 守卫集预铸成 `sensitivePathPolicy`
|
|
296
|
+
* 从这里**无条件施加**——它只在守卫段上产 deny(core 语义 deny/"safety"),对其余目标返回的是
|
|
297
|
+
* `action:"allow"`(**不是**「无意见」这种第三态:core 的 ToolPolicy 没有弃权值。它在
|
|
298
|
+
* `combinePolicies` 的 deny/ask 优先折叠里不构成一票,所以在**折叠语境**下等价于弃权;若日后有人把
|
|
299
|
+
* 这条策略单独当成唯一 policy 用,那个 allow 就是真放行——别照抄这句话去别的语境)。于是对 bypass
|
|
300
|
+
* 只挡守卫段、不给普通目标加 ask 门([820] 表第四行「bypass=不加门非开门」保持逐字)。策略在这里
|
|
301
|
+
* 只组合不构造:裁决 env 的 lane 分形(host 真 fs / 沙箱 deferred 代理)是 resolve-spec 的属主知识,
|
|
302
|
+
* governance 层不重复它。
|
|
232
303
|
*/
|
|
233
304
|
export function applyRuntimeGovernance(base, governance) {
|
|
305
|
+
// 显式注入(测试/将来的多实例形)优先;否则**每次裁决时**取当前 run 腿的 ALS 表(顶注「作用域」段)。
|
|
306
|
+
const marksOf = () => governance.askMarks ?? currentGovernanceAskMarks();
|
|
234
307
|
const overrides = { ...autonomyOverrides(governance.autonomy) };
|
|
235
308
|
const coarse = compileCommandPolicy(governance.commandPolicy);
|
|
236
|
-
|
|
237
|
-
|
|
309
|
+
const governancePolicies = [coarse, governance.sensitivePathPolicy].filter((p) => p !== undefined);
|
|
310
|
+
// [2942]/[2943]:治理层自己产的 `ask` 打标 —— 包在**合成之后**,于是 commandPolicy 的两模式
|
|
311
|
+
// (allowlist 的 defaultAction:"ask" / blocklist 的 ask-list)与 sensitivePathPolicy 走同一个铸点,
|
|
312
|
+
// 不会漏掉将来加进这个数组的第四条腿。
|
|
313
|
+
const governancePolicy = governancePolicies.length > 0
|
|
314
|
+
? createGovernanceAskMarkingPolicy(governancePolicies.length === 1 ? governancePolicies[0] : combinePolicies(...governancePolicies), marksOf)
|
|
315
|
+
: undefined;
|
|
316
|
+
if (governancePolicy !== undefined)
|
|
317
|
+
overrides.toolPolicy = governancePolicy;
|
|
238
318
|
if (governance.manualModeShellGate !== undefined) {
|
|
239
319
|
const candidate = overrides.shellGate !== undefined && SHELL_GATE_RANK[overrides.shellGate] >= SHELL_GATE_RANK[governance.manualModeShellGate]
|
|
240
320
|
? overrides.shellGate
|
|
@@ -249,6 +329,13 @@ export function applyRuntimeGovernance(base, governance) {
|
|
|
249
329
|
else
|
|
250
330
|
delete overrides.shellGate;
|
|
251
331
|
}
|
|
332
|
+
// [2942]/[2943] shellGate 腿:治理层**自己**把门抬到 `"always"` 时,挂上纯观察器给 shell 调用打标
|
|
333
|
+
// (理由与 `"classify"` 为何不装,见 createGovernanceShellGateMarkPolicy 顶注)。判据取
|
|
334
|
+
// `overrides.shellGate` 而不是有效档 —— base 已经更严时上面那支会 `delete`,那时门不是治理层给的。
|
|
335
|
+
if (overrides.shellGate === "always") {
|
|
336
|
+
const shellObserver = createGovernanceShellGateMarkPolicy(marksOf);
|
|
337
|
+
overrides.toolPolicy = governancePolicy === undefined ? shellObserver : combinePolicies(governancePolicy, shellObserver);
|
|
338
|
+
}
|
|
252
339
|
if (overrides.toolPolicy === undefined && overrides.handsReadOnly === undefined && overrides.shellGate === undefined) {
|
|
253
340
|
return base; // nothing to govern → avoid a needless tightenTaskSpec pass
|
|
254
341
|
}
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -192,16 +192,10 @@ export interface FsWriteGateWiring {
|
|
|
192
192
|
* allow with decisionReason "rule", a THROWN probe ⇒ not exempt = fail-closed to ask). Same canonical
|
|
193
193
|
* toolName key space as the ask-policy layer's exemption probe — ONE grant serves both layers; the gate-level
|
|
194
194
|
* check is what makes "allow all session" bite SAME-TURN for the parent and inherited child tasks (the ask
|
|
195
|
-
* path is never re-entered). (codex M1
|
|
196
|
-
*
|
|
197
|
-
*
|
|
195
|
+
* path is never re-entered). (codex M1 的边界注记:sensitive-path DENY 腿与本探针的「豁免越不过 deny」
|
|
196
|
+
* 保证仍成立——但那条腿已随 #177 搬到 governance 层,deny-wins 是 tightenTaskSpec 折叠给的,不再依赖
|
|
197
|
+
* 本 wiring 的组合点。) */
|
|
198
198
|
isExempt?: (toolName: string, canonicalPath: string) => boolean | Promise<boolean>;
|
|
199
|
-
/** ③ (core 1.295): the sensitive-path write DENY set composed onto the fs-write gate leg —
|
|
200
|
-
* `createSensitivePathPolicy({env, patterns, rootPath: cwd})`, deny-wins in the SAME combinePolicies fold, so a
|
|
201
|
-
* session exemption (isExempt) or an acceptEdits auto-allow can never cross it (the 1.199-batch documented gap,
|
|
202
|
-
* now closed). Source = config `SENSITIVE_WRITE_PATTERNS` (unset ⇒ core RECOMMENDED_SENSITIVE_PATTERNS; the
|
|
203
|
-
* set curation is core's — the server passes through). Absent/empty ⇒ no sensitive policy (pre-1.295 shape). */
|
|
204
|
-
sensitivePatterns?: string[];
|
|
205
199
|
}
|
|
206
200
|
export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): {
|
|
207
201
|
toolPolicy?: ToolPolicy;
|