@sema-agent/server 7.10.0 → 7.11.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/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/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 +38 -5
- package/dist/boot/parked-revive-gate.js +53 -6
- package/dist/boot/runner-deps.d.ts +10 -2
- package/dist/boot/runner-deps.js +12 -1
- package/dist/config-types.d.ts +16 -1
- package/dist/config.js +5 -1
- package/dist/http/routes/adoption.d.ts +26 -0
- package/dist/http/routes/adoption.js +120 -0
- package/dist/http/routes/capabilities.js +12 -0
- package/dist/http/routes/rules.d.ts +23 -0
- package/dist/http/routes/rules.js +117 -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 +19 -1
- package/dist/http/server.js +42 -0
- package/dist/main.js +52 -3
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- 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-sql.d.ts +242 -0
- package/dist/plugins/permission-rule-store-sql.js +817 -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 +30 -0
- package/dist/plugins/store-backend.js +14 -0
- package/dist/plugins/tidb-pool.js +47 -0
- package/dist/rules-consent.d.ts +126 -0
- package/dist/rules-consent.js +198 -0
- package/dist/shared-memory-scope-authorizer.d.ts +29 -0
- package/dist/shared-memory-scope-authorizer.js +17 -0
- package/dist/tool-approval.d.ts +55 -0
- package/dist/tool-approval.js +124 -5
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/package.json +3 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import mysql from "mysql2/promise";
|
|
2
2
|
import { TIDB_APPROVAL_ASK_STATEMENTS } from "./approval-ask-store-sql.js";
|
|
3
|
+
import { TIDB_ADOPTION_LOG_STATEMENTS } from "./adoption-log-sql.js";
|
|
4
|
+
import { TIDB_PERMISSION_RULE_STATEMENTS } from "./permission-rule-store-sql.js";
|
|
3
5
|
/**
|
|
4
6
|
* Shared TiDB (MySQL wire-compatible) connection pool for the durable Session center (L1).
|
|
5
7
|
* Owned by `main.ts`; closed on shutdown.
|
|
@@ -648,6 +650,51 @@ export const SCHEMA_STATEMENTS = [
|
|
|
648
650
|
// 跟着中央 `ensureSchema` 在 **named-lock 的那条 conn** 上建 —— 把 pool 级 ensure 函数塞进执行循环
|
|
649
651
|
// 会绕开 DDL 串行化。挂进来同时也是「这张表算生产 schema」的声明(基线导出按定义只收录被执行的语句)。
|
|
650
652
|
...TIDB_APPROVAL_ASK_STATEMENTS,
|
|
653
|
+
// design/177 shared-memory store(#192 单数纪律)—— org 盘的三张表。消费面 = shared-memory-store-sql.ts。
|
|
654
|
+
// 🔴 与 `agent_memory_engine_*`(design/138 本地记忆链)**零关系**:不 join、不共享 scope 登记、不互写
|
|
655
|
+
// (设计 182 §13)。两族同库不同面,合表/建视图会让 harvest 的私人笔记漏进团队库。
|
|
656
|
+
`CREATE TABLE IF NOT EXISTS shared_memory_scope (
|
|
657
|
+
-- 一个 org scope 一行。行缺席 = 该 org 从未开通 ⇒ 语义等价于 connected 且零 store(不是故障)。
|
|
658
|
+
scope_key VARCHAR(190) NOT NULL,
|
|
659
|
+
-- 闭集词:connecting|unavailable|connected。读出时走 planeStateOf 的穷举 switch,未知词整拒快照。
|
|
660
|
+
state VARCHAR(16) NOT NULL,
|
|
661
|
+
-- host 自述原因,仅 unavailable 有意义;core 折成一行喂模型面。
|
|
662
|
+
message VARCHAR(512) NULL,
|
|
663
|
+
updated_at_ms BIGINT NOT NULL,
|
|
664
|
+
PRIMARY KEY (scope_key)
|
|
665
|
+
) COLLATE utf8mb4_bin`,
|
|
666
|
+
`CREATE TABLE IF NOT EXISTS shared_memory_store (
|
|
667
|
+
-- 模型面把手,**全局唯一**:同 id 在两个 org 各一份会在跨 org 成员的快照里撞名,而 core 对撞名 id
|
|
668
|
+
-- 的处置是整行丢弃 —— 那是一次静默的库消失,所以用 PK 在库层堵死。
|
|
669
|
+
store_id VARCHAR(64) NOT NULL,
|
|
670
|
+
scope_key VARCHAR(190) NOT NULL,
|
|
671
|
+
description VARCHAR(512) NOT NULL,
|
|
672
|
+
-- 治理面结论,原样转达;core 对它零动作(模型面根本没有写通道)。
|
|
673
|
+
writable TINYINT(1) NOT NULL DEFAULT 0,
|
|
674
|
+
updated_at_ms BIGINT NOT NULL,
|
|
675
|
+
PRIMARY KEY (store_id),
|
|
676
|
+
KEY idx_shared_memory_store_scope (scope_key, store_id)
|
|
677
|
+
) COLLATE utf8mb4_bin`,
|
|
678
|
+
`CREATE TABLE IF NOT EXISTS shared_memory_entry (
|
|
679
|
+
store_id VARCHAR(64) NOT NULL,
|
|
680
|
+
-- 绝对路径。PK 即 keyset 分页索引:WHERE store_id = ? AND path > ? ORDER BY path。
|
|
681
|
+
path VARCHAR(512) NOT NULL,
|
|
682
|
+
content LONGTEXT NOT NULL,
|
|
683
|
+
-- ISO 8601 原文逐字回放(core 只渲染匹配的 YYYY-MM-DD 前缀)。数值毫秒列是下面那根,两者不同义。
|
|
684
|
+
updated_at_iso VARCHAR(64) NULL,
|
|
685
|
+
size_bytes INT NOT NULL,
|
|
686
|
+
updated_at_ms BIGINT NOT NULL,
|
|
687
|
+
PRIMARY KEY (store_id, path)
|
|
688
|
+
) COLLATE utf8mb4_bin`,
|
|
689
|
+
// design/183 form b(纯身份重绑)的 **SQL 收编日志**。真源 = adoption-log-sql.ts 的
|
|
690
|
+
// `TIDB_ADOPTION_LOG_STATEMENTS`(与 PG twin 并排);展开进来的理由与 approval-ask 同款:
|
|
691
|
+
// 跟着中央 ensureSchema 在 named-lock 的那条 conn 上建,不绕开 DDL 串行化。
|
|
692
|
+
// 🔴 它**必须**与被迁的行同库(183 I2):phase 真源住别处 ⇒ 发起 host 被摧毁时与半迁移状态分家。
|
|
693
|
+
...TIDB_ADOPTION_LOG_STATEMENTS,
|
|
694
|
+
// #154 车二(core 5.18.0 design/179 + 5.22.0 design/182)持久化权限规则店的三张表。真源 =
|
|
695
|
+
// permission-rule-store-sql.ts 的 `TIDB_PERMISSION_RULE_STATEMENTS`(与 PG twin 并排);展开进这个
|
|
696
|
+
// 数组的理由与上面那族逐字相同(named-lock 的那条 conn + 「这张表算生产 schema」的声明)。
|
|
697
|
+
...TIDB_PERMISSION_RULE_STATEMENTS,
|
|
651
698
|
];
|
|
652
699
|
/** Named MySQL/TiDB advisory lock that serializes the whole `ensureSchema` DDL run (see below). */
|
|
653
700
|
export const ENSURE_SCHEMA_LOCK = "sema_ensure_schema";
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type ImportPreview, type ImportResult, type ImportedSettingsLayer, type RuleScope } from "@sema-agent/core";
|
|
2
|
+
import type { PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
3
|
+
import { type RuleImportTicket, type RuleTicketRedeemResult } from "./plugins/permission-rule-store-sql.js";
|
|
4
|
+
/**
|
|
5
|
+
* 本车道**真正**依赖的三面(**结构**接口,不是 SQL 类)。
|
|
6
|
+
*
|
|
7
|
+
* 🔴 为什么不直接吃 `PermissionRuleStores`(SQL 束):同意车道是**纯协议逻辑**,它对「行躺在 MySQL 还是
|
|
8
|
+
* PG 还是别处」一无所知也不该知道。吃结构接口有两个真收益:①单元测试能用 core 自己的
|
|
9
|
+
* `InMemoryPermissionRuleStore` / `InMemoryRuleApprovalRecordStore` 驱动**同一段**代码(而不是为了测协议
|
|
10
|
+
* 去拉一个数据库);②将来若真出了 File 形规则店,本文件一个字不用改。SQL 束结构上满足本接口。
|
|
11
|
+
*/
|
|
12
|
+
export interface RuleConsentStores {
|
|
13
|
+
provider: PermissionRuleStoreProvider;
|
|
14
|
+
/** core 的记录店面 + 一个**可选**的加项:`discardPendingRecord`(超帽拒绝时收掉 `prepareCcImport`
|
|
15
|
+
* 已经落盘的那一条 pending 记录)。没有这个加项的后端只是留一条孤儿行,不影响任何判决
|
|
16
|
+
* (语义与「已确认的记录永不被收」的硬条款见 SQL 侧同名方法)。 */
|
|
17
|
+
approvals: RuleApprovalRecordStore & {
|
|
18
|
+
discardPendingRecord?(recordId: string): Promise<boolean>;
|
|
19
|
+
};
|
|
20
|
+
tickets: {
|
|
21
|
+
mint(input: {
|
|
22
|
+
ticketId: string;
|
|
23
|
+
principal: string;
|
|
24
|
+
approvalId: string;
|
|
25
|
+
candidates: ReadonlyArray<{
|
|
26
|
+
rule: string;
|
|
27
|
+
scope: RuleScope;
|
|
28
|
+
}>;
|
|
29
|
+
ttlMs: number;
|
|
30
|
+
}): Promise<RuleImportTicket>;
|
|
31
|
+
/** 一次**认领**(不是「已完成」)。语义与「认领之后怎么办」见 {@link RuleConsentStores.tickets.release}。 */
|
|
32
|
+
consume(ticketId: string, principal: string): Promise<RuleTicketRedeemResult>;
|
|
33
|
+
/** 把认领**放回去**(认领之后遇到不确定/瞬时失败时)。`mustRemainValidMs` = 放回之后这张票至少还要
|
|
34
|
+
* 能用多久 —— 撑不过就**不算放回成功**(理由逐字见 store 侧 `release` 的头注)。 */
|
|
35
|
+
release(ticketId: string, principal: string, mustRemainValidMs?: number): Promise<boolean>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** 导入票的存活窗。人从「看到预览」到「按下确认」是一次交互,不是一段会话——十分钟宽到不会误伤,
|
|
39
|
+
* 窄到一张泄漏的票不会常驻。运维旋钮暂不开(没有部署形要求它可调;要开时走 config.ts 的既有姿势)。 */
|
|
40
|
+
export declare const RULE_IMPORT_TICKET_TTL_MS: number;
|
|
41
|
+
/** 可重试那一支通告给客户端的等待秒数(`state.rule_import_retry` 的 `retryAfterSec` / `Retry-After`)。
|
|
42
|
+
* 短 —— 它对应的是一次瞬时 store 抖动,不是排队。**单一真源在本文件**:放回认领时要用它判「这张票
|
|
43
|
+
* 还能不能撑过这段窗」,HTTP 层要用它填响应,两处用同一个数才谈得上「承诺兑得出来」。 */
|
|
44
|
+
export declare const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
45
|
+
/**
|
|
46
|
+
* **全部层合计**的候选上限(codex 交叉复审 round7 [high] 二,验真后修)。
|
|
47
|
+
*
|
|
48
|
+
* 单层 16 KiB 只管住了**请求体**,管不住**下游工作量**:紧凑的合法规则(`Bash(ls)` 十个字符)能让三层
|
|
49
|
+
* 塞进上千条候选,而 core 的 `redeemRuleBatch` 是**逐候选**跑一遍「读店 + CAS 写」的串行循环 —— 一个
|
|
50
|
+
* 已鉴权的请求就能把连接池占满、把响应撑到兆字节。全局限流器默认是关的,拦不住它(一次准入就够了)。
|
|
51
|
+
*
|
|
52
|
+
* 帽数的是 **core 已经解析好的候选**(不是我们自己再数一遍 JSON):零重复解析,而且数的正好是那条串行
|
|
53
|
+
* 循环的**真实**长度。位置在**铸票之前** —— 超限就没有票,那条昂贵的循环一次都跑不起来(代价只剩一条
|
|
54
|
+
* 已铸的 pending 记录,与任何一次没去兑付的 prepare 留下的残余同形)。
|
|
55
|
+
* 200 的取值:一份人手写的 CC settings 极少超过几十条;200 给了一个数量级的余量,又把最坏工作量钉在
|
|
56
|
+
* 「几百次 SQL」而不是「几万次」。
|
|
57
|
+
*/
|
|
58
|
+
export declare const MAX_IMPORT_CANDIDATES = 200;
|
|
59
|
+
/** 一次导入 prepare 的产物(HTTP 200 体的素材),或**超帽**的拒绝(见 {@link MAX_IMPORT_CANDIDATES})。 */
|
|
60
|
+
export type RuleImportPrepared = {
|
|
61
|
+
ok: true;
|
|
62
|
+
preview: ImportPreview;
|
|
63
|
+
ticket: string;
|
|
64
|
+
expiresAtMs: number;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
reason: "too-many-candidates";
|
|
68
|
+
candidates: number;
|
|
69
|
+
};
|
|
70
|
+
/** 导入 redeem 的结果。`refused` 的四个成因**在 wire 面折成同一个 404**(见顶注 ①)——这里保留分类
|
|
71
|
+
* 只为服务端日志/诊断。 */
|
|
72
|
+
export type RuleImportRedeemed = {
|
|
73
|
+
ok: true;
|
|
74
|
+
result: ImportResult;
|
|
75
|
+
}
|
|
76
|
+
/** `retryable` = 这次失败**没有裁定任何事**(store 抛错 / CAS 冲突),认领已放回,属主原样重试即可。
|
|
77
|
+
* 它与裁定性拒绝(载荷被篡改、记录不存在)分家是承重的:把两者塌成一格,一次数据库抖动就会被
|
|
78
|
+
* 客户端读成「这张票永远不能用了」。⚠️ **wire 面仍然折成同一个 404**(零存在性 oracle 不因此松动);
|
|
79
|
+
* 这一位只进服务端日志与本层的重试判据。 */
|
|
80
|
+
| {
|
|
81
|
+
ok: false;
|
|
82
|
+
reason: "ticket-unusable" | "record-unusable" | "payload-mismatch";
|
|
83
|
+
detail: string;
|
|
84
|
+
retryable?: true;
|
|
85
|
+
};
|
|
86
|
+
/** 卡道兑付的结果。 */
|
|
87
|
+
export type CardRulePersisted = {
|
|
88
|
+
ok: true;
|
|
89
|
+
rule: string;
|
|
90
|
+
rev: number;
|
|
91
|
+
alreadyRedeemed: boolean;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
reason: "no-candidates" | "unknown-candidate" | "confirm-refused" | "redeem-refused";
|
|
95
|
+
detail: string;
|
|
96
|
+
};
|
|
97
|
+
/** 一层用户交上来的 settings(HTTP 载荷已 zod 校验过的形)。 */
|
|
98
|
+
export interface RuleImportLayerInput {
|
|
99
|
+
layer: ImportedSettingsLayer;
|
|
100
|
+
path: string;
|
|
101
|
+
root: string;
|
|
102
|
+
content: string;
|
|
103
|
+
}
|
|
104
|
+
export interface RuleConsentLane {
|
|
105
|
+
/** 卡道兑付口:一次 ask 决议携规则确认 ⇒ prepare→confirm→redeem 一气呵成。 */
|
|
106
|
+
persistCardRule(input: {
|
|
107
|
+
principal: string;
|
|
108
|
+
toolName: string;
|
|
109
|
+
/** 被裁决的命令**原字节**(与 ask 上的那份同源;规则文本由引擎从它铸,调用方不供文本)。 */
|
|
110
|
+
command: string;
|
|
111
|
+
/** 客户端选中的候选**文本**——用来在引擎铸出的候选表里定位下标。文本对不上 ⇒ 拒(不猜)。 */
|
|
112
|
+
ruleText: string;
|
|
113
|
+
toolCallId?: string;
|
|
114
|
+
boundInputHash?: string;
|
|
115
|
+
scope?: RuleScope;
|
|
116
|
+
}): Promise<CardRulePersisted>;
|
|
117
|
+
/** 导入道:读层 → 预览 + pending 记录 + 票。 */
|
|
118
|
+
prepareImport(principal: string, layers: readonly RuleImportLayerInput[]): Promise<RuleImportPrepared>;
|
|
119
|
+
/** 导入道:原子消费票 → confirm → redeemRuleBatch。 */
|
|
120
|
+
redeemImport(principal: string, ticket: string): Promise<RuleImportRedeemed>;
|
|
121
|
+
}
|
|
122
|
+
export declare function createRuleConsentLane(stores: RuleConsentStores, opts?: {
|
|
123
|
+
ticketTtlMs?: number;
|
|
124
|
+
newTicketId?: () => string;
|
|
125
|
+
}): RuleConsentLane;
|
|
126
|
+
//# sourceMappingURL=rules-consent.d.ts.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #154 车二 —— **同意车道的 server 半场**(core 5.18.0 design/179 `permission-rule-consent.ts` 的宿主侧)。
|
|
3
|
+
*
|
|
4
|
+
* core 把一条规则进店的路钉成三步:`durable approval record (pending)` → `authenticated confirmation
|
|
5
|
+
* transfer (approved)` → `principal-bound redemption (redeemed)`,而且**只有第三步**够得着写面。core 明写
|
|
6
|
+
* 「The confirmation transfer is an in-process call. **A deployment must reach it only from its
|
|
7
|
+
* authenticated approval channel**」—— 那句话是把一段责任**交给宿主**,不是一段已经完成的检查。本文件就是
|
|
8
|
+
* 那段责任的落点:
|
|
9
|
+
*
|
|
10
|
+
* · 卡道(兑付口):一次 ask 决议携「不再询问」时,由**这里**去 prepare→confirm→redeem。人是谁 =
|
|
11
|
+
* 回决端点已验明的 principal(不是请求体里的任何字段);规则文本 = **引擎**从被裁决的命令铸的候选
|
|
12
|
+
* (`prepareCardApproval` 不收调用方候选,core 在那条入口上已经把「写自己的规则」这件事变成不可拼写),
|
|
13
|
+
* 客户端只能说「我选第几个」。
|
|
14
|
+
*
|
|
15
|
+
* · 导入道(CC settings):`prepare` 读用户交上来的 settings 层、铸 pending 记录 + **server 自铸的票**;
|
|
16
|
+
* `redeem` 原子消费票、confirm、`redeemRuleBatch`。
|
|
17
|
+
*
|
|
18
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
* 🔴 票的信任边界四条,**全部是 server 附加的**(不是 core 的语义)
|
|
20
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
* 亲读 core 的 `mintRuleTicket`(dist,5.22.0):`return \`rt.${candidateIndex}.${recordId}\`` —— 它**没有**
|
|
22
|
+
* TTL、没有签名、没有消费位、没有载荷绑定。core 的票在**进程内**是够的(它的相邻语义是「重放同一次兑付是
|
|
23
|
+
* 幂等的」),但一条跨网络、由客户端保管、可被复制的票需要另外四样:
|
|
24
|
+
* ① **principal 绑定** —— redeem 的已验明 principal 必须 = prepare 铸票的那一位。跨 principal redeem 与
|
|
25
|
+
* unknown ticket **同形 404**:两者返回不同的码就是一台存在性 oracle(拿别人的票撞库能探到「它存在」)。
|
|
26
|
+
* ② **过期** —— `RULE_IMPORT_TICKET_TTL_MS`;过期 redeem 拒。
|
|
27
|
+
* ③ **一次性原子消费** —— 同票二次 redeem 拒,并发双 redeem **恰一成功**。判据由 SQL 的单条条件
|
|
28
|
+
* `UPDATE … WHERE consumed_at_ms IS NULL` 回答,**禁 read-then-write**(见 store 那侧的 🔴 注)。
|
|
29
|
+
* ④ **载荷绑定** —— redeem 应用的规则集 = prepare 时刻的快照。core 已经结构上保证了「规则文本来自记录」,
|
|
30
|
+
* 本层再加一道:票上钉着 prepare 时刻候选集的摘要,redeem 时与记录上的候选集重算比对,不符即拒 ——
|
|
31
|
+
* 于是「篡改记录再拿旧票兑付」这条路上多一道等式,而不是只靠「记录只有我们能写」这一条信念。
|
|
32
|
+
*
|
|
33
|
+
* 命名(CLAUDE.md 工厂律):`build*` = 纯数据;`create*` = 活对象。本文件的 {@link createRuleConsentLane}
|
|
34
|
+
* 返回一个带方法的束 ⇒ `create*`。
|
|
35
|
+
*/
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
37
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
38
|
+
import { confirmRuleApproval, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, } from "@sema-agent/core";
|
|
39
|
+
import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
|
|
40
|
+
/** 导入票的存活窗。人从「看到预览」到「按下确认」是一次交互,不是一段会话——十分钟宽到不会误伤,
|
|
41
|
+
* 窄到一张泄漏的票不会常驻。运维旋钮暂不开(没有部署形要求它可调;要开时走 config.ts 的既有姿势)。 */
|
|
42
|
+
export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
|
|
43
|
+
/** 可重试那一支通告给客户端的等待秒数(`state.rule_import_retry` 的 `retryAfterSec` / `Retry-After`)。
|
|
44
|
+
* 短 —— 它对应的是一次瞬时 store 抖动,不是排队。**单一真源在本文件**:放回认领时要用它判「这张票
|
|
45
|
+
* 还能不能撑过这段窗」,HTTP 层要用它填响应,两处用同一个数才谈得上「承诺兑得出来」。 */
|
|
46
|
+
export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
47
|
+
/**
|
|
48
|
+
* **全部层合计**的候选上限(codex 交叉复审 round7 [high] 二,验真后修)。
|
|
49
|
+
*
|
|
50
|
+
* 单层 16 KiB 只管住了**请求体**,管不住**下游工作量**:紧凑的合法规则(`Bash(ls)` 十个字符)能让三层
|
|
51
|
+
* 塞进上千条候选,而 core 的 `redeemRuleBatch` 是**逐候选**跑一遍「读店 + CAS 写」的串行循环 —— 一个
|
|
52
|
+
* 已鉴权的请求就能把连接池占满、把响应撑到兆字节。全局限流器默认是关的,拦不住它(一次准入就够了)。
|
|
53
|
+
*
|
|
54
|
+
* 帽数的是 **core 已经解析好的候选**(不是我们自己再数一遍 JSON):零重复解析,而且数的正好是那条串行
|
|
55
|
+
* 循环的**真实**长度。位置在**铸票之前** —— 超限就没有票,那条昂贵的循环一次都跑不起来(代价只剩一条
|
|
56
|
+
* 已铸的 pending 记录,与任何一次没去兑付的 prepare 留下的残余同形)。
|
|
57
|
+
* 200 的取值:一份人手写的 CC settings 极少超过几十条;200 给了一个数量级的余量,又把最坏工作量钉在
|
|
58
|
+
* 「几百次 SQL」而不是「几万次」。
|
|
59
|
+
*/
|
|
60
|
+
export const MAX_IMPORT_CANDIDATES = 200;
|
|
61
|
+
export function createRuleConsentLane(stores, opts) {
|
|
62
|
+
const deps = { provider: stores.provider, approvals: stores.approvals };
|
|
63
|
+
const ticketTtlMs = opts?.ticketTtlMs ?? RULE_IMPORT_TICKET_TTL_MS;
|
|
64
|
+
const newTicketId = opts?.newTicketId ?? (() => randomUUID());
|
|
65
|
+
return {
|
|
66
|
+
async persistCardRule(input) {
|
|
67
|
+
// 🔴 候选**由引擎铸**(core `prepareCardApproval` 不收调用方候选;那条入口上「写自己的规则」
|
|
68
|
+
// 结构上不可拼写)。本层只把「人选了哪一个」翻译成下标。
|
|
69
|
+
const prepared = await prepareCardApproval({
|
|
70
|
+
principal: input.principal,
|
|
71
|
+
toolName: input.toolName,
|
|
72
|
+
command: input.command,
|
|
73
|
+
...(input.toolCallId !== undefined ? { toolCallId: input.toolCallId } : {}),
|
|
74
|
+
...(input.boundInputHash !== undefined ? { boundInputHash: input.boundInputHash } : {}),
|
|
75
|
+
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
76
|
+
deps,
|
|
77
|
+
});
|
|
78
|
+
if (prepared === undefined) {
|
|
79
|
+
return { ok: false, reason: "no-candidates", detail: "the rule lane cannot speak for this command (compound / redirection / unsupported tool)" };
|
|
80
|
+
}
|
|
81
|
+
const index = prepared.candidates.findIndex((c) => c.rule === input.ruleText);
|
|
82
|
+
const ticket = index < 0 ? undefined : prepared.tickets[index];
|
|
83
|
+
if (index < 0 || ticket === undefined) {
|
|
84
|
+
return { ok: false, reason: "unknown-candidate", detail: "the chosen rule text is not one the engine minted for this command" };
|
|
85
|
+
}
|
|
86
|
+
const confirmed = await confirmRuleApproval({ approvalId: prepared.approvalId, principal: input.principal, selectedCandidate: index, deps });
|
|
87
|
+
if (!confirmed.ok)
|
|
88
|
+
return { ok: false, reason: "confirm-refused", detail: confirmed.reason };
|
|
89
|
+
const redeemed = await redeemRuleTicket({ ticket, principal: input.principal, deps });
|
|
90
|
+
if (redeemed.status !== "redeemed")
|
|
91
|
+
return { ok: false, reason: "redeem-refused", detail: redeemed.reason };
|
|
92
|
+
return { ok: true, rule: redeemed.rule, rev: redeemed.rev, alreadyRedeemed: redeemed.alreadyRedeemed };
|
|
93
|
+
},
|
|
94
|
+
async prepareImport(principal, layers) {
|
|
95
|
+
// core 的 `CcImportLayer.readFile` 是一条**注入的**读口 —— 云端部署没有用户的文件系统,内容由
|
|
96
|
+
// 调用方交上来(这是本仓的部署形,不是绕过:读的仍然只是 allow 桶,拒/问桶是收紧方向、另有通道)。
|
|
97
|
+
const ccLayers = layers.map((l) => ({
|
|
98
|
+
layer: l.layer,
|
|
99
|
+
path: l.path,
|
|
100
|
+
root: l.root,
|
|
101
|
+
readFile: async () => l.content,
|
|
102
|
+
}));
|
|
103
|
+
const { preview, approvalId } = await prepareCcImport({ layers: ccLayers, principal, deps });
|
|
104
|
+
// 帽在**铸票之前**(见 {@link MAX_IMPORT_CANDIDATES}):没有票 ⇒ 那条逐候选的串行 SQL 循环跑不起来。
|
|
105
|
+
if (preview.candidates.length > MAX_IMPORT_CANDIDATES) {
|
|
106
|
+
// core 在**返回预览之前**就把 pending 记录落了盘 ⇒ 这次不做的导入会留一条永远没人要的行。
|
|
107
|
+
// 收掉它(codex round8 [medium]:被拒的 prepare 不该在共享库里长东西)。best-effort:收不掉
|
|
108
|
+
// 也只是留一条 pending 孤儿,不影响任何判决 —— 但**绝不**因此把 413 变成 500。
|
|
109
|
+
try {
|
|
110
|
+
await stores.approvals.discardPendingRecord?.(approvalId);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
recordFailOpen("server.rules.rejected-prepare-row-left", err instanceof Error ? err.message : String(err));
|
|
114
|
+
}
|
|
115
|
+
return { ok: false, reason: "too-many-candidates", candidates: preview.candidates.length };
|
|
116
|
+
}
|
|
117
|
+
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
118
|
+
return { ok: true, preview, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
119
|
+
},
|
|
120
|
+
async redeemImport(principal, ticket) {
|
|
121
|
+
// ①②③ 一次原子**认领**同时回答(principal 绑定 / 未过期 / 未认领);四类拒绝在 wire 面同形。
|
|
122
|
+
const consumed = await stores.tickets.consume(ticket, principal);
|
|
123
|
+
if (!consumed.ok)
|
|
124
|
+
return { ok: false, reason: "ticket-unusable", detail: consumed.reason };
|
|
125
|
+
// 🔴 codex round1 [high] 一(验真后修):认领**之后**的每一步都可能失败,而旧形把认领当终局 ⇒
|
|
126
|
+
// 一次 store 抖动就永久烧掉这张票,客户端拿到与「票不存在」同形的 404,重试无门。
|
|
127
|
+
// 处置:裁定性拒绝(载荷被篡改 / 记录不存在)**留着**认领(重试不改判);不确定/瞬时失败**放回**
|
|
128
|
+
// 认领并标 `retryable`。放回不打开双兑付的门(认领仍是单持有者),兑付腿本身按 core 的设计幂等。
|
|
129
|
+
// 🔴 codex 交叉复审 round3 [high] 一(验真后修):返回值是**「认领确实放回去了吗」**,不是 void。
|
|
130
|
+
// 旧形无条件回 `retryable: true`,而释放本身也可能失败(同一场 store 故障往往两跳一起挂)——
|
|
131
|
+
// 那就变成对客户端**撒谎**:它拿着一张其实已经烧掉的票去重试,只会收到 404,人的确认永久丢失。
|
|
132
|
+
// 判据必须是「释放**确认**成功」;没确认就不许承诺重试(如实按不可重试报,四类同形 404)。
|
|
133
|
+
const releaseClaim = async () => {
|
|
134
|
+
try {
|
|
135
|
+
return await stores.tickets.release(ticket, principal, RULE_IMPORT_RETRY_AFTER_SEC * 1000);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
// 放不回去 = 回到「认领即烧票」的旧形(属主这一轮不能重试,票随 TTL 消失)。**不**把一次
|
|
139
|
+
// 已经失败的请求再变成一次抛错;但也**不**静默 —— 登记的 F 类兜底,理由逐字见 tag 词表。
|
|
140
|
+
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
/** 「不确定」那一支的统一收口:**释放确认成功**才承诺可重试。 */
|
|
145
|
+
const indeterminate = async (detail) => {
|
|
146
|
+
// 放回之后这张票至少要能撑过我们通告的等待窗 —— 撑不过就不承诺重试(见 store 侧 `release` 头注)。
|
|
147
|
+
const released = await releaseClaim();
|
|
148
|
+
return { ok: false, reason: "record-unusable", detail, ...(released ? { retryable: true } : {}) };
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
// ④ 载荷绑定:记录上的候选集必须与铸票时刻的快照同摘要。
|
|
152
|
+
const record = await stores.approvals.get(consumed.approvalId);
|
|
153
|
+
if (record === undefined)
|
|
154
|
+
return { ok: false, reason: "record-unusable", detail: "the approval record behind this ticket is gone" };
|
|
155
|
+
if (buildRulePayloadHash(record.candidates) !== consumed.payloadHash) {
|
|
156
|
+
return { ok: false, reason: "payload-mismatch", detail: "the approval record's candidates changed after the ticket was minted" };
|
|
157
|
+
}
|
|
158
|
+
// 🔴 确认是**一次性状态跃迁**,不是每次兑付都要过的门:重试一张**放回过认领**的票时,记录
|
|
159
|
+
// 早已不是 `pending` 了(core 的崩溃序:`redeemRuleTicket` 在调用写面**之前**就把记录推到
|
|
160
|
+
// `redeemed`,所以哪怕一个候选都没落地,状态也已经变了)。对已确认的记录再调一次
|
|
161
|
+
// `confirmRuleApproval` 会拿到 `not_pending` —— 把它当拒绝就等于「放回了认领却永远兑不动」,
|
|
162
|
+
// round1/round3/round5 那一串修复到此全部白做。判据用记录**自己的状态**,不是猜错误串。
|
|
163
|
+
if (record.state === "pending") {
|
|
164
|
+
const confirmed = await confirmRuleApproval({ approvalId: consumed.approvalId, principal, deps });
|
|
165
|
+
if (!confirmed.ok) {
|
|
166
|
+
// `conflict` = CAS 输给了一个并发写者(瞬时);其余是裁定(记录不存在/状态不对)。
|
|
167
|
+
if (confirmed.reason === "conflict")
|
|
168
|
+
return await indeterminate(confirmed.reason);
|
|
169
|
+
return { ok: false, reason: "record-unusable", detail: confirmed.reason };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const result = await redeemRuleBatch({ approvalId: consumed.approvalId, principal, deps });
|
|
173
|
+
if ("status" in result) {
|
|
174
|
+
// core 的 refused 里既有裁定也有瞬时(OCC 用尽 / 店读不出来)——分不清就按**可重试**放回:
|
|
175
|
+
// 兑付是幂等的,多放一次认领最坏是多跑一遍同样的重放;烧掉一张真票是不可逆的。
|
|
176
|
+
return await indeterminate(result.reason);
|
|
177
|
+
}
|
|
178
|
+
// 🔴 codex 交叉复审 round5 [high] 二(验真后修):core 的 `redeemRuleBatch` 把**逐候选**的失败
|
|
179
|
+
// 记在 `skippedAtRedeem` 里,顶层照样返回一个 ImportResult —— 只判顶层 `status` 会把「一半没落地」
|
|
180
|
+
// 当成功回 200,而认领已经烧掉,那一半再也补不回来。
|
|
181
|
+
// 为什么这条车道可以把「非空 skippedAtRedeem」一律当**不确定**:候选在 `prepareCcImport` 那一步
|
|
182
|
+
// 就已经全过了同一个验证器(过不了的进 `preview.skipped`,压根不进记录),所以兑付期的逐候选拒绝
|
|
183
|
+
// 实际只剩「店读不出/写不进 / OCC 用尽 / 隔离栅栏」这类**瞬时或异常**成因。重放是幂等的
|
|
184
|
+
// (记录里存着每个候选已铸的 dot),所以放回认领让人重试,代价上界只是再跑一遍同样的重放。
|
|
185
|
+
// 残留(如实登记):真出现一条**永久**被拒的候选时,客户端会一直拿到 503 直到票到期(10 分钟)——
|
|
186
|
+
// 有界、且不破坏任何已落地的规则;要更好得让 core 把逐候选拒绝分成瞬时/裁定两类,已列后续件。
|
|
187
|
+
if (result.skippedAtRedeem.length > 0) {
|
|
188
|
+
return await indeterminate(`partial import: ${result.skippedAtRedeem.length} candidate(s) did not land (${result.skippedAtRedeem.map((x) => x.reason).join("; ")})`);
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, result };
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
return await indeterminate(err instanceof Error ? err.message : String(err));
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=rules-consent.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/177 v2/F4 —— shared-memory 读面的**授权折叠**:principal → 可读 org scope 集。
|
|
3
|
+
*
|
|
4
|
+
* 为什么复用 {@link OrgMemoryDirectory} 而不是另立一张表:「谁属于 org:acme」在一个进程里必须只有一个
|
|
5
|
+
* 答案。core 的记忆准入 seam、`/v1/memory/*` 的属主门、以及这里的共享库读面读的是**同一个目录实例**
|
|
6
|
+
* (`createOrgMemoryAdmissionWiring` 的产物),因此共享同一份 TTL 缓存 / 退避窗 / gen 高水位——吊销窗内
|
|
7
|
+
* 三面不会公开分歧。各建各的目录 = 三个答案,那正是 design/170 §7 收编要消灭的东西。
|
|
8
|
+
*
|
|
9
|
+
* 🔴 判别联合原样透传(N3/C5 裁定的下游一致性):目录说 `unavailable` 时**绝不**折成「你不属于任何
|
|
10
|
+
* org」。前者在模型面渲染成「连接不可用,恢复前拒读」(可重试),后者渲染成「本会话没有连接任何记忆库」
|
|
11
|
+
* (终局,模型就此放弃)。两句话是相反的指令,把前者塌进后者是一次静默的 fail-open。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 deployment-origin scope 的多租户切断:operator 在配置里自证的 org scope(`MEMORY_SCOPE=org:x`、
|
|
14
|
+
* 单用户部署的 projects 登记簿)只在**无租户边界**的部署里授予。多租户部署(`requirePrincipal`)下,
|
|
15
|
+
* 一条部署级声明会同时授予每一个 principal —— 那是跨租户读,不是配置便利。装配点因此按部署形态把这个
|
|
16
|
+
* 集合择净后再传进来(见 main.ts 的构造行),本模块只忠实使用它:哪些 scope 算「部署自证」是装配点的
|
|
17
|
+
* 判断,不是这里的。
|
|
18
|
+
*/
|
|
19
|
+
import type { SharedMemoryScopeAuthorizer } from "./plugins/shared-memory-store-sql.js";
|
|
20
|
+
import type { OrgMemoryDirectory } from "./org-memory-admission.js";
|
|
21
|
+
export interface SharedMemoryScopeAuthorizerOptions {
|
|
22
|
+
/** 目录实例。**必须**与 core 准入 seam / memory-policy 面同一只(见头注)。 */
|
|
23
|
+
directory: OrgMemoryDirectory;
|
|
24
|
+
/** 部署自证的 org scope(装配点已按部署形态择净;多租户下应为空)。 */
|
|
25
|
+
deploymentScopes?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** 活对象(闭包持有目录与集合)⇒ `create*`(CLAUDE.md 工厂命名律)。 */
|
|
28
|
+
export declare function createSharedMemoryScopeAuthorizer(opts: SharedMemoryScopeAuthorizerOptions): SharedMemoryScopeAuthorizer;
|
|
29
|
+
//# sourceMappingURL=shared-memory-scope-authorizer.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** 活对象(闭包持有目录与集合)⇒ `create*`(CLAUDE.md 工厂命名律)。 */
|
|
2
|
+
export function createSharedMemoryScopeAuthorizer(opts) {
|
|
3
|
+
const deploymentScopes = [...new Set(opts.deploymentScopes ?? [])];
|
|
4
|
+
return {
|
|
5
|
+
async resolve(principal) {
|
|
6
|
+
// principal 缺席 = 单用户部署(多租户下 core 与 HTTP 门都先要求身份)。此时唯一的授权事实就是
|
|
7
|
+
// operator 自证的那一组;目录无从查起,但这不是「不可用」——它是一个**已知的**答案。
|
|
8
|
+
if (principal === undefined)
|
|
9
|
+
return { kind: "granted", scopes: deploymentScopes };
|
|
10
|
+
const lookup = await opts.directory.lookup(principal);
|
|
11
|
+
if (lookup.kind === "unavailable")
|
|
12
|
+
return { kind: "unavailable", reason: lookup.reason };
|
|
13
|
+
return { kind: "granted", scopes: [...new Set([...deploymentScopes, ...Object.keys(lookup.scopes)])] };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=shared-memory-scope-authorizer.js.map
|
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,24 @@ 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";
|
|
122
150
|
/** Validate the respond body — the closed three-choice enum (rationale in the module header). */
|
|
123
151
|
export declare function parseToolApprovalResponse(body: unknown): {
|
|
124
152
|
ok: true;
|
|
125
153
|
value: ToolApprovalDecision;
|
|
126
154
|
updatedInput?: unknown;
|
|
155
|
+
persistRule?: string;
|
|
127
156
|
} | {
|
|
128
157
|
ok: false;
|
|
129
158
|
error: string;
|
|
@@ -310,6 +339,10 @@ export declare class ToolApprovalCoordinator {
|
|
|
310
339
|
* 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
|
|
311
340
|
* 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
|
|
312
341
|
private readonly governanceAskMarks;
|
|
342
|
+
/** #154 车二:持久化权限规则的**同意车道**。在场 ⇔ 规则店真装配(main.ts 与 core 的
|
|
343
|
+
* `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `ruleSuggestions`;②回决带
|
|
344
|
+
* `persistRule` 时兑付进店。缺席 ⇒ 两件都不做(诚实缺席,不发无处可兑的候选)。 */
|
|
345
|
+
private readonly ruleConsent;
|
|
313
346
|
constructor(opts?: {
|
|
314
347
|
ttlMs?: number;
|
|
315
348
|
askStore?: ApprovalAskStore;
|
|
@@ -318,6 +351,8 @@ export declare class ToolApprovalCoordinator {
|
|
|
318
351
|
admitMaxPerOwner?: number;
|
|
319
352
|
/** 缺省 = 进程级单表(写侧默认同一张)。注入口只为测试与将来的多实例形。 */
|
|
320
353
|
governanceAskMarks?: GovernanceAskMarks;
|
|
354
|
+
/** #154 车二:同意车道(在场 = 规则店已装配)。 */
|
|
355
|
+
ruleConsent?: RuleConsentLane;
|
|
321
356
|
});
|
|
322
357
|
/**
|
|
323
358
|
* #151 车3 刀 3b —— design/172 §3.3 / 设计稿 §0 X-2 的**写侧准入门**。
|
|
@@ -339,6 +374,14 @@ export declare class ToolApprovalCoordinator {
|
|
|
339
374
|
* 多副本级的总量控制登记为后续件(汇报存疑单)。
|
|
340
375
|
*/
|
|
341
376
|
private admit;
|
|
377
|
+
/**
|
|
378
|
+
* #154 车二:一只 ask 的**规则车道素材**,或 `undefined`(= 本 ask 不投候选、不接受 `persistRule`)。
|
|
379
|
+
*
|
|
380
|
+
* 三个合取项(缺一即 `undefined`,理由逐字见调用点上方注):①店在场 ②引擎铸了候选 ③命令原字节可读。
|
|
381
|
+
* `req.args` 是 `unknown` ⇒ 窄读,**禁裸 as-cast**(宪法 [2704]:一个形状漂了的 args 若被 cast,
|
|
382
|
+
* 会把 `undefined` 当命令送进 `prepareCardApproval`,那是放宽面上的静默垃圾)。
|
|
383
|
+
*/
|
|
384
|
+
private buildRuleLaneMaterial;
|
|
342
385
|
/** 测试/可观测性钩子(X-2):某 (taskId) 维当前占用的准入名额数。 */
|
|
343
386
|
admittedCount(taskId: string): number;
|
|
344
387
|
/** #151 车2(D5 store 故障姿势):任何 store 调用 throw ⇒ fail-open 到进程内机械照旧(store 是记账/
|
|
@@ -466,6 +509,18 @@ export declare class ToolApprovalCoordinator {
|
|
|
466
509
|
status: number;
|
|
467
510
|
body: unknown;
|
|
468
511
|
}>;
|
|
512
|
+
/**
|
|
513
|
+
* #154 车二:回决携规则确认 ⇒ prepare→confirm→redeem(实现在 `rules-consent.ts`,本方法只做门与回显)。
|
|
514
|
+
*
|
|
515
|
+
* 三道门,每道都是**拒绝**而不是降级:
|
|
516
|
+
* ① 裁决必须真落定(200)—— 见调用点注;
|
|
517
|
+
* ② 必须有已验明的 owner —— 规则是**某个人**的持久同意,一条 `owner === null` 的 ask 没有归属人可写;
|
|
518
|
+
* ③ 本 ask 必须登记过规则车道素材(店在场 ∧ 引擎铸过候选 ∧ 命令可读)。
|
|
519
|
+
*
|
|
520
|
+
* 失败**不翻转裁决**:人的「允许这次」已经生效并且已经交给引擎了,把整个回决改判成 4xx 会让一次真实的
|
|
521
|
+
* 放行凭空消失。诚实的形是 200 + `rulePersisted:false`(壳据它告诉人「这次放行了,但『不再询问』没存上」)。
|
|
522
|
+
*/
|
|
523
|
+
private persistRuleAfterDecision;
|
|
469
524
|
/** store 在场时的回决 CAS 门(D2)——`respond()` 的异步分支,拆出以保持 `respond()` 本身在 store 缺席
|
|
470
525
|
* 时仍是纯同步函数(D1)。`askStore`/`askId`/`batchId` 由调用方在已窄化的分支里传入(避免非空断言)。 */
|
|
471
526
|
private respondWithCas;
|