@sema-agent/server 7.30.0 → 7.31.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/dist/boot/session-faces.js +31 -1
- package/dist/http/route-ctx.d.ts +6 -1
- package/dist/http/routes/approvals-assistant.js +14 -2
- package/dist/http/routes/sessions-list.js +8 -4
- package/dist/http/routes/sessions.js +11 -5
- package/dist/http/server.js +44 -3
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/plugins/caching-session-store.d.ts +1 -0
- package/dist/plugins/file-run-store.d.ts +11 -3
- package/dist/plugins/file-run-store.js +28 -7
- package/dist/plugins/local-session-store.d.ts +2 -0
- package/dist/plugins/local-session-store.js +2 -2
- package/dist/plugins/memory-run-store.d.ts +13 -2
- package/dist/plugins/memory-run-store.js +17 -5
- package/dist/plugins/permission-rule-store-file.d.ts +3 -3
- package/dist/plugins/permission-rule-store-file.js +8 -7
- package/dist/plugins/permission-rule-store-sql.d.ts +24 -2
- package/dist/plugins/permission-rule-store-sql.js +29 -7
- package/dist/plugins/pg-session-storage.d.ts +1 -0
- package/dist/plugins/pg-session-storage.js +2 -1
- package/dist/plugins/retention-store-sql.d.ts +9 -1
- package/dist/plugins/retention-store-sql.js +13 -8
- package/dist/plugins/run-store-sql.d.ts +31 -7
- package/dist/plugins/run-store-sql.js +42 -13
- package/dist/plugins/store-backend.d.ts +9 -2
- package/dist/plugins/store-backend.js +30 -7
- package/dist/plugins/tidb-session-store.d.ts +1 -0
- package/dist/plugins/tidb-session-store.js +3 -1
- package/dist/rules-consent.d.ts +7 -4
- package/dist/rules-consent.js +77 -2
- package/dist/security.d.ts +52 -1
- package/dist/security.js +51 -0
- package/dist/wall-clock-jump-guard.d.ts +91 -0
- package/dist/wall-clock-jump-guard.js +118 -0
- package/package.json +1 -1
- package/dist/boot/lexical-path-env.d.ts +0 -10
- package/dist/boot/lexical-path-env.js +0 -88
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/finance/cost-taxonomy.d.ts +0 -34
- package/dist/finance/cost-taxonomy.js +0 -26
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -151
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
- package/dist/sema-registry.d.ts +0 -41
- package/dist/sema-registry.js +0 -40
package/dist/security.d.ts
CHANGED
|
@@ -38,12 +38,63 @@ export interface SessionListItem {
|
|
|
38
38
|
* 不含读面,也不含 `POST /v1/tasks` 那条提交腿 —— 它有自己的同族门(别重复拦,双拦会让错误归因变糊)。
|
|
39
39
|
*/
|
|
40
40
|
export declare function isDestructiveSessionWrite(method: string, url: string): boolean;
|
|
41
|
+
/** [4198] 窄互认第二臂的租户键字面量 —— 单机形下 core 写侧 `spec.principal ?? "default"` 铸出的那一个,
|
|
42
|
+
* 也是壳 [4195]G1 翻面后随请求带的那一个。**不是哨兵**([4183] 措辞):它是单机形的真租户键。 */
|
|
43
|
+
export declare const LEGACY_UNOWNED_AMNESTY_TENANT = "default";
|
|
44
|
+
/**
|
|
45
|
+
* [4198] 窄互认**第二臂**(clay 裁 A / [4225] 下达;cli [4238] 表态即发)—— `legacyShellOwner`
|
|
46
|
+
* (本文件 createAuthorizer 内 + http/server.ts `runOwnerOk`)的对称孪生。
|
|
47
|
+
*
|
|
48
|
+
* 病灶:7.8.0 第一臂承接的是 `owner === "anon:shell-live"` 哨兵行;此后壳把无主表示法归一为
|
|
49
|
+
* **缺席**,于是 1.0.82 及更早留下的存量会话 owner 列是 **null**。壳 1.0.83 起随请求带
|
|
50
|
+
* `default` 主([4195] G1 闸口),`null !== "default"` ⇒ 存量会话在 session 面
|
|
51
|
+
* **列表不出现 / fork 404 / delete 静默 {deleted:false} / workspace 404**(三面矩阵 [4198] 面3;
|
|
52
|
+
* fleet 面与 run 读面天然连续,面1/面2 零施工)。
|
|
53
|
+
*
|
|
54
|
+
* 🔒 **三条件缺一不放行**(与第一臂同源,不是兼容臂):
|
|
55
|
+
* ① 单机形 `!requirePrincipal` —— 多租部署本臂**死路**(header 必到、键必具名);
|
|
56
|
+
* ② 存量行恰为无主 `owner === null` —— 任何具名 owner 的行不受影响;
|
|
57
|
+
* ③ 调用方恰为 `default` 主 —— 非 default 的具名主对无主行仍走原门(404 / deleted:false)。
|
|
58
|
+
* 反向风险有界:①限定的单机形本就单用户,把无主行判给 default 主不产生跨租泄露。
|
|
59
|
+
*
|
|
60
|
+
* 🪦 墓碑注(与第一臂同源):这是两个表示法之间的**过渡承接**。存量随会话生命周期消亡后,本臂
|
|
61
|
+
* 连同 {@link LEGACY_UNOWNED_AMNESTY_TENANT} 与几处调用点可**整删**,不留兼容层。
|
|
62
|
+
*
|
|
63
|
+
* ── 逐面判过、**刻意不放行**的三处(免得后人当遗漏再补一次)────────────────────────────────
|
|
64
|
+
* · `runOwnerOk` 三读面(head / audit 单读 / events):`owner === null` 那一支**本来就放行**
|
|
65
|
+
* (判据是 `owner !== null && principal !== owner`),不需要本臂。[4198] 帖文里「单读 404」一句
|
|
66
|
+
* 经亲读实现行**不成立**,已加现状钉(session-head-probe.test.ts)锁住这个事实。
|
|
67
|
+
* · `PUT /v1/sessions/:id/policy` 的 `owner === null ⇒ 409 conflict.session_ownerless`:那是**有意的
|
|
68
|
+
* fail-closed** —— 无主会话没有稳定的 run-principal,存下去的规则读不到、静默失效。放宽它等于制造
|
|
69
|
+
* 一条 fail-open,不在本臂辖域。
|
|
70
|
+
* · `…/sync/*`(PULL/PUSH/import):PULL 虽是只读、形同 workspace,但这一族**带一条破坏性 import 腿**
|
|
71
|
+
* (`isDestructiveSessionWrite` 明列),而 [4198]/[4225] 裁的是会话**读面与生命周期**四门。放宽一个
|
|
72
|
+
* 写面族超出裁定射程 —— 要做另立一次表态,不在本臂里顺手带过。
|
|
73
|
+
*
|
|
74
|
+
* @param requirePrincipal `config.requirePrincipal`(条件①)
|
|
75
|
+
* @param gateOwner `sessionOwnerScope(ForWrite)` 判出的**已验证**主(条件③;fleet-wide 面不经本臂)
|
|
76
|
+
* @param owner 存量行的 owner 列(条件②)
|
|
77
|
+
*/
|
|
78
|
+
export declare function legacyUnownedSessionOk(requirePrincipal: boolean, gateOwner: string | null, owner: string | null): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* {@link legacyUnownedSessionOk} 的**条件①③**半支 —— 只判「这个部署形 × 这个调用方」是否落在窄臂辖域内。
|
|
81
|
+
*
|
|
82
|
+
* 🔴 单独存在的唯一理由是**列表面**:`GET /v1/sessions` 的第三条件(`owner === null`)不在 JS 里判,
|
|
83
|
+
* 它是 store 侧 `owner = ? OR owner IS NULL` 这一句 SQL 谓词 —— 三条件仍然齐,只是第三条件由 SQL 承载
|
|
84
|
+
* (`listSessions({ owner, includeUnowned })` 的 `includeUnowned` 位即本函数的返回值)。
|
|
85
|
+
* 单实体读/写点(fork/delete/workspace)**一律**用三条件齐的 {@link legacyUnownedSessionOk},不要用本函数。
|
|
86
|
+
*/
|
|
87
|
+
export declare function legacyUnownedAmnestyOpen(requirePrincipal: boolean, gateOwner: string | null): boolean;
|
|
41
88
|
export type OwnerAwareSessionStore = SessionStore & {
|
|
42
89
|
ownerOf?: (sessionId: string) => Promise<string | null | undefined>;
|
|
43
90
|
register?: (sessionId: string, owner: string | null) => Promise<void>;
|
|
44
|
-
/** E16 — enumerate the caller's sessions (owner-filtered on the session store's OWN owner column).
|
|
91
|
+
/** E16 — enumerate the caller's sessions (owner-filtered on the session store's OWN owner column).
|
|
92
|
+
* `includeUnowned` = [4198] 窄互认第二臂的**第三条件**(`owner IS NULL`)的 SQL 承载位:置真时
|
|
93
|
+
* 谓词放宽为 `owner = ? OR owner IS NULL`。只在 `owner` 在场时有意义(路由已判①③);`owner` 缺席
|
|
94
|
+
* (fleet-wide)时本位无效——那一面本来就不钉 owner。 */
|
|
45
95
|
listSessions?: (opts: {
|
|
46
96
|
owner?: string;
|
|
97
|
+
includeUnowned?: boolean;
|
|
47
98
|
cursor?: {
|
|
48
99
|
lastActivityAt: string;
|
|
49
100
|
sessionId: string;
|
package/dist/security.js
CHANGED
|
@@ -37,6 +37,57 @@ export function isDestructiveSessionWrite(method, url) {
|
|
|
37
37
|
return /^\/v1\/sessions\/[^/]+\/(sync\/import|fork)$/.test(url);
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
|
+
/** [4198] 窄互认第二臂的租户键字面量 —— 单机形下 core 写侧 `spec.principal ?? "default"` 铸出的那一个,
|
|
41
|
+
* 也是壳 [4195]G1 翻面后随请求带的那一个。**不是哨兵**([4183] 措辞):它是单机形的真租户键。 */
|
|
42
|
+
export const LEGACY_UNOWNED_AMNESTY_TENANT = "default";
|
|
43
|
+
/**
|
|
44
|
+
* [4198] 窄互认**第二臂**(clay 裁 A / [4225] 下达;cli [4238] 表态即发)—— `legacyShellOwner`
|
|
45
|
+
* (本文件 createAuthorizer 内 + http/server.ts `runOwnerOk`)的对称孪生。
|
|
46
|
+
*
|
|
47
|
+
* 病灶:7.8.0 第一臂承接的是 `owner === "anon:shell-live"` 哨兵行;此后壳把无主表示法归一为
|
|
48
|
+
* **缺席**,于是 1.0.82 及更早留下的存量会话 owner 列是 **null**。壳 1.0.83 起随请求带
|
|
49
|
+
* `default` 主([4195] G1 闸口),`null !== "default"` ⇒ 存量会话在 session 面
|
|
50
|
+
* **列表不出现 / fork 404 / delete 静默 {deleted:false} / workspace 404**(三面矩阵 [4198] 面3;
|
|
51
|
+
* fleet 面与 run 读面天然连续,面1/面2 零施工)。
|
|
52
|
+
*
|
|
53
|
+
* 🔒 **三条件缺一不放行**(与第一臂同源,不是兼容臂):
|
|
54
|
+
* ① 单机形 `!requirePrincipal` —— 多租部署本臂**死路**(header 必到、键必具名);
|
|
55
|
+
* ② 存量行恰为无主 `owner === null` —— 任何具名 owner 的行不受影响;
|
|
56
|
+
* ③ 调用方恰为 `default` 主 —— 非 default 的具名主对无主行仍走原门(404 / deleted:false)。
|
|
57
|
+
* 反向风险有界:①限定的单机形本就单用户,把无主行判给 default 主不产生跨租泄露。
|
|
58
|
+
*
|
|
59
|
+
* 🪦 墓碑注(与第一臂同源):这是两个表示法之间的**过渡承接**。存量随会话生命周期消亡后,本臂
|
|
60
|
+
* 连同 {@link LEGACY_UNOWNED_AMNESTY_TENANT} 与几处调用点可**整删**,不留兼容层。
|
|
61
|
+
*
|
|
62
|
+
* ── 逐面判过、**刻意不放行**的三处(免得后人当遗漏再补一次)────────────────────────────────
|
|
63
|
+
* · `runOwnerOk` 三读面(head / audit 单读 / events):`owner === null` 那一支**本来就放行**
|
|
64
|
+
* (判据是 `owner !== null && principal !== owner`),不需要本臂。[4198] 帖文里「单读 404」一句
|
|
65
|
+
* 经亲读实现行**不成立**,已加现状钉(session-head-probe.test.ts)锁住这个事实。
|
|
66
|
+
* · `PUT /v1/sessions/:id/policy` 的 `owner === null ⇒ 409 conflict.session_ownerless`:那是**有意的
|
|
67
|
+
* fail-closed** —— 无主会话没有稳定的 run-principal,存下去的规则读不到、静默失效。放宽它等于制造
|
|
68
|
+
* 一条 fail-open,不在本臂辖域。
|
|
69
|
+
* · `…/sync/*`(PULL/PUSH/import):PULL 虽是只读、形同 workspace,但这一族**带一条破坏性 import 腿**
|
|
70
|
+
* (`isDestructiveSessionWrite` 明列),而 [4198]/[4225] 裁的是会话**读面与生命周期**四门。放宽一个
|
|
71
|
+
* 写面族超出裁定射程 —— 要做另立一次表态,不在本臂里顺手带过。
|
|
72
|
+
*
|
|
73
|
+
* @param requirePrincipal `config.requirePrincipal`(条件①)
|
|
74
|
+
* @param gateOwner `sessionOwnerScope(ForWrite)` 判出的**已验证**主(条件③;fleet-wide 面不经本臂)
|
|
75
|
+
* @param owner 存量行的 owner 列(条件②)
|
|
76
|
+
*/
|
|
77
|
+
export function legacyUnownedSessionOk(requirePrincipal, gateOwner, owner) {
|
|
78
|
+
return owner === null && legacyUnownedAmnestyOpen(requirePrincipal, gateOwner);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* {@link legacyUnownedSessionOk} 的**条件①③**半支 —— 只判「这个部署形 × 这个调用方」是否落在窄臂辖域内。
|
|
82
|
+
*
|
|
83
|
+
* 🔴 单独存在的唯一理由是**列表面**:`GET /v1/sessions` 的第三条件(`owner === null`)不在 JS 里判,
|
|
84
|
+
* 它是 store 侧 `owner = ? OR owner IS NULL` 这一句 SQL 谓词 —— 三条件仍然齐,只是第三条件由 SQL 承载
|
|
85
|
+
* (`listSessions({ owner, includeUnowned })` 的 `includeUnowned` 位即本函数的返回值)。
|
|
86
|
+
* 单实体读/写点(fork/delete/workspace)**一律**用三条件齐的 {@link legacyUnownedSessionOk},不要用本函数。
|
|
87
|
+
*/
|
|
88
|
+
export function legacyUnownedAmnestyOpen(requirePrincipal, gateOwner) {
|
|
89
|
+
return !requirePrincipal && gateOwner === LEGACY_UNOWNED_AMNESTY_TENANT;
|
|
90
|
+
}
|
|
40
91
|
/** The shape core's `uuidv7()` mints: canonical 8-4-4-4-12 lowercase hex with version nibble `7` and the RFC-4122
|
|
41
92
|
* variant (`8`/`9`/`a`/`b`). Used to shape-validate a caller-supplied session id on the §0.5 fork/delete routes so
|
|
42
93
|
* a crafted id (LIKE metacharacters, over-long, non-canonical) is rejected up front — a SECOND layer behind the
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 墙钟阶跃守卫 —— 给「按墙钟绝对 deadline 做终局判定」的清算腿加一道**单调钟佐证**。
|
|
3
|
+
*
|
|
4
|
+
* 病灶(test [4232] 场景①,libfaketime 真实构造,宿主真钟全程未碰):审批 SLA=45s,把进程内墙钟
|
|
5
|
+
* **前拨 1 小时**,真实只等了约 14 秒审批卡就被 D-D deny-sweep 判超时自动拒绝。链条是清楚的:
|
|
6
|
+
* `checkpoint.deadline` 是 core 在 `put` 时按**当时的墙钟**铸的绝对时刻(必须是墙钟——它要跨进程
|
|
7
|
+
* 重启存活,单调基准一重启就没了),而 sweep 每拍拿 `Date.now()` 去比 `deadline <= cutoff`。墙钟凭空
|
|
8
|
+
* 前跳 J 毫秒 ⇒ 判定钟凭空多走 J 毫秒 ⇒ 人还没走开,窗就没了。
|
|
9
|
+
*
|
|
10
|
+
* [4235] core 半场三定性把判定半场划给本仓:`totalWaitMs` 的墙钟**记账**是设计如此(要跨 durable
|
|
11
|
+
* resume 延续),不改形;**判定**侧可以在进程内用单调钟做窗口佐证,与账面字段互不牵连。本模块就是那道佐证。
|
|
12
|
+
*
|
|
13
|
+
* ## 判据(为什么是「宽限一个完整 SLA」)
|
|
14
|
+
* 直觉修法是把 cutoff 改成 `now - 累计前跳量`。它不成立:core 在跳变**之后**铸的 deadline 用的是**新**
|
|
15
|
+
* 墙钟,拿旧墙钟去比 ⇒ 新卡凭空多活 J 毫秒,而 J 可以是任意大。补偿量若衰减,cutoff 又会以 2 倍速前进。
|
|
16
|
+
* 两条路都把一个有界缺陷换成一个无界缺陷。
|
|
17
|
+
*
|
|
18
|
+
* 本模块改判**宽限**:检出一次前跳后,按**单调钟**静默 `grace = slaMs` 毫秒不做判定。
|
|
19
|
+
*
|
|
20
|
+
* 🔴 **宽限量是一个完整 SLA,不是 `min(J, S)`**(codex 对抗复审 R1 [high] 三,验真后修 —— 第一版正是
|
|
21
|
+
* `min(J, S)`,而那条「J < S 时宽限恰好补平损失」的证明**是错的**)。错在哪:宽限只是**推迟**判定,
|
|
22
|
+
* 它消不掉墙钟从此**永久领先** J 毫秒这件事。反例(逐字复算):S=45s,gate 在真实 t=0 铸出(deadline =
|
|
23
|
+
* W0+45s),真实 t=1s 时前跳 J=10s。`min(J,S)`=10s ⇒ 宽限到真实 t=11s;此后墙钟恒为 `W0+t+10`,
|
|
24
|
+
* 于是 deadline 在 **真实 t=35s** 就被判到期 —— 仍旧少给了整整 10 秒。
|
|
25
|
+
*
|
|
26
|
+
* 取满一个 S 才成立,证明是:宽限结束的时刻 t_g = t_jump + S(单调钟量)。
|
|
27
|
+
* · 跳变**之前或当时**就在等的 gate:它在 t_g 之前不可能被判,故真实等待 ≥ S ✔
|
|
28
|
+
* · 跳变**之后**在 t_x 铸出的 gate:deadline 用的是跳变后的钟 ⇒ 它按墙钟到期的时刻恰好对应真实
|
|
29
|
+
* t_x + S(墙钟与单调钟从跳变后起同速),宽限只可能把它再往后推,绝不会提前 ✔
|
|
30
|
+
* 即**任一审批席都拿得到不少于一个完整 SLA 的真实时间**;单次阶跃下判定最多被推迟一个 S。
|
|
31
|
+
*
|
|
32
|
+
* 🔴 **「有界且一次性自清」只对单次阶跃成立**(codex 对抗复审 R2 [high] 五,验真后修 —— 第一版把这句话
|
|
33
|
+
* 写成了无条件的,那是错的):每次前跳都把窗延到「此刻 + 一个 S」,所以**只要每个 S 内再跳一次**,窗就
|
|
34
|
+
* 永远续下去,deny 腿被静默关停 —— 而它是一条 **fail-closed** 腿,永久不跑比跑早了更坏。
|
|
35
|
+
* 收口 = 一串连续跳变的**累计宽限总上限**(缺省 `2 × S`:一个 S 覆盖有证明的单次阶跃,第二个 S 容一串
|
|
36
|
+
* 短促跳变即 NTP 连续大步修正的真实形);撞上上限就**响亮**恢复判定(`onGraceCapped`),此后宁可按一个
|
|
37
|
+
* 不稳的钟判,也不让 fail-closed 腿永远不跑。绝对兜底始终另有一层:`checkpoint.terminal_at_ms` 由别的
|
|
38
|
+
* reap 腿执行、不经本守卫。
|
|
39
|
+
*
|
|
40
|
+
* ## 方向与响亮度(#157 安全轴)
|
|
41
|
+
* 宽限只会**推迟**一个 fail-closed 动作(deny),绝不会放行任何东西 —— 方向安全。但它必须**响亮**:
|
|
42
|
+
* 检出即 warn(带跳变量与宽限量),宽限期内每拍 info。绝对兜底仍在:`checkpoint.terminal_at_ms`
|
|
43
|
+
* (put 时钉死的绝对上限)由另一条 reap 腿执行,不经本守卫 ⇒ 悬挂不可能无限。
|
|
44
|
+
*
|
|
45
|
+
* 后拨(墙钟相对单调钟**倒退**)不触发宽限:它只会让 deadline 显得更远 ⇒ 判定被推迟,已经是安全方向。
|
|
46
|
+
* 仍然 warn —— 一次没人知道的钟倒退是运维事实,不是可以吞掉的噪声。
|
|
47
|
+
*/
|
|
48
|
+
/** 判定为「阶跃」的漂移阈值。低于它的漂移是 tick 抖动 / 事件循环延迟 / NTP 缓变(≤500ppm,60s 拍上仅 30ms),
|
|
49
|
+
* 不是拨表。误判的代价只是**推迟**一次 deny ≤ 宽限量,所以宁可取得低一些。 */
|
|
50
|
+
export declare const DEFAULT_CLOCK_JUMP_THRESHOLD_MS = 2000;
|
|
51
|
+
/** `slaMs` 缺省(部署没配 `APPROVAL_TIMEOUT_SEC` 时)—— 与 `tool-approval.ts` 的
|
|
52
|
+
* `DEFAULT_APPROVAL_TTL_MS` 同值同义:一次审批交互的量级。 */
|
|
53
|
+
export declare const DEFAULT_CLOCK_JUMP_GRACE_MS: number;
|
|
54
|
+
export interface WallClockJumpGuard {
|
|
55
|
+
/**
|
|
56
|
+
* 每次清算前调用一次,传入本次清算**要用的那个墙钟读数**(判定基准本身,不是另取一次 `Date.now()`
|
|
57
|
+
* ——否则守卫看的钟和判定用的钟不是同一个)。
|
|
58
|
+
*
|
|
59
|
+
* @returns `true` = 本拍可按墙钟判定;`false` = 刚检出前跳,单调宽限未走完,本拍**整拍让开**。
|
|
60
|
+
*/
|
|
61
|
+
admit(wallNowMs: number): boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface WallClockJumpGuardOptions {
|
|
64
|
+
/** 宽限上界 = 被保护窗口的 SLA(审批腿=`approvalTimeoutSec * 1000`);缺省 {@link DEFAULT_CLOCK_JUMP_GRACE_MS}。 */
|
|
65
|
+
slaMs?: number;
|
|
66
|
+
/** 单调钟 seam。缺省 `performance.now()` —— 本仓既定单调域(server.ts/shutdown.ts/sighup-idle.ts 同源)。 */
|
|
67
|
+
monotonic?: () => number;
|
|
68
|
+
/** 墙钟 seam,**只用于构造当拍取首个锚点**(判定仍用 `admit` 的入参)。缺省 `Date.now`。
|
|
69
|
+
* codex R2 [high] 五:没有它,`lastWall` 要等第一次 `admit` 才有值 ⇒ 构造之后、首拍之前的那次前跳
|
|
70
|
+
* **测不出来**,而那正是「进程刚起来、pending gate 已在库里」的真实窗口。 */
|
|
71
|
+
now?: () => number;
|
|
72
|
+
/** 阈值 seam;缺省 {@link DEFAULT_CLOCK_JUMP_THRESHOLD_MS}。 */
|
|
73
|
+
jumpThresholdMs?: number;
|
|
74
|
+
/** 一串连续跳变累计宽限的**总上限**,自本串第一次跳变起算;缺省 `2 × slaMs`(理由见 `admit` 内注)。 */
|
|
75
|
+
maxTotalGraceMs?: number;
|
|
76
|
+
/** 检出阶跃:`jumpMs > 0` = 前拨(触发宽限),`< 0` = 后拨(只留痕,不宽限)。 */
|
|
77
|
+
onJump?: (info: {
|
|
78
|
+
jumpMs: number;
|
|
79
|
+
graceMs: number;
|
|
80
|
+
}) => void;
|
|
81
|
+
/** 宽限期内每拍一次。 */
|
|
82
|
+
onSuppressed?: (info: {
|
|
83
|
+
remainingMs: number;
|
|
84
|
+
}) => void;
|
|
85
|
+
/** 累计宽限撞上总上限、守卫**强制恢复判定**时一次(响亮放行,不是悄悄放行)。 */
|
|
86
|
+
onGraceCapped?: (info: {
|
|
87
|
+
totalGraceMs: number;
|
|
88
|
+
}) => void;
|
|
89
|
+
}
|
|
90
|
+
export declare function createWallClockJumpGuard(opts?: WallClockJumpGuardOptions): WallClockJumpGuard;
|
|
91
|
+
//# sourceMappingURL=wall-clock-jump-guard.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 墙钟阶跃守卫 —— 给「按墙钟绝对 deadline 做终局判定」的清算腿加一道**单调钟佐证**。
|
|
3
|
+
*
|
|
4
|
+
* 病灶(test [4232] 场景①,libfaketime 真实构造,宿主真钟全程未碰):审批 SLA=45s,把进程内墙钟
|
|
5
|
+
* **前拨 1 小时**,真实只等了约 14 秒审批卡就被 D-D deny-sweep 判超时自动拒绝。链条是清楚的:
|
|
6
|
+
* `checkpoint.deadline` 是 core 在 `put` 时按**当时的墙钟**铸的绝对时刻(必须是墙钟——它要跨进程
|
|
7
|
+
* 重启存活,单调基准一重启就没了),而 sweep 每拍拿 `Date.now()` 去比 `deadline <= cutoff`。墙钟凭空
|
|
8
|
+
* 前跳 J 毫秒 ⇒ 判定钟凭空多走 J 毫秒 ⇒ 人还没走开,窗就没了。
|
|
9
|
+
*
|
|
10
|
+
* [4235] core 半场三定性把判定半场划给本仓:`totalWaitMs` 的墙钟**记账**是设计如此(要跨 durable
|
|
11
|
+
* resume 延续),不改形;**判定**侧可以在进程内用单调钟做窗口佐证,与账面字段互不牵连。本模块就是那道佐证。
|
|
12
|
+
*
|
|
13
|
+
* ## 判据(为什么是「宽限一个完整 SLA」)
|
|
14
|
+
* 直觉修法是把 cutoff 改成 `now - 累计前跳量`。它不成立:core 在跳变**之后**铸的 deadline 用的是**新**
|
|
15
|
+
* 墙钟,拿旧墙钟去比 ⇒ 新卡凭空多活 J 毫秒,而 J 可以是任意大。补偿量若衰减,cutoff 又会以 2 倍速前进。
|
|
16
|
+
* 两条路都把一个有界缺陷换成一个无界缺陷。
|
|
17
|
+
*
|
|
18
|
+
* 本模块改判**宽限**:检出一次前跳后,按**单调钟**静默 `grace = slaMs` 毫秒不做判定。
|
|
19
|
+
*
|
|
20
|
+
* 🔴 **宽限量是一个完整 SLA,不是 `min(J, S)`**(codex 对抗复审 R1 [high] 三,验真后修 —— 第一版正是
|
|
21
|
+
* `min(J, S)`,而那条「J < S 时宽限恰好补平损失」的证明**是错的**)。错在哪:宽限只是**推迟**判定,
|
|
22
|
+
* 它消不掉墙钟从此**永久领先** J 毫秒这件事。反例(逐字复算):S=45s,gate 在真实 t=0 铸出(deadline =
|
|
23
|
+
* W0+45s),真实 t=1s 时前跳 J=10s。`min(J,S)`=10s ⇒ 宽限到真实 t=11s;此后墙钟恒为 `W0+t+10`,
|
|
24
|
+
* 于是 deadline 在 **真实 t=35s** 就被判到期 —— 仍旧少给了整整 10 秒。
|
|
25
|
+
*
|
|
26
|
+
* 取满一个 S 才成立,证明是:宽限结束的时刻 t_g = t_jump + S(单调钟量)。
|
|
27
|
+
* · 跳变**之前或当时**就在等的 gate:它在 t_g 之前不可能被判,故真实等待 ≥ S ✔
|
|
28
|
+
* · 跳变**之后**在 t_x 铸出的 gate:deadline 用的是跳变后的钟 ⇒ 它按墙钟到期的时刻恰好对应真实
|
|
29
|
+
* t_x + S(墙钟与单调钟从跳变后起同速),宽限只可能把它再往后推,绝不会提前 ✔
|
|
30
|
+
* 即**任一审批席都拿得到不少于一个完整 SLA 的真实时间**;单次阶跃下判定最多被推迟一个 S。
|
|
31
|
+
*
|
|
32
|
+
* 🔴 **「有界且一次性自清」只对单次阶跃成立**(codex 对抗复审 R2 [high] 五,验真后修 —— 第一版把这句话
|
|
33
|
+
* 写成了无条件的,那是错的):每次前跳都把窗延到「此刻 + 一个 S」,所以**只要每个 S 内再跳一次**,窗就
|
|
34
|
+
* 永远续下去,deny 腿被静默关停 —— 而它是一条 **fail-closed** 腿,永久不跑比跑早了更坏。
|
|
35
|
+
* 收口 = 一串连续跳变的**累计宽限总上限**(缺省 `2 × S`:一个 S 覆盖有证明的单次阶跃,第二个 S 容一串
|
|
36
|
+
* 短促跳变即 NTP 连续大步修正的真实形);撞上上限就**响亮**恢复判定(`onGraceCapped`),此后宁可按一个
|
|
37
|
+
* 不稳的钟判,也不让 fail-closed 腿永远不跑。绝对兜底始终另有一层:`checkpoint.terminal_at_ms` 由别的
|
|
38
|
+
* reap 腿执行、不经本守卫。
|
|
39
|
+
*
|
|
40
|
+
* ## 方向与响亮度(#157 安全轴)
|
|
41
|
+
* 宽限只会**推迟**一个 fail-closed 动作(deny),绝不会放行任何东西 —— 方向安全。但它必须**响亮**:
|
|
42
|
+
* 检出即 warn(带跳变量与宽限量),宽限期内每拍 info。绝对兜底仍在:`checkpoint.terminal_at_ms`
|
|
43
|
+
* (put 时钉死的绝对上限)由另一条 reap 腿执行,不经本守卫 ⇒ 悬挂不可能无限。
|
|
44
|
+
*
|
|
45
|
+
* 后拨(墙钟相对单调钟**倒退**)不触发宽限:它只会让 deadline 显得更远 ⇒ 判定被推迟,已经是安全方向。
|
|
46
|
+
* 仍然 warn —— 一次没人知道的钟倒退是运维事实,不是可以吞掉的噪声。
|
|
47
|
+
*/
|
|
48
|
+
/** 判定为「阶跃」的漂移阈值。低于它的漂移是 tick 抖动 / 事件循环延迟 / NTP 缓变(≤500ppm,60s 拍上仅 30ms),
|
|
49
|
+
* 不是拨表。误判的代价只是**推迟**一次 deny ≤ 宽限量,所以宁可取得低一些。 */
|
|
50
|
+
export const DEFAULT_CLOCK_JUMP_THRESHOLD_MS = 2_000;
|
|
51
|
+
/** `slaMs` 缺省(部署没配 `APPROVAL_TIMEOUT_SEC` 时)—— 与 `tool-approval.ts` 的
|
|
52
|
+
* `DEFAULT_APPROVAL_TTL_MS` 同值同义:一次审批交互的量级。 */
|
|
53
|
+
export const DEFAULT_CLOCK_JUMP_GRACE_MS = 5 * 60_000;
|
|
54
|
+
export function createWallClockJumpGuard(opts = {}) {
|
|
55
|
+
const monotonic = opts.monotonic ?? (() => performance.now());
|
|
56
|
+
const threshold = opts.jumpThresholdMs ?? DEFAULT_CLOCK_JUMP_THRESHOLD_MS;
|
|
57
|
+
const graceCap = opts.slaMs !== undefined && opts.slaMs > 0 ? opts.slaMs : DEFAULT_CLOCK_JUMP_GRACE_MS;
|
|
58
|
+
// codex R2 [high] 五(验真后修):首拍锚点**真的**在构造时取(旧形只初始化了单调锚,墙钟锚留 undefined,
|
|
59
|
+
// 而注释却写着「构造时取」—— 注在撒谎,且后果具体:构造之后、第一次 admit 之前的那次前跳测不出来,
|
|
60
|
+
// 那正是「进程刚起来、pending gate 已在库里」的真实窗口)。
|
|
61
|
+
let lastWall = (opts.now ?? Date.now)();
|
|
62
|
+
let lastMono = monotonic();
|
|
63
|
+
let graceUntilMono;
|
|
64
|
+
/** 本串连续跳变的起点(单调钟)。`undefined` = 当前不在宽限中。 */
|
|
65
|
+
let burstStartMono;
|
|
66
|
+
// 一串跳变累计宽限的总上限。为什么需要它(codex R2 [high] 五的第二半):每次前跳都把窗延到「此刻 +
|
|
67
|
+
// 一个 SLA」,于是**只要每个 SLA 内再跳一次**,窗就永远续下去 —— deny 腿被静默关停,而它是一条
|
|
68
|
+
// **fail-closed** 腿,永久不跑比跑早了更坏。旧注「有界且一次性自清」在连续跳变下是假的。
|
|
69
|
+
// 2×SLA 的取值:一个 SLA 覆盖单次阶跃(那是有证明的那一支),第二个 SLA 容一串短促跳变(NTP 连续
|
|
70
|
+
// 大步修正的真实形);再往后钟已属病态,此时**宁可按一个不稳的钟判**也不能让 fail-closed 腿永远不跑。
|
|
71
|
+
const maxTotalGrace = opts.maxTotalGraceMs !== undefined && opts.maxTotalGraceMs > 0 ? opts.maxTotalGraceMs : graceCap * 2;
|
|
72
|
+
return {
|
|
73
|
+
admit(wallNowMs) {
|
|
74
|
+
const mono = monotonic();
|
|
75
|
+
const prevWall = lastWall;
|
|
76
|
+
const monoDelta = mono - lastMono;
|
|
77
|
+
lastWall = wallNowMs;
|
|
78
|
+
lastMono = mono;
|
|
79
|
+
// 漂移 = 墙钟走的量 − 单调钟走的量。两者本该逐毫秒并进;差额就是拨表量。
|
|
80
|
+
const driftMs = wallNowMs - prevWall - monoDelta;
|
|
81
|
+
if (driftMs > threshold) {
|
|
82
|
+
// codex R1 [high] 三:**整一个 SLA**,与跳变量无关(`min(driftMs, graceCap)` 的证明是错的,
|
|
83
|
+
// 反例逐字见模块头注 —— 宽限推迟不了「墙钟从此永久领先」这件事)。
|
|
84
|
+
const graceMs = graceCap;
|
|
85
|
+
if (burstStartMono === undefined)
|
|
86
|
+
burstStartMono = mono; // 本串的起点(总上限自它起算)
|
|
87
|
+
const until = mono + graceMs;
|
|
88
|
+
// 取 max:宽限期内又跳一次 ⇒ 窗延到更晚的那个,不被后一次的小跳缩短。
|
|
89
|
+
if (graceUntilMono === undefined || until > graceUntilMono)
|
|
90
|
+
graceUntilMono = until;
|
|
91
|
+
opts.onJump?.({ jumpMs: Math.round(driftMs), graceMs: Math.round(graceMs) });
|
|
92
|
+
}
|
|
93
|
+
else if (driftMs < -threshold) {
|
|
94
|
+
// 后拨:判定被推迟=安全方向,不宽限(宽限只会把安全方向再推远)。但要留痕。
|
|
95
|
+
opts.onJump?.({ jumpMs: Math.round(driftMs), graceMs: 0 });
|
|
96
|
+
}
|
|
97
|
+
if (graceUntilMono !== undefined && burstStartMono !== undefined) {
|
|
98
|
+
const elapsedInBurst = mono - burstStartMono;
|
|
99
|
+
if (elapsedInBurst >= maxTotalGrace) {
|
|
100
|
+
// 撞上总上限 ⇒ **响亮**恢复判定(不是悄悄放行)。绝对兜底另有一层:`checkpoint.terminal_at_ms`
|
|
101
|
+
// 由别的 reap 腿执行、不经本守卫,所以即使这里也失灵,悬挂仍不可能无限。
|
|
102
|
+
opts.onGraceCapped?.({ totalGraceMs: Math.round(elapsedInBurst) });
|
|
103
|
+
graceUntilMono = undefined;
|
|
104
|
+
burstStartMono = undefined;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (mono < graceUntilMono) {
|
|
108
|
+
opts.onSuppressed?.({ remainingMs: Math.round(graceUntilMono - mono) });
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
graceUntilMono = undefined;
|
|
112
|
+
burstStartMono = undefined;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=wall-clock-jump-guard.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.31.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",
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { FileError, StubExecutionEnv, type Result } from "@sema-agent/core";
|
|
2
|
-
/**
|
|
3
|
-
* 无文件系统的路径裁决 env(见文件头)。只覆写 `absolutePath` 一面,其余全部继承 `StubExecutionEnv`
|
|
4
|
-
* 的 `not_supported` ——**继承而非逐一手写**是刻意的:core 日后给 `ExecutionEnv` 加必填面时,新面会
|
|
5
|
-
* 随 `StubExecutionEnv` 一起到位并保持同一个诚实答案,不会在这里留下一个悄悄编出来的假答案。
|
|
6
|
-
*/
|
|
7
|
-
export declare class LexicalPathExecutionEnv extends StubExecutionEnv {
|
|
8
|
-
absolutePath(path: string): Promise<Result<string, FileError>>;
|
|
9
|
-
}
|
|
10
|
-
//# sourceMappingURL=lexical-path-env.d.ts.map
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #156 —— 非 host lane(e2b/k8s/ssh/adb/local-docker)写门的**纯路径规则**过渡形所用的裁决 env。
|
|
3
|
-
*
|
|
4
|
-
* ## 它解决的缺口
|
|
5
|
-
*
|
|
6
|
-
* core 的 `createFsWriteGatePolicy` / `createSensitivePathPolicy` 都把写目标交给
|
|
7
|
-
* `canonicalizeTarget(env, …)` 去「问 fs 要真身」。host lane 上那个 env 就是 hand 工具真正写的那块盘,
|
|
8
|
-
* 答案可信。沙箱 lane 不是:per-task 的沙箱 env 由 core 的 `executionEnvFactory` 在 spec **之后**才铸,
|
|
9
|
-
* spec 期手边只有 worker 本机的 fs——拿它去裁沙箱里的路径会答错(exists/symlink 全是别人机器上的事实),
|
|
10
|
-
* 而**错误的 allow 比没有门更糟**。所以 [816]/[820] 当年在沙箱 lane 诚实不挂门(wiring=undefined ⇒
|
|
11
|
-
* 三个模式臂全回落 base 规则,写门整条缺席)。#153 的 ③ 号注把这笔记成了余款,#156 是它的过渡还款。
|
|
12
|
-
*
|
|
13
|
-
* ## 这个 env 是什么
|
|
14
|
-
*
|
|
15
|
-
* 一个**没有文件系统**的 env:除 `absolutePath` 外的每一个 fs 面(exists / canonicalPath / fileInfo /
|
|
16
|
-
* read* / write* / listDir / createDir / …)与 shell 面都继承 core 的 `StubExecutionEnv`,
|
|
17
|
-
* 恒返回 `not_supported` 错误(`readLink` 是可选面,Stub 干脆没有——core 把缺席当「symlink 不可解」,
|
|
18
|
-
* 且该分支在本 env 恒不可达:exists 先错就进了 canonicalizeNewPath)——这不是伪装,这就是本 env 的诚实回答:**spec 期我们确实不知道沙箱里
|
|
19
|
-
* 那块盘上有什么**。`absolutePath` 是唯一被赋予真实语义的一面,而它本来就是纯路径运算(不碰盘):
|
|
20
|
-
* POSIX 绝对路径做词法归一后原样奉还,其余一律「不知道」。
|
|
21
|
-
*
|
|
22
|
-
* ## 由此得到的裁决语义(core dist 亲读推出,`tools/fs/safety.js` canonicalizeTarget)
|
|
23
|
-
*
|
|
24
|
-
* · 绝对路径(`/…`):`absolutePath` ok ⇒ core 进 exists 探测 ⇒ 我们返回错误 ⇒ core 按「该路径尚不存在」
|
|
25
|
-
* 走 `canonicalizeNewPath`,那里逐级 exists 也全错 ⇒ 原样返回我们给的词法绝对路径。于是 canonical key
|
|
26
|
-
* = **词法归一后的路径**,`canonicalPath` / `fileInfo` / `readLink` 三面在这条路上根本不可达。
|
|
27
|
-
* · 相对路径 / `~/…` / Windows 盘符形:`absolutePath` 返回错误 ⇒ `canon.ok=false` ⇒ 写门直接
|
|
28
|
-
* `ask`(fail-closed)。沙箱的 cwd 在 spec 期不可知,这正是我们要的偏置——**不猜沙箱工作目录**。
|
|
29
|
-
* · 反斜杠 UNC 形(`\\host\share\…`)是唯一**不经过**本 env 的形:core 的 `canonicalizeTarget` 在入口
|
|
30
|
-
* 就把它短路成 `ok:true, key=原样`(复审 2026-08-05 dist 亲读+实测)。结果不变——写门无放行域可越,
|
|
31
|
-
* 落 defaultWrite 的 `ask`;sensitive 段匹配按 `[\\/]` 双分隔符切段,`\\host\share\.env` 照样 deny。
|
|
32
|
-
* · sensitivePatterns 的 deny 腿照常施加:它按 canonical key 的**路径段**做 glob 匹配,词法 key 足够,
|
|
33
|
-
* 且在 `combinePolicies` 折叠里 deny 恒胜(session 豁免 / accept 域越不过)。
|
|
34
|
-
* · `isExempt`(会话「本会话不再询问」探针)是 name-keyed 的,不做 fs 裁决,任何 lane 都安全,照接。
|
|
35
|
-
*
|
|
36
|
-
* ## 已知残余面(过渡形的边界,不是疏漏)
|
|
37
|
-
*
|
|
38
|
-
* · **symlink 形**:词法裁决看不见符号链接。沙箱里一个名字普通的软链可以指向守卫段(`.ssh` 等),
|
|
39
|
-
* 我们只会给 `ask` 而不是 `deny`;host lane 上 core 会 canonicalize 出真身并 deny。相应地,core 的
|
|
40
|
-
* `unresolvedSymlink ⇒ deny` 那条腿在本 env 下永不触发。注意豁免会话下的口径(codex 复审 E):
|
|
41
|
-
* name-keyed 豁免不看路径,这类词法无害的软链目标在豁免会话里是 **allow** 而非 ask——这是本残余面
|
|
42
|
-
* 在「操作员已授 don't-ask-again」情形下的完整代价,host lane 同情形仍会 deny。
|
|
43
|
-
* · **exempt/accept 域**:沙箱 lane 既无可信 cwd 也无 scratchpad 对应物,故本过渡形**一个自动放行域都
|
|
44
|
-
* 不铸**(见 resolve-spec 的 wiring)。代价是 acceptEdits 在沙箱 lane 退化成与 default 同形(全 ask);
|
|
45
|
-
* 这是 fail-safe 方向,与 host lane「accept 域解析不出 ⇒ 该域不生效」的哨兵先例同口径。
|
|
46
|
-
* · **`cwd` 字段**:继承 `StubExecutionEnv` 的 `"/"` 占位。本 env 只活在 policy 折叠里,core 的两个
|
|
47
|
-
* policy 都不读 `env.cwd`(相对路径基准走 `rootPath` 形参,而我们不给)——它不代表沙箱的工作目录。
|
|
48
|
-
*
|
|
49
|
-
* ## 终局 seam
|
|
50
|
-
*
|
|
51
|
-
* 正解是让写门在**工具执行时刻**拿到真沙箱 env:core [2751] 已排期给 `HookToolContext` 加一个只读的
|
|
52
|
-
* env 能力窄面(174/#22 窗)。那一面到货后,本过渡形整体退役换正解——届时沙箱 lane 与 host lane 走同
|
|
53
|
-
* 一条真 fs 裁决,上面三条残余面一并消失。
|
|
54
|
-
*/
|
|
55
|
-
import { posix } from "node:path";
|
|
56
|
-
import { FileError, StubExecutionEnv, err, ok } from "@sema-agent/core";
|
|
57
|
-
/** 词法归一:纯字符串运算,折 `.` / `..` / 重复分隔符,不碰 fs。
|
|
58
|
-
* `posix.normalize` 把 `..` 在根部截断(`/../x` → `/x`),与「沙箱根之上没有东西」的语义一致。
|
|
59
|
-
* 尾部分隔符统一剥掉(根 `/` 除外),让同一目标只有一个 key ——前缀判域靠的就是 key 的唯一性。
|
|
60
|
-
* (core 内部有同形的 `normalizeAbsPathLexically`,但未从包根导出;此处是 Node 标准库的等价运算,
|
|
61
|
-
* 不是它的抄本——若日后 core 导出,这里应改为直接复用。) */
|
|
62
|
-
function normalizeAbsolutePathLexically(path) {
|
|
63
|
-
const collapsed = posix.normalize(path.replace(/^\/+/, "/"));
|
|
64
|
-
return collapsed.length > 1 ? collapsed.replace(/\/+$/, "") : collapsed;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* 无文件系统的路径裁决 env(见文件头)。只覆写 `absolutePath` 一面,其余全部继承 `StubExecutionEnv`
|
|
68
|
-
* 的 `not_supported` ——**继承而非逐一手写**是刻意的:core 日后给 `ExecutionEnv` 加必填面时,新面会
|
|
69
|
-
* 随 `StubExecutionEnv` 一起到位并保持同一个诚实答案,不会在这里留下一个悄悄编出来的假答案。
|
|
70
|
-
*/
|
|
71
|
-
export class LexicalPathExecutionEnv extends StubExecutionEnv {
|
|
72
|
-
absolutePath(path) {
|
|
73
|
-
// POSIX 绝对形是沙箱 lane(全 Linux)上 hand 工具的书面契约形。其余一切——相对路径、`~`、
|
|
74
|
-
// `C:\…`——在 spec 期都无法诚实解析成一个沙箱内的绝对路径,返回错误让 core 落 ask。
|
|
75
|
-
// (反斜杠 UNC 形根本到不了这里:core 在 canonicalizeTarget 入口短路,见文件头「由此得到的裁决语义」。)
|
|
76
|
-
// codex 复审 A(2026-08-05):含 NUL 的字符串不是合法 POSIX 路径(任何 fs 面都写不进去),但词法归一
|
|
77
|
-
// 会照样给它铸出 canonical key——`\0` 尾巴让守卫段 glob 失配,豁免会话下还能拿 allow。拒收让 canon
|
|
78
|
-
// 在豁免咨询**之前**就失败 ⇒ 恒 ask。
|
|
79
|
-
if (path.includes("\u0000")) {
|
|
80
|
-
return Promise.resolve(err(new FileError("not_supported", "this environment adjudicates paths lexically and rejects a path containing a NUL byte (not a representable POSIX path)", path)));
|
|
81
|
-
}
|
|
82
|
-
if (!path.startsWith("/")) {
|
|
83
|
-
return Promise.resolve(err(new FileError("not_supported", "this environment adjudicates paths lexically and cannot resolve a non-absolute path (the sandbox working directory is unknown at spec time)", path)));
|
|
84
|
-
}
|
|
85
|
-
return Promise.resolve(ok(normalizeAbsolutePathLexically(path)));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
//# sourceMappingURL=lexical-path-env.js.map
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { ToolSpec } from "@sema-agent/core";
|
|
2
|
-
/**
|
|
3
|
-
* OA scenario — tools are thin HTTP clients to the OA backend's existing APIs (read = /api/v1/*,
|
|
4
|
-
* write = /api/assets/*). The shared service token + the request's principal are forwarded so OA
|
|
5
|
-
* enforces THAT end-user's RBAC + audit (the service never holds per-user creds). Decoupled: the
|
|
6
|
-
* service knows no OA schema/DB — OA stays the source of truth (its /api/v1/meta is self-describing).
|
|
7
|
-
*/
|
|
8
|
-
export interface OaCtx {
|
|
9
|
-
baseUrl: string;
|
|
10
|
-
token: string;
|
|
11
|
-
principal?: string;
|
|
12
|
-
}
|
|
13
|
-
export declare function oaTools(ctx: OaCtx): ToolSpec[];
|
|
14
|
-
export declare const oaPrompt: import("@sema-agent/core").PromptProvider;
|
|
15
|
-
//# sourceMappingURL=oa-tools.d.ts.map
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import { stablePrompt } from "./prompt.js";
|
|
3
|
-
async function oaFetch(ctx, path, init) {
|
|
4
|
-
if (!ctx.baseUrl || !ctx.token)
|
|
5
|
-
return "OA 后端未配置(OA_API_BASEURL / OA_SERVICE_TOKEN)";
|
|
6
|
-
try {
|
|
7
|
-
const res = await fetch(ctx.baseUrl + path, {
|
|
8
|
-
method: init?.method ?? "GET",
|
|
9
|
-
headers: {
|
|
10
|
-
"content-type": "application/json",
|
|
11
|
-
authorization: `Bearer ${ctx.token}`,
|
|
12
|
-
...(ctx.principal ? { "x-agent-principal": ctx.principal } : {}),
|
|
13
|
-
},
|
|
14
|
-
body: init?.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
15
|
-
});
|
|
16
|
-
const text = await res.text();
|
|
17
|
-
if (!res.ok)
|
|
18
|
-
return `OA ${res.status}: ${text.slice(0, 600)}`;
|
|
19
|
-
return text.slice(0, 60_000);
|
|
20
|
-
}
|
|
21
|
-
catch (e) {
|
|
22
|
-
return `调用 OA 失败:${e.message}`;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const needWrite = (ctx) => (!ctx.principal ? "缺少身份(principal),无法代表用户写库;请让调用方带上 x-agent-principal。" : null);
|
|
26
|
-
export function oaTools(ctx) {
|
|
27
|
-
const obj = Type.Object({}, { additionalProperties: true });
|
|
28
|
-
return [
|
|
29
|
-
{ name: "oa_schema", effect: "read", description: "取 OA 数据结构(每类资产的字段/必填/条件必填 requiredWhen/联动 showWhen/枚举、生命周期、表名)。建/改前先调它,别凭记忆。", parameters: Type.Object({}), execute: () => oaFetch(ctx, "/api/v1/meta") },
|
|
30
|
-
{ name: "oa_alerts", effect: "read", description: "派生告警:GP 闲置(已注册未变现) / APP 未上架(已接广告未 LIVE)。", parameters: Type.Object({}), execute: () => oaFetch(ctx, "/api/v1/alerts") },
|
|
31
|
-
{ name: "oa_list", effect: "read", description: "列出某类资产。type∈gp/package/ad-platform/ad-unit/email/environment/proxy。可选 q 关键字。", parameters: Type.Object({ type: Type.String(), q: Type.Optional(Type.String()) }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/entities/${encodeURIComponent(x.type)}${x.q ? `?q=${encodeURIComponent(x.q)}` : ""}`); } },
|
|
32
|
-
{ name: "oa_get", effect: "read", description: "取某资产详情(敏感字段为掩码)。", parameters: Type.Object({ type: Type.String(), id: Type.String() }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/entities/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}`); } },
|
|
33
|
-
{ name: "oa_lookup", effect: "read", description: "约定字段值反查(同手机/证件=同一实物)。field: phone|idCardNo|bizLicenseNo。", parameters: Type.Object({ field: Type.String(), value: Type.String() }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/lookup?field=${encodeURIComponent(x.field)}&value=${encodeURIComponent(x.value)}`); } },
|
|
34
|
-
{ name: "oa_create", effect: "write", description: "创建实体(走 OA 校验+当前用户 RBAC+审计)。type + data(字段对象,参照 oa_schema)。失败会返回缺失/非法字段。", parameters: Type.Object({ type: Type.String(), data: obj }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
35
|
-
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}`, { method: "POST", body: x.data ?? {} }); } },
|
|
36
|
-
{ name: "oa_update", effect: "write", description: "改字段(敏感留空=保持原值;状态字段改不动,用 oa_transition)。", parameters: Type.Object({ type: Type.String(), id: Type.String(), data: obj }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
37
|
-
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}`, { method: "PATCH", body: x.data ?? {} }); } },
|
|
38
|
-
{ name: "oa_transition", effect: "write", description: "受控状态流转(先 oa_schema 看可用迁移;风险/弃用等需 note)。", parameters: Type.Object({ type: Type.String(), id: Type.String(), to: Type.String(), note: Type.Optional(Type.String()) }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
39
|
-
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}/transition`, { method: "POST", body: { to: x.to, note: x.note ?? null } }); } },
|
|
40
|
-
];
|
|
41
|
-
}
|
|
42
|
-
// OA business prompt MOVED OUT — migration complete (design/29, 2026-06-05). Persona / 工作准则 / UI
|
|
43
|
-
// markers (::FORM:: / ::GOTO:: / ::SUGGEST:: / ::ISSUE_FORM::) / 分级确认 / 报bug流程 are OA's business,
|
|
44
|
-
// and OA now injects the WHOLE thing per request via `request.systemPrompt` (it holds the service token,
|
|
45
|
-
// content is stable per OA version → cacheable prefix; page context still rides `objective`). Keeping
|
|
46
|
-
// any of it here would just DUPLICATE OA's prompt, so the base is EMPTY — single source of truth = OA.
|
|
47
|
-
// The service stays business-agnostic: it provides the oa_* tools (referenced by NAME from OA's prompt;
|
|
48
|
-
// they carry their own descriptions) and nothing else. `stablePrompt` keeps STABLE→VARIABLE order —
|
|
49
|
-
// OA's systemPrompt is appended after this (empty) base, before core's <user_memory>.
|
|
50
|
-
// (::ISSUE_FORM:: is parsed/rendered + submitted entirely by OA's frontend via OA /api/ai/issue — the
|
|
51
|
-
// service never touches it; `open_gitea_issue` remains only as the unattended/automated fallback.)
|
|
52
|
-
const OA_SYSTEM = "";
|
|
53
|
-
export const oaPrompt = stablePrompt(OA_SYSTEM);
|
|
54
|
-
//# sourceMappingURL=oa-tools.js.map
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export interface InfraCostRates {
|
|
2
|
-
toolCallMicroUsd: number;
|
|
3
|
-
sandboxSecMicroUsd: number;
|
|
4
|
-
egressGbMicroUsd: number;
|
|
5
|
-
}
|
|
6
|
-
export interface InfraUsage {
|
|
7
|
-
toolCalls: number;
|
|
8
|
-
sandboxWalltimeMs: number;
|
|
9
|
-
egressBytes: number;
|
|
10
|
-
}
|
|
11
|
-
export interface InfraCostBreakdown {
|
|
12
|
-
toolCallMicroUsd: number;
|
|
13
|
-
sandboxWalltimeMicroUsd: number;
|
|
14
|
-
egressMicroUsd: number;
|
|
15
|
-
totalMicroUsd: number;
|
|
16
|
-
}
|
|
17
|
-
export interface LlmCostBreakdown {
|
|
18
|
-
llmRootMicroUsd: number;
|
|
19
|
-
nestedSubagentMicroUsd: number;
|
|
20
|
-
memoryConsolidationMicroUsd: number;
|
|
21
|
-
compactionMicroUsd: number;
|
|
22
|
-
}
|
|
23
|
-
export interface SupervisorCostBreakdown {
|
|
24
|
-
llm: LlmCostBreakdown | null;
|
|
25
|
-
infra: InfraCostBreakdown;
|
|
26
|
-
totalMicroUsd: number;
|
|
27
|
-
}
|
|
28
|
-
export declare function infraCost(usage: InfraUsage, rates: InfraCostRates): InfraCostBreakdown;
|
|
29
|
-
export declare function composeSupervisorCost(llm: LlmCostBreakdown | null, llmTotalMicroUsd: number, infra: InfraCostBreakdown): SupervisorCostBreakdown;
|
|
30
|
-
export declare function hasInfraPricing(r: InfraCostRates): boolean;
|
|
31
|
-
export declare function infraUsageFromEvents(events: ReadonlyArray<{
|
|
32
|
-
type?: string;
|
|
33
|
-
}>, runDurationMs: number): InfraUsage;
|
|
34
|
-
//# sourceMappingURL=cost-taxonomy.d.ts.map
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const nonNeg = (n) => (Number.isFinite(n) && n > 0 ? n : 0);
|
|
2
|
-
export function infraCost(usage, rates) {
|
|
3
|
-
const toolCallMicroUsd = nonNeg(usage.toolCalls) * nonNeg(rates.toolCallMicroUsd);
|
|
4
|
-
const sandboxWalltimeMicroUsd = (nonNeg(usage.sandboxWalltimeMs) / 1000) * nonNeg(rates.sandboxSecMicroUsd);
|
|
5
|
-
const egressMicroUsd = (nonNeg(usage.egressBytes) / 1_000_000_000) * nonNeg(rates.egressGbMicroUsd);
|
|
6
|
-
return {
|
|
7
|
-
toolCallMicroUsd,
|
|
8
|
-
sandboxWalltimeMicroUsd,
|
|
9
|
-
egressMicroUsd,
|
|
10
|
-
totalMicroUsd: toolCallMicroUsd + sandboxWalltimeMicroUsd + egressMicroUsd,
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
export function composeSupervisorCost(llm, llmTotalMicroUsd, infra) {
|
|
14
|
-
return { llm, infra, totalMicroUsd: nonNeg(llmTotalMicroUsd) + infra.totalMicroUsd };
|
|
15
|
-
}
|
|
16
|
-
export function hasInfraPricing(r) {
|
|
17
|
-
return r.toolCallMicroUsd > 0 || r.sandboxSecMicroUsd > 0 || r.egressGbMicroUsd > 0;
|
|
18
|
-
}
|
|
19
|
-
export function infraUsageFromEvents(events, runDurationMs) {
|
|
20
|
-
return {
|
|
21
|
-
toolCalls: events.reduce((n, e) => (e.type === "tool_start" ? n + 1 : n), 0),
|
|
22
|
-
sandboxWalltimeMs: nonNeg(runDurationMs),
|
|
23
|
-
egressBytes: 0,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
//# sourceMappingURL=cost-taxonomy.js.map
|