@sema-agent/server 7.93.2 → 7.93.3
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 +12 -4
- package/dist/adoption/wire.d.ts +5 -5
- package/dist/approval-ask-audit-store.js +6 -1
- package/dist/boot/shutdown.d.ts +2 -1
- package/dist/boot/shutdown.js +40 -3
- package/dist/host-decision.d.ts +29 -8
- package/dist/host-decision.js +2 -2
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/resume-legs.js +4 -1
- package/dist/http/routes/approvals-assistant.js +20 -7
- package/dist/http/routes/memory-origin.d.ts +2 -2
- package/dist/plugins/host-platform.d.ts +21 -6
- package/dist/plugins/host-platform.js +1 -1
- package/dist/plugins/store-backend.js +4 -2
- package/dist/plugins/store-open-refusal.d.ts +11 -0
- package/dist/plugins/store-open-refusal.js +23 -0
- package/dist/run-local.js +5 -4
- package/dist/tool-approval.d.ts +58 -0
- package/dist/tool-approval.js +11 -1
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -375,12 +375,20 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
|
|
|
375
375
|
core 5.54.0 起拒启错误带**可分派 code**(`FileStoreLockError`,message 点名占用方 pid),运维按码
|
|
376
376
|
三动作:`store.dir_in_use`=另一个活进程持有该目录 → 停掉它(或换数据根/迁 SQL 后端);
|
|
377
377
|
`store.dir_claiming`=另一进程正在接管崩溃者留下的锁 → **瞬态,稍候重试**(接管方要么完成要么
|
|
378
|
-
自己也被收割);`store.lock_unreadable
|
|
379
|
-
|
|
378
|
+
自己也被收割);`store.lock_unreadable`=锁**读不出可解析的 owner**
|
|
379
|
+
(真坏形:无 owner / 嵌套接管残留;**或者**本进程读不动它 —— core 把一切非 ENOENT 的读失败都折进这一码)
|
|
380
|
+
→ 人工处置,message 会点名要删的东西(⚠️ 三条消息点名的**不是同一个**:`LOCK` / `LOCK.claim.*` / 本进程
|
|
381
|
+
自己那把删不掉的锁),且 core 的句子**带条件** ——「stale 才删」:读不动的那一形背后可能是个**活**写者
|
|
382
|
+
(权限/属主不对),盲删 = 拆掉围栏 ⇒ 两个写者共用一个数据根。**不要盲删数据根下其它东西**。
|
|
383
|
+
server 刻意不把这族错**吞掉**(fail-fast:
|
|
380
384
|
两进程共用 file 数据目录是禁止形,静默容忍比拒启更坏)。
|
|
385
|
+
⚠️ **拒启句本身就带这三条里对应的那一条出路**(不必再回来查这张表);拒启原因**不是**锁冲突时
|
|
386
|
+
(磁盘满 / 权限 / 只读挂载 …)拒启句**不加任何出路猜测** —— 此前它一律拼「另一个实例可能占着,停掉它」,
|
|
387
|
+
把运维送去找一个不存在的进程。HTTP 服务与 `run-local` 共用同一只派生,只有「换一个空目录」那半句按通道
|
|
388
|
+
措辞不同(`LOCAL_DATA_ROOT` / `--root`)。
|
|
381
389
|
⚠️ 自 7.93.1 起 `store.dir_in_use` **这一码**多一道有界等待(见下面 `STORE_LOCK_WAIT_MS` 那条):
|
|
382
|
-
|
|
383
|
-
与非锁类失败**一秒都不等**,原样抛。
|
|
390
|
+
**等待腿本身**不改拒启形、也不改上面那句出路(它只在拒之前先在窗内反复重取,并在拒句里多写一句
|
|
391
|
+
`waited Nms`);另两码(`dir_claiming` / `lock_unreadable`)与非锁类失败**一秒都不等**,原样抛。
|
|
384
392
|
`DB_BACKEND=memory` 并非全无盘:workflow 相关账本仍挂数据根下——多个 memory 形引擎**不要共用**
|
|
385
393
|
数据根/HOME(1.309 起并发 boot 不再拒启,但账本内容级共享仍不受支持)。
|
|
386
394
|
- **`STORE_LOCK_WAIT_MS`(缺省 `60000`,单位 ms)—— 撞上活持锁人时等一会儿再拒**:
|
package/dist/adoption/wire.d.ts
CHANGED
|
@@ -28,8 +28,8 @@ export declare const AdoptionSourceSchema: z.ZodObject<{
|
|
|
28
28
|
export type AdoptionSource = z.infer<typeof AdoptionSourceSchema>;
|
|
29
29
|
/** per-store 腿的动作词(183 §6 闭集;form b 只会产出前两个 + `none`)。 */
|
|
30
30
|
export declare const AdoptionLegActionSchema: z.ZodEnum<{
|
|
31
|
-
reset: "reset";
|
|
32
31
|
none: "none";
|
|
32
|
+
reset: "reset";
|
|
33
33
|
"bucket-rebind": "bucket-rebind";
|
|
34
34
|
"row-rewrite": "row-rewrite";
|
|
35
35
|
carried: "carried";
|
|
@@ -38,8 +38,8 @@ export type AdoptionLegAction = z.infer<typeof AdoptionLegActionSchema>;
|
|
|
38
38
|
export declare const AdoptionLegSchema: z.ZodObject<{
|
|
39
39
|
store: z.ZodString;
|
|
40
40
|
action: z.ZodEnum<{
|
|
41
|
-
reset: "reset";
|
|
42
41
|
none: "none";
|
|
42
|
+
reset: "reset";
|
|
43
43
|
"bucket-rebind": "bucket-rebind";
|
|
44
44
|
"row-rewrite": "row-rewrite";
|
|
45
45
|
carried: "carried";
|
|
@@ -109,8 +109,8 @@ export declare const AdoptionReportSchema: z.ZodObject<{
|
|
|
109
109
|
legs: z.ZodArray<z.ZodObject<{
|
|
110
110
|
store: z.ZodString;
|
|
111
111
|
action: z.ZodEnum<{
|
|
112
|
-
reset: "reset";
|
|
113
112
|
none: "none";
|
|
113
|
+
reset: "reset";
|
|
114
114
|
"bucket-rebind": "bucket-rebind";
|
|
115
115
|
"row-rewrite": "row-rewrite";
|
|
116
116
|
carried: "carried";
|
|
@@ -170,8 +170,8 @@ export declare const AdoptionReceiptSchema: z.ZodObject<{
|
|
|
170
170
|
legs: z.ZodArray<z.ZodObject<{
|
|
171
171
|
store: z.ZodString;
|
|
172
172
|
action: z.ZodEnum<{
|
|
173
|
-
reset: "reset";
|
|
174
173
|
none: "none";
|
|
174
|
+
reset: "reset";
|
|
175
175
|
"bucket-rebind": "bucket-rebind";
|
|
176
176
|
"row-rewrite": "row-rewrite";
|
|
177
177
|
carried: "carried";
|
|
@@ -214,8 +214,8 @@ export type AdoptionReceipt = z.infer<typeof AdoptionReceiptSchema>;
|
|
|
214
214
|
export declare const AdoptionLegsSchema: z.ZodArray<z.ZodObject<{
|
|
215
215
|
store: z.ZodString;
|
|
216
216
|
action: z.ZodEnum<{
|
|
217
|
-
reset: "reset";
|
|
218
217
|
none: "none";
|
|
218
|
+
reset: "reset";
|
|
219
219
|
"bucket-rebind": "bucket-rebind";
|
|
220
220
|
"row-rewrite": "row-rewrite";
|
|
221
221
|
carried: "carried";
|
|
@@ -7,6 +7,11 @@ export const CRASH_CONVERGED_LIST_MAX = 200;
|
|
|
7
7
|
export function buildCheckpointAnchorHash(token) {
|
|
8
8
|
return createHash("sha256").update(token, "utf8").digest("hex").slice(0, 16);
|
|
9
9
|
}
|
|
10
|
+
const HOST_DECIDED_BY_WORDS = ["person", "sla_timeout", "policy"];
|
|
11
|
+
const HOST_DECIDED_BY = new Set(HOST_DECIDED_BY_WORDS);
|
|
12
|
+
function isHostDecidedBy(v) {
|
|
13
|
+
return typeof v === "string" && HOST_DECIDED_BY.has(v);
|
|
14
|
+
}
|
|
10
15
|
function readLedgerRecord(u) {
|
|
11
16
|
if (u === null || typeof u !== "object" || Array.isArray(u))
|
|
12
17
|
return undefined;
|
|
@@ -87,7 +92,7 @@ function readLedgerRecord(u) {
|
|
|
87
92
|
const decidedByV = o.decidedBy;
|
|
88
93
|
const legacySettledByV = decidedByV === undefined ? o.settledBy : undefined;
|
|
89
94
|
const legacySettledBy = legacySettledByV === "human" || legacySettledByV === "timeout" || legacySettledByV === "aborted" ? legacySettledByV : undefined;
|
|
90
|
-
const decidedBy = decidedByV
|
|
95
|
+
const decidedBy = isHostDecidedBy(decidedByV) ? decidedByV : legacySettledBy !== undefined ? "person" : undefined;
|
|
91
96
|
const approver = s("approver");
|
|
92
97
|
const settledAtMs = num("settledAtMs");
|
|
93
98
|
const sessionId = s("sessionId");
|
package/dist/boot/shutdown.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* "第二个信号"的语义读数。
|
|
13
13
|
* 3. `clearInterval(reaper)` 必须是 hardShutdown 的第一件事(在 `server.close()` 之前),否则
|
|
14
14
|
* 收尾期还会有 reaper tick 打向正在关闭的池。
|
|
15
|
-
* 4. **进程级监听一律成对**([ref] 二轮件4)
|
|
15
|
+
* 4. **进程级监听一律成对**([ref] 二轮件4):本函数装的六只(三信号 + [ref]① 双钩 + S-557 退出边界钩)
|
|
16
|
+
* 只能经返回的
|
|
16
17
|
* {@link ShutdownHandle} 摘 —— 调用方手写「摘三信号」的收尾会把双钩漏在进程上,而陈旧的那只
|
|
17
18
|
* rejection 钩仍握着 `process.exit(1)`,会对后续无关的 rejection 把进程打掉。
|
|
18
19
|
*/
|
package/dist/boot/shutdown.js
CHANGED
|
@@ -2,7 +2,8 @@ import { Runner, defaultTaskRegistry } from "@sema-agent/core";
|
|
|
2
2
|
import { writeCrashLast, clearCrashLast, readCrashLast } from "./crash-last.js";
|
|
3
3
|
import { createSighupIdleHandler } from "../sighup-idle.js";
|
|
4
4
|
import { createParentWatch } from "../parent-watch.js";
|
|
5
|
-
import { reapHostForegroundChildren } from "../plugins/host-platform.js";
|
|
5
|
+
import { liveHostForegroundChildCount, reapHostForegroundChildren } from "../plugins/host-platform.js";
|
|
6
|
+
import { liveEphemeralHostWorkspaceCount, reapEphemeralHostWorkspaces } from "../plugins/host-workspace-registry.js";
|
|
6
7
|
import { createEngineLeaseTracker } from "./engine-lease.js";
|
|
7
8
|
import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
|
|
8
9
|
import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
|
|
@@ -51,9 +52,45 @@ export function installShutdownHandlers(ctx) {
|
|
|
51
52
|
logger.warn("shutdown_foreground_reap_failed", { err: String(err) });
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
|
-
const
|
|
55
|
+
const reapOwnHostWorkspaces = () => {
|
|
56
|
+
try {
|
|
57
|
+
const { removed, failed } = reapEphemeralHostWorkspaces();
|
|
58
|
+
if (removed.length > 0)
|
|
59
|
+
logger.info("shutdown_host_workspaces_reaped", { removed: removed.length });
|
|
60
|
+
if (failed.length > 0)
|
|
61
|
+
logger.warn("shutdown_host_workspace_reap_incomplete", { dirs: failed.map((f) => f.dir) });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
logger.warn("shutdown_host_workspace_reap_failed", { err: String(err) });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const onProcessExit = () => {
|
|
68
|
+
const onBooks = { foregroundChildren: liveHostForegroundChildCount(), hostWorkspaces: liveEphemeralHostWorkspaceCount() };
|
|
55
69
|
try {
|
|
56
70
|
reapOwnChildProcesses();
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error("shutdown_exit_boundary_child_reap_threw", err);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
reapOwnHostWorkspaces();
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error("shutdown_exit_boundary_workspace_reap_threw", err);
|
|
80
|
+
}
|
|
81
|
+
const left = { foregroundChildren: liveHostForegroundChildCount(), hostWorkspaces: liveEphemeralHostWorkspaceCount() };
|
|
82
|
+
try {
|
|
83
|
+
logger.info("shutdown_exit_boundary", { ...onBooks, leftForegroundChildren: left.foregroundChildren, leftHostWorkspaces: left.hostWorkspaces });
|
|
84
|
+
if (left.foregroundChildren > 0 || left.hostWorkspaces > 0)
|
|
85
|
+
logger.warn("shutdown_exit_boundary_incomplete", left);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error("shutdown_exit_boundary_log_threw", err);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
process.once("exit", onProcessExit);
|
|
92
|
+
const onUncaughtException = (err) => {
|
|
93
|
+
try {
|
|
57
94
|
writeLastWords(err);
|
|
58
95
|
console.error(err);
|
|
59
96
|
}
|
|
@@ -72,7 +109,6 @@ export function installShutdownHandlers(ctx) {
|
|
|
72
109
|
return;
|
|
73
110
|
}
|
|
74
111
|
try {
|
|
75
|
-
reapOwnChildProcesses();
|
|
76
112
|
writeLastWords(err);
|
|
77
113
|
console.error(err);
|
|
78
114
|
}
|
|
@@ -212,6 +248,7 @@ export function installShutdownHandlers(ctx) {
|
|
|
212
248
|
if (disposed)
|
|
213
249
|
return;
|
|
214
250
|
disposed = true;
|
|
251
|
+
process.removeListener("exit", onProcessExit);
|
|
215
252
|
process.removeListener("uncaughtException", onUncaughtException);
|
|
216
253
|
process.removeListener("unhandledRejection", onUnhandledRejection);
|
|
217
254
|
process.removeListener("SIGTERM", drainThenShutdown);
|
package/dist/host-decision.d.ts
CHANGED
|
@@ -20,20 +20,41 @@ import { type ResumeOutcome } from "@sema-agent/core";
|
|
|
20
20
|
* · `decidedBy: "person"` —— 运维经审批通道做的决定(两条 HTTP `/decide` 腿);
|
|
21
21
|
* · `decidedBy: "sla_timeout"` —— 宿主自己的 SLA 扫在行的 deadline 上判的(FACET A;**只有** sweep 能这么说,
|
|
22
22
|
* 而 sweep 结构上不能 `allow`)。
|
|
23
|
+
* · `decidedBy: "policy"`(core 7.26.2 / #1016)—— **这台部署自己的策略**拒的:答话的是机器不是人。调用方
|
|
24
|
+
* 经决裁体的 `settledBy` 自报;core 据它给模型铸一句**策略句**(而不是「用户不同意,停下来等他」),
|
|
25
|
+
* 并把结算记成 `policy_refused`(`who.party:"none"`)。`allow` 侧结构上不可(core pre-CAS 拒:只有人批准)。
|
|
23
26
|
* · `approver` —— 通道自报的结算方标识串。**缺席永不读成**「没人批」或「有人批」,只意味着通道没报名字。
|
|
24
27
|
*/
|
|
25
|
-
/** 直接取 core `ResumeOutcome` policy_ask 臂的 `hostDecision` 型 ——
|
|
28
|
+
/** 直接取 core `ResumeOutcome` policy_ask 臂的 `hostDecision` 型 —— 本仓不复述那几个词(第二铸点)。 */
|
|
26
29
|
export type HostDecision = Extract<ResumeOutcome, {
|
|
27
30
|
gate: "policy_ask";
|
|
28
31
|
}>["hostDecision"];
|
|
32
|
+
/** **谁结束了这次等待**的闭集,型从 core 的那一格派生(本仓永不抄词表:core 加/退一个词,读它的每一处
|
|
33
|
+
* 编译期先红)。今天 = `"person"` | `"sla_timeout"` | `"policy"`。 */
|
|
34
|
+
export type HostDecidedBy = HostDecision["decidedBy"];
|
|
29
35
|
/**
|
|
30
|
-
*
|
|
31
|
-
* 没有名字可报)。
|
|
36
|
+
* 一次 decide 的事实,**一处成形**(两条 HTTP decide 腿 + 活卡 PARKED 赎回席共用)。
|
|
32
37
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
38
|
+
* `decidedBy` = 调用方**陈述**的决策者(缺席 ⇒ `"person"`,= 本面自古以来的形:一条不说话的 decide 就是
|
|
39
|
+
* 一个人按的)。🔴 **本仓零判据**:词表的属主是 core,不认识的词由它 pre-CAS 响亮拒
|
|
40
|
+
* (`checkpoint.invalid_outcome`,`hostDecision.decidedBy … is outside the closed set`)。在这里再判一次
|
|
41
|
+
* 就是同一语义面的第二个判官(源头修复纪律 [ref]),而且本仓抄一份词表的那天 core 加第四个词就会被
|
|
42
|
+
* 静默 400 掉 —— 那正是 #1016 的 `"policy"` 加进来时本可以发生的事。
|
|
43
|
+
*
|
|
44
|
+
* `principal` = 那条腿已经验过身份的决策者(缺席 = 匿名档的部署,通道确实没有名字可报)。
|
|
45
|
+
* 归属串走 core 的 `screenApproverAttribution` **先筛后带**:core 对形不合的归属是 pre-CAS 响亮拒,
|
|
46
|
+
* 而一个奇形怪状的 principal 把一次合法的决策变成 400 是纯回归。筛不过 ⇒ **不带这一格**(而不是带一个
|
|
47
|
+
* 被改造过的串):归属是转录,不是可以「修一修」的东西 —— 修出来的名字既不是通道报的那个,也不是
|
|
48
|
+
* 「通道没报」,两头都不真。
|
|
49
|
+
*
|
|
50
|
+
* 🔴 归属**不按词分岔**:core 的 `policy_refused` 结算记录上确实没有归属席(`who.party:"none"`),但那是
|
|
51
|
+
* **core 的记录**怎么写,不是**本服务观察到的事实**是什么 —— 送出这次请求的那把已验证身份是真的,本仓的
|
|
52
|
+
* 迟到结算账本(`approval-ask-audit-store.ts`)记的就是它。按词删一格 = 给账本挖一个只在某个词上出现的洞。
|
|
53
|
+
*
|
|
54
|
+
* 🔴 **入参是 `string`,不是 `HostDecidedBy` —— 这一格刻意让类型说真话**(wire 边界强类型宪法):两条
|
|
55
|
+
* decide 腿能证得出的只有「调用方给了个串」(形门在各自的口上),**证不出**它是词表成员;把参数声明成
|
|
56
|
+
* 闭集就是让类型替一件本层从没做过的检查背书。⇒ 收 `string`,在**这一处**(本座的边界)做唯一一次 cast,
|
|
57
|
+
* 而那次 cast 说的是「我把一个未判词的串放到只收词的座上,判词的是座的属主(core,pre-CAS 响亮拒)」。
|
|
37
58
|
*/
|
|
38
|
-
export declare function
|
|
59
|
+
export declare function hostDecisionFacts(decidedBy: string | undefined, principal: string | undefined): HostDecision;
|
|
39
60
|
//# sourceMappingURL=host-decision.d.ts.map
|
package/dist/host-decision.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { screenApproverAttribution } from "@sema-agent/core";
|
|
2
|
-
export function
|
|
2
|
+
export function hostDecisionFacts(decidedBy, principal) {
|
|
3
3
|
const screened = screenApproverAttribution(principal);
|
|
4
|
-
return { decidedBy: "person", ...(screened.approver !== undefined ? { approver: screened.approver } : {}) };
|
|
4
|
+
return { decidedBy: (decidedBy ?? "person"), ...(screened.approver !== undefined ? { approver: screened.approver } : {}) };
|
|
5
5
|
}
|
|
6
6
|
//# sourceMappingURL=host-decision.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { redactHead } from "../observability/run-terminal.js";
|
|
2
2
|
import { findParkOrigin } from "../parked-decide.js";
|
|
3
3
|
import { decodeCheckpointScope } from "../security.js";
|
|
4
|
-
import {
|
|
4
|
+
import { hostDecisionFacts } from "../host-decision.js";
|
|
5
5
|
import { APPROVAL_GATE_KINDS } from "../tool-approval.js";
|
|
6
6
|
import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
7
7
|
import { hasServiceAuth } from "./dispatch.js";
|
|
@@ -130,7 +130,7 @@ export function createApprovalSweeps(ctx) {
|
|
|
130
130
|
...(req.gateBoundInputHash !== null ? { boundInputHash: req.gateBoundInputHash } : {}),
|
|
131
131
|
...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
|
|
132
132
|
};
|
|
133
|
-
const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note,
|
|
133
|
+
const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, hostDecisionFacts(req.settledBy, req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
|
|
134
134
|
return { status: out.status, body: out.body };
|
|
135
135
|
}
|
|
136
136
|
return { denyExpiredApprovals, redeemParkedAsk };
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -62,6 +62,7 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
62
62
|
function checkpointRowRedeemableElsewhere(reason) {
|
|
63
63
|
return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
|
|
64
64
|
}
|
|
65
|
+
const NON_BINDING_INVALID_OUTCOME_FIELDS = new Set(["hostDecision", "approver"]);
|
|
65
66
|
export function createResumeLegs(ctx) {
|
|
66
67
|
const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
|
|
67
68
|
const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
|
|
@@ -908,7 +909,9 @@ export function createResumeLegs(ctx) {
|
|
|
908
909
|
...(taskId !== undefined ? { taskId } : {}),
|
|
909
910
|
error: e.message,
|
|
910
911
|
errorCode: e.code === "checkpoint.invalid_outcome"
|
|
911
|
-
? (outcome.gate === "policy_ask"
|
|
912
|
+
? (outcome.gate === "policy_ask" && !NON_BINDING_INVALID_OUTCOME_FIELDS.has(String(e.detail?.field ?? ""))
|
|
913
|
+
? "approval_binding_mismatch"
|
|
914
|
+
: "resume_outcome_invalid")
|
|
912
915
|
: e.code,
|
|
913
916
|
...(e.detail?.field ? { field: e.detail.field } : {}),
|
|
914
917
|
...((e.code === "resume.usage_window_exhausted" || e.code === "resume.preflight_rejected") && typeof e.detail?.retryAfterMs === "number" && Number.isFinite(e.detail.retryAfterMs) && e.detail.retryAfterMs > 0
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { screenedGate } from "../../trace/project.js";
|
|
2
|
-
import {
|
|
2
|
+
import { hostDecisionFacts } from "../../host-decision.js";
|
|
3
3
|
import { sameExecutionRecord } from "@sema-agent/core";
|
|
4
4
|
import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
|
|
5
|
-
import { APPROVAL_GATE_KINDS } from "../../tool-approval.js";
|
|
5
|
+
import { APPROVAL_GATE_KINDS, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING } from "../../tool-approval.js";
|
|
6
6
|
import { isParkedRunStatus } from "../../plugins/store-contracts.js";
|
|
7
7
|
import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
|
|
8
8
|
import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
|
|
@@ -21,10 +21,10 @@ import { sendResumeOutcome } from "../route-ctx.js";
|
|
|
21
21
|
import { assertNever } from "../route-ctx.js";
|
|
22
22
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
|
|
23
23
|
const DECIDE_BODY_DECLARED_KEYS = [
|
|
24
|
-
"answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "updatedInput",
|
|
24
|
+
"answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "settledBy", "updatedInput",
|
|
25
25
|
];
|
|
26
26
|
const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
|
|
27
|
-
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "updatedInput"];
|
|
27
|
+
const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "settledBy", "updatedInput"];
|
|
28
28
|
const ACCEPTED_DECIDE_BODY = new Set(DECIDE_BODY_DECLARED_KEYS);
|
|
29
29
|
const UNSUPPORTED_DECIDE_BODY = new Set(["editedPlan", "sessionId", "taskId"]);
|
|
30
30
|
export function decideBodyKeyIssue(raw) {
|
|
@@ -71,7 +71,8 @@ function buildApprovalEnvelope(sessionId, covered, reading) {
|
|
|
71
71
|
}
|
|
72
72
|
const ACCEPTED_PLAN_REVIEW_BODY = new Set(PLAN_REVIEW_BODY_DECLARED_KEYS);
|
|
73
73
|
const UNSUPPORTED_PLAN_REVIEW_BODY = new Set([
|
|
74
|
-
|
|
74
|
+
...DECIDE_BODY_DECLARED_KEYS.filter((k) => !PLAN_REVIEW_BODY_DECLARED_KEYS.includes(k)),
|
|
75
|
+
"permissionMode", "sessionId", "taskId",
|
|
75
76
|
]);
|
|
76
77
|
export function planReviewBodyKeyIssue(raw) {
|
|
77
78
|
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -553,7 +554,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
553
554
|
const body = (await readJson(req));
|
|
554
555
|
const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
|
|
555
556
|
if (!decision) {
|
|
556
|
-
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember? }");
|
|
557
|
+
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, settledBy? }");
|
|
557
558
|
return;
|
|
558
559
|
}
|
|
559
560
|
{
|
|
@@ -624,6 +625,18 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
624
625
|
sendError(res, 400, "checkpoint_token_not_in_proof", "checkpointToken is not supported on a direct-door worker (not covered by the decision proof); bind the decision with boundCallId and boundInputHash instead");
|
|
625
626
|
return;
|
|
626
627
|
}
|
|
628
|
+
if (deps.config.directDoorActive && body.settledBy !== undefined) {
|
|
629
|
+
sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (body.settledBy !== undefined && decision !== "deny") {
|
|
633
|
+
sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (body.settledBy !== undefined && typeof body.settledBy !== "string") {
|
|
637
|
+
sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
627
640
|
const binding = {
|
|
628
641
|
checkpointToken: body.checkpointToken,
|
|
629
642
|
boundCallId: body.boundCallId,
|
|
@@ -672,7 +685,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
672
685
|
pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
|
|
673
686
|
}
|
|
674
687
|
: undefined;
|
|
675
|
-
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined,
|
|
688
|
+
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, hostDecisionFacts(body.settledBy, deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
|
|
676
689
|
const outErrorCode = out.body.errorCode;
|
|
677
690
|
if (pendingGrant !== undefined && out.status === 200 && outErrorCode !== "cancelled" && deps.approvalExemptionStore) {
|
|
678
691
|
try {
|
|
@@ -97,7 +97,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
97
97
|
detail?: string | undefined;
|
|
98
98
|
eventId: string;
|
|
99
99
|
at: number;
|
|
100
|
-
to: "
|
|
100
|
+
to: "done" | "failed";
|
|
101
101
|
requestId: string;
|
|
102
102
|
}[];
|
|
103
103
|
custodyBytes: number;
|
|
@@ -105,7 +105,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
105
105
|
requestId: string;
|
|
106
106
|
reason: string;
|
|
107
107
|
at: number;
|
|
108
|
-
status: "
|
|
108
|
+
status: "done" | "failed" | "pending";
|
|
109
109
|
originUnknownKeys?: string[] | undefined;
|
|
110
110
|
clearanceId: string;
|
|
111
111
|
entryId: string;
|
|
@@ -58,7 +58,11 @@ export declare function trackHostForegroundChild(pid: number): ForegroundChildTi
|
|
|
58
58
|
/** 注销(`exit` / settle / 领养移交)。按回执身份删 ⇒ 幂等,且**绝不**碰同号的其它登记。 */
|
|
59
59
|
export declare function untrackHostForegroundChild(ticket: ForegroundChildTicket): void;
|
|
60
60
|
/** 一次前台收割的读数。三格分开是**刻意**的:`alreadyDead` 是想要的结果(不是失败),`failed` 是**真的**
|
|
61
|
-
* 没收上来 —— 两者混成一个数就等于把后者静默掉([ref]:这条腿上不许有安静的兜底)。
|
|
61
|
+
* 没收上来 —— 两者混成一个数就等于把后者静默掉([ref]:这条腿上不许有安静的兜底)。
|
|
62
|
+
*
|
|
63
|
+
* 🔴 **三格只描述调用的那一拍**(S-557):`failed: []` 读作「这一拍发出去的信号没有被拒」,**不是**
|
|
64
|
+
* 「机器上一个都没落下」—— 簿在这一拍之后照样可以再登记(受控退的窗里在飞 turn 还在 spawn)。
|
|
65
|
+
* 「一个都没落下」这句话的承重点是退出边界那只钩,不是任何一次读数。 */
|
|
62
66
|
export interface ForegroundReapReading {
|
|
63
67
|
/** 真发出了杀伤信号的组数。 */
|
|
64
68
|
readonly signalled: number;
|
|
@@ -71,9 +75,19 @@ export interface ForegroundReapReading {
|
|
|
71
75
|
* 收割簿上全部前台子进程组。返回**发出**信号的读数,不是确认死亡数 —— 确认要等,而本函数的调用点之一
|
|
72
76
|
* 是 uncaught 出口。
|
|
73
77
|
*
|
|
74
|
-
*
|
|
75
|
-
* `
|
|
76
|
-
* (
|
|
78
|
+
* **同步形、一次到位、不等宽限**(S-557 起自述与实现同一句):承重的调用点是收尾链的
|
|
79
|
+
* `process.once("exit")` 钩 —— `exit` 之后**没有续段**,`await` 一个宽限梯就是把收割赌在一个永远等不到的
|
|
80
|
+
* 时刻上(`hardShutdown` 终点那次早按只是优化,见 `boot/shutdown.ts` 的两处注)。上界 = 簿上条数 ×
|
|
81
|
+
* 一次 `kill(-pid)` syscall(簿条数被 host lane 的在飞 turn 数界住),没有定时器、没有 promise。
|
|
82
|
+
*
|
|
83
|
+
* 🔴 **「没有定时器」只对 POSIX 成立**(codex 对抗复审 [high],亲读 core dist 验真后如实标注,**不在本仓绕**):
|
|
84
|
+
* win32 的 `killTreeHard` 落到 core 的 `signalProcessTree`,而它在**装了 Git Bash** 的机器上先 spawn 一只
|
|
85
|
+
* MSYS 组杀脚本(detached ⇒ 本进程退了它照跑),随后把 `taskkill /F /T` 的**兜底**排在一只 1200ms
|
|
86
|
+
* `setTimeout` 上 —— 那只定时器在退出边界这一拍跑不到。射程:MSYS 组杀抓不到的**原生**后代可能幸存;
|
|
87
|
+
* 没装 Git Bash 的机器走的是同步 spawn 的 `taskkill /F /T`,不吃这条。**属主在上游**(本仓只是调用方),
|
|
88
|
+
* 正解 = core 给一条**无定时器的同步硬收**面(与后台腿那条已登记的上游诉求同一族),下游自铸第二把
|
|
89
|
+
* win32 杀伤 = 同一语义面两个写者,禁。修前对照:崩溃双钩那两次收割紧挨 `exit(1)`,同样跑不到那只
|
|
90
|
+
* 定时器 ⇒ 这是**既有**暴露面,不是退出边界新开的洞;而受控退的早收割仍有十秒量级的窗,定时器跑得完。
|
|
77
91
|
*
|
|
78
92
|
* 🔴 **为什么是 `killTreeHard` 而不是 SIGTERM**(codex 对抗复审 [high],亲跑复现后改):软鞍的语义是
|
|
79
93
|
* 「给它一个收尾窗,到点我再升级」,而这两个调用点之后**没有任何人在场**去升级 —— 一个
|
|
@@ -85,8 +99,9 @@ export interface ForegroundReapReading {
|
|
|
85
99
|
* 真的成立。后台腿的宽限梯不受影响(那条腿有自己的属主,且不在本簿里)。
|
|
86
100
|
*/
|
|
87
101
|
export declare function reapHostForegroundChildren(): ForegroundReapReading;
|
|
88
|
-
/**
|
|
89
|
-
|
|
102
|
+
/** 簿上现存条数。S-557 起**产品面也读它**(退出边界钩的「exit 时簿上还剩几条」哨兵),与第二本同形簿的
|
|
103
|
+
* {@link liveEphemeralHostWorkspaceCount} 同名同形 —— 所以名字里不再带 `ForTest`(零别名,旧名直接消失)。 */
|
|
104
|
+
export declare function liveHostForegroundChildCount(): number;
|
|
90
105
|
/**
|
|
91
106
|
* win32 env-key case collapse (design D1). Windows env keys are case-insensitive; Node hands the
|
|
92
107
|
* object to CreateProcess as-is, so a case-sensitive merge that produced BOTH `Path` (inherited) and `PATH`
|
|
@@ -83,7 +83,7 @@ export function reapHostForegroundChildren() {
|
|
|
83
83
|
liveHostForegroundGroups.clear();
|
|
84
84
|
return { signalled, alreadyDead, failed };
|
|
85
85
|
}
|
|
86
|
-
export function
|
|
86
|
+
export function liveHostForegroundChildCount() {
|
|
87
87
|
return liveHostForegroundGroups.size;
|
|
88
88
|
}
|
|
89
89
|
export function collapseWin32EnvKeys(env) {
|
|
@@ -8,6 +8,7 @@ import { FileOutcomeSink } from "./file-outcome-sink.js";
|
|
|
8
8
|
import { TiDBWorkflowRunStore, TiDBWorkflowCompletionInbox, TiDBWorkflowNotifyJournalStore, TiDBWorkflowAgentSessionIndex, PgWorkflowRunStore, PgWorkflowCompletionInbox, PgWorkflowNotifyJournalStore, PgWorkflowAgentSessionIndex } from "./workflow-run-store-sql.js";
|
|
9
9
|
import { FileRunStore } from "./file-run-store.js";
|
|
10
10
|
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
11
|
+
import { buildStoreOpenRefusalTail } from "./store-open-refusal.js";
|
|
11
12
|
import { acquireLocalBackendWithWait } from "./store-lock-wait.js";
|
|
12
13
|
import { NO_SQL_MIGRATIONS, runSqlOneTimeMigrations } from "./one-time-migrations.js";
|
|
13
14
|
import { LocalSessionStore } from "./local-session-store.js";
|
|
@@ -234,8 +235,9 @@ class LocalBackend {
|
|
|
234
235
|
catch (e) {
|
|
235
236
|
if (e instanceof AdoptionError)
|
|
236
237
|
throw e;
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
const tail = buildStoreOpenRefusalTail(e, "http");
|
|
239
|
+
throw new Error(`DB_BACKEND=local cannot open data dir ${root}: ${e instanceof Error ? e.message : String(e)}` +
|
|
240
|
+
(tail === undefined ? "" : ` ${tail}`));
|
|
239
241
|
}
|
|
240
242
|
this.sessionStore = new LocalSessionStore(new FileSessionRepo(this.fileBackend.root), this.fileBackend.root);
|
|
241
243
|
this.runStore = new FileRunStore(this.fileBackend.root, {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 取数据根的两条路。语义上只是「换根的通道叫什么」,不是两套出路。 */
|
|
2
|
+
export type StoreOpenLane = "http" | "run-local";
|
|
3
|
+
/**
|
|
4
|
+
* 数据根拒启句的**尾巴**:锁类失败 ⇒ 该码的出路;其余一切 ⇒ `undefined`(不加话)。
|
|
5
|
+
*
|
|
6
|
+
* @param err 构造 `FileStorageBackend` 时抛出来的东西(未经重铸 —— `store-lock-wait.ts` 的等待腿到窗
|
|
7
|
+
* 重抛时刻意保留 `code` 与类型身份,正是为了让这里分派得成)。
|
|
8
|
+
* @param lane 哪条路在问。只影响「换根」半句的措辞。
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildStoreOpenRefusalTail(err: unknown, lane: StoreOpenLane): string | undefined;
|
|
11
|
+
//# sourceMappingURL=store-open-refusal.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { FileStoreLockError } from "@sema-agent/core";
|
|
2
|
+
const FREE_ROOT_KNOB = {
|
|
3
|
+
http: "set LOCAL_DATA_ROOT to a free dir",
|
|
4
|
+
"run-local": "use a different --root",
|
|
5
|
+
};
|
|
6
|
+
export function buildStoreOpenRefusalTail(err, lane) {
|
|
7
|
+
if (!(err instanceof FileStoreLockError))
|
|
8
|
+
return undefined;
|
|
9
|
+
const code = err.code;
|
|
10
|
+
switch (code) {
|
|
11
|
+
case "store.dir_in_use":
|
|
12
|
+
return `(a LIVE process — possibly this one — owns this data dir; stop that process first, or ${FREE_ROOT_KNOB[lane]})`;
|
|
13
|
+
case "store.dir_claiming":
|
|
14
|
+
return "(transient: another process is taking over a crashed owner's lock — it either finishes or is itself reclaimed, so just retry in a moment)";
|
|
15
|
+
case "store.lock_unreadable":
|
|
16
|
+
return '(only a human can resolve this: do what the line above says, and nothing more — "unreadable" is NOT proof the owner is gone: a lock this process merely lacks permission to read looks identical, so confirm the holder is really stale (or fix the permission) before removing anything)';
|
|
17
|
+
default: {
|
|
18
|
+
const unhandled = code;
|
|
19
|
+
return `(unrecognized file-store lock code ${String(unhandled)}: this server build has no verified way out for it — treat the line above as the whole story)`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=store-open-refusal.js.map
|
package/dist/run-local.js
CHANGED
|
@@ -7,7 +7,7 @@ import { realpathSync } from "node:fs";
|
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { loadRemoteExec } from "@sema-agent/settings-schema/node";
|
|
10
|
-
import { Runner, uuidv7, parseModelMention,
|
|
10
|
+
import { Runner, uuidv7, parseModelMention, FileStorageBackend, NodeExecutionEnv, NodeLspManager, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
|
|
11
11
|
import { hasOperatorGateIntent } from "./approval.js";
|
|
12
12
|
import { createBrain } from "./brain.js";
|
|
13
13
|
import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
|
|
@@ -17,6 +17,7 @@ import { taskWallClockSec } from "./task-workflow.js";
|
|
|
17
17
|
import { applyCatalogToSource } from "./capabilities/center-prompts.js";
|
|
18
18
|
import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
|
|
19
19
|
import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
|
|
20
|
+
import { buildStoreOpenRefusalTail } from "./plugins/store-open-refusal.js";
|
|
20
21
|
import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
|
|
21
22
|
import { createConfigProvider } from "./config-provider.js";
|
|
22
23
|
import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
|
|
@@ -272,9 +273,9 @@ export async function runLocal(argv, deps = {}) {
|
|
|
272
273
|
}
|
|
273
274
|
catch (e) {
|
|
274
275
|
printErr(`cannot open local data dir ${root}: ${e instanceof Error ? e.message : String(e)}`);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
const tail = buildStoreOpenRefusalTail(e, "run-local");
|
|
277
|
+
if (tail !== undefined)
|
|
278
|
+
printErr(tail);
|
|
278
279
|
return 2;
|
|
279
280
|
}
|
|
280
281
|
const sessionStore = fileBackend.sessionStore;
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HostDecidedBy } from "./host-decision.js";
|
|
1
2
|
import type { IncomingMessage } from "node:http";
|
|
2
3
|
import { type AskRequest, type AskOutcome, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
3
4
|
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
@@ -53,6 +54,13 @@ export interface ParkedAskRedeemRequest {
|
|
|
53
54
|
decision: "approve" | "deny";
|
|
54
55
|
/** [ref] 同源的人写理由(deny 时 core 把它交给模型;allow 只进审计面)。 */
|
|
55
56
|
note?: string;
|
|
57
|
+
/** #1016:回决体自报的**结算来源**,原样过境到耐久腿的 `hostDecision.decidedBy`(缺席 ⇒ `"person"`)。
|
|
58
|
+
* live 命中臂与本 PARKED 臂读**同一格** —— 同一份体落在哪条臂上只取决于副本与时序。
|
|
59
|
+
* 型 = `string`(本层证得出的全部);判词的是耐久座的属主 core,cast 在 `hostDecisionFacts` 那一处。
|
|
60
|
+
* ⚠️ 两座的**词表并不相同**(live = `human`/`timeout`/`policy`,耐久 = `person`/`sla_timeout`/`policy`,
|
|
61
|
+
* 只在 `policy` 上相交)—— 这正是这条 wire 键只开 `"policy"` 一个词的原因,也是它不能声明成其中任何
|
|
62
|
+
* 一只闭集的原因:同一个串要落到两个不同词表的座上。 */
|
|
63
|
+
settledBy?: string;
|
|
56
64
|
/** [ref] ctrl+g 编辑放行的实参整体替换(approve 才有意义)。 */
|
|
57
65
|
updatedInput?: unknown;
|
|
58
66
|
/** 发起本次 respond 的**已验证**调用方(live 腿 `gatedPrincipal` 的那一把)。席在赎回之前拿它与
|
|
@@ -575,6 +583,23 @@ export declare const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOffe
|
|
|
575
583
|
* 指定 scope = 让一次「在项目 A 里点的同意」写成一条覆盖项目 B(或全局)的常驻放行 —— 那是这条轴上
|
|
576
584
|
* 最贵的一次扩权。静默忽略同样不行:客户端会以为自己设上了。响亮拒,一句话说清谁拥有这一格。
|
|
577
585
|
*/
|
|
586
|
+
/**
|
|
587
|
+
* #1016 —— `settledBy` 与 allow 族**互斥**的那一句(两条回决腿共用一份文本:同一个体在两条口上必须
|
|
588
|
+
* 拿到同一句话)。判的是**键在不在**,不是它是什么词。
|
|
589
|
+
*/
|
|
590
|
+
/**
|
|
591
|
+
* #1016 —— `settledBy` 的**形**拒句(两条回决腿共用一份文本,与 {@link SETTLED_BY_DENY_ONLY} 同姿势)。
|
|
592
|
+
*
|
|
593
|
+
* 🔴 **判形 ≠ 判词**:本层证得出的只有「它是个串」;「它是不是词表里的那个串」由**引擎**判(两条腿各有
|
|
594
|
+
* 一处响亮拒,读数在收车档)。加这道形门的理由是**类型必须说真话**(wire 边界强类型宪法):
|
|
595
|
+
* 一个非串值往下会落进 `AskOutcome.settledBy`(core 的串闭集)与本仓审计账本的 `settledBy?: string` 席 ——
|
|
596
|
+
* 两处都声明成串,而运行期塞进去一个数字。引擎对它照样响亮拒(实测三形一致:`"bogus"` / `1` / `null`
|
|
597
|
+
* 都是 `approver_contract` 的 fail-closed deny),所以这不是在补一个 fail-open,而是**不让本层的类型撒谎**;
|
|
598
|
+
* 顺带把「一个非串永远不可能是闭集成员」这件必然的事拒在最早的地方,与同一只 parser 里 `note` /
|
|
599
|
+
* `persistRule.rule` / `persistRule.edited` 三道既有形门**同族**(那三道在,只有本格没有 = 本格才是特例)。
|
|
600
|
+
*/
|
|
601
|
+
export declare const SETTLED_BY_MUST_BE_STRING = "settledBy must be a string \u2014 the settlement source is one word from the engine's closed set, and this layer judges only the SHAPE (which word it is, the engine decides)";
|
|
602
|
+
export declare const SETTLED_BY_DENY_ONLY = "settledBy is only meaningful with decision \"deny\" \u2014 only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door's own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision";
|
|
578
603
|
export declare const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope \u2014 the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
|
|
579
604
|
/** Validate the respond body — the closed three-choice enum (rationale in the module header). */
|
|
580
605
|
export declare function parseToolApprovalResponse(body: unknown): {
|
|
@@ -583,10 +608,42 @@ export declare function parseToolApprovalResponse(body: unknown): {
|
|
|
583
608
|
updatedInput?: unknown;
|
|
584
609
|
persistRule?: ParsedPersistRule;
|
|
585
610
|
note?: string;
|
|
611
|
+
settledBy?: string;
|
|
586
612
|
} | {
|
|
587
613
|
ok: false;
|
|
588
614
|
error: string;
|
|
589
615
|
};
|
|
616
|
+
/**
|
|
617
|
+
* core 5.23.0([ref]/[ref]②)—— **宿主自报**的结算来源,`AskOutcome` 对象臂新开的口。本仓在这一格上有
|
|
618
|
+
* **两种身份**,合成一只座是刻意的(两只座 = 同一个 wire 位的两个写者):
|
|
619
|
+
* · **铸**(本仓自己观察到的事实):只有 `"timeout"` —— 同步 `onAsk` 腿的等待窗归**宿主**所有(引擎不为
|
|
620
|
+
* 一个它没排程的回调起表),所以「本仓的 TTL 窗走完了、没人答」这件事引擎结构上观察不到,不自报就
|
|
621
|
+
* 永远被记成「一个人拒绝了」;
|
|
622
|
+
* · **转**(调用方在回决体里自报的事实,core 7.26.2 / #1016 起):`settledBy:"policy"` = 答这张卡的是
|
|
623
|
+
* **这台部署自己的策略**,不是人。本仓**零判据、原样过境**:词的拒绝逻辑归引擎(`resolveAsk` 对
|
|
624
|
+
* 自报词表外的值是响亮 fail-closed deny),本仓抄一份词表的那天 core 加词就会被静默 400 掉。
|
|
625
|
+
* 逐词(词表属主恒是 core):
|
|
626
|
+
* · `"human"` —— 与「不铸这个键」逐字同义(core 缺席即按人处理);
|
|
627
|
+
* · `"aborted"` —— core **明令拒收**宿主自报(那是引擎自家 fail-closed 端的词,宿主自报等于把自己的
|
|
628
|
+
* 拒绝改标成引擎故障),所以它**不在** `AskOutcome` 的这一格上,本型也就够不着它。
|
|
629
|
+
* 类型从 core 的**同步 outcome 面**推导(不手抄):`AskOutcome` 的对象臂上 `settledBy` 是**宿主报的事实**,
|
|
630
|
+
* core 7.6.0([ref] S6-A)删掉的是那张**结算词表**(`ApprovalSettledBy`),不是这一格 —— 词由 core 从
|
|
631
|
+
* 这个事实铸(`human_refused` / `approval_window_expired` / `policy_refused`)。core 若改这一格的词表,
|
|
632
|
+
* 这里编译期先红。
|
|
633
|
+
*/
|
|
634
|
+
type HostSettledBy = NonNullable<Extract<AskOutcome, {
|
|
635
|
+
allow: boolean;
|
|
636
|
+
}>["settledBy"]>;
|
|
637
|
+
/**
|
|
638
|
+
* #1016 —— wire 上**开给调用方**的那一个词,从 core 的两只闭集里取**交集**。
|
|
639
|
+
*
|
|
640
|
+
* 🔴 它是一条**文档钉**,不是过境值的型(过境值是 `string`,理由在 `parseToolApprovalResponse` 那处注):
|
|
641
|
+
* `"human"` 与缺席同义、`"timeout"` 是本仓自己观察到的窗(客户端说出来就是替服务端伪证一件只有服务端
|
|
642
|
+
* 看得见的事),而耐久座那边的 `"person"` / `"sla_timeout"` 同理。两只词表**只在 `"policy"` 上相交** ——
|
|
643
|
+
* 这条 wire 键因此恰好只开那一个词。core 哪天退役它,这里**编译期先红**,而不是在 wire 上静默留一个
|
|
644
|
+
* 两个座都已经不收的词;`test/tool-approval.test.ts` 的 #1016 那组按它写判据。
|
|
645
|
+
*/
|
|
646
|
+
export type RespondSettledBy = Extract<HostSettledBy, HostDecidedBy>;
|
|
590
647
|
/**
|
|
591
648
|
* [ref] R-13 C —— `UNATTENDED_APPROVAL_POLICY` 的**闭集**词表([ref]Q1「裁2:A+C」/[ref] 裁②)。
|
|
592
649
|
*
|
|
@@ -1436,4 +1493,5 @@ export declare class ToolApprovalCoordinator {
|
|
|
1436
1493
|
/** [ref]:当前在场的 park 墓碑数(有界性的可观测面 —— 判据据它证「帽是硬的、过期会被清」)。 */
|
|
1437
1494
|
parkTombstoneCount(): number;
|
|
1438
1495
|
}
|
|
1496
|
+
export {};
|
|
1439
1497
|
//# sourceMappingURL=tool-approval.d.ts.map
|
package/dist/tool-approval.js
CHANGED
|
@@ -83,6 +83,8 @@ export const PERSIST_RULE_TEXT_ERROR = `persistRule.rule must be a non-empty str
|
|
|
83
83
|
export const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present";
|
|
84
84
|
export const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
|
|
85
85
|
export const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited — send exactly one arm";
|
|
86
|
+
export const SETTLED_BY_MUST_BE_STRING = "settledBy must be a string — the settlement source is one word from the engine's closed set, and this layer judges only the SHAPE (which word it is, the engine decides)";
|
|
87
|
+
export const SETTLED_BY_DENY_ONLY = 'settledBy is only meaningful with decision "deny" — only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door\'s own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision';
|
|
86
88
|
export const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope — the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
|
|
87
89
|
export function parseToolApprovalResponse(body) {
|
|
88
90
|
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
@@ -127,12 +129,19 @@ export function parseToolApprovalResponse(body) {
|
|
|
127
129
|
}
|
|
128
130
|
note = rawNote;
|
|
129
131
|
}
|
|
132
|
+
const rawSettledBy = body.settledBy;
|
|
133
|
+
if (rawSettledBy !== undefined && d !== "deny")
|
|
134
|
+
return { ok: false, error: SETTLED_BY_DENY_ONLY };
|
|
135
|
+
if (rawSettledBy !== undefined && typeof rawSettledBy !== "string")
|
|
136
|
+
return { ok: false, error: SETTLED_BY_MUST_BE_STRING };
|
|
137
|
+
const settledBy = rawSettledBy;
|
|
130
138
|
return {
|
|
131
139
|
ok: true,
|
|
132
140
|
value: d,
|
|
133
141
|
...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}),
|
|
134
142
|
...(persistRule !== undefined ? { persistRule } : {}),
|
|
135
143
|
...(note !== undefined ? { note } : {}),
|
|
144
|
+
...(rawSettledBy !== undefined ? { settledBy } : {}),
|
|
136
145
|
};
|
|
137
146
|
}
|
|
138
147
|
return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
|
|
@@ -1656,6 +1665,7 @@ export class ToolApprovalCoordinator {
|
|
|
1656
1665
|
gateBoundInputHash: row.gateBoundInputHash,
|
|
1657
1666
|
decision,
|
|
1658
1667
|
...(parsed.note !== undefined ? { note: parsed.note } : {}),
|
|
1668
|
+
...(parsed.settledBy !== undefined ? { settledBy: parsed.settledBy } : {}),
|
|
1659
1669
|
...(parsed.updatedInput !== undefined && parsed.value !== "deny" ? { updatedInput: parsed.updatedInput } : {}),
|
|
1660
1670
|
...(principal !== undefined ? { principal } : {}),
|
|
1661
1671
|
...(httpReq !== undefined ? { httpReq } : {}),
|
|
@@ -1952,7 +1962,7 @@ export class ToolApprovalCoordinator {
|
|
|
1952
1962
|
this.allowAllSessions.add(sessionAllowKey(entry.owner, entry.sessionId, sessionAllowCategory(entry.toolName)));
|
|
1953
1963
|
}
|
|
1954
1964
|
const allowed = parsed.value !== "deny";
|
|
1955
|
-
entry.settle(allowed, allowed ? "allowed" : "denied", opts?.settlePayload !== undefined ? opts.settlePayload.updatedInput : parsed.updatedInput,
|
|
1965
|
+
entry.settle(allowed, allowed ? "allowed" : "denied", opts?.settlePayload !== undefined ? opts.settlePayload.updatedInput : parsed.updatedInput, parsed.settledBy, allowed ? undefined : parsed.note);
|
|
1956
1966
|
const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
|
|
1957
1967
|
const noteRecorded = opts?.noteRecorded ?? false;
|
|
1958
1968
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.93.
|
|
3
|
+
"version": "7.93.3",
|
|
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",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@sema-agent/core": "7.26.
|
|
62
|
+
"@sema-agent/core": "7.26.2",
|
|
63
63
|
"@sema-agent/settings-schema": "3.0.0",
|
|
64
64
|
"e2b": "^2.28.0",
|
|
65
65
|
"libsodium-wrappers": "^0.8.4",
|