@sema-agent/server 7.93.2 → 7.93.4
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 +57 -9
- 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/http/routes/runs.js +5 -5
- package/dist/plugins/approval-ask-store-memory.js +5 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +56 -0
- package/dist/plugins/approval-ask-store-sql.js +29 -5
- 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 +66 -2
- package/dist/tool-approval.js +67 -44
- package/package.json +3 -3
|
@@ -12,6 +12,18 @@ export function assertIdempotencyKeyShape(key) {
|
|
|
12
12
|
throw new Error(`invalid idempotency key: must be 1-255 chars with no leading/trailing whitespace (got ${JSON.stringify(key.slice(0, 32))}${key.length > 32 ? "…" : ""}, length ${key.length})`);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
export const MAX_SETTLED_BY_CHARS = 16;
|
|
16
|
+
export function assertDecideSettledBy(decision) {
|
|
17
|
+
const v = decision.settledBy;
|
|
18
|
+
if (v === undefined)
|
|
19
|
+
return;
|
|
20
|
+
if (v.length === 0 || v.length > MAX_SETTLED_BY_CHARS) {
|
|
21
|
+
throw new Error(`invalid settledBy: the settlement source is one word from the engine's closed set and lands in a VARCHAR(${MAX_SETTLED_BY_CHARS}) column — must be 1-${MAX_SETTLED_BY_CHARS} chars (got ${JSON.stringify(v.slice(0, 32))}${v.length > 32 ? "…" : ""}, length ${v.length})`);
|
|
22
|
+
}
|
|
23
|
+
if (decision.decision !== "deny") {
|
|
24
|
+
throw new Error(`invalid settledBy: a settlement source is only representable on a deny row (got decision="${decision.decision}") — only a person approves an action that EXECUTES, so an approve row carrying a non-human settlement source is a row no replay could honestly hand back to the engine`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
15
27
|
export class ApprovalAskReadAbortedError extends Error {
|
|
16
28
|
constructor(where) {
|
|
17
29
|
super(`approval-ask read ${where} aborted by caller`);
|
|
@@ -47,6 +59,7 @@ export function projectAskTerminalClaimTruth(row) {
|
|
|
47
59
|
rev: row.rev,
|
|
48
60
|
decision: row.decision,
|
|
49
61
|
...(row.updatedInput !== undefined ? { updatedInput: row.updatedInput } : {}),
|
|
62
|
+
settledBy: row.settledBy,
|
|
50
63
|
};
|
|
51
64
|
}
|
|
52
65
|
export const APPROVAL_ASK_TABLE = "approval_ask";
|
|
@@ -87,6 +100,12 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
|
87
100
|
decision VARCHAR(16) NULL,
|
|
88
101
|
decision_actor TEXT NULL,
|
|
89
102
|
decision_note TEXT NULL,
|
|
103
|
+
-- settled_by(S-558 / #1016 持久半场):这条决议的结算来源词(NULL = 人答的)。语义、两态读法与
|
|
104
|
+
-- 「为什么是自己一列而不是塞进 decision_actor」的全文见 AskRow.settledBy 顶注。
|
|
105
|
+
-- VARCHAR(16):词表属主是 core(AskOutcome.settledBy,今天最长 7 字符 "timeout"),宽度与同表
|
|
106
|
+
-- decision 同尺同理由 —— 闭集词列,不是自由文本。截断面由入店形门堵死(assertDecideSettledBy),
|
|
107
|
+
-- 不靠「列够宽就不会截」这种运气(MySQL 非严格模式静默截、PG 直接报错 = 两方言不同答)。
|
|
108
|
+
settled_by VARCHAR(16) NULL,
|
|
90
109
|
-- updated_input(S-26 / #304,A-054.15 收口):人批准时带的 ctrl+g 编辑实参,JSON 文本,与决议同一条
|
|
91
110
|
-- UPDATE 原子落列;NULL = 没带编辑(与 JSON "null" = 编辑成 null 分家)。LONGTEXT 照 card_json 先例:
|
|
92
111
|
-- 编辑实参是整只工具入参,VARCHAR/TEXT 会造静默截断面。语义三态与读法见 AskRow.updatedInput 顶注。
|
|
@@ -166,6 +185,8 @@ export async function ensurePgApprovalAskSchema(q) {
|
|
|
166
185
|
decision VARCHAR(16) COLLATE "C",
|
|
167
186
|
decision_actor TEXT COLLATE "C",
|
|
168
187
|
decision_note TEXT COLLATE "C",
|
|
188
|
+
-- settled_by(S-558)—— MySQL 孪生逐字同形(宽度/语义/形门的理由写在那侧的行内注)。
|
|
189
|
+
settled_by VARCHAR(16) COLLATE "C",
|
|
169
190
|
-- updated_input(S-26 / #304)—— MySQL 孪生逐字同形(语义/类型理由见那侧行内注;PG TEXT 无长度上限)。
|
|
170
191
|
updated_input TEXT COLLATE "C",
|
|
171
192
|
decided_at_ms BIGINT,
|
|
@@ -211,7 +232,7 @@ export async function assertApprovalAskRuleMaterialSchema(query, dialect) {
|
|
|
211
232
|
}
|
|
212
233
|
}
|
|
213
234
|
const ASK_COLS = "ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, " +
|
|
214
|
-
"provisional, rev, decision, decision_actor, decision_note, updated_input, decided_at_ms, denied_reason, gate_token, " +
|
|
235
|
+
"provisional, rev, decision, decision_actor, decision_note, settled_by, updated_input, decided_at_ms, denied_reason, gate_token, " +
|
|
215
236
|
"gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, " +
|
|
216
237
|
"created_at_ms, updated_at_ms";
|
|
217
238
|
const BATCH_COLS = "batch_id, task_id, state, bound_ask_id, rev, created_at_ms, updated_at_ms";
|
|
@@ -245,6 +266,7 @@ function mapAskRow(r) {
|
|
|
245
266
|
decision: strOrNull(r.decision) ?? null,
|
|
246
267
|
decisionActor: r.decision_actor == null ? null : parseJsonColumn(r.decision_actor),
|
|
247
268
|
decisionNote: strOrNull(r.decision_note),
|
|
269
|
+
settledBy: strOrNull(r.settled_by),
|
|
248
270
|
...(r.updated_input == null ? {} : { updatedInput: parseJsonColumn(r.updated_input) }),
|
|
249
271
|
decidedAtMs: numOrNull(r.decided_at_ms),
|
|
250
272
|
deniedReason: strOrNull(r.denied_reason),
|
|
@@ -331,9 +353,9 @@ export class SqlApprovalAskStore {
|
|
|
331
353
|
try {
|
|
332
354
|
await conn.begin();
|
|
333
355
|
await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
|
|
334
|
-
const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
335
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
336
|
-
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15, $16, $17) ON CONFLICT (ask_id) DO NOTHING"), [
|
|
356
|
+
const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
357
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
|
|
358
|
+
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15, $16, $17) ON CONFLICT (ask_id) DO NOTHING"), [
|
|
337
359
|
row.askId,
|
|
338
360
|
row.taskId,
|
|
339
361
|
row.sourceTaskId,
|
|
@@ -389,6 +411,7 @@ export class SqlApprovalAskStore {
|
|
|
389
411
|
async decideAsk(askId, batchId, decision) {
|
|
390
412
|
if (decision.idempotencyKey != null)
|
|
391
413
|
assertIdempotencyKeyShape(decision.idempotencyKey);
|
|
414
|
+
assertDecideSettledBy(decision);
|
|
392
415
|
const now = Date.now();
|
|
393
416
|
const dialect = this.db.dialect;
|
|
394
417
|
const conn = await this.db.connect();
|
|
@@ -401,10 +424,11 @@ export class SqlApprovalAskStore {
|
|
|
401
424
|
}
|
|
402
425
|
let askRes;
|
|
403
426
|
try {
|
|
404
|
-
askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)},
|
|
427
|
+
askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)}, settled_by=${ph(dialect, 4)}, updated_input=${ph(dialect, 5)}, decided_at_ms=${ph(dialect, 6)}, idempotency_key=${ph(dialect, 7)}, rev=rev+1, updated_at_ms=${ph(dialect, 8)} WHERE ${pendingClaimWhere(dialect, 9)}`, [
|
|
405
428
|
decision.decision,
|
|
406
429
|
decision.decisionActor == null ? null : this.json(decision.decisionActor),
|
|
407
430
|
decision.decisionNote ?? null,
|
|
431
|
+
decision.settledBy ?? null,
|
|
408
432
|
decision.updatedInput === undefined ? null : this.json(decision.updatedInput),
|
|
409
433
|
now,
|
|
410
434
|
decision.idempotencyKey ?? null,
|
|
@@ -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,8 +1,9 @@
|
|
|
1
|
+
import type { HostDecidedBy, HostSettledBy } 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";
|
|
4
5
|
import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
5
|
-
import { type ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
|
|
6
|
+
import { type ApprovalAskStore, type AskRow } from "./plugins/approval-ask-store-sql.js";
|
|
6
7
|
import type { ApprovalAskAuditSink } from "./approval-ask-audit-store.js";
|
|
7
8
|
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
8
9
|
/** [ref]:审批 gate kind 闭集(core gateMatch 的 `human`/`irreversible_ask` ↔ outcome.gate "policy_ask")。
|
|
@@ -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,21 @@ 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
|
+
* #1016 —— wire 上**开给调用方**的那一个词,从 core 的两只闭集里取**交集**。
|
|
618
|
+
*
|
|
619
|
+
* 🔴 它是一条**文档钉**,不是过境值的型(过境值是 `string`,理由在 `parseToolApprovalResponse` 那处注):
|
|
620
|
+
* `"human"` 与缺席同义、`"timeout"` 是本仓自己观察到的窗(客户端说出来就是替服务端伪证一件只有服务端
|
|
621
|
+
* 看得见的事),而耐久座那边的 `"person"` / `"sla_timeout"` 同理。两只词表**只在 `"policy"` 上相交** ——
|
|
622
|
+
* 这条 wire 键因此恰好只开那一个词。core 哪天退役它,这里**编译期先红**,而不是在 wire 上静默留一个
|
|
623
|
+
* 两个座都已经不收的词;`test/tool-approval.test.ts` 的 #1016 那组按它写判据。
|
|
624
|
+
*/
|
|
625
|
+
export type RespondSettledBy = Extract<HostSettledBy, HostDecidedBy>;
|
|
590
626
|
/**
|
|
591
627
|
* [ref] R-13 C —— `UNATTENDED_APPROVAL_POLICY` 的**闭集**词表([ref]Q1「裁2:A+C」/[ref] 裁②)。
|
|
592
628
|
*
|
|
@@ -745,6 +781,33 @@ export declare function resolveApprovalLeg(input: {
|
|
|
745
781
|
* 「窗偏短」(安全侧,§7.3 的误差方向论证)。 */
|
|
746
782
|
nowMonotonicMs: number;
|
|
747
783
|
}): ApprovalLegAssembly;
|
|
784
|
+
/** 一条 **DECIDED 行**能交还给引擎的全部形(`AskOutcome` 减掉 park 路由那一臂 —— 一条有决议的行
|
|
785
|
+
* 从定义上不是「投递面拿走了它」)。{@link ToolApprovalCoordinator.notifyExternalDecision} 收的就是它。 */
|
|
786
|
+
export type DecidedAskOutcome = Exclude<AskOutcome, "unavailable">;
|
|
787
|
+
/** 铸点的入参形 = 行上与**这条决议本身**有关的三格。整行(`AskRow`)与 claim 败方读数都满足。 */
|
|
788
|
+
type DecidedRowFacts = Pick<AskRow, "decision" | "settledBy"> & {
|
|
789
|
+
updatedInput?: unknown;
|
|
790
|
+
};
|
|
791
|
+
/**
|
|
792
|
+
* 🔴 **一条 DECIDED 行 → 交还给引擎的那个 outcome:全仓唯一铸点**(S-558)。
|
|
793
|
+
*
|
|
794
|
+
* 存在理由 = 这条投影此前被**手抄**在每个消费点上(`row.decision === "approve"` + `row.updatedInput`,
|
|
795
|
+
* 六处),于是给行加一格新事实就得记得改六处 —— `settledBy` 正是那个没人改到的格:一次**策略拒**在
|
|
796
|
+
* 重放 / 兄弟广播 / 歧义提交恢复三条路上全都退化成裸 `false`,而裸 `false` 在 core 那边逐字是
|
|
797
|
+
* 「**一个人**拒绝了」。手抄投影全面下线([ref]):消费点交行,形由这里铸。
|
|
798
|
+
*
|
|
799
|
+
* 三条读法,每条的属主注在别处,这里只落笔:
|
|
800
|
+
* · **方向** —— `decision !== "approve"` 即拒(列的二值由店的 CAS 保证);
|
|
801
|
+
* · **编辑载荷**([ref])—— `updatedInput` 键缺席 = 没带编辑 ⇒ 裸 `true`;在场(**含 `null`**)= 编辑的
|
|
802
|
+
* 字面值 ⇒ 对象臂(语义三态的唯一属主 = `AskRow.updatedInput` 顶注);
|
|
803
|
+
* · **结算来源**(S-558)—— 行上非 null 即宿主自报的那个词(`null` = 人决,与「不铸这个键」同义;
|
|
804
|
+
* 语义属主 = `AskRow.settledBy` 顶注)。
|
|
805
|
+
*
|
|
806
|
+
* 🔴 approve 臂**不读**来源位,而且那不是「静默丢」:store 的入店形门(`assertDecideSettledBy`)让
|
|
807
|
+
* 「approve 行带来源位」在**写面不可表示** —— core 对 `{allow:true, settledBy}` 是 fail-closed 拒,
|
|
808
|
+
* 一条那样的行没有任何读回路径能诚实地交还给引擎,所以它在落库那一刻就被拒掉了,而不是在这里被吞掉。
|
|
809
|
+
*/
|
|
810
|
+
export declare function outcomeOfDecidedRow(row: DecidedRowFacts): DecidedAskOutcome;
|
|
748
811
|
/**
|
|
749
812
|
* Coordinates the live tool-approval HITL for the singleton runner. Process-local + same-replica (the pending map is
|
|
750
813
|
* in memory, like QuestionCoordinator): a respond that lands on another replica finds nothing → 404. Present (passed
|
|
@@ -1137,7 +1200,7 @@ export declare class ToolApprovalCoordinator {
|
|
|
1137
1200
|
* 真决议」,重复注册必须拿到**同一个**决议(approve 就是 approve)——合并会把外部批准的重复条目错
|
|
1138
1201
|
* 结算成拒绝。已被结算过的条目再调 settle 天然无操作(幂等),所以本口对「赢家自己已 settle」安全。
|
|
1139
1202
|
*/
|
|
1140
|
-
notifyExternalDecision(askId: string,
|
|
1203
|
+
notifyExternalDecision(askId: string, outcome: DecidedAskOutcome): {
|
|
1141
1204
|
settled: number;
|
|
1142
1205
|
};
|
|
1143
1206
|
/**
|
|
@@ -1436,4 +1499,5 @@ export declare class ToolApprovalCoordinator {
|
|
|
1436
1499
|
/** [ref]:当前在场的 park 墓碑数(有界性的可观测面 —— 判据据它证「帽是硬的、过期会被清」)。 */
|
|
1437
1500
|
parkTombstoneCount(): number;
|
|
1438
1501
|
}
|
|
1502
|
+
export {};
|
|
1439
1503
|
//# sourceMappingURL=tool-approval.d.ts.map
|
package/dist/tool-approval.js
CHANGED
|
@@ -83,6 +83,11 @@ 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
|
+
function respondSettledBy(parsed) {
|
|
88
|
+
return parsed.settledBy;
|
|
89
|
+
}
|
|
90
|
+
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
91
|
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
92
|
export function parseToolApprovalResponse(body) {
|
|
88
93
|
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
@@ -127,12 +132,19 @@ export function parseToolApprovalResponse(body) {
|
|
|
127
132
|
}
|
|
128
133
|
note = rawNote;
|
|
129
134
|
}
|
|
135
|
+
const rawSettledBy = body.settledBy;
|
|
136
|
+
if (rawSettledBy !== undefined && d !== "deny")
|
|
137
|
+
return { ok: false, error: SETTLED_BY_DENY_ONLY };
|
|
138
|
+
if (rawSettledBy !== undefined && typeof rawSettledBy !== "string")
|
|
139
|
+
return { ok: false, error: SETTLED_BY_MUST_BE_STRING };
|
|
140
|
+
const settledBy = rawSettledBy;
|
|
130
141
|
return {
|
|
131
142
|
ok: true,
|
|
132
143
|
value: d,
|
|
133
144
|
...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}),
|
|
134
145
|
...(persistRule !== undefined ? { persistRule } : {}),
|
|
135
146
|
...(note !== undefined ? { note } : {}),
|
|
147
|
+
...(rawSettledBy !== undefined ? { settledBy } : {}),
|
|
136
148
|
};
|
|
137
149
|
}
|
|
138
150
|
return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
|
|
@@ -213,21 +225,28 @@ function effectiveAskWindowMs(ttlMs, windowMarginMs, legDeadlineMonotonic, nowMo
|
|
|
213
225
|
return Math.min(ttlMs, Math.max(0, legRemainingMs - windowMarginMs));
|
|
214
226
|
}
|
|
215
227
|
function replayTerminalAskRow(row) {
|
|
216
|
-
if (row.state === "DECIDED")
|
|
217
|
-
|
|
218
|
-
return false;
|
|
219
|
-
return row.updatedInput !== undefined ? { allow: true, updatedInput: row.updatedInput } : true;
|
|
220
|
-
}
|
|
228
|
+
if (row.state === "DECIDED")
|
|
229
|
+
return outcomeOfDecidedRow(row);
|
|
221
230
|
if (row.state === "PARKING" || row.state === "PARKED")
|
|
222
231
|
return "unavailable";
|
|
223
232
|
return false;
|
|
224
233
|
}
|
|
225
|
-
function
|
|
226
|
-
if (
|
|
227
|
-
return
|
|
234
|
+
export function outcomeOfDecidedRow(row) {
|
|
235
|
+
if (row.decision !== "approve")
|
|
236
|
+
return row.settledBy !== null ? { allow: false, settledBy: row.settledBy } : false;
|
|
237
|
+
return row.updatedInput !== undefined ? { allow: true, updatedInput: row.updatedInput } : true;
|
|
238
|
+
}
|
|
239
|
+
function settleArgsOfDecided(outcome) {
|
|
228
240
|
if (typeof outcome === "boolean")
|
|
229
241
|
return { allowed: outcome };
|
|
230
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
allowed: outcome.allow,
|
|
244
|
+
...(outcome.updatedInput !== undefined ? { updatedInput: outcome.updatedInput } : {}),
|
|
245
|
+
...(outcome.settledBy !== undefined ? { settledBy: outcome.settledBy } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function settleArgsOf(outcome) {
|
|
249
|
+
return outcome === "unavailable" ? "unavailable" : settleArgsOfDecided(outcome);
|
|
231
250
|
}
|
|
232
251
|
export class ToolApprovalCoordinator {
|
|
233
252
|
als = new AsyncLocalStorage();
|
|
@@ -607,16 +626,17 @@ export class ToolApprovalCoordinator {
|
|
|
607
626
|
classifyClaimLoss(askId, current, _where) {
|
|
608
627
|
if (current.state === "absent")
|
|
609
628
|
return { kind: "absent" };
|
|
610
|
-
const
|
|
611
|
-
if (
|
|
629
|
+
const replayedOutcome = replayTerminalAskRow(current);
|
|
630
|
+
if (replayedOutcome === "unavailable")
|
|
612
631
|
return { kind: "parked" };
|
|
632
|
+
const replayed = settleArgsOfDecided(replayedOutcome);
|
|
613
633
|
if (!replayed.allowed)
|
|
614
|
-
return { kind: "
|
|
634
|
+
return { kind: "decided", outcome: replayedOutcome };
|
|
615
635
|
if (replayed.updatedInput !== undefined)
|
|
616
|
-
return { kind: "
|
|
636
|
+
return { kind: "decided", outcome: replayedOutcome };
|
|
617
637
|
if (this.editedDecisionInFlight(askId))
|
|
618
638
|
return { kind: "edit-in-flight" };
|
|
619
|
-
return { kind: "
|
|
639
|
+
return { kind: "decided", outcome: replayedOutcome };
|
|
620
640
|
}
|
|
621
641
|
hasOtherSuspendedOwner(askId, self) {
|
|
622
642
|
if (askId === undefined)
|
|
@@ -704,13 +724,14 @@ export class ToolApprovalCoordinator {
|
|
|
704
724
|
}
|
|
705
725
|
return tomb;
|
|
706
726
|
}
|
|
707
|
-
notifyExternalDecision(askId,
|
|
727
|
+
notifyExternalDecision(askId, outcome) {
|
|
708
728
|
const entries = this.pendingByAskId.get(askId);
|
|
709
729
|
if (!entries)
|
|
710
730
|
return { settled: 0 };
|
|
731
|
+
const args = settleArgsOfDecided(outcome);
|
|
711
732
|
let settled = 0;
|
|
712
733
|
for (const entry of [...entries]) {
|
|
713
|
-
entry.settle(allowed, allowed ? "allowed" : "denied", updatedInput);
|
|
734
|
+
entry.settle(args.allowed, args.allowed ? "allowed" : "denied", args.updatedInput, args.settledBy);
|
|
714
735
|
settled += 1;
|
|
715
736
|
}
|
|
716
737
|
return { settled };
|
|
@@ -1082,11 +1103,8 @@ export class ToolApprovalCoordinator {
|
|
|
1082
1103
|
if (askId === undefined)
|
|
1083
1104
|
return;
|
|
1084
1105
|
switch (read.kind) {
|
|
1085
|
-
case "
|
|
1086
|
-
this.notifyExternalDecision(askId,
|
|
1087
|
-
return;
|
|
1088
|
-
case "denied":
|
|
1089
|
-
this.notifyExternalDecision(askId, false);
|
|
1106
|
+
case "decided":
|
|
1107
|
+
this.notifyExternalDecision(askId, read.outcome);
|
|
1090
1108
|
return;
|
|
1091
1109
|
case "parked":
|
|
1092
1110
|
this.settleSameAskIdParkRoute(askId);
|
|
@@ -1102,14 +1120,12 @@ export class ToolApprovalCoordinator {
|
|
|
1102
1120
|
};
|
|
1103
1121
|
const settleFromClaimLoss = (read) => {
|
|
1104
1122
|
switch (read.kind) {
|
|
1105
|
-
case "
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
return true;
|
|
1109
|
-
case "denied":
|
|
1110
|
-
settle(false, "denied");
|
|
1123
|
+
case "decided": {
|
|
1124
|
+
const args = settleArgsOfDecided(read.outcome);
|
|
1125
|
+
settle(args.allowed, args.allowed ? "allowed" : "denied", args.updatedInput, args.settledBy);
|
|
1111
1126
|
broadcastClaimTruth(read);
|
|
1112
1127
|
return true;
|
|
1128
|
+
}
|
|
1113
1129
|
case "parked":
|
|
1114
1130
|
windowRouteUnavailable = true;
|
|
1115
1131
|
settle(false, "expired");
|
|
@@ -1613,7 +1629,7 @@ export class ToolApprovalCoordinator {
|
|
|
1613
1629
|
});
|
|
1614
1630
|
return this.notPending();
|
|
1615
1631
|
}
|
|
1616
|
-
this.notifyExternalDecision(row.askId, row
|
|
1632
|
+
this.notifyExternalDecision(row.askId, outcomeOfDecidedRow(row));
|
|
1617
1633
|
return {
|
|
1618
1634
|
status: 200,
|
|
1619
1635
|
body: {
|
|
@@ -1656,6 +1672,7 @@ export class ToolApprovalCoordinator {
|
|
|
1656
1672
|
gateBoundInputHash: row.gateBoundInputHash,
|
|
1657
1673
|
decision,
|
|
1658
1674
|
...(parsed.note !== undefined ? { note: parsed.note } : {}),
|
|
1675
|
+
...(parsed.settledBy !== undefined ? { settledBy: parsed.settledBy } : {}),
|
|
1659
1676
|
...(parsed.updatedInput !== undefined && parsed.value !== "deny" ? { updatedInput: parsed.updatedInput } : {}),
|
|
1660
1677
|
...(principal !== undefined ? { principal } : {}),
|
|
1661
1678
|
...(httpReq !== undefined ? { httpReq } : {}),
|
|
@@ -1885,21 +1902,23 @@ export class ToolApprovalCoordinator {
|
|
|
1885
1902
|
}
|
|
1886
1903
|
}
|
|
1887
1904
|
async respondWithCas(askStore, askId, batchId, id, entry, parsed, ruleEligibility) {
|
|
1888
|
-
let
|
|
1889
|
-
let
|
|
1905
|
+
let winnerRow;
|
|
1906
|
+
let ambiguousConfirmedRow;
|
|
1890
1907
|
let noteLanded = false;
|
|
1891
1908
|
try {
|
|
1892
1909
|
const decided = await this.withStoreDeadline(askStore.decideAsk(askId, batchId, {
|
|
1893
1910
|
decision: parsed.value === "deny" ? "deny" : "approve",
|
|
1894
1911
|
...(parsed.note !== undefined ? { decisionNote: parsed.note } : {}),
|
|
1895
1912
|
...(parsed.value !== "deny" && parsed.updatedInput !== undefined ? { updatedInput: parsed.updatedInput } : {}),
|
|
1913
|
+
...(parsed.settledBy !== undefined ? { settledBy: respondSettledBy(parsed) } : {}),
|
|
1896
1914
|
}), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
1897
1915
|
if (late.ok)
|
|
1898
|
-
this.notifyExternalDecision(askId,
|
|
1916
|
+
this.notifyExternalDecision(askId, outcomeOfDecidedRow(late.row));
|
|
1899
1917
|
});
|
|
1900
|
-
|
|
1901
|
-
|
|
1918
|
+
if (decided.ok) {
|
|
1919
|
+
winnerRow = decided.row;
|
|
1902
1920
|
noteLanded = true;
|
|
1921
|
+
}
|
|
1903
1922
|
if (!decided.ok) {
|
|
1904
1923
|
return this.notPending();
|
|
1905
1924
|
}
|
|
@@ -1910,8 +1929,8 @@ export class ToolApprovalCoordinator {
|
|
|
1910
1929
|
try {
|
|
1911
1930
|
const row = await this.withStoreDeadline(askStore.getAsk(askId), "getAsk(decideAsk-confirm)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
1912
1931
|
if (row?.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve")) {
|
|
1913
|
-
|
|
1914
|
-
|
|
1932
|
+
winnerRow = row;
|
|
1933
|
+
ambiguousConfirmedRow = row;
|
|
1915
1934
|
noteLanded = parsed.note === undefined || row.decisionNote === parsed.note;
|
|
1916
1935
|
}
|
|
1917
1936
|
}
|
|
@@ -1920,22 +1939,25 @@ export class ToolApprovalCoordinator {
|
|
|
1920
1939
|
}
|
|
1921
1940
|
}
|
|
1922
1941
|
const wantAllowed = parsed.value !== "deny";
|
|
1942
|
+
const decidedWon = winnerRow !== undefined;
|
|
1923
1943
|
if (this.pending.get(id) !== entry) {
|
|
1924
|
-
if (
|
|
1925
|
-
this.notifyExternalDecision(askId,
|
|
1944
|
+
if (winnerRow !== undefined)
|
|
1945
|
+
this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow));
|
|
1926
1946
|
const converged = decidedWon && entry.settledOutcome === (wantAllowed ? "allowed" : "denied");
|
|
1927
1947
|
if (!converged) {
|
|
1928
1948
|
return this.notPending();
|
|
1929
1949
|
}
|
|
1930
1950
|
return this.finishRespond(id, entry, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded });
|
|
1931
1951
|
}
|
|
1932
|
-
const payloadIsMine =
|
|
1933
|
-
approvalPayloadFingerprint({ updatedInput:
|
|
1952
|
+
const payloadIsMine = ambiguousConfirmedRow === undefined ||
|
|
1953
|
+
approvalPayloadFingerprint({ updatedInput: ambiguousConfirmedRow.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.updatedInput });
|
|
1934
1954
|
const result = this.finishRespond(id, entry, parsed, {
|
|
1935
1955
|
noteRecorded: noteLanded,
|
|
1936
|
-
...(
|
|
1956
|
+
...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow) } : {}),
|
|
1957
|
+
...(ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
|
|
1937
1958
|
});
|
|
1938
|
-
|
|
1959
|
+
if (winnerRow !== undefined)
|
|
1960
|
+
this.notifyExternalDecision(askId, outcomeOfDecidedRow(winnerRow));
|
|
1939
1961
|
return result;
|
|
1940
1962
|
}
|
|
1941
1963
|
finishRespond(id, entry, parsed, opts) {
|
|
@@ -1952,7 +1974,8 @@ export class ToolApprovalCoordinator {
|
|
|
1952
1974
|
this.allowAllSessions.add(sessionAllowKey(entry.owner, entry.sessionId, sessionAllowCategory(entry.toolName)));
|
|
1953
1975
|
}
|
|
1954
1976
|
const allowed = parsed.value !== "deny";
|
|
1955
|
-
|
|
1977
|
+
const settleArgs = opts?.settleOutcome !== undefined ? settleArgsOfDecided(opts.settleOutcome) : { allowed, updatedInput: parsed.updatedInput, settledBy: respondSettledBy(parsed) };
|
|
1978
|
+
entry.settle(settleArgs.allowed, settleArgs.allowed ? "allowed" : "denied", settleArgs.updatedInput, settleArgs.settledBy, settleArgs.allowed ? undefined : parsed.note);
|
|
1956
1979
|
const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
|
|
1957
1980
|
const noteRecorded = opts?.noteRecorded ?? false;
|
|
1958
1981
|
return {
|
|
@@ -2025,14 +2048,14 @@ export class ToolApprovalCoordinator {
|
|
|
2025
2048
|
continue;
|
|
2026
2049
|
if (this.editedDecisionInFlight(askId))
|
|
2027
2050
|
continue;
|
|
2028
|
-
const outcome =
|
|
2051
|
+
const outcome = replayTerminalAskRow(row);
|
|
2029
2052
|
if (outcome === "unavailable") {
|
|
2030
2053
|
const before = this.pendingByAskId.get(askId)?.size ?? 0;
|
|
2031
2054
|
this.settleSameAskIdParkRoute(askId);
|
|
2032
2055
|
settled += before;
|
|
2033
2056
|
continue;
|
|
2034
2057
|
}
|
|
2035
|
-
const delivered = this.notifyExternalDecision(askId, outcome
|
|
2058
|
+
const delivered = this.notifyExternalDecision(askId, outcome).settled;
|
|
2036
2059
|
settled += delivered;
|
|
2037
2060
|
}
|
|
2038
2061
|
const backlog = Math.max(0, candidates.length - polled);
|