@sema-agent/server 7.9.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/USAGE.md +6 -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/approval-reconciler.d.ts +1 -1
- package/dist/approval-reconciler.js +1 -1
- 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/reapers.js +3 -3
- package/dist/boot/runner-deps.d.ts +10 -2
- package/dist/boot/runner-deps.js +12 -1
- package/dist/capabilities/repo-tools.d.ts +36 -2
- package/dist/capabilities/repo-tools.js +125 -11
- package/dist/capabilities/scenarios.d.ts +2 -2
- package/dist/capabilities/scenarios.js +1 -1
- package/dist/config-types.d.ts +22 -2
- package/dist/config.js +44 -2
- package/dist/fleet/fleet-terminal-window.d.ts +1 -1
- package/dist/fleet/fleet-terminal-window.js +1 -1
- package/dist/git-api-kind.d.ts +7 -0
- package/dist/git-api-kind.js +8 -0
- 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 -2
- 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 +47 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +54 -5
- 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 +22 -9
- package/dist/plugins/checkpoint-store-sql.js +40 -29
- package/dist/plugins/local-checkpoint-store.d.ts +11 -1
- package/dist/plugins/local-checkpoint-store.js +10 -2
- package/dist/plugins/memory-engine-pg.js +3 -3
- 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 +44 -7
- package/dist/plugins/run-store-sql.d.ts +3 -3
- package/dist/plugins/run-store-sql.js +3 -3
- 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 +55 -8
- package/dist/plugins/workflow-journal-store-sql.d.ts +3 -3
- package/dist/plugins/workflow-journal-store-sql.js +6 -6
- package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-run-store-sql.js +12 -12
- package/dist/rules-consent.d.ts +126 -0
- package/dist/rules-consent.js +198 -0
- package/dist/run-local.js +2 -2
- 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
|
@@ -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;
|
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,33 @@ 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) {
|
|
434
|
+
if (this.ruleConsent === undefined)
|
|
435
|
+
return undefined;
|
|
436
|
+
const suggestions = req.ruleSuggestions;
|
|
437
|
+
if (!Array.isArray(suggestions) || suggestions.length === 0)
|
|
438
|
+
return undefined;
|
|
439
|
+
const args = req.args;
|
|
440
|
+
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
441
|
+
return undefined;
|
|
442
|
+
const command = args.command;
|
|
443
|
+
if (typeof command !== "string" || command === "")
|
|
444
|
+
return undefined;
|
|
445
|
+
const boundInputHash = readBoundInputHash(req);
|
|
446
|
+
return {
|
|
447
|
+
command,
|
|
448
|
+
suggestions,
|
|
449
|
+
...(typeof req.toolCallId === "string" && req.toolCallId !== "" ? { toolCallId: req.toolCallId } : {}),
|
|
450
|
+
...(boundInputHash !== null ? { boundInputHash } : {}),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
407
453
|
/** 测试/可观测性钩子(X-2):某 (taskId) 维当前占用的准入名额数。 */
|
|
408
454
|
admittedCount(taskId) {
|
|
409
455
|
return this.admitByTask.get(taskId) ?? 0;
|
|
@@ -704,6 +750,12 @@ export class ToolApprovalCoordinator {
|
|
|
704
750
|
const id = uuidv7();
|
|
705
751
|
const child = isChildAsk(req);
|
|
706
752
|
const bounded = boundArgs(req.args);
|
|
753
|
+
// #154 车二:规则候选的投影素材。**三个合取项**,缺一不投:
|
|
754
|
+
// ① 规则店真装配(`this.ruleConsent` 在场)—— 店缺席仍投 = 无处可兑的「不再询问」= wire 谎言;
|
|
755
|
+
// ② 引擎真铸了候选(`req.ruleSuggestions` 非空)—— 命令不可匹配(复合/重定向/替换)时 core 交空数组;
|
|
756
|
+
// ③ 命令原字节读得出(回决时**引擎**要拿它重铸候选;读不出就没有可兑付的路,投候选等于骗人)。
|
|
757
|
+
// 读 `req.args.command` 走窄读:`args` 是 `unknown`,禁裸 as-cast(宪法 [2704])。
|
|
758
|
+
const ruleLaneMaterial = this.buildRuleLaneMaterial(req);
|
|
707
759
|
const frame = {
|
|
708
760
|
type: "tool_approval",
|
|
709
761
|
approvalId: id,
|
|
@@ -736,6 +788,8 @@ export class ToolApprovalCoordinator {
|
|
|
736
788
|
...((this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(primary.owner, primary.sessionId, origin.taskId))).isMarked(req.toolCallId)
|
|
737
789
|
? { governanceForced: true }
|
|
738
790
|
: {}),
|
|
791
|
+
// #154 车二:候选逐字透传(条件见 `ruleLaneMaterial` 上方注)。
|
|
792
|
+
...(ruleLaneMaterial !== undefined ? { ruleSuggestions: ruleLaneMaterial.suggestions } : {}),
|
|
739
793
|
...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
|
|
740
794
|
};
|
|
741
795
|
// #151 车2(design/172 §3.3 D3 窗长三元):有效窗——legDeadlineMonotonic 缺席时退化成 ttlMs(D1 零
|
|
@@ -1237,6 +1291,9 @@ export class ToolApprovalCoordinator {
|
|
|
1237
1291
|
...(child ? { originTaskId: req.sourceTaskId } : { targetCtxs: new Set(aliveCtxs), onTargetGone }),
|
|
1238
1292
|
...(primary.sessionId ? { sessionId: primary.sessionId } : {}),
|
|
1239
1293
|
toolName: req.toolName,
|
|
1294
|
+
// #154 车二:规则车道素材 —— 与帧上的 `ruleSuggestions` **同一个条件**(店在场 ∧ 引擎真铸了候选 ∧
|
|
1295
|
+
// 命令原字节读得出)。三者任一不成立 ⇒ 不登记,回决带 `persistRule` 时如实拒(不猜命令)。
|
|
1296
|
+
...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
|
|
1240
1297
|
...(askId !== undefined && batchId !== undefined ? { askId, batchId } : {}),
|
|
1241
1298
|
};
|
|
1242
1299
|
selfEntry = pendingEntry; // 供 settle() 结算时把自己从 pendingByAskId 的 Set 里摘除(上方顶注)
|
|
@@ -1418,10 +1475,72 @@ export class ToolApprovalCoordinator {
|
|
|
1418
1475
|
// #151 车2 D2(回决竞争者):askStore 缺席 ⇒ finishRespond 同步收尾,逐字不变(D1)。在场 ⇒ 先打一把
|
|
1419
1476
|
// decideAsk CAS,赢了才 finishRespond;输(某个其他竞争者已经决定了这只 ask 的终局)⇒ 不 settle,照旧
|
|
1420
1477
|
// 回现有 404 形(响应体的 409/410 分化是车4 的活,本车只保证「输者绝不覆盖赢者」)。
|
|
1421
|
-
|
|
1422
|
-
|
|
1478
|
+
const settled = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
|
|
1479
|
+
? this.respondWithCas(this.askStore, entry.askId, entry.batchId, id, entry, parsed)
|
|
1480
|
+
: this.finishRespond(id, entry, parsed);
|
|
1481
|
+
// #154 车二:「不再询问」的兑付口。**在裁决落定之后**(顺序是硬的:规则是「这次同意的持久化」,
|
|
1482
|
+
// 一次没有生效的裁决不该留下一条永久规则),且只在 200 那一支 —— 404(CAS 输/条目已被别人结算)
|
|
1483
|
+
// 意味着这次回决不是有效终局,连带的规则确认也就不成立。
|
|
1484
|
+
if (parsed.persistRule === undefined)
|
|
1485
|
+
return settled;
|
|
1486
|
+
return this.persistRuleAfterDecision(settled, entry, parsed.persistRule, parsed.updatedInput !== undefined);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* #154 车二:回决携规则确认 ⇒ prepare→confirm→redeem(实现在 `rules-consent.ts`,本方法只做门与回显)。
|
|
1490
|
+
*
|
|
1491
|
+
* 三道门,每道都是**拒绝**而不是降级:
|
|
1492
|
+
* ① 裁决必须真落定(200)—— 见调用点注;
|
|
1493
|
+
* ② 必须有已验明的 owner —— 规则是**某个人**的持久同意,一条 `owner === null` 的 ask 没有归属人可写;
|
|
1494
|
+
* ③ 本 ask 必须登记过规则车道素材(店在场 ∧ 引擎铸过候选 ∧ 命令可读)。
|
|
1495
|
+
*
|
|
1496
|
+
* 失败**不翻转裁决**:人的「允许这次」已经生效并且已经交给引擎了,把整个回决改判成 4xx 会让一次真实的
|
|
1497
|
+
* 放行凭空消失。诚实的形是 200 + `rulePersisted:false`(壳据它告诉人「这次放行了,但『不再询问』没存上」)。
|
|
1498
|
+
*/
|
|
1499
|
+
async persistRuleAfterDecision(settled, entry, ruleText, inputWasEdited) {
|
|
1500
|
+
const result = await settled;
|
|
1501
|
+
if (result.status !== 200)
|
|
1502
|
+
return result;
|
|
1503
|
+
const lane = this.ruleConsent;
|
|
1504
|
+
const material = entry.ruleLane;
|
|
1505
|
+
const owner = entry.owner;
|
|
1506
|
+
const withFlag = (rulePersisted, ruleRefusal) => ({
|
|
1507
|
+
status: result.status,
|
|
1508
|
+
body: { ...result.body, rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}) },
|
|
1509
|
+
});
|
|
1510
|
+
if (lane === undefined || material === undefined || owner === null)
|
|
1511
|
+
return withFlag(false, "rule_lane_unavailable");
|
|
1512
|
+
// 🔴 codex 交叉复审 round3 [high] 二(验真后修,**真扩权面**):`updatedInput`(ctrl+g 编辑放行)
|
|
1513
|
+
// 与 `persistRule` 同时在场时,**生效的**调用是编辑后的那一份,而候选是引擎从**原始**命令铸的 ——
|
|
1514
|
+
// 于是「把一条危险命令改安全 + 勾上不再询问」会给原始那条危险命令装上一条常驻放行规则。
|
|
1515
|
+
// v1 处置 = **拒绝这次持久化**(裁决本身照常生效;人只是这一次没能顺手存下规则)。
|
|
1516
|
+
// 不选「从编辑后的输入重铸候选」:`updatedInput` 是 `unknown` 的整体替换形,它与这只 ask 上引擎
|
|
1517
|
+
// 铸的候选之间没有任何等式可对 —— 那条路要先给编辑面立形,是另一件事(登记为后续件)。
|
|
1518
|
+
if (inputWasEdited)
|
|
1519
|
+
return withFlag(false, "rule_input_edited");
|
|
1520
|
+
// 报出的文本必须**在引擎铸的候选里**(等式一道)。对不上 ⇒ 拒,不猜:一条不在候选里的文本要么是
|
|
1521
|
+
// 旧卡的残留,要么是有人在试着自铸规则,两种都不该落库。
|
|
1522
|
+
if (!material.suggestions.some((s) => s.rule === ruleText))
|
|
1523
|
+
return withFlag(false, "rule_not_offered");
|
|
1524
|
+
try {
|
|
1525
|
+
const persisted = await lane.persistCardRule({
|
|
1526
|
+
principal: owner,
|
|
1527
|
+
toolName: entry.toolName,
|
|
1528
|
+
command: material.command,
|
|
1529
|
+
ruleText,
|
|
1530
|
+
...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
|
|
1531
|
+
...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
|
|
1532
|
+
});
|
|
1533
|
+
if (!persisted.ok) {
|
|
1534
|
+
defaultLogger.warn("permission rule was not persisted after an approval", { reason: persisted.reason, detail: persisted.detail });
|
|
1535
|
+
return withFlag(false, persisted.reason);
|
|
1536
|
+
}
|
|
1537
|
+
return withFlag(true);
|
|
1538
|
+
}
|
|
1539
|
+
catch (err) {
|
|
1540
|
+
// D5 同族姿势:店抖动不改判人的裁决,只如实报「没存上」。
|
|
1541
|
+
this.noteStoreError(err, "persistCardRule");
|
|
1542
|
+
return withFlag(false, "rule_store_error");
|
|
1423
1543
|
}
|
|
1424
|
-
return this.finishRespond(id, entry, parsed);
|
|
1425
1544
|
}
|
|
1426
1545
|
/** store 在场时的回决 CAS 门(D2)——`respond()` 的异步分支,拆出以保持 `respond()` 本身在 store 缺席
|
|
1427
1546
|
* 时仍是纯同步函数(D1)。`askStore`/`askId`/`batchId` 由调用方在已窄化的分支里传入(避免非空断言)。 */
|
|
@@ -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>>;
|
package/dist/trace/project.d.ts
CHANGED
package/dist/trace/project.js
CHANGED
|
@@ -561,6 +561,7 @@ export function brainStatusEventData(ev) {
|
|
|
561
561
|
...(num(ev.attempt) ? { attempt: ev.attempt } : {}),
|
|
562
562
|
...(num(ev.maxRetries) ? { maxRetries: ev.maxRetries } : {}),
|
|
563
563
|
...(num(ev.retryInMs) ? { retryInMs: ev.retryInMs } : {}),
|
|
564
|
+
...(typeof ev.errClass === "string" && ev.errClass.length > 0 ? { errClass: ev.errClass } : {}),
|
|
564
565
|
...identityFields(ev),
|
|
565
566
|
};
|
|
566
567
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.11.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
57
|
+
"@sema-agent/core": "^5.22.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.16.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"sharp": "^0.35.3"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@sema-agent/sdk": "^6.
|
|
72
|
+
"@sema-agent/sdk": "^6.13.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|