@sema-agent/server 7.44.0 → 7.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/USAGE.md +10 -1
- package/dist/approval-card.d.ts +5 -0
- package/dist/approval-card.js +2 -0
- package/dist/boot/config-center.js +22 -11
- package/dist/boot/execution-env.js +1 -0
- package/dist/boot/shutdown.d.ts +18 -2
- package/dist/boot/shutdown.js +54 -12
- package/dist/boot/side-query-lane.d.ts +31 -46
- package/dist/boot/side-query-lane.js +3 -15
- package/dist/boot/webfetch-summarize-lane.d.ts +3 -2
- package/dist/boot/webfetch-summarize-lane.js +1 -1
- package/dist/brain.js +32 -1
- package/dist/config-center/apply-effective.js +3 -2
- package/dist/config-types.d.ts +16 -1
- package/dist/config.js +68 -3
- package/dist/hooks/hook-llm.js +29 -20
- package/dist/hooks/hook-runner.js +3 -0
- package/dist/http/routes/capabilities.js +10 -1
- package/dist/http/routes/runs.js +4 -0
- package/dist/http/server.d.ts +12 -1
- package/dist/http/server.js +6 -3
- package/dist/leader/merge.js +4 -2
- package/dist/main.js +6 -2
- package/dist/memory-sync.js +15 -2
- package/dist/observability/fail-open.d.ts +10 -2
- package/dist/observability/fail-open.js +10 -2
- package/dist/orchestration/hardened-vm-runner.d.ts +29 -0
- package/dist/orchestration/hardened-vm-runner.js +52 -4
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +20 -1
- package/dist/plugins/send-user-file.js +2 -1
- package/dist/plugins/tidb-session-storage.d.ts +8 -6
- package/dist/plugins/tidb-session-storage.js +2 -5
- package/dist/rules-consent.d.ts +19 -16
- package/dist/rules-consent.js +18 -1
- package/dist/run-local.js +6 -1
- package/dist/session-titler.js +2 -1
- package/dist/sighup-idle.d.ts +16 -2
- package/dist/sighup-idle.js +10 -1
- package/dist/ssh-host-key.d.ts +65 -0
- package/dist/ssh-host-key.js +117 -0
- package/dist/text-bidi.d.ts +50 -0
- package/dist/text-bidi.js +8 -0
- package/dist/tool-approval.d.ts +56 -2
- package/dist/tool-approval.js +95 -14
- package/package.json +2 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text-bidi.ts` —— 双向控制符(bidi)与 C0 控制符字符类的**单一属主**。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么有这个文件
|
|
5
|
+
* 同一条字符类此前在本仓有两份逐字手抄(`session-titler.ts` 的 `sanitizeTitle`、
|
|
6
|
+
* `plugins/send-user-file.ts` 的 `sanitizeSendFileName`),第三份还要给审批卡的 bidi **在场检测**用。
|
|
7
|
+
* 这是一条安全语义上的词表(它划定「人眼看到的顺序可能不是字节顺序」那一族码点),三份手抄必然漂移 ——
|
|
8
|
+
* 单一属主的纪律与 `APPROVAL_GATE_KINDS` 同源(CLAUDE.md 硬条款:词表是闭集,不许散落副本)。
|
|
9
|
+
*
|
|
10
|
+
* ## 两条类刻意分开(**别合并**)
|
|
11
|
+
* · {@link BIDI_CONTROL_CHARS} —— **只有** bidi 一族(LRM/RLM、嵌入与覆写、隔离符)。它是**检测**用的:
|
|
12
|
+
* 问的是「这串文本里有没有能让人眼读到与执行顺序不同的东西」。C0 控制符不属于这一问 —— 一条 Bash
|
|
13
|
+
* 命令里出现换行、一段 diff 里出现制表符都是**正常**的,把它们算进来会让检测位恒为真、从而毫无判别力。
|
|
14
|
+
* · {@link CONTROL_AND_BIDI_CHARS} —— bidi ∪ C0/DEL,**清洗**用的(标题、文件名这类要落一行的短串)。
|
|
15
|
+
* 清洗面比检测面宽是对的:那两处要的是「只留可打印的一行」。
|
|
16
|
+
*
|
|
17
|
+
* ## 案锚
|
|
18
|
+
* Trojan Source([4537]②;`docs/DISEASE-SHAPES.md` 的不可见字符条):字面控制符让**人审与机器门看到
|
|
19
|
+
* 两份不同的文本**。审批卡是本仓唯一一处「把模型写的命令交给人眼判断」的面,所以它需要的是**披露**
|
|
20
|
+
* (告诉壳「这串里有 bidi」),不是清洗 —— 清洗会改掉即将被执行的那串字节,让卡上显示的与真正跑的不是
|
|
21
|
+
* 同一个东西,那比不披露更坏。
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* bidi 一族的字符类**内容**(不含方括号,便于拼进更大的类)。
|
|
25
|
+
*
|
|
26
|
+
* 逐字 = Unicode 的 `Bidi_Control=Yes` **全集**(12 个码点),不是「常见的那几个」:
|
|
27
|
+
* · U+061C = ALM(Arabic Letter Mark);U+200E/200F = LRM/RLM;
|
|
28
|
+
* · U+202A-202E = 嵌入/覆写族(含 RLO);U+2066-2069 = 隔离符族。
|
|
29
|
+
*
|
|
30
|
+
* 🔴 **U+061C 是 codex 对抗复审 [medium] 验真后补的**:首版漏了它 —— 而漏一个码点在**检测**面上不是
|
|
31
|
+
* 「覆盖率差一点」,是一条完整的绕过路(只用 ALM 构造的输入让在场位缺席,壳于是照常按干净渲染)。
|
|
32
|
+
* 交叉锚:core 自己的 `APPROVER_REFUSED_CHARS_RE`(`dist/core/tool-policy.js`)同样含 `\\u061c`。
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ 与 core 那只**更宽**的集合的差额如实登记(刻意,不是漏):core 还含 C1(`\\u0080-\\u009f`)与
|
|
35
|
+
* U+2028/U+2029(行/段分隔符)。那两族**不是 bidi**(不影响阅读方向),属于「不可打印 / 换行伪装」的
|
|
36
|
+
* 另一问;本类是 bidi 专用,混进来会让 {@link hasBidiControls} 的语义从「方向可能骗你」滑成「有奇怪
|
|
37
|
+
* 字符」。清洗面要不要跟着加宽是另一件(见 {@link CONTROL_AND_BIDI_CHARS} 的注)。
|
|
38
|
+
*/
|
|
39
|
+
export declare const BIDI_CONTROL_CHARS = "\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
|
|
40
|
+
/** bidi ∪ C0/DEL —— **清洗**面用(见文件头注:清洗面比检测面宽是刻意的)。
|
|
41
|
+
* ⚠️ 仍**不含** C1(`-`)与 U+2028/U+2029:加宽是行为变更、要各自的判据支撑,不搭本批的车
|
|
42
|
+
* (登记在此,别当成漏)。两处存量消费方(标题、文件名)较其手抄前身**多剥一枚 U+061C**
|
|
43
|
+
* (经 {@link BIDI_CONTROL_CHARS} 内插带入)—— 对本清洗类是有意收紧,merge-rescan 745 验真后
|
|
44
|
+
* 钉在两消费方各自的测试里;除此之外与手抄前身逐字同集。 */
|
|
45
|
+
export declare const CONTROL_AND_BIDI_CHARS = "\\u0000-\\u001f\\u007f\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
|
|
46
|
+
/** 清洗用的全局正则(`String.replace` 会在返回前把 `lastIndex` 归零,故模块级常量可安全复用)。 */
|
|
47
|
+
export declare const CONTROL_AND_BIDI_STRIP_RE: RegExp;
|
|
48
|
+
/** 这串文本里**有没有** bidi 控制符。纯谓词:不改一个字节、不做任何清洗(理由见文件头注)。 */
|
|
49
|
+
export declare function hasBidiControls(text: string): boolean;
|
|
50
|
+
//# sourceMappingURL=text-bidi.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const BIDI_CONTROL_CHARS = "\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
|
|
2
|
+
export const CONTROL_AND_BIDI_CHARS = `\\u0000-\\u001f\\u007f${BIDI_CONTROL_CHARS}`;
|
|
3
|
+
export const CONTROL_AND_BIDI_STRIP_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`, "g");
|
|
4
|
+
const BIDI_DETECT_RE = new RegExp(`[${BIDI_CONTROL_CHARS}]`);
|
|
5
|
+
export function hasBidiControls(text) {
|
|
6
|
+
return BIDI_DETECT_RE.test(text);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=text-bidi.js.map
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage } from "node:http";
|
|
2
2
|
import { type AskRequest, type AskOutcome } from "@sema-agent/core";
|
|
3
|
-
import type
|
|
3
|
+
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
4
4
|
import { type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
5
5
|
import type { ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
|
|
6
6
|
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
@@ -102,6 +102,31 @@ export interface ToolApprovalFrame {
|
|
|
102
102
|
* 本键让 live-only 部署(店缺席、帧无卡)也能读到出身位;安全类标记不得依赖店在场([4392] 结构性
|
|
103
103
|
* 缺口成文)。消费语义(cli 挂点已预埋):在场 ⇒ 一切自动放行让位(含记住的规则与 bypass 姿态)。 */
|
|
104
104
|
requiresRealApproval?: true;
|
|
105
|
+
/**
|
|
106
|
+
* **E-14**([4537]② Trojan Source 族;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**工具输入**
|
|
107
|
+
* (`AskRequest.args`)里含 bidi 控制符(LRM/RLM、嵌入/覆写、隔离符;字符类属主 `text-bidi.ts`)。
|
|
108
|
+
*
|
|
109
|
+
* 🔴 **真才带,缺席绝不编 `false`**(与 `governanceForced`/`requiresRealApproval` 同形)。缺席的含义是
|
|
110
|
+
* 「没扫到」——它同时覆盖「真的没有」与「args 序列化不了(循环引用)所以扫不了」两形,消费端**禁**把
|
|
111
|
+
* 缺席读成「已确认干净」。
|
|
112
|
+
*
|
|
113
|
+
* 🔴 **披露位,不是清洗位;字节零改**。审批卡是本仓唯一一处把模型写的命令交给**人眼**判断的面,而
|
|
114
|
+
* bidi 覆写让人眼读到的顺序与真正执行的字节顺序不同 —— 人批准的是 A、跑起来的是 B。清洗会改掉即将被
|
|
115
|
+
* 执行的那串字节(卡上显示的与真跑的不是同一个东西),**比不披露更坏**;所以 server 只报「有」,显形
|
|
116
|
+
* 归壳的渲染面(转义/高亮/加标记,壳自己定)。
|
|
117
|
+
*
|
|
118
|
+
* **判据素材** = `boundArgs` 那**一次**序列化的结果(脱敏后的 args),且在**字节帽判定之前**算:
|
|
119
|
+
* · 超帽时 args 以 `argsOmitted:true` 缺席上帧(卡上根本没有 args)—— 那正是人最看不见输入的一格,
|
|
120
|
+
* 更需要知道「你看不到的那份里有隐形字符」,所以帽不影响本键;
|
|
121
|
+
* · 序列化失败(循环引用 / 抛错的 `toJSON` / BigInt)⇒ 两件事一起交白卷:args 不上帧,本键**不铸**;
|
|
122
|
+
* · 用脱敏后那份而不是原文,是因为它与卡面/`card_json` 是**同一份**素材(一次算定,三面同源),
|
|
123
|
+
* 且人眼看到的正是它。理论残余如实记:一段**被遮蔽掉的密钥**里的 bidi 会随遮蔽一起消失 —— 已知、
|
|
124
|
+
* 可忽略(密钥形不含 bidi),且方向安全(少报一次,不会误报干净为脏)。
|
|
125
|
+
*
|
|
126
|
+
* 判据**不看 `message`**(引擎/策略写的说明文本,不是待执行输入):本键被壳读作「**待执行的输入**可能
|
|
127
|
+
* 在骗你的眼睛」,把两个来源折进一个布尔会让壳无法判断该给哪一段加显形标记。
|
|
128
|
+
*/
|
|
129
|
+
inputHasBidi?: true;
|
|
105
130
|
/** #288([4429]② cli 请托;**ADDITIVE**,`"tool_approval"` only,7.34.0+)——窗三键,与
|
|
106
131
|
* `approval_request` 帧(design/172 呈卡帧)同名同义:`expiresAtMs` = 本 ask 的绝对到期墙钟
|
|
107
132
|
* (有店=行上一次铸定的 `expiresAtMs`,与卡帧/落库同值;无店 D1=注册时刻算定的同名初值),
|
|
@@ -579,6 +604,8 @@ export declare class ToolApprovalCoordinator {
|
|
|
579
604
|
private readonly parkTombstones;
|
|
580
605
|
private readonly parkTombstoneMax;
|
|
581
606
|
private readonly parkTombstoneTtlMs;
|
|
607
|
+
/** #343:per-principal 子帽(语义与取值理由见 {@link DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX})。 */
|
|
608
|
+
private readonly parkTombstonePerPrincipalMax;
|
|
582
609
|
/** #329:PARKED 臂的赎回席**取值口**(晚绑 —— 赎回腿要 checkpoint/bg 店与裸 Agent 工具,它们在协调器
|
|
583
610
|
* 构造之后才装配;同款 holder 先例 = main.ts 的 `getRunDenySweep`)。取到 undefined ⇒ 该臂逐字回落
|
|
584
611
|
* 修前 404,partial 部署安全。 */
|
|
@@ -603,6 +630,8 @@ export declare class ToolApprovalCoordinator {
|
|
|
603
630
|
* 不是部署旋钮(不接 env),可注入只为让「有界」这件事被判据观测得到。 */
|
|
604
631
|
parkTombstoneMax?: number;
|
|
605
632
|
parkTombstoneTtlMs?: number;
|
|
633
|
+
/** #343:per-principal 子帽(同上不接 env;生效值恒 `min(本值, parkTombstoneMax)`)。 */
|
|
634
|
+
parkTombstonePerPrincipalMax?: number;
|
|
606
635
|
});
|
|
607
636
|
/**
|
|
608
637
|
* #280 R-13 C —— 「**无人可答**」这一类终局的**唯一**成形口(park 路由 vs deny 政策)。
|
|
@@ -772,7 +801,19 @@ export declare class ToolApprovalCoordinator {
|
|
|
772
801
|
* 故对它重复调用天然无操作。 */
|
|
773
802
|
private settleSameAskIdParkRoute;
|
|
774
803
|
/** #329:落一条 park 墓碑(容量帽按**插入序**逐出最旧,与 `allowAllSessions` 同款有界纪律)。
|
|
775
|
-
* 调用点唯一 = `settle` 里的 park 路由支(见那处注);真终局绝不调它。
|
|
804
|
+
* 调用点唯一 = `settle` 里的 park 路由支(见那处注);真终局绝不调它。
|
|
805
|
+
*
|
|
806
|
+
* 🔴 **同键第二次调用 = 整个无操作**(A-070 二轮重扫,双 opus confirmed 的修法后半;红先钉
|
|
807
|
+
* `test/parked-late-decision.test.ts` 的 A-070-④)。键换成 wire `approvalId` 之后,「同一把
|
|
808
|
+
* approvalId 落两次墓碑」从不可能变成**常态**(同 askId 的重复本地注册各自按 park 路由收尾)。
|
|
809
|
+
* 两件事因此必须同时成立:
|
|
810
|
+
* · **不占第二格、不动插入序** —— 帽与逐出序是按「对外可见的 ask」记的;兄弟刷新插入序会把一条
|
|
811
|
+
* 早该轮到逐出的墓碑一次次续命,同样是记账失真。
|
|
812
|
+
* · **不覆写既有条目** —— `redeemed`/`inflight` 是本副本**已经对客户端做出的受理承诺**(见
|
|
813
|
+
* {@link ParkTombstone});覆写等于把一次已受理的决议重新打开,200 丢包后的重试会去动一张
|
|
814
|
+
* **已被消费**的 checkpoint(409/404),与本口承诺的「幂等回放首决」相反。
|
|
815
|
+
* 代价如实登记:`parkedAtMs` 保留**首次** park 的时刻 ⇒ TTL 从首次起算(晚到的兄弟不给续期)。
|
|
816
|
+
* 方向安全(墓碑早退 = 回落修前的 404,不会错兑),且与「一只 ask 一格」的记账口径自洽。 */
|
|
776
817
|
private recordParkTombstone;
|
|
777
818
|
/** #329:取墓碑(**惰性清扫**:过期条目读到即删,不另起定时器 —— 这张表只在迟到回决那一刻被读)。
|
|
778
819
|
* 属主门在调用方(与 live 腿同一句判据:`owner === null` 或逐字相等,否则按不存在处置)。 */
|
|
@@ -882,6 +923,19 @@ export declare class ToolApprovalCoordinator {
|
|
|
882
923
|
status: number;
|
|
883
924
|
body: unknown;
|
|
884
925
|
}>;
|
|
926
|
+
/**
|
|
927
|
+
* #345:自由文本规则的**裁决前**文本预检 —— 回 `undefined` = 放行(继续正常回决),回一个响应 = 400。
|
|
928
|
+
*
|
|
929
|
+
* 🔴 **触发条件与兑付口的前置门逐条同源**,一个都不许多(这是本方法最容易写错的地方):只有在这次回决
|
|
930
|
+
* **真的会去兑付一条自由文本规则**时才预检。任何一条前置门本来就会把兑付挡掉的形(治理档 ask /
|
|
931
|
+
* 车道不供 / 无属主 / ctrl+g 编辑放行),都**必须**照旧走「裁决落定 + `rulePersisted:false` + 各自的
|
|
932
|
+
* 拒因词」那条老路 —— 否则一次带着坏规则文本的**治理档回决**会被 400 掉,而它本来是能决的:那是把一个
|
|
933
|
+
* 附带愿望的失败升级成主动作的失败,方向完全错。逐门对照见 {@link persistRuleAfterDecision}。
|
|
934
|
+
*
|
|
935
|
+
* 候选臂(`edited !== true`)不进这道门:它的等式是「文本必须在引擎铸的候选里」(`rule_not_offered`),
|
|
936
|
+
* 那是记录级判据、不是文本级判据,`precheckEditedRuleText` 答不了也不该答。
|
|
937
|
+
*/
|
|
938
|
+
private precheckEditedRuleBeforeDecision;
|
|
885
939
|
/** #340:回执上两个新键的**唯一**成形口 —— 只在这次请求真的走了**自由文本臂**时铸。
|
|
886
940
|
* 候选臂的回执因此逐字节不变(老壳按固定键集解析,多一个键就是一次没人要的 wire 变更)。 */
|
|
887
941
|
private editedArmEcho;
|
package/dist/tool-approval.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { uuidv7, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
|
|
3
|
+
import { precheckCardRuleText } from "./rules-consent.js";
|
|
3
4
|
import { redactDeep, redactSecrets } from "./trace/redact.js";
|
|
5
|
+
import { hasBidiControls } from "./text-bidi.js";
|
|
4
6
|
import { createLogger } from "./observability/logger.js";
|
|
5
7
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
6
8
|
import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
|
|
@@ -20,6 +22,7 @@ const DEFAULT_ADMIT_MAX_PER_TASK = 32;
|
|
|
20
22
|
const DEFAULT_ADMIT_MAX_PER_OWNER = 256;
|
|
21
23
|
const DEFAULT_PARK_TOMBSTONE_MAX = 2000;
|
|
22
24
|
const DEFAULT_PARK_TOMBSTONE_TTL_MS = 60 * 60_000;
|
|
25
|
+
const DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX = 128;
|
|
23
26
|
const PARKING_RETRY_AFTER_MS = 250;
|
|
24
27
|
const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
|
|
25
28
|
const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
|
|
@@ -87,14 +90,17 @@ function isChildAsk(req) {
|
|
|
87
90
|
}
|
|
88
91
|
function boundArgs(args) {
|
|
89
92
|
const redacted = redactDeep(args);
|
|
93
|
+
let hasBidi = false;
|
|
90
94
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
const text = JSON.stringify(redacted);
|
|
96
|
+
hasBidi = hasBidiControls(text);
|
|
97
|
+
if (Buffer.byteLength(text, "utf8") > MAX_APPROVAL_ARGS_BYTES)
|
|
98
|
+
return { omitted: true, hasBidi };
|
|
93
99
|
}
|
|
94
100
|
catch {
|
|
95
|
-
return { omitted: true };
|
|
101
|
+
return { omitted: true, hasBidi: false };
|
|
96
102
|
}
|
|
97
|
-
return { args: redacted, omitted: false };
|
|
103
|
+
return { args: redacted, omitted: false, hasBidi };
|
|
98
104
|
}
|
|
99
105
|
function readBoundInputHash(req) {
|
|
100
106
|
if (req === null || typeof req !== "object")
|
|
@@ -188,6 +194,7 @@ export class ToolApprovalCoordinator {
|
|
|
188
194
|
parkTombstones = new Map();
|
|
189
195
|
parkTombstoneMax;
|
|
190
196
|
parkTombstoneTtlMs;
|
|
197
|
+
parkTombstonePerPrincipalMax;
|
|
191
198
|
parkedRedeem;
|
|
192
199
|
constructor(opts) {
|
|
193
200
|
this.ruleConsent = opts?.ruleConsent;
|
|
@@ -202,6 +209,7 @@ export class ToolApprovalCoordinator {
|
|
|
202
209
|
this.parkedRedeem = opts?.parkedRedeem;
|
|
203
210
|
this.parkTombstoneMax = opts?.parkTombstoneMax ?? DEFAULT_PARK_TOMBSTONE_MAX;
|
|
204
211
|
this.parkTombstoneTtlMs = opts?.parkTombstoneTtlMs ?? DEFAULT_PARK_TOMBSTONE_TTL_MS;
|
|
212
|
+
this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
|
|
205
213
|
}
|
|
206
214
|
unattendedOutcome() {
|
|
207
215
|
return this.unattendedPolicy === "deny" ? false : "unavailable";
|
|
@@ -371,10 +379,51 @@ export class ToolApprovalCoordinator {
|
|
|
371
379
|
entry.settleParkRoute();
|
|
372
380
|
}
|
|
373
381
|
recordParkTombstone(approvalId, tomb) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (
|
|
377
|
-
|
|
382
|
+
const existing = this.parkTombstones.get(approvalId);
|
|
383
|
+
if (existing !== undefined) {
|
|
384
|
+
if (existing.redeemed !== undefined || existing.inflight !== undefined)
|
|
385
|
+
return;
|
|
386
|
+
if (Date.now() - existing.parkedAtMs <= this.parkTombstoneTtlMs)
|
|
387
|
+
return;
|
|
388
|
+
this.parkTombstones.delete(approvalId);
|
|
389
|
+
}
|
|
390
|
+
const effectivePerPrincipalMax = Math.min(this.parkTombstonePerPrincipalMax, this.parkTombstoneMax);
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
let ownCount = 0;
|
|
393
|
+
let ownExpiredVictim;
|
|
394
|
+
let ownPlainVictim;
|
|
395
|
+
let ownEvictable;
|
|
396
|
+
let globalExpiredVictim;
|
|
397
|
+
for (const [k, t] of this.parkTombstones) {
|
|
398
|
+
const expired = now - t.parkedAtMs > this.parkTombstoneTtlMs;
|
|
399
|
+
if (globalExpiredVictim === undefined && expired && t.inflight === undefined)
|
|
400
|
+
globalExpiredVictim = k;
|
|
401
|
+
if (t.owner !== tomb.owner)
|
|
402
|
+
continue;
|
|
403
|
+
ownCount++;
|
|
404
|
+
if (t.inflight !== undefined)
|
|
405
|
+
continue;
|
|
406
|
+
if (ownEvictable === undefined)
|
|
407
|
+
ownEvictable = k;
|
|
408
|
+
if (ownExpiredVictim === undefined && expired)
|
|
409
|
+
ownExpiredVictim = k;
|
|
410
|
+
if (ownPlainVictim === undefined && t.redeemed === undefined)
|
|
411
|
+
ownPlainVictim = k;
|
|
412
|
+
}
|
|
413
|
+
if (ownCount >= effectivePerPrincipalMax) {
|
|
414
|
+
const victim = ownExpiredVictim ?? ownPlainVictim ?? ownEvictable;
|
|
415
|
+
if (victim === undefined) {
|
|
416
|
+
recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"} reason=all-inflight`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.parkTombstones.delete(victim);
|
|
420
|
+
}
|
|
421
|
+
else if (this.parkTombstones.size >= this.parkTombstoneMax && globalExpiredVictim !== undefined) {
|
|
422
|
+
this.parkTombstones.delete(globalExpiredVictim);
|
|
423
|
+
}
|
|
424
|
+
else if (this.parkTombstones.size >= this.parkTombstoneMax) {
|
|
425
|
+
recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"}`);
|
|
426
|
+
return;
|
|
378
427
|
}
|
|
379
428
|
this.parkTombstones.set(approvalId, tomb);
|
|
380
429
|
}
|
|
@@ -383,7 +432,8 @@ export class ToolApprovalCoordinator {
|
|
|
383
432
|
if (tomb === undefined)
|
|
384
433
|
return undefined;
|
|
385
434
|
if (Date.now() - tomb.parkedAtMs > this.parkTombstoneTtlMs) {
|
|
386
|
-
|
|
435
|
+
if (tomb.inflight === undefined)
|
|
436
|
+
this.parkTombstones.delete(approvalId);
|
|
387
437
|
return undefined;
|
|
388
438
|
}
|
|
389
439
|
return tomb;
|
|
@@ -530,6 +580,7 @@ export class ToolApprovalCoordinator {
|
|
|
530
580
|
...((c) => (c !== undefined ? { probeCause: c } : {}))(readProbeCause(req)),
|
|
531
581
|
...((c) => (c !== undefined ? { ruleEvidence: c } : {}))(readRuleEvidence(req)),
|
|
532
582
|
...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
|
|
583
|
+
...(bounded.hasBidi ? { inputHasBidi: true } : {}),
|
|
533
584
|
};
|
|
534
585
|
const effectiveWindowMs = effectiveAskWindowMs(this.ttlMs, this.windowMarginMs, origin.legDeadlineMonotonic, performance.now());
|
|
535
586
|
const expiresAtMs = Date.now() + effectiveWindowMs;
|
|
@@ -604,6 +655,10 @@ export class ToolApprovalCoordinator {
|
|
|
604
655
|
frame.persistedRuleShadowed = cardForFrame.persistedRuleShadowed;
|
|
605
656
|
else
|
|
606
657
|
delete frame.persistedRuleShadowed;
|
|
658
|
+
if (cardForFrame.inputHasBidi === true)
|
|
659
|
+
frame.inputHasBidi = true;
|
|
660
|
+
else
|
|
661
|
+
delete frame.inputHasBidi;
|
|
607
662
|
if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
|
|
608
663
|
effectiveGovernanceForced = true;
|
|
609
664
|
ruleLaneMaterial = undefined;
|
|
@@ -659,7 +714,7 @@ export class ToolApprovalCoordinator {
|
|
|
659
714
|
let emitFailed = false;
|
|
660
715
|
let emitSettled = false;
|
|
661
716
|
let windowRouteUnavailable = false;
|
|
662
|
-
let
|
|
717
|
+
let parkRouted = false;
|
|
663
718
|
let timer;
|
|
664
719
|
let resolveAllowed;
|
|
665
720
|
const allowedP = new Promise((resolve) => { resolveAllowed = resolve; });
|
|
@@ -698,8 +753,8 @@ export class ToolApprovalCoordinator {
|
|
|
698
753
|
sig.removeEventListener("abort", fn);
|
|
699
754
|
this.pending.delete(id);
|
|
700
755
|
if (windowRouteUnavailable && askId !== undefined && batchId !== undefined && this.unattendedOutcome() !== false) {
|
|
701
|
-
this.recordParkTombstone(
|
|
702
|
-
|
|
756
|
+
this.recordParkTombstone(wireApprovalId, { askId, batchId, owner: primary.owner, parkedAtMs: Date.now() });
|
|
757
|
+
parkRouted = true;
|
|
703
758
|
}
|
|
704
759
|
if (askId !== undefined) {
|
|
705
760
|
const siblings = this.pendingByAskId.get(askId);
|
|
@@ -878,7 +933,7 @@ export class ToolApprovalCoordinator {
|
|
|
878
933
|
...(primary.sessionId ? { sessionId: primary.sessionId } : {}),
|
|
879
934
|
toolName: req.toolName,
|
|
880
935
|
registeredAtMs: Date.now(),
|
|
881
|
-
expiresAtMs,
|
|
936
|
+
expiresAtMs: persistedExpiresAtMs,
|
|
882
937
|
...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
|
|
883
938
|
...(effectiveGovernanceForced ? { governanceForced: true } : {}),
|
|
884
939
|
...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
@@ -962,7 +1017,7 @@ export class ToolApprovalCoordinator {
|
|
|
962
1017
|
type: "tool_approval_complete",
|
|
963
1018
|
approvalId: wireApprovalId,
|
|
964
1019
|
outcome: lastOutcome,
|
|
965
|
-
...(
|
|
1020
|
+
...(parkRouted ? { parked: true } : {}),
|
|
966
1021
|
})
|
|
967
1022
|
: undefined)
|
|
968
1023
|
.catch(() => undefined);
|
|
@@ -983,6 +1038,9 @@ export class ToolApprovalCoordinator {
|
|
|
983
1038
|
if (!entry || (entry.owner !== null && entry.owner !== principal)) {
|
|
984
1039
|
return this.respondLate(id, principal, parsed, httpReq);
|
|
985
1040
|
}
|
|
1041
|
+
const preRule = this.precheckEditedRuleBeforeDecision(entry, parsed);
|
|
1042
|
+
if (preRule !== undefined)
|
|
1043
|
+
return preRule;
|
|
986
1044
|
const ruleEligibility = { eligible: true };
|
|
987
1045
|
const settled = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
|
|
988
1046
|
? this.respondWithCas(this.askStore, entry.askId, entry.batchId, id, entry, parsed, ruleEligibility)
|
|
@@ -991,6 +1049,29 @@ export class ToolApprovalCoordinator {
|
|
|
991
1049
|
return settled;
|
|
992
1050
|
return this.persistRuleAfterDecision(settled, entry, parsed.persistRule, parsed.updatedInput !== undefined, ruleEligibility);
|
|
993
1051
|
}
|
|
1052
|
+
precheckEditedRuleBeforeDecision(entry, parsed) {
|
|
1053
|
+
const persistRule = parsed.persistRule;
|
|
1054
|
+
if (persistRule === undefined || !persistRule.edited)
|
|
1055
|
+
return undefined;
|
|
1056
|
+
if (parsed.updatedInput !== undefined)
|
|
1057
|
+
return undefined;
|
|
1058
|
+
if (entry.governanceForced === true)
|
|
1059
|
+
return undefined;
|
|
1060
|
+
const command = entry.ruleLane?.command;
|
|
1061
|
+
if (this.ruleConsent === undefined || command === undefined || entry.owner === null)
|
|
1062
|
+
return undefined;
|
|
1063
|
+
const verdict = precheckCardRuleText(persistRule.rule, command);
|
|
1064
|
+
if (verdict === undefined || verdict.ok)
|
|
1065
|
+
return undefined;
|
|
1066
|
+
return {
|
|
1067
|
+
status: 400,
|
|
1068
|
+
body: {
|
|
1069
|
+
error: verdict.message,
|
|
1070
|
+
errorCode: "request.body_shape",
|
|
1071
|
+
...(verdict.code !== undefined ? { ruleRejectCode: verdict.code } : {}),
|
|
1072
|
+
},
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
994
1075
|
editedArmEcho(persistRule, persisted) {
|
|
995
1076
|
if (!persistRule.edited || !persisted.ok)
|
|
996
1077
|
return {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.45.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
57
|
+
"@sema-agent/core": "^5.57.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.19.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|