@sema-agent/server 7.10.0 → 7.12.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 +1 -1
- package/USAGE.md +56 -0
- package/dist/adoption/plan.d.ts +152 -0
- package/dist/adoption/plan.js +513 -0
- package/dist/adoption/runner.d.ts +54 -0
- package/dist/adoption/runner.js +505 -0
- package/dist/adoption/sql.d.ts +76 -0
- package/dist/adoption/sql.js +106 -0
- package/dist/adoption/wire.d.ts +250 -0
- package/dist/adoption/wire.js +153 -0
- package/dist/approval-card.d.ts +24 -0
- package/dist/approval-card.js +32 -0
- package/dist/auth-keys.d.ts +28 -4
- package/dist/auth-keys.js +60 -15
- package/dist/boot/adoption.d.ts +30 -0
- package/dist/boot/adoption.js +57 -0
- package/dist/boot/coordinators.d.ts +4 -0
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/parked-revive-gate.d.ts +56 -7
- package/dist/boot/parked-revive-gate.js +177 -8
- package/dist/boot/permission-rules-audit.d.ts +49 -0
- package/dist/boot/permission-rules-audit.js +57 -0
- package/dist/boot/resolve-spec.js +43 -12
- package/dist/boot/runner-deps.d.ts +10 -2
- package/dist/boot/runner-deps.js +12 -1
- package/dist/budget.js +22 -0
- package/dist/config-types.d.ts +24 -1
- package/dist/config.d.ts +28 -2
- package/dist/config.js +348 -75
- package/dist/governance-ask-marks.js +8 -2
- package/dist/http/route-ctx.d.ts +6 -3
- package/dist/http/routes/adoption.d.ts +26 -0
- package/dist/http/routes/adoption.js +120 -0
- package/dist/http/routes/approvals-assistant.js +2 -1
- package/dist/http/routes/capabilities.js +49 -2
- package/dist/http/routes/rules.d.ts +35 -0
- package/dist/http/routes/rules.js +293 -0
- package/dist/http/routes/shared-memory.d.ts +31 -0
- package/dist/http/routes/shared-memory.js +181 -0
- package/dist/http/routes/trace-usage.js +139 -3
- package/dist/http/server.d.ts +23 -3
- package/dist/http/server.js +123 -3
- package/dist/http/wire-types.d.ts +48 -0
- package/dist/main.js +70 -3
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/metrics.js +2 -1
- package/dist/observability/tool-trace.d.ts +5 -1
- package/dist/observability/tool-trace.js +33 -6
- package/dist/parked-decide.d.ts +13 -3
- package/dist/parked-decide.js +10 -1
- package/dist/plugins/adoption-log-sql.d.ts +191 -0
- package/dist/plugins/adoption-log-sql.js +273 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +13 -0
- package/dist/plugins/checkpoint-store-sql.js +11 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +8 -0
- package/dist/plugins/permission-rule-store-file.d.ts +83 -0
- package/dist/plugins/permission-rule-store-file.js +371 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +249 -0
- package/dist/plugins/permission-rule-store-sql.js +828 -0
- package/dist/plugins/pg-pool.js +37 -0
- package/dist/plugins/session-policy-store-sql.d.ts +6 -0
- package/dist/plugins/session-policy-store-sql.js +7 -1
- package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
- package/dist/plugins/shared-memory-store-sql.js +516 -0
- package/dist/plugins/store-backend.d.ts +38 -2
- package/dist/plugins/store-backend.js +69 -6
- package/dist/plugins/tidb-pool.js +47 -0
- package/dist/rules-consent.d.ts +194 -0
- package/dist/rules-consent.js +240 -0
- package/dist/run-local.js +120 -13
- package/dist/runtime-governance.js +9 -3
- package/dist/shared-memory-scope-authorizer.d.ts +29 -0
- package/dist/shared-memory-scope-authorizer.js +17 -0
- package/dist/task-settings.d.ts +44 -0
- package/dist/task-settings.js +57 -1
- package/dist/tool-approval.d.ts +60 -0
- package/dist/tool-approval.js +223 -25
- package/dist/trace/core-keyset-guard.d.ts +15 -4
- package/dist/trace/project.d.ts +20 -2
- package/dist/trace/project.js +25 -4
- package/package.json +3 -3
package/dist/task-settings.js
CHANGED
|
@@ -78,6 +78,60 @@ export function withPermissionMode(settings, mode) {
|
|
|
78
78
|
const base = settings ?? {};
|
|
79
79
|
return { ...base, permissions: { ...base.permissions, defaultMode: mode } };
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* design/201 §6-1 —— 本请求的**生效** permission mode,单点。优先序逐字同 {@link withPermissionMode}:
|
|
83
|
+
* 顶层 `body.permissionMode`(本轮显式表态)赢过 bundle 的 `settings.permissions.defaultMode`;两处
|
|
84
|
+
* 都没有 ⇒ `undefined` = **无表态**(调用点据此不写键,而不是替调用方选一个默认值)。
|
|
85
|
+
*
|
|
86
|
+
* 🔴 为什么必须是一只函数:同一个「生效模式」此前在 resolve-spec 里被算了两次(hooks 腿的
|
|
87
|
+
* `permission_mode` 载荷、settings 折叠腿的 `withPermissionMode`),三审同点判定翻译表**不得**成为
|
|
88
|
+
* 第三个算点 —— 三处各算各的,任何一次优先序修订都会让三面分家,而分家在这条轴上的形态是
|
|
89
|
+
* 「壳发的 bundle bypass 在 A 面生效、在 B 面没生效」这种最难被外部发现的静默偏差。
|
|
90
|
+
*/
|
|
91
|
+
export function effectivePermissionMode(body, settings) {
|
|
92
|
+
return coercePermissionMode(body.permissionMode) ?? settings?.permissions?.defaultMode;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* design/201 §2 —— 显式 permission mode → `TaskSpec.shellGate` 档位的**翻译表(单一真源)**。
|
|
96
|
+
*
|
|
97
|
+
* · `bypassPermissions` ⇒ `"off"` —— CC `--dangerously-skip-permissions` 的对位裁定
|
|
98
|
+
* (与 fs-write 面的 bypass 臂两面归一,见 {@link deriveSettingsPolicy})。
|
|
99
|
+
* · `auto` ⇒ `"classify"` —— **design/201 §8 开口按施工期新披露亲裁改译**(原裁定字面是 off,
|
|
100
|
+
* §8 保留「core auto 分类器覆盖面有新披露时可独立再裁,表驱动一行」——新披露见下段):
|
|
101
|
+
*
|
|
102
|
+
* ⚠️ **`auto` 这一行有一条待裁的开口(design/201 §8 明列「core auto 分类器覆盖面有新披露时可独立再裁,
|
|
103
|
+
* 表驱动一行」)。施工期对抗复审给出了那条新披露,已亲读安装包核实**:
|
|
104
|
+
* · core 只在**已经产生 `ask`** 之后才咨询分类器(`dist/core/hooks.js`:`if (input.autoMode &&
|
|
105
|
+
* decision.action === "ask" …)`);
|
|
106
|
+
* · 而 `shellGate:"off"` 下 core **不铸任何 shell 面的门**(`dist/core/runner/prepare-task.js` 的
|
|
107
|
+
* `shellGate === "off"` 臂反而发一条 `classification:"shell-gate-off"` 的 onError:「真可写 Bash 挂着
|
|
108
|
+
* 却没有 shell 安全轴折叠」)。
|
|
109
|
+
* 两条合起来:`auto` × off ⇒ Bash 既不产 ask、分类器也就永不被咨询 —— off 档下「筛选权交给分类器」
|
|
110
|
+
* 不成立。故 auto 归 classify 组:分类器坐在 classify 产的 ask 之上,语义才真是「交给分类器」
|
|
111
|
+
* (良性只读命令由分类器自动放行,其余 ask;这正是 auto 模式的本义)。发车帖向 [3378] 裁定链披露此
|
|
112
|
+
* 一行偏离;core auto 分类器若来日在 off 档下也有咨询点,可再裁回(表驱动一行)。
|
|
113
|
+
* · `default` / `acceptEdits` / `plan` ⇒ `"classify"` —— 这一档原先由壳无条件注入 `MANUAL_MODE_SHELL_GATE`
|
|
114
|
+
* 供给(车道缺省走了 operator 通道),现在归位到表态轴。`plan` 本身 handsReadOnly(core 明写此档下
|
|
115
|
+
* shellGate 无效),给它 classify 纯为一致性。
|
|
116
|
+
*
|
|
117
|
+
* 🔴 **入参非可选**:缺席(无表态)不是这张表的一行 —— 它的语义是「不写这个键」,只能在调用点判。
|
|
118
|
+
* 把它折进来会逼出一个 `undefined` 返回值,而那正是「写 off」与「不写」被混同的入口(core 的
|
|
119
|
+
* 缺席默认是 off,但**写**一个 off 会成为 governance 的 base,与缺席不是同一件事)。
|
|
120
|
+
*
|
|
121
|
+
* 🔴 **闭集 exhaustive switch,无 default 臂**:五个模式词是封闭词表,新增一个模式而不更新本表
|
|
122
|
+
* 是**编译错误**(#157 的安全轴纪律:词表的未知项不许有静默兜底臂)。
|
|
123
|
+
*/
|
|
124
|
+
export function shellGateForMode(effMode) {
|
|
125
|
+
switch (effMode) {
|
|
126
|
+
case "bypassPermissions":
|
|
127
|
+
return "off";
|
|
128
|
+
case "auto":
|
|
129
|
+
case "default":
|
|
130
|
+
case "acceptEdits":
|
|
131
|
+
case "plan":
|
|
132
|
+
return "classify";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
81
135
|
/** Cap on `outputStyle` length — it lands in the system prompt, so it shares the `MAX_SYSTEM_PROMPT_CHARS` (16384)
|
|
82
136
|
* posture (an uncapped prompt body is a per-turn token-cost hole; adversarial review). Exported so the HTTP layer
|
|
83
137
|
* (prepareSpec) can 400 fail-loud on submit with the SAME bound this defensive parse enforces on every path. */
|
|
@@ -470,7 +524,9 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
|
470
524
|
* Returns `base` untouched when the parsed settings project nothing onto the spec.
|
|
471
525
|
*/
|
|
472
526
|
// ([1557]§四 的 SHELL_GATE_RANK 副本与 shellGateSafe 守卫已随 #153 搬 runtime-governance ——
|
|
473
|
-
// settings 折叠不再触碰 TaskSpec.shellGate,base 上 governance 层施加的值原样透传。
|
|
527
|
+
// settings 折叠不再触碰 TaskSpec.shellGate,base 上 governance 层施加的值原样透传。design/201 的
|
|
528
|
+
// {@link shellGateForMode} 也不改这句话:那只纯函数产的是 governance 的 base,由 resolve-spec 阶段④
|
|
529
|
+
// 写进 spec 字面量;本折叠拿到的 `base` 里可能因此已经带着一档 shellGate,而它照旧原样透传。)
|
|
474
530
|
export function applyTaskSettings(base, settings, gate, workflowGate) {
|
|
475
531
|
const { toolPolicy, handsReadOnly, enablePlanMode } = deriveSettingsPolicy(settings, gate, workflowGate);
|
|
476
532
|
let next = base;
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AskRequest, type AskOutcome } from "@sema-agent/core";
|
|
2
|
+
import type { CardRulePersisted, RuleConsentLane } from "./rules-consent.js";
|
|
2
3
|
import { type ApprovalRequestFrame, type ApprovalRevokeFrame } from "./approval-card.js";
|
|
3
4
|
import type { ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
|
|
4
5
|
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
@@ -60,6 +61,21 @@ export interface ToolApprovalFrame {
|
|
|
60
61
|
* `governance-ask-marks.ts` 顶注。
|
|
61
62
|
*/
|
|
62
63
|
governanceForced?: true;
|
|
64
|
+
/**
|
|
65
|
+
* #154 车二(core 5.18.0 design/179):`"tool_approval"` only —— 引擎为这次 ask 铸的**规则候选**
|
|
66
|
+
* (`AskRequest.ruleSuggestions`,逐字透传:闭词表 `match` + 引擎铸的文本,server 不重铸不重排)。
|
|
67
|
+
* 壳据它渲「不再询问」,回决时用 `persistRule.rule` 报出选中的那一条。
|
|
68
|
+
*
|
|
69
|
+
* 🔴 **在场性即承诺**:只在**规则店真装配**(协调器拿到 `ruleConsent`,与 core 的
|
|
70
|
+
* `RunnerDeps.permissionRuleStore` 同源于 main.ts 的同一个对象 ⇒ 与 manifest 的
|
|
71
|
+
* `permissionRules.storeWired` 不可能相左)时在场。店缺席仍投 = 一格按下去无处可兑的「不再询问」,
|
|
72
|
+
* 是 wire 谎言(判据逐字见 `trace/core-keyset-guard.ts` ④ 面本键那一段)。
|
|
73
|
+
*/
|
|
74
|
+
ruleSuggestions?: ReadonlyArray<{
|
|
75
|
+
rule: string;
|
|
76
|
+
match: "exact" | "prefix";
|
|
77
|
+
command: string;
|
|
78
|
+
}>;
|
|
63
79
|
/** "tool_approval" only: the tool call's args, secret-redacted, UNTRUSTED-for-display. Absent (with
|
|
64
80
|
* `argsOmitted: true`) when over the byte cap or unserializable. */
|
|
65
81
|
args?: unknown;
|
|
@@ -119,11 +135,29 @@ export interface ToolApprovalRunContext {
|
|
|
119
135
|
legDeadlineMonotonic?: number;
|
|
120
136
|
}
|
|
121
137
|
export type ToolApprovalDecision = "allow" | "allow_session" | "deny";
|
|
138
|
+
/**
|
|
139
|
+
* #154 车二:回决回执上 `ruleRefusal` 的**闭词表**(「不再询问」这次为什么没存上)。
|
|
140
|
+
*
|
|
141
|
+
* 🔴 为什么是闭集而不是 `string`(#157 词表纪律):这一格是 wire 可见的**机器可读**位 —— 壳要据它分
|
|
142
|
+
* 「这次不能存」(`rule_input_edited`:编辑过输入,换下一次)与「这台部署压根不供规则」
|
|
143
|
+
* (`rule_lane_unavailable`)。自由串会让每个消费端各自猜词,而 server 加一个新拒绝理由时没人会红。
|
|
144
|
+
* 三格 server 自铸 + 车道自己的四格(`CardRulePersisted["reason"]`,从那个类型**派生**而不是抄):
|
|
145
|
+
* 车道加员 ⇒ 这里编译期自动跟。
|
|
146
|
+
*/
|
|
147
|
+
export type RuleRefusalReason = Extract<CardRulePersisted, {
|
|
148
|
+
ok: false;
|
|
149
|
+
}>["reason"] | "rule_lane_unavailable" | "rule_input_edited" | "rule_not_offered" | "rule_store_error"
|
|
150
|
+
/** #204 件7:这只 ask 的门来自运维治理层(`governanceForced`)⇒ 它不进规则车道。**与
|
|
151
|
+
* `rule_lane_unavailable` 刻意分词**:那个说的是「这台部署压根不供规则」(壳可以从此不渲这一格),
|
|
152
|
+
* 这个说的是「规则车道好好的,只是**这一只** ask 归 operator 管」—— 折成同一个词会让壳把一台正常
|
|
153
|
+
* 部署整条车道判死。 */
|
|
154
|
+
| "rule_governance_forced";
|
|
122
155
|
/** Validate the respond body — the closed three-choice enum (rationale in the module header). */
|
|
123
156
|
export declare function parseToolApprovalResponse(body: unknown): {
|
|
124
157
|
ok: true;
|
|
125
158
|
value: ToolApprovalDecision;
|
|
126
159
|
updatedInput?: unknown;
|
|
160
|
+
persistRule?: string;
|
|
127
161
|
} | {
|
|
128
162
|
ok: false;
|
|
129
163
|
error: string;
|
|
@@ -310,6 +344,10 @@ export declare class ToolApprovalCoordinator {
|
|
|
310
344
|
* 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
|
|
311
345
|
* 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
|
|
312
346
|
private readonly governanceAskMarks;
|
|
347
|
+
/** #154 车二:持久化权限规则的**同意车道**。在场 ⇔ 规则店真装配(main.ts 与 core 的
|
|
348
|
+
* `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `ruleSuggestions`;②回决带
|
|
349
|
+
* `persistRule` 时兑付进店。缺席 ⇒ 两件都不做(诚实缺席,不发无处可兑的候选)。 */
|
|
350
|
+
private readonly ruleConsent;
|
|
313
351
|
constructor(opts?: {
|
|
314
352
|
ttlMs?: number;
|
|
315
353
|
askStore?: ApprovalAskStore;
|
|
@@ -318,6 +356,8 @@ export declare class ToolApprovalCoordinator {
|
|
|
318
356
|
admitMaxPerOwner?: number;
|
|
319
357
|
/** 缺省 = 进程级单表(写侧默认同一张)。注入口只为测试与将来的多实例形。 */
|
|
320
358
|
governanceAskMarks?: GovernanceAskMarks;
|
|
359
|
+
/** #154 车二:同意车道(在场 = 规则店已装配)。 */
|
|
360
|
+
ruleConsent?: RuleConsentLane;
|
|
321
361
|
});
|
|
322
362
|
/**
|
|
323
363
|
* #151 车3 刀 3b —— design/172 §3.3 / 设计稿 §0 X-2 的**写侧准入门**。
|
|
@@ -339,6 +379,14 @@ export declare class ToolApprovalCoordinator {
|
|
|
339
379
|
* 多副本级的总量控制登记为后续件(汇报存疑单)。
|
|
340
380
|
*/
|
|
341
381
|
private admit;
|
|
382
|
+
/**
|
|
383
|
+
* #154 车二:一只 ask 的**规则车道素材**,或 `undefined`(= 本 ask 不投候选、不接受 `persistRule`)。
|
|
384
|
+
*
|
|
385
|
+
* 三个合取项(缺一即 `undefined`,理由逐字见调用点上方注):①店在场 ②引擎铸了候选 ③命令原字节可读。
|
|
386
|
+
* `req.args` 是 `unknown` ⇒ 窄读,**禁裸 as-cast**(宪法 [2704]:一个形状漂了的 args 若被 cast,
|
|
387
|
+
* 会把 `undefined` 当命令送进 `prepareCardApproval`,那是放宽面上的静默垃圾)。
|
|
388
|
+
*/
|
|
389
|
+
private buildRuleLaneMaterial;
|
|
342
390
|
/** 测试/可观测性钩子(X-2):某 (taskId) 维当前占用的准入名额数。 */
|
|
343
391
|
admittedCount(taskId: string): number;
|
|
344
392
|
/** #151 车2(D5 store 故障姿势):任何 store 调用 throw ⇒ fail-open 到进程内机械照旧(store 是记账/
|
|
@@ -466,6 +514,18 @@ export declare class ToolApprovalCoordinator {
|
|
|
466
514
|
status: number;
|
|
467
515
|
body: unknown;
|
|
468
516
|
}>;
|
|
517
|
+
/**
|
|
518
|
+
* #154 车二:回决携规则确认 ⇒ prepare→confirm→redeem(实现在 `rules-consent.ts`,本方法只做门与回显)。
|
|
519
|
+
*
|
|
520
|
+
* 三道门,每道都是**拒绝**而不是降级:
|
|
521
|
+
* ① 裁决必须真落定(200)—— 见调用点注;
|
|
522
|
+
* ② 必须有已验明的 owner —— 规则是**某个人**的持久同意,一条 `owner === null` 的 ask 没有归属人可写;
|
|
523
|
+
* ③ 本 ask 必须登记过规则车道素材(店在场 ∧ 引擎铸过候选 ∧ 命令可读)。
|
|
524
|
+
*
|
|
525
|
+
* 失败**不翻转裁决**:人的「允许这次」已经生效并且已经交给引擎了,把整个回决改判成 4xx 会让一次真实的
|
|
526
|
+
* 放行凭空消失。诚实的形是 200 + `rulePersisted:false`(壳据它告诉人「这次放行了,但『不再询问』没存上」)。
|
|
527
|
+
*/
|
|
528
|
+
private persistRuleAfterDecision;
|
|
469
529
|
/** store 在场时的回决 CAS 门(D2)——`respond()` 的异步分支,拆出以保持 `respond()` 本身在 store 缺席
|
|
470
530
|
* 时仍是纯同步函数(D1)。`askStore`/`askId`/`batchId` 由调用方在已窄化的分支里传入(避免非空断言)。 */
|
|
471
531
|
private respondWithCas;
|
package/dist/tool-approval.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* and asks are turn-blocking by construction — the TTL bounds the human-side exposure instead.
|
|
45
45
|
*/
|
|
46
46
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
47
|
-
import { uuidv7 } from "@sema-agent/core";
|
|
47
|
+
import { uuidv7, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
|
|
48
48
|
import { redactDeep, redactSecrets } from "./trace/redact.js";
|
|
49
49
|
import { createLogger } from "./observability/logger.js";
|
|
50
50
|
import { deriveAskId, deriveBatchId } from "./approval-ask-machine.js";
|
|
@@ -145,7 +145,21 @@ export function parseToolApprovalResponse(body) {
|
|
|
145
145
|
// [1458] ctrl+g 编辑放行:allow 族可带 updatedInput(编辑后的 args 整体替换形,durable /decide 腿
|
|
146
146
|
// 同名位既有语义);deny 带=无意义,忽略不 400(宽收)。
|
|
147
147
|
const u = body.updatedInput;
|
|
148
|
-
|
|
148
|
+
// #154 车二:「不再询问」的回决位。**只带候选的文本**,不是自由规则 —— 服务端拿它在引擎铸出的
|
|
149
|
+
// 候选表里定位下标(对不上就拒),于是「点一次某条无害命令换来一条别的规则」在这条路上不可拼写。
|
|
150
|
+
// deny 带 = 无意义(人拒了这次操作,谈不上持久化同意),忽略不 400(同 updatedInput 的宽收姿势)。
|
|
151
|
+
const pr = body.persistRule;
|
|
152
|
+
let persistRule;
|
|
153
|
+
if (d !== "deny" && pr !== undefined) {
|
|
154
|
+
if (pr === null || typeof pr !== "object" || Array.isArray(pr))
|
|
155
|
+
return { ok: false, error: "persistRule must be an object" };
|
|
156
|
+
const rule = pr.rule;
|
|
157
|
+
if (typeof rule !== "string" || rule === "" || rule.length > MAX_RULE_TEXT_CHARS) {
|
|
158
|
+
return { ok: false, error: `persistRule.rule must be a non-empty string of at most ${MAX_RULE_TEXT_CHARS} characters` };
|
|
159
|
+
}
|
|
160
|
+
persistRule = rule;
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, value: d, ...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}), ...(persistRule !== undefined ? { persistRule } : {}) };
|
|
149
163
|
}
|
|
150
164
|
return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
|
|
151
165
|
}
|
|
@@ -349,7 +363,12 @@ export class ToolApprovalCoordinator {
|
|
|
349
363
|
* 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
|
|
350
364
|
* 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
|
|
351
365
|
governanceAskMarks;
|
|
366
|
+
/** #154 车二:持久化权限规则的**同意车道**。在场 ⇔ 规则店真装配(main.ts 与 core 的
|
|
367
|
+
* `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `ruleSuggestions`;②回决带
|
|
368
|
+
* `persistRule` 时兑付进店。缺席 ⇒ 两件都不做(诚实缺席,不发无处可兑的候选)。 */
|
|
369
|
+
ruleConsent;
|
|
352
370
|
constructor(opts) {
|
|
371
|
+
this.ruleConsent = opts?.ruleConsent;
|
|
353
372
|
this.governanceAskMarks = opts?.governanceAskMarks;
|
|
354
373
|
this.ttlMs = opts?.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
|
|
355
374
|
this.askStore = opts?.askStore;
|
|
@@ -404,6 +423,49 @@ export class ToolApprovalCoordinator {
|
|
|
404
423
|
this.admitByOwner.set(ownerKey, o);
|
|
405
424
|
};
|
|
406
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* #154 车二:一只 ask 的**规则车道素材**,或 `undefined`(= 本 ask 不投候选、不接受 `persistRule`)。
|
|
428
|
+
*
|
|
429
|
+
* 三个合取项(缺一即 `undefined`,理由逐字见调用点上方注):①店在场 ②引擎铸了候选 ③命令原字节可读。
|
|
430
|
+
* `req.args` 是 `unknown` ⇒ 窄读,**禁裸 as-cast**(宪法 [2704]:一个形状漂了的 args 若被 cast,
|
|
431
|
+
* 会把 `undefined` 当命令送进 `prepareCardApproval`,那是放宽面上的静默垃圾)。
|
|
432
|
+
*/
|
|
433
|
+
buildRuleLaneMaterial(req, owner, governanceForced) {
|
|
434
|
+
if (this.ruleConsent === undefined)
|
|
435
|
+
return undefined;
|
|
436
|
+
// 🔴 **治理档的 ask 不进规则车道**(#204 件7)。`governanceForced` 的语义(wire 契约逐字)是「这只
|
|
437
|
+
// ask 的门来自运维治理层,客户端表态掀不掉」;而一条持久规则会让引擎侧的规则命中臂短路掉后续
|
|
438
|
+
// **同命令**的 ask(core `hooks.js` 只排除 `decisionReason === "hook"` 与 `requiresRealApproval`),
|
|
439
|
+
// 于是一次「不再询问」= 静默拆掉 operator 的档位,且拆完之后遥测里连 ask 都不再出现。
|
|
440
|
+
// 与下面 `owner === null` 同族:这是**铸侧第一道门**,回决那侧 `persistRuleAfterDecision` 有同源的
|
|
441
|
+
// 第二道(拒因 `rule_governance_forced`)。两侧同源的理由见那一行。
|
|
442
|
+
if (governanceForced)
|
|
443
|
+
return undefined;
|
|
444
|
+
// 🔴 **归属人也是合取项**(codex 对抗复审 R1-F1,#203,验真后修)。这一条与回决那侧
|
|
445
|
+
// `persistRuleAfterDecision` 的第二道门(`owner === null ⇒ rule_lane_unavailable`)**同源** ——
|
|
446
|
+
// 一条规则是「**某个人**的持久同意」,没有人就没有桶可写。两侧不同源时的后果不是多问一次,而是
|
|
447
|
+
// 一张卡上渲出一格按下去恒被拒的「不再询问」= 本域顶注自己定义的 wire 谎言。
|
|
448
|
+
// 它在 #203 之前碰不到:local 车道没有规则店 ⇒ 上一行就返回了;本车给 local 接上 File 店之后,
|
|
449
|
+
// 「单机 + 无 principal」的部署第一次同时满足其余三项。
|
|
450
|
+
if (owner === null)
|
|
451
|
+
return undefined;
|
|
452
|
+
const suggestions = req.ruleSuggestions;
|
|
453
|
+
if (!Array.isArray(suggestions) || suggestions.length === 0)
|
|
454
|
+
return undefined;
|
|
455
|
+
const args = req.args;
|
|
456
|
+
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
457
|
+
return undefined;
|
|
458
|
+
const command = args.command;
|
|
459
|
+
if (typeof command !== "string" || command === "")
|
|
460
|
+
return undefined;
|
|
461
|
+
const boundInputHash = readBoundInputHash(req);
|
|
462
|
+
return {
|
|
463
|
+
command,
|
|
464
|
+
suggestions,
|
|
465
|
+
...(typeof req.toolCallId === "string" && req.toolCallId !== "" ? { toolCallId: req.toolCallId } : {}),
|
|
466
|
+
...(boundInputHash !== null ? { boundInputHash } : {}),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
407
469
|
/** 测试/可观测性钩子(X-2):某 (taskId) 维当前占用的准入名额数。 */
|
|
408
470
|
admittedCount(taskId) {
|
|
409
471
|
return this.admitByTask.get(taskId) ?? 0;
|
|
@@ -704,6 +766,24 @@ export class ToolApprovalCoordinator {
|
|
|
704
766
|
const id = uuidv7();
|
|
705
767
|
const child = isChildAsk(req);
|
|
706
768
|
const bounded = boundArgs(req.args);
|
|
769
|
+
// [2942]/[2943]:治理来源标的**一次** peek(非消费式)。此前它只在下面的帧字面量里就地读 ——
|
|
770
|
+
// #204 件7 起规则车道也要它(治理档的 ask 不进车道),两处读同一个值必须是**同一次** peek:
|
|
771
|
+
// 分开读会让「帧说这是治理门 / 车道说不是」这种自相矛盾的卡在理论上存在。
|
|
772
|
+
const governanceForced = (this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(primary.owner, primary.sessionId, origin.taskId))).isMarked(req.toolCallId);
|
|
773
|
+
// 🔴 **规则车道用的是本地 peek 与持久行的并集**(#204 件7,codex 对抗复审 R1-中,验真后修)。
|
|
774
|
+
// 本地表是进程内的:failover / 重启后幂等命中一条既有行时它恒空,而行上的卡里存着落库那一刻的判定。
|
|
775
|
+
// 下面 `ensureAsk` 的对账段按「行 = 真源」同步**帧上**那个键(那是出处属性,行说了算);但规则车道
|
|
776
|
+
// 是一次**放宽**动作,它的合成必须 fail-closed —— 任一侧说「治理门」就不进车道。只同步帧键会留下
|
|
777
|
+
// 一张「说是治理门却仍带 ruleSuggestions」的卡,且回决侧第二道门跟着没武装。
|
|
778
|
+
let effectiveGovernanceForced = governanceForced;
|
|
779
|
+
// #154 车二:规则候选的投影素材。**四个合取项**,缺一不投:
|
|
780
|
+
// ① 规则店真装配(`this.ruleConsent` 在场)—— 店缺席仍投 = 无处可兑的「不再询问」= wire 谎言;
|
|
781
|
+
// ② 这只 ask 不是治理档产的(#204 件7)—— 治理门上铸规则 = 静默拆掉 operator 档位;
|
|
782
|
+
// ③ 引擎真铸了候选(`req.ruleSuggestions` 非空)—— 命令不可匹配(复合/重定向/替换)时 core 交空数组;
|
|
783
|
+
// ④ 命令原字节读得出(回决时**引擎**要拿它重铸候选;读不出就没有可兑付的路,投候选等于骗人)。
|
|
784
|
+
// 读 `req.args.command` 走窄读:`args` 是 `unknown`,禁裸 as-cast(宪法 [2704])。
|
|
785
|
+
// `let`:上面那条并集裁定可能在对账后把它撤回 undefined(行说这是治理门)。
|
|
786
|
+
let ruleLaneMaterial = this.buildRuleLaneMaterial(req, primary.owner, governanceForced);
|
|
707
787
|
const frame = {
|
|
708
788
|
type: "tool_approval",
|
|
709
789
|
approvalId: id,
|
|
@@ -732,10 +812,10 @@ export class ToolApprovalCoordinator {
|
|
|
732
812
|
: {}),
|
|
733
813
|
message: typeof req.message === "string" ? redactDeep(req.message) : "",
|
|
734
814
|
// [2942]/[2943]:治理来源标(additive,只在为真时在场——缺席绝不编 false,见字段顶注)。
|
|
735
|
-
//
|
|
736
|
-
...(
|
|
737
|
-
|
|
738
|
-
|
|
815
|
+
// 值来自上方那一次**非消费式** peek(同一 toolCallId 的重播/failover 再入必须拿到同一份判定)。
|
|
816
|
+
...(governanceForced ? { governanceForced: true } : {}),
|
|
817
|
+
// #154 车二:候选逐字透传(条件见 `ruleLaneMaterial` 上方注)。
|
|
818
|
+
...(ruleLaneMaterial !== undefined ? { ruleSuggestions: ruleLaneMaterial.suggestions } : {}),
|
|
739
819
|
...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
|
|
740
820
|
};
|
|
741
821
|
// #151 车2(design/172 §3.3 D3 窗长三元):有效窗——legDeadlineMonotonic 缺席时退化成 ttlMs(D1 零
|
|
@@ -898,6 +978,22 @@ export class ToolApprovalCoordinator {
|
|
|
898
978
|
frame.governanceForced = true;
|
|
899
979
|
else
|
|
900
980
|
delete frame.governanceForced;
|
|
981
|
+
// 🔴 #204 件7(codex 对抗复审 R1-中,验真后修):行说这是治理门 ⇒ **规则车道当场撤回**。
|
|
982
|
+
// 上面那行只同步了帧上的出处键;素材、两族帧上的候选、以及回决侧第二道门的输入都还是按**本地**
|
|
983
|
+
// 表算的,而本地表在 failover / 重启后恒空。不撤就正是本件要防的形:一张卡同时带着
|
|
984
|
+
// `governanceForced:true` 与 `ruleSuggestions`,回决口还照收 —— operator 档位被一次点击拆掉。
|
|
985
|
+
// 撤三处(缺一即漏):①旧帧的候选键;②呈卡帧读的**持久卡**上的同名键(它来自行,不是本地铸的);
|
|
986
|
+
// ③素材本身(它是下面 `pendingEntry.ruleLane` 的唯一来源,也就是回决口的等式左边)。
|
|
987
|
+
// 反向(行说不是治理门、本地表说是)**刻意不放开**:见上方并集裁定 —— 少渲一格是安全方向。
|
|
988
|
+
if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
|
|
989
|
+
effectiveGovernanceForced = true;
|
|
990
|
+
ruleLaneMaterial = undefined;
|
|
991
|
+
delete frame.ruleSuggestions;
|
|
992
|
+
if (cardForFrame.ruleSuggestions !== undefined) {
|
|
993
|
+
const { ruleSuggestions: _dropped, ...rest } = cardForFrame;
|
|
994
|
+
cardForFrame = rest;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
901
997
|
}
|
|
902
998
|
catch (err) {
|
|
903
999
|
this.noteStoreError(err, "ensureAsk");
|
|
@@ -975,10 +1071,34 @@ export class ToolApprovalCoordinator {
|
|
|
975
1071
|
// 稍后才赋值的引用格(鸡生蛋:settle 是 pendingEntry 的一个字段,pendingEntry 建好之后才能把它自己
|
|
976
1072
|
// 塞进这个引用里)。
|
|
977
1073
|
let selfEntry;
|
|
978
|
-
|
|
1074
|
+
// core 5.23.0(#187/#114②):本次结算的**宿主自报来源**(见 {@link HostSettledBy})。
|
|
1075
|
+
// 🔴 记在 `settle` 的 **done 卫兵之内**,不是在调用点旁边立一个标志:窗到期的臂是「先算、后 settle」,
|
|
1076
|
+
// 而 `settle` 可能因为人已经答过了而整个是空操作 —— 标志式写法会在那种赛跑里给一个**人类的**终局盖上
|
|
1077
|
+
// 「窗到期」的章。只有真正落定这次终局的那一次调用才有资格留下自报词。
|
|
1078
|
+
let hostSettled;
|
|
1079
|
+
/**
|
|
1080
|
+
* 已落定的本地终局 → `AskOutcome` 的**唯一**成形口。本方法有两个出口(方法尾的正常路,与
|
|
1081
|
+
* `earlyAbortDuringEnsure` 的早退路),它们此前各写了一份等价分派 —— 结果就是本批的 `settledBy`
|
|
1082
|
+
* 只加到了一份上(codex 复审 round2 抓获)。合成一个函数,判据只写一次。
|
|
1083
|
+
* · `windowRouteUnavailable` ⇒ park 路由(持久终局是 PARKING/PARKED,压过一切);
|
|
1084
|
+
* · allow + 编辑 ⇒ 对象臂带 `updatedInput`;
|
|
1085
|
+
* · deny + 宿主自报 ⇒ 对象臂带 `settledBy`(见 {@link HostSettledBy};allow 侧恒不带 —— core 对
|
|
1086
|
+
* `{allow:true, settledBy:"timeout"}` 是响亮拒,而自报只在 deny 分支产生);
|
|
1087
|
+
* · 其余 ⇒ 裸 boolean(falsy 纪律)。
|
|
1088
|
+
*/
|
|
1089
|
+
const shapeOutcome = (settled) => {
|
|
1090
|
+
if (windowRouteUnavailable)
|
|
1091
|
+
return "unavailable";
|
|
1092
|
+
if (settled.allowed) {
|
|
1093
|
+
return settled.updatedInput !== undefined ? { allow: true, updatedInput: settled.updatedInput } : true;
|
|
1094
|
+
}
|
|
1095
|
+
return hostSettled !== undefined ? { allow: false, settledBy: hostSettled } : false;
|
|
1096
|
+
};
|
|
1097
|
+
const settle = (allowed, outcome, updatedInput, hostSettledBy) => {
|
|
979
1098
|
if (done)
|
|
980
1099
|
return;
|
|
981
1100
|
done = true;
|
|
1101
|
+
hostSettled = hostSettledBy;
|
|
982
1102
|
if (timer)
|
|
983
1103
|
clearTimeout(timer);
|
|
984
1104
|
for (const [sig, fn] of abortListeners)
|
|
@@ -1069,7 +1189,9 @@ export class ToolApprovalCoordinator {
|
|
|
1069
1189
|
const windowExpired = () => {
|
|
1070
1190
|
void (async () => {
|
|
1071
1191
|
if (!this.askStore || askId === undefined || batchId === undefined) {
|
|
1072
|
-
|
|
1192
|
+
// D1(无持久 ask 店)——终局**完全**由本仓这个窗决定,没有第二个写者。core 5.23.0 起如实自报
|
|
1193
|
+
// `"timeout"`:这条腿的窗归宿主,引擎观察不到它走完,不说就被记成「一个人拒绝了」。
|
|
1194
|
+
settle(false, "expired", undefined, "timeout");
|
|
1073
1195
|
return;
|
|
1074
1196
|
}
|
|
1075
1197
|
try {
|
|
@@ -1103,7 +1225,10 @@ export class ToolApprovalCoordinator {
|
|
|
1103
1225
|
// 两种结局都成立:真提交了 ⇒ 与行一致;没提交 ⇒ core 自己走 durable park,不劣于 deny。
|
|
1104
1226
|
if (err instanceof ApprovalStoreDeadlineError)
|
|
1105
1227
|
windowRouteUnavailable = true;
|
|
1106
|
-
|
|
1228
|
+
// fail-open(D5):店抖动不挡窗到期的进程内机械照旧完成。自报仍是 `"timeout"` 且如实 —— 结束这次
|
|
1229
|
+
// 等待的**确实**是本仓的窗(店只是没能给出更精确的终局);deadline 那一支已经改判 park 路由,
|
|
1230
|
+
// 走不到用得上这个词的返回臂。
|
|
1231
|
+
settle(false, "expired", undefined, "timeout");
|
|
1107
1232
|
}
|
|
1108
1233
|
})();
|
|
1109
1234
|
};
|
|
@@ -1217,12 +1342,11 @@ export class ToolApprovalCoordinator {
|
|
|
1217
1342
|
// `done` 为真而落定值是 `true`,写死 false 会把一次真实的人类批准悄悄吞成拒绝(且与 durable 行矛盾)。
|
|
1218
1343
|
// 改成把**已经落定的那个值**如实交回去(与方法尾部的返回值分派同一套判据)。
|
|
1219
1344
|
if (done) {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
return early.allowed;
|
|
1345
|
+
// 🔴 [3372] codex 复审 round2(验真后修):这里**曾经**是尾部那套分派的手抄第二份,于是本批给
|
|
1346
|
+
// 窗到期加的 `settledBy` 自报只进了尾部、没进这里 —— 「窗先落定、取消臂随后才走完」的交错里,
|
|
1347
|
+
// 一个由**窗**决定的终局从这个出口以裸 `false` 溜出去,core 照旧记成人类拒绝。两个出口现在共用
|
|
1348
|
+
// {@link shapeOutcome} 这**一个**成形函数,结构上不可能再分家(红先钉:codex-R2-2)。
|
|
1349
|
+
return shapeOutcome(await allowedP);
|
|
1226
1350
|
}
|
|
1227
1351
|
}
|
|
1228
1352
|
// Register BEFORE emitting so a (fast) respond can never miss the entry. 子代 ask 的生存期挂子代
|
|
@@ -1237,6 +1361,12 @@ export class ToolApprovalCoordinator {
|
|
|
1237
1361
|
...(child ? { originTaskId: req.sourceTaskId } : { targetCtxs: new Set(aliveCtxs), onTargetGone }),
|
|
1238
1362
|
...(primary.sessionId ? { sessionId: primary.sessionId } : {}),
|
|
1239
1363
|
toolName: req.toolName,
|
|
1364
|
+
// #154 车二:规则车道素材 —— 与帧上的 `ruleSuggestions` **同一个条件**(店在场 ∧ 非治理档 ∧ 引擎真
|
|
1365
|
+
// 铸了候选 ∧ 命令原字节读得出)。任一不成立 ⇒ 不登记,回决带 `persistRule` 时如实拒(不猜命令)。
|
|
1366
|
+
...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
|
|
1367
|
+
// #204 件7:治理标随条目落定(回决侧第二道门的输入,理由见字段顶注)。读的是**并集后**的值
|
|
1368
|
+
// (`effectiveGovernanceForced`),不是本地 peek —— 幂等命中既有行那条路上,只有并集才是武装的。
|
|
1369
|
+
...(effectiveGovernanceForced ? { governanceForced: true } : {}),
|
|
1240
1370
|
...(askId !== undefined && batchId !== undefined ? { askId, batchId } : {}),
|
|
1241
1371
|
};
|
|
1242
1372
|
selfEntry = pendingEntry; // 供 settle() 结算时把自己从 pendingByAskId 的 Set 里摘除(上方顶注)
|
|
@@ -1391,13 +1521,12 @@ export class ToolApprovalCoordinator {
|
|
|
1391
1521
|
// 方法顶注。
|
|
1392
1522
|
if (!emitSettled && lastOutcome === "expired")
|
|
1393
1523
|
return "unavailable";
|
|
1394
|
-
// [1458]
|
|
1395
|
-
//
|
|
1396
|
-
//
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
return settled.allowed;
|
|
1524
|
+
// [1458] 编辑放行对象臂 + core 5.23.0(#187/#114②)宿主自报臂 —— 两条判据都在
|
|
1525
|
+
// {@link shapeOutcome} 里,与早退出口**共用同一份**(codex 复审 round2:此前这里与早退臂各写一份
|
|
1526
|
+
// 等价分派,本批的 `settledBy` 于是只加到了其中一份上)。
|
|
1527
|
+
// 到得了这里说明卡**确实送达过**(emit 全灭 / park 路由两条早就 return 掉了),所以自报的「窗到期」
|
|
1528
|
+
// 是这次等待的真实结局,而不是「没人可送」。
|
|
1529
|
+
return shapeOutcome(settled);
|
|
1401
1530
|
}
|
|
1402
1531
|
/** `POST /v1/tool-approvals/:id/respond` — settle a parked approval with the shell's decision. Owner-gated with a
|
|
1403
1532
|
* 404 (no existence oracle), body validated first (400 is existence-independent) — question/steer parity. The HTTP
|
|
@@ -1418,10 +1547,79 @@ export class ToolApprovalCoordinator {
|
|
|
1418
1547
|
// #151 车2 D2(回决竞争者):askStore 缺席 ⇒ finishRespond 同步收尾,逐字不变(D1)。在场 ⇒ 先打一把
|
|
1419
1548
|
// decideAsk CAS,赢了才 finishRespond;输(某个其他竞争者已经决定了这只 ask 的终局)⇒ 不 settle,照旧
|
|
1420
1549
|
// 回现有 404 形(响应体的 409/410 分化是车4 的活,本车只保证「输者绝不覆盖赢者」)。
|
|
1421
|
-
|
|
1422
|
-
|
|
1550
|
+
const settled = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
|
|
1551
|
+
? this.respondWithCas(this.askStore, entry.askId, entry.batchId, id, entry, parsed)
|
|
1552
|
+
: this.finishRespond(id, entry, parsed);
|
|
1553
|
+
// #154 车二:「不再询问」的兑付口。**在裁决落定之后**(顺序是硬的:规则是「这次同意的持久化」,
|
|
1554
|
+
// 一次没有生效的裁决不该留下一条永久规则),且只在 200 那一支 —— 404(CAS 输/条目已被别人结算)
|
|
1555
|
+
// 意味着这次回决不是有效终局,连带的规则确认也就不成立。
|
|
1556
|
+
if (parsed.persistRule === undefined)
|
|
1557
|
+
return settled;
|
|
1558
|
+
return this.persistRuleAfterDecision(settled, entry, parsed.persistRule, parsed.updatedInput !== undefined);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* #154 车二:回决携规则确认 ⇒ prepare→confirm→redeem(实现在 `rules-consent.ts`,本方法只做门与回显)。
|
|
1562
|
+
*
|
|
1563
|
+
* 三道门,每道都是**拒绝**而不是降级:
|
|
1564
|
+
* ① 裁决必须真落定(200)—— 见调用点注;
|
|
1565
|
+
* ② 必须有已验明的 owner —— 规则是**某个人**的持久同意,一条 `owner === null` 的 ask 没有归属人可写;
|
|
1566
|
+
* ③ 本 ask 必须登记过规则车道素材(店在场 ∧ 引擎铸过候选 ∧ 命令可读)。
|
|
1567
|
+
*
|
|
1568
|
+
* 失败**不翻转裁决**:人的「允许这次」已经生效并且已经交给引擎了,把整个回决改判成 4xx 会让一次真实的
|
|
1569
|
+
* 放行凭空消失。诚实的形是 200 + `rulePersisted:false`(壳据它告诉人「这次放行了,但『不再询问』没存上」)。
|
|
1570
|
+
*/
|
|
1571
|
+
async persistRuleAfterDecision(settled, entry, ruleText, inputWasEdited) {
|
|
1572
|
+
const result = await settled;
|
|
1573
|
+
if (result.status !== 200)
|
|
1574
|
+
return result;
|
|
1575
|
+
const lane = this.ruleConsent;
|
|
1576
|
+
const material = entry.ruleLane;
|
|
1577
|
+
const owner = entry.owner;
|
|
1578
|
+
const withFlag = (rulePersisted, ruleRefusal) => ({
|
|
1579
|
+
status: result.status,
|
|
1580
|
+
body: { ...result.body, rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}) },
|
|
1581
|
+
});
|
|
1582
|
+
// 🔴 **治理档第二道门**(#204 件7),排在 `rule_lane_unavailable` 那道**之前**:两道门都会命中一只
|
|
1583
|
+
// 治理 ask(铸侧不登记素材 ⇒ `material === undefined`),但回给壳的词必须是精确的那一个 ——
|
|
1584
|
+
// `rule_lane_unavailable` 意为「这台部署压根不供规则」,壳据它可以从此不渲这一格,而这里的真相是
|
|
1585
|
+
// 「车道好好的,只是**这一只** ask 归 operator 管」。与铸侧 `buildRuleLaneMaterial` 的同源门成对:
|
|
1586
|
+
// 少了这一道,一次回滚/旧壳(或将来某条不经铸侧登记的回决路径)就能把 operator 档位绕过去。
|
|
1587
|
+
if (entry.governanceForced === true)
|
|
1588
|
+
return withFlag(false, "rule_governance_forced");
|
|
1589
|
+
if (lane === undefined || material === undefined || owner === null)
|
|
1590
|
+
return withFlag(false, "rule_lane_unavailable");
|
|
1591
|
+
// 🔴 codex 交叉复审 round3 [high] 二(验真后修,**真扩权面**):`updatedInput`(ctrl+g 编辑放行)
|
|
1592
|
+
// 与 `persistRule` 同时在场时,**生效的**调用是编辑后的那一份,而候选是引擎从**原始**命令铸的 ——
|
|
1593
|
+
// 于是「把一条危险命令改安全 + 勾上不再询问」会给原始那条危险命令装上一条常驻放行规则。
|
|
1594
|
+
// v1 处置 = **拒绝这次持久化**(裁决本身照常生效;人只是这一次没能顺手存下规则)。
|
|
1595
|
+
// 不选「从编辑后的输入重铸候选」:`updatedInput` 是 `unknown` 的整体替换形,它与这只 ask 上引擎
|
|
1596
|
+
// 铸的候选之间没有任何等式可对 —— 那条路要先给编辑面立形,是另一件事(登记为后续件)。
|
|
1597
|
+
if (inputWasEdited)
|
|
1598
|
+
return withFlag(false, "rule_input_edited");
|
|
1599
|
+
// 报出的文本必须**在引擎铸的候选里**(等式一道)。对不上 ⇒ 拒,不猜:一条不在候选里的文本要么是
|
|
1600
|
+
// 旧卡的残留,要么是有人在试着自铸规则,两种都不该落库。
|
|
1601
|
+
if (!material.suggestions.some((s) => s.rule === ruleText))
|
|
1602
|
+
return withFlag(false, "rule_not_offered");
|
|
1603
|
+
try {
|
|
1604
|
+
const persisted = await lane.persistCardRule({
|
|
1605
|
+
principal: owner,
|
|
1606
|
+
toolName: entry.toolName,
|
|
1607
|
+
command: material.command,
|
|
1608
|
+
ruleText,
|
|
1609
|
+
...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
|
|
1610
|
+
...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
|
|
1611
|
+
});
|
|
1612
|
+
if (!persisted.ok) {
|
|
1613
|
+
defaultLogger.warn("permission rule was not persisted after an approval", { reason: persisted.reason, detail: persisted.detail });
|
|
1614
|
+
return withFlag(false, persisted.reason);
|
|
1615
|
+
}
|
|
1616
|
+
return withFlag(true);
|
|
1617
|
+
}
|
|
1618
|
+
catch (err) {
|
|
1619
|
+
// D5 同族姿势:店抖动不改判人的裁决,只如实报「没存上」。
|
|
1620
|
+
this.noteStoreError(err, "persistCardRule");
|
|
1621
|
+
return withFlag(false, "rule_store_error");
|
|
1423
1622
|
}
|
|
1424
|
-
return this.finishRespond(id, entry, parsed);
|
|
1425
1623
|
}
|
|
1426
1624
|
/** store 在场时的回决 CAS 门(D2)——`respond()` 的异步分支,拆出以保持 `respond()` 本身在 store 缺席
|
|
1427
1625
|
* 时仍是纯同步函数(D1)。`askStore`/`askId`/`batchId` 由调用方在已窄化的分支里传入(避免非空断言)。 */
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 维护:core 新键到来时,transparently 透传的加进对应 PROJECTED 并去实现投影;有意不上 wire 的加进
|
|
16
16
|
* EXCLUDED 并写一行理由。谁改投影(project.ts / fleet-bus.ts / roster-store-sql.ts)谁同步本文件。
|
|
17
17
|
*/
|
|
18
|
-
import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, TaskEvent, MailboxMessage, MailboxStore } from "@sema-agent/core";
|
|
18
|
+
import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, TaskEvent, TraceEvent, MailboxMessage, MailboxStore } from "@sema-agent/core";
|
|
19
19
|
/** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
|
|
20
20
|
type AssertAllKeysHandled<T extends never> = T;
|
|
21
21
|
type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode";
|
|
@@ -26,8 +26,8 @@ type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "descripti
|
|
|
26
26
|
type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, BgNotifProjected | BgNotifExcluded>>;
|
|
27
27
|
type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
|
|
28
28
|
type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
|
|
29
|
-
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
|
|
30
|
-
type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash"
|
|
29
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleSuggestions";
|
|
30
|
+
type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash";
|
|
31
31
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
32
32
|
type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest";
|
|
33
33
|
type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
|
|
@@ -36,6 +36,14 @@ type _GuardMailbox = AssertAllKeysHandled<Exclude<keyof MailboxMessage, MailboxP
|
|
|
36
36
|
type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
|
|
37
37
|
type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain";
|
|
38
38
|
type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
|
|
39
|
+
type PermissionTraceKind = Extract<TraceEvent["kind"], `permission.${string}`>;
|
|
40
|
+
type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable";
|
|
41
|
+
type PermissionTraceDropped = "permission.sandbox_admitted" | "permission.rule_sync_resurrected" | "permission.rule_sync_dropped" | "permission.org_snapshot_unavailable";
|
|
42
|
+
type _GuardPermissionTrace = AssertAllKeysHandled<Exclude<PermissionTraceKind, PermissionTraceProjected | PermissionTraceDropped>>;
|
|
43
|
+
type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTraceProjected | PermissionTraceDropped, PermissionTraceKind>>;
|
|
44
|
+
/** `T` 非 `never` 时收敛到 `never`(空集 ⇒ 残留 `"EMPTY"` ⇒ tsc 红)。 */
|
|
45
|
+
type AssertNonEmpty<T> = [T] extends [never] ? "EMPTY" : never;
|
|
46
|
+
type _GuardPermissionTraceNonEmpty = AssertAllKeysHandled<AssertNonEmpty<PermissionTraceKind>>;
|
|
39
47
|
export type CoreKeysetGuards = [
|
|
40
48
|
_GuardNotification,
|
|
41
49
|
_GuardBgNotif,
|
|
@@ -43,7 +51,10 @@ export type CoreKeysetGuards = [
|
|
|
43
51
|
_GuardAsk,
|
|
44
52
|
_GuardTaskEvent,
|
|
45
53
|
_GuardMailbox,
|
|
46
|
-
_GuardMailboxAppend
|
|
54
|
+
_GuardMailboxAppend,
|
|
55
|
+
_GuardPermissionTrace,
|
|
56
|
+
_GuardPermissionTraceReverse,
|
|
57
|
+
_GuardPermissionTraceNonEmpty
|
|
47
58
|
];
|
|
48
59
|
export {};
|
|
49
60
|
//# sourceMappingURL=core-keyset-guard.d.ts.map
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -291,8 +291,25 @@ export declare function workspaceChangedEventData(ev: {
|
|
|
291
291
|
* 某个可选设施」的部署形自述,而那三个早已租户可见。消费端的真需求也在租户侧:审批卡要不要渲染
|
|
292
292
|
* 「不再询问」这一格,取决于这台 worker 有没有规则店 —— 剥掉它,客户端只能靠试(提交后被拒)才知道。
|
|
293
293
|
* 它**不在** `governance` 段里(core 自己把它放在段外),也不构成那 4 个治理布尔的侧信道。
|
|
294
|
-
* ⚠️ 恒在段(core 侧 `permissionRules` 无 `?`)
|
|
295
|
-
*
|
|
294
|
+
* ⚠️ 恒在段(core 侧 `permissionRules` 无 `?`):两个真值都可能出现,**别把 false 读成缺席**。
|
|
295
|
+
* 本仓自 #154 车二 / design/203 起真接了规则店(`main.ts` 在 `PERMISSION_RULES_ENABLED` 为真 ∧
|
|
296
|
+
* backend 在场时把 `permissionRuleStore` 装进 `RunnerDeps`,SQL 与 File 两形)⇒ 那样的部署报
|
|
297
|
+
* `storeWired: true`;旋钮关掉或后端缺席的部署仍报 `false`。消费端读到 false 就是「这台 worker
|
|
298
|
+
* 今天没有这条车道」,不许当成「这版引擎还没这个概念」。
|
|
299
|
+
* · `permissionRules.syncWired` / `permissionRules.orgGoverned`(布尔两位;core 5.23.0 design/182 §9/§7
|
|
300
|
+
* 给**同一段**加的员,[3372] 提货批 codex 复审 high 抓获——首版把它们静默剥了,正是本仓建这份台账要防
|
|
301
|
+
* 的「投影白名单剥键」病)—— **裁:租户可见,与同段的 `storeWired` 同待遇**。判据三条:
|
|
302
|
+
* ① **同段同类**:core 把它们放进 `permissionRules` 而不是 `governance`(唯一带 audience 标签的段),
|
|
303
|
+
* 且形状与 `storeWired` 逐字同构(恒在的布尔,present-and-false 表示「有这个概念,这里没开」);
|
|
304
|
+
* ② **消费端真需求在租户侧**:这两位说的是「按下『不再询问』之后,这条同意到底落到多大的信任域里」——
|
|
305
|
+
* `syncWired` = 这台 worker 会把规则并进跨设备/传输/服务端的**同一个同意信任域**;`orgGoverned` =
|
|
306
|
+
* 组织治理层武装着(且它的 fail-closed 契约意味着读不到 org 快照时**每一次**放行都会被抬成真人审批)。
|
|
307
|
+
* 同一张审批卡上的同一个动作,后果因这两位而不同,剥掉它们等于让客户端渲一格语义不明的开关;
|
|
308
|
+
* ③ **不是治理侧信道**:两位都不参与 `governance` 那 4 个布尔的还原(它们描述规则店这条车道自身的
|
|
309
|
+
* 形状,与 lockedConfig/compliance/memoryAdmission/retention 无函数关系)。
|
|
310
|
+
* ⚠️ 与 `storeWired` **不同**:本仓这两条至今未接(`RunnerDeps.permissionRuleSyncWired` /
|
|
311
|
+
* `permissionRuleOrg` 全树零声明点,2026-08-10 #204 复扫亲验)⇒ 今天恒 `false`,这是**真值**不是
|
|
312
|
+
* 缺席。哪天接上其中任一条,连本句一起改 —— 这一行是它的登记处。
|
|
296
313
|
* 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
|
|
297
314
|
* (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
|
|
298
315
|
*/
|
|
@@ -369,6 +386,7 @@ export declare function brainStatusEventData(ev: {
|
|
|
369
386
|
attempt?: number;
|
|
370
387
|
maxRetries?: number;
|
|
371
388
|
retryInMs?: number;
|
|
389
|
+
errClass?: string;
|
|
372
390
|
eventId?: string;
|
|
373
391
|
parentToolCallId?: string;
|
|
374
392
|
}): Record<string, unknown>;
|