@sema-agent/server 7.45.0 → 7.46.0-rc.2
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 +38 -0
- package/dist/approval-card.d.ts +106 -40
- package/dist/approval-card.js +45 -8
- package/dist/boot/coordinators.d.ts +1 -1
- package/dist/boot/memory-consolidation.d.ts +191 -0
- package/dist/boot/memory-consolidation.js +132 -0
- package/dist/boot/runner-deps.d.ts +1 -1
- package/dist/config-types.d.ts +48 -2
- package/dist/config-types.js +1 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +53 -3
- package/dist/http/routes/capabilities.js +4 -0
- package/dist/http/routes/memory-compliance.d.ts +102 -0
- package/dist/http/routes/memory-compliance.js +113 -0
- package/dist/http/routes/memory-consolidation.d.ts +60 -0
- package/dist/http/routes/memory-consolidation.js +155 -0
- package/dist/http/routes/memory-origin.d.ts +124 -0
- package/dist/http/routes/memory-origin.js +193 -0
- package/dist/http/routes/rules.js +1 -1
- package/dist/http/routes/side-query.js +1 -0
- package/dist/http/server.d.ts +71 -2
- package/dist/http/server.js +27 -2
- package/dist/main.js +55 -2
- package/dist/memory-operator-faces.d.ts +211 -0
- package/dist/memory-operator-faces.js +76 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
- package/dist/plugins/checkpoint-store-sql.js +31 -17
- package/dist/plugins/local-checkpoint-store.js +3 -3
- package/dist/plugins/permission-rule-store-file.d.ts +2 -2
- package/dist/plugins/permission-rule-store-file.js +41 -4
- package/dist/plugins/permission-rule-store-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.js +59 -18
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/tidb-pool.js +3 -3
- package/dist/rules-consent.d.ts +30 -3
- package/dist/rules-consent.js +50 -7
- package/dist/task-cwd.d.ts +1 -1
- package/dist/tool-approval.d.ts +43 -10
- package/dist/tool-approval.js +105 -27
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/package.json +3 -3
package/dist/rules-consent.js
CHANGED
|
@@ -70,10 +70,11 @@ function confirmRefusalWord(reason) {
|
|
|
70
70
|
case "edit_rejected":
|
|
71
71
|
return "edit-rejected";
|
|
72
72
|
case "record_not_found":
|
|
73
|
+
case "record_schema_stale":
|
|
74
|
+
case "record_malformed":
|
|
73
75
|
case "selection_missing":
|
|
74
76
|
case "selection_invalid":
|
|
75
77
|
case "selection_mismatch":
|
|
76
|
-
case "batch_takes_no_selection":
|
|
77
78
|
case "not_pending":
|
|
78
79
|
case "conflict":
|
|
79
80
|
return "confirm-refused";
|
|
@@ -84,6 +85,22 @@ function confirmRefusalWord(reason) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
function refusedMembersOf(members) {
|
|
89
|
+
const refused = [];
|
|
90
|
+
for (const m of members) {
|
|
91
|
+
switch (m.status) {
|
|
92
|
+
case "persisted":
|
|
93
|
+
case "deduped":
|
|
94
|
+
break;
|
|
95
|
+
case "refused":
|
|
96
|
+
refused.push(m);
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`unknown redeemed-batch member status: ${JSON.stringify(m)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return refused;
|
|
103
|
+
}
|
|
87
104
|
export function createRuleConsentLane(stores, opts) {
|
|
88
105
|
const deps = { provider: stores.provider, approvals: stores.approvals, cardEdits: true };
|
|
89
106
|
const ticketTtlMs = opts?.ticketTtlMs ?? RULE_IMPORT_TICKET_TTL_MS;
|
|
@@ -125,14 +142,33 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
125
142
|
return { ok: false, reason: "no-candidates", detail: "the rule lane cannot speak for this command (compound / redirection / unsupported tool)" };
|
|
126
143
|
}
|
|
127
144
|
const redemption = input.redemption;
|
|
145
|
+
if (redemption.kind === "batch") {
|
|
146
|
+
const wanted = new Set(redemption.offer.rules.map((r) => r.rule));
|
|
147
|
+
const offerIndex = prepared.offers.findIndex((o) => o.kind === "batch" && o.rules.length === wanted.size && o.rules.every((r) => wanted.has(r.rule)));
|
|
148
|
+
if (offerIndex < 0) {
|
|
149
|
+
return { ok: false, reason: "unknown-candidate", detail: "the confirmed batch is not the batch the engine now mints for this command (coverage drift) — re-trigger the command for a fresh card" };
|
|
150
|
+
}
|
|
151
|
+
const confirmed = await confirmRuleApproval({ approvalId: prepared.approvalId, principal: input.principal, selectedOffer: offerIndex, deps });
|
|
152
|
+
if (!confirmed.ok)
|
|
153
|
+
return { ok: false, reason: "confirm-refused", detail: confirmed.reason };
|
|
154
|
+
const result = await redeemRuleBatch({ approvalId: prepared.approvalId, principal: input.principal, deps });
|
|
155
|
+
if ("status" in result)
|
|
156
|
+
return { ok: false, reason: "redeem-refused", detail: result.reason };
|
|
157
|
+
const refused = refusedMembersOf(result.members);
|
|
158
|
+
if (refused.length > 0) {
|
|
159
|
+
return { ok: false, reason: "redeem-refused", detail: `partial batch: ${refused.length} member(s) refused (${refused.map((x) => x.reason).join("; ")})` };
|
|
160
|
+
}
|
|
161
|
+
return { ok: true, kind: "batch", rules: result.members.map((m) => m.rule), rev: result.rev };
|
|
162
|
+
}
|
|
128
163
|
let ticket;
|
|
129
164
|
if (redemption.kind === "offered") {
|
|
165
|
+
const offerIndex = prepared.offers.findIndex((o) => o.kind === "single" && o.rule === redemption.ruleText);
|
|
130
166
|
const index = prepared.candidates.findIndex((c) => c.rule === redemption.ruleText);
|
|
131
167
|
const offered = index < 0 ? undefined : prepared.tickets[index];
|
|
132
|
-
if (index < 0 || offered === undefined) {
|
|
133
|
-
return { ok: false, reason: "unknown-candidate", detail: "the chosen rule text is not
|
|
168
|
+
if (offerIndex < 0 || index < 0 || offered === undefined) {
|
|
169
|
+
return { ok: false, reason: "unknown-candidate", detail: "the chosen rule text is not a single offer the engine minted for this command" };
|
|
134
170
|
}
|
|
135
|
-
const confirmed = await confirmRuleApproval({ approvalId: prepared.approvalId, principal: input.principal,
|
|
171
|
+
const confirmed = await confirmRuleApproval({ approvalId: prepared.approvalId, principal: input.principal, selectedOffer: offerIndex, deps });
|
|
136
172
|
if (!confirmed.ok)
|
|
137
173
|
return { ok: false, reason: "confirm-refused", detail: confirmed.reason };
|
|
138
174
|
ticket = offered;
|
|
@@ -169,7 +205,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
169
205
|
const redeemed = await redeemRuleTicket({ ticket, principal: input.principal, deps });
|
|
170
206
|
if (redeemed.status !== "redeemed")
|
|
171
207
|
return { ok: false, reason: "redeem-refused", detail: redeemed.reason };
|
|
172
|
-
return { ok: true, rule: redeemed.rule, rev: redeemed.rev, alreadyRedeemed: redeemed.alreadyRedeemed };
|
|
208
|
+
return { ok: true, kind: "single", rule: redeemed.rule, rev: redeemed.rev, alreadyRedeemed: redeemed.alreadyRedeemed };
|
|
173
209
|
},
|
|
174
210
|
async prepareImport(principal, layers) {
|
|
175
211
|
const ccLayers = layers.map((l) => ({
|
|
@@ -179,6 +215,9 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
179
215
|
readFile: async () => l.content,
|
|
180
216
|
}));
|
|
181
217
|
const { preview, approvalId } = await prepareCcImport({ layers: ccLayers, principal, deps });
|
|
218
|
+
if (approvalId === undefined) {
|
|
219
|
+
return { ok: true, preview };
|
|
220
|
+
}
|
|
182
221
|
if (preview.candidates.length > MAX_IMPORT_CANDIDATES) {
|
|
183
222
|
try {
|
|
184
223
|
await stores.approvals.discardPendingRecord?.(approvalId);
|
|
@@ -216,6 +255,9 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
216
255
|
const record = await stores.approvals.get(consumed.approvalId);
|
|
217
256
|
if (record === undefined)
|
|
218
257
|
return { ok: false, reason: "record-unusable", detail: "the approval record behind this ticket is gone" };
|
|
258
|
+
if ("staleSchema" in record) {
|
|
259
|
+
return { ok: false, reason: "record-unusable", detail: "this approval record predates the schema:2 form — re-run the import to mint a fresh record" };
|
|
260
|
+
}
|
|
219
261
|
if (buildRulePayloadHash(record.candidates) !== consumed.payloadHash) {
|
|
220
262
|
return { ok: false, reason: "payload-mismatch", detail: "the approval record's candidates changed after the ticket was minted" };
|
|
221
263
|
}
|
|
@@ -231,8 +273,9 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
231
273
|
if ("status" in result) {
|
|
232
274
|
return await indeterminate(result.reason);
|
|
233
275
|
}
|
|
234
|
-
|
|
235
|
-
|
|
276
|
+
const refused = refusedMembersOf(result.members);
|
|
277
|
+
if (refused.length > 0) {
|
|
278
|
+
return await indeterminate(`partial import: ${refused.length} member(s) refused (${refused.map((x) => x.reason).join("; ")})`);
|
|
236
279
|
}
|
|
237
280
|
return { ok: true, result };
|
|
238
281
|
}
|
package/dist/task-cwd.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare function inProcessSingleUserLane(config: {
|
|
|
44
44
|
* in-process 单用户 lane({@link inProcessSingleUserLane})取引擎自身 `process.cwd()`。它读起来像在
|
|
45
45
|
* 服务一个真部署形,其实到不了 —— 该 lane 的 `remoteExec` 未设 ⇒ boot/execution-env.ts 六条工厂臂全
|
|
46
46
|
* 不命中 ⇒ core 落 `StubExecutionEnv` ⇒ `handsEnabled=false` ⇒ 不挂 Bash ⇒ 未知工具在 agent-loop 走
|
|
47
|
-
* `tool.not_found`(**不进审批座**)⇒ 无 `
|
|
47
|
+
* `tool.not_found`(**不进审批座**)⇒ 无 `ruleOffers` ⇒ 本函数在规则车道上根本不被调用;同 lane
|
|
48
48
|
* 第 1 臂也因写侧闸只在 `cwdHonored` 下开而恒空。危害口径按 refuter **降级**:死枝 ≡ 落第 3 臂
|
|
49
49
|
* `undefined` ≡ core global 缺省 ≡ #295 修前行为,**今天零用户可见伤害**,不是「恒不命中」类缺陷。
|
|
50
50
|
* 🚧 **给将来接手的人的红线**:这条 lane 若哪天真接上 hands,root 必须取 `executionEnv.cwd`(core 的
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage } from "node:http";
|
|
2
|
-
import { type AskRequest, type AskOutcome } from "@sema-agent/core";
|
|
2
|
+
import { type AskRequest, type AskOutcome, type RuleOffer } from "@sema-agent/core";
|
|
3
3
|
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
4
4
|
import { type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
5
5
|
import type { ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
|
|
@@ -170,7 +170,8 @@ export interface ToolApprovalFrame {
|
|
|
170
170
|
persistedRuleShadowed?: string;
|
|
171
171
|
/**
|
|
172
172
|
* #154 车二(core 5.18.0 design/179):`"tool_approval"` only —— 引擎为这次 ask 铸的**规则候选**
|
|
173
|
-
* (`AskRequest.
|
|
173
|
+
* (`AskRequest.ruleOffers`,core 5.58.0 判别联合逐字透传:闭词表 `kind`/`match` + 引擎铸的文本,
|
|
174
|
+
* server 不重铸不重排;序即契约——exact single 恒 0、batch 恒末,选择键=offer index)。
|
|
174
175
|
* 壳据它渲「不再询问」,回决时用 `persistRule.rule` 报出选中的那一条。
|
|
175
176
|
*
|
|
176
177
|
* 🔴 **在场性即承诺**:只在**规则店真装配**(协调器拿到 `ruleConsent`,与 core 的
|
|
@@ -178,11 +179,7 @@ export interface ToolApprovalFrame {
|
|
|
178
179
|
* `permissionRules.storeWired` 不可能相左)时在场。店缺席仍投 = 一格按下去无处可兑的「不再询问」,
|
|
179
180
|
* 是 wire 谎言(判据逐字见 `trace/core-keyset-guard.ts` ④ 面本键那一段)。
|
|
180
181
|
*/
|
|
181
|
-
|
|
182
|
-
rule: string;
|
|
183
|
-
match: "exact" | "prefix";
|
|
184
|
-
command: string;
|
|
185
|
-
}>;
|
|
182
|
+
ruleOffers?: readonly RuleOffer[];
|
|
186
183
|
/**
|
|
187
184
|
* #253 件 G1(core 5.33.0 backlog #239,判据帖 [3930] G1;**ADDITIVE**,`"tool_approval"` only)——
|
|
188
185
|
* 这次 ask **为什么**被收紧的**结构化**因由(`AskRequest.probeCause`:工具的 `reversibilityProbe`
|
|
@@ -358,16 +355,29 @@ export type RuleRefusalReason = Extract<CardRulePersisted, {
|
|
|
358
355
|
* `edited` 是判别位而不是「一个可选的第二字段」:候选臂的 `rule` 是**卡上那条候选的逐字文本**(定位键),
|
|
359
356
|
* 编辑臂的 `rule` 是**人自己写的规则**。同一个字段两种含义,判别位必须显式带在同一个对象上。
|
|
360
357
|
*/
|
|
361
|
-
export
|
|
358
|
+
export type ParsedPersistRule = {
|
|
359
|
+
readonly kind: "text";
|
|
362
360
|
readonly rule: string;
|
|
363
361
|
readonly edited: boolean;
|
|
364
362
|
}
|
|
363
|
+
/** design/377(cli 1.0.92 合窗):人勾了 **batch** offer——合取批没有单条文本可抄,选择键=帧上
|
|
364
|
+
* `ruleOffers` 的下标(卡=行素材=呈卡帧同源;server 侧兑付前另过防漂等式,见
|
|
365
|
+
* `persistRuleAfterDecision` 的 batch 臂)。窄开只指 batch:single 臂的防伪锚是文本等式,
|
|
366
|
+
* index 不许旁路它。 */
|
|
367
|
+
| {
|
|
368
|
+
readonly kind: "batch";
|
|
369
|
+
readonly batchOfferIndex: number;
|
|
370
|
+
};
|
|
365
371
|
/** #340:`persistRule.rule` 的形/上限拒句(wire 可见文案的**唯一**成形口 —— 冻结在
|
|
366
372
|
* `test/api-error-text-freeze.test.ts` 的常量锚格,与 `src/http/` 桶里的 `sendError` 站点同纪律)。 */
|
|
367
373
|
export declare const PERSIST_RULE_TEXT_ERROR = "persistRule.rule must be a non-empty string of at most 512 characters";
|
|
368
374
|
/** #340:`persistRule.edited` 的形拒句。非 boolean **绝不静默当 false** —— 那会把一次「我要落自由文本」
|
|
369
375
|
* 悄悄折回候选臂,回来一句 `rule_not_offered`,而人以为自己写的规则被拒了。 */
|
|
370
376
|
export declare const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present";
|
|
377
|
+
/** design/377:batch 选择键的形拒句(负数/非整数/非数一律响亮 400——一个被 |0 折过的下标指向的是另一条 offer)。 */
|
|
378
|
+
export declare const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
|
|
379
|
+
/** design/377:三臂互斥拒句。同场不静默取一——两臂说的是两次不同的授权,猜哪个都等于替人改主意。 */
|
|
380
|
+
export declare const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited \u2014 send exactly one arm";
|
|
371
381
|
/**
|
|
372
382
|
* #340 定界5:回决体**不收 scope**。
|
|
373
383
|
*
|
|
@@ -584,7 +594,7 @@ export declare class ToolApprovalCoordinator {
|
|
|
584
594
|
* 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
|
|
585
595
|
private readonly governanceAskMarks;
|
|
586
596
|
/** #154 车二:持久化权限规则的**同意车道**。在场 ⇔ 规则店真装配(main.ts 与 core 的
|
|
587
|
-
* `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `
|
|
597
|
+
* `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `ruleOffers`;②回决带
|
|
588
598
|
* `persistRule` 时兑付进店。缺席 ⇒ 两件都不做(诚实缺席,不发无处可兑的候选)。 */
|
|
589
599
|
private readonly ruleConsent;
|
|
590
600
|
/** #280 R-13 C(`UNATTENDED_APPROVAL_POLICY`,设计稿 §2):**无人可答**时这条部署要的终局。
|
|
@@ -693,7 +703,30 @@ export declare class ToolApprovalCoordinator {
|
|
|
693
703
|
*/
|
|
694
704
|
/** 两份候选是否**逐条逐键**相等(顺序即展示序 ⇒ 顺序敏感)。两侧都缺席 = 相等;一侧缺席 = 不等。
|
|
695
705
|
* 用在幂等重入的「行 = 真源」对账上(见 `ensureAsk` 那段撤回臂)。 */
|
|
696
|
-
private static
|
|
706
|
+
private static ruleOffersEqual;
|
|
707
|
+
/**
|
|
708
|
+
* #346 / 5.58 复审 F-1 —— OFFER 的**文本座尺**(红先修)。
|
|
709
|
+
*
|
|
710
|
+
* 🔴 病灶(真复现,`npm run build --flag=<700 字> && git status`):`batch` 成员的 `segment` 是复合命令
|
|
711
|
+
* 那一段的**原字节**,它**不经** `parseAllowRuleText`,所以 core 的 `MAX_RULE_TEXT_CHARS` 对它一个字
|
|
712
|
+
* 都不管;而卡的 `ApprovalCardSchema` 对四个文本座一律 `.max(MAX_RULE_TEXT_CHARS)`。逐字拷进卡 ⇒
|
|
713
|
+
* `card_json` 落库 ⇒ `ensureAsk` 当场回读 `safeParse` 判假 ⇒ `persisted-row-unusable` ⇒ **整只 ask 走
|
|
714
|
+
* park**,重放腿一并跳过该行。与「重扫二轮 §基数帽」是**同一条悬崖的另一条边**(那次是 offer 条数
|
|
715
|
+
* 没执法,这次是文本长度),5.58 换形之前不存在——旧 `RuleSuggestion` 三个座全是解析器输出,结构性 ≤512。
|
|
716
|
+
*
|
|
717
|
+
* 两个座两种处置,**刻意不同**:
|
|
718
|
+
* · `segment` = **纯渲染座**(core 明写「never adjudication input」)⇒ 截长是诚实省略,与
|
|
719
|
+
* `toolName`/`message` 的 `clip` 同待遇,也与耐久腿 `boundedRuleOffers` 的同名处置同形。
|
|
720
|
+
* · `rule`/`command` = **兑付等式的左边**(回决 `persistRule.rule` 按文本在引擎重铸的 offer 表里定位)
|
|
721
|
+
* ⇒ 截一个字就恒 `rule_not_offered`,所以**绝不截**;真超尺(= core 放宽了自己的 512 或本仓与 core
|
|
722
|
+
* 版本不同步)⇒ **整条车道撤回**(返回 `undefined`),与本文件既有的治理撤回 / 漂移撤回逐字同形:
|
|
723
|
+
* 少渲一格是安全方向,渲一格按不动的才是 wire 谎言。今天这条臂不可达(每个座都是解析器产物),
|
|
724
|
+
* 它守的是版本不同步那一形 —— 与 `isSupportedRuleMatch` 的运行期 `default` 同族。
|
|
725
|
+
*
|
|
726
|
+
* 位置在**素材铸点**(与基数帽同一处):两族帧 + `card_json` + 回决口的等式左边从此拿的是**同一份**
|
|
727
|
+
* 已截素材,而不是两处各截一遍。
|
|
728
|
+
*/
|
|
729
|
+
private static boundRuleOfferTexts;
|
|
697
730
|
private buildRuleLaneMaterial;
|
|
698
731
|
/** #241([3731]/[3730] 双属主裁定):把某 wire run 的**流内未决 ask** 立即转 durable park——断连支专用。
|
|
699
732
|
* 匹配键=`ctxTaskId`(wire run id)。该字段的顶注说它「不是清扫判据」——那是因为清扫要判**连接**的
|
package/dist/tool-approval.js
CHANGED
|
@@ -6,7 +6,7 @@ import { hasBidiControls } from "./text-bidi.js";
|
|
|
6
6
|
import { createLogger } from "./observability/logger.js";
|
|
7
7
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
8
8
|
import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
|
|
9
|
-
import { ApprovalCardEnvelopeSchema,
|
|
9
|
+
import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readProbeCause, readRuleEvidence, } from "./approval-card.js";
|
|
10
10
|
import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
|
|
11
11
|
const defaultLogger = createLogger();
|
|
12
12
|
export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
|
|
@@ -42,6 +42,8 @@ function sessionAllowKey(owner, sessionId, category) {
|
|
|
42
42
|
}
|
|
43
43
|
export const PERSIST_RULE_TEXT_ERROR = `persistRule.rule must be a non-empty string of at most ${MAX_RULE_TEXT_CHARS} characters`;
|
|
44
44
|
export const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present";
|
|
45
|
+
export const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
|
|
46
|
+
export const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited — send exactly one arm";
|
|
45
47
|
export const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope — the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
|
|
46
48
|
export function parseToolApprovalResponse(body) {
|
|
47
49
|
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
@@ -59,13 +61,24 @@ export function parseToolApprovalResponse(body) {
|
|
|
59
61
|
if (pr.scope !== undefined)
|
|
60
62
|
return { ok: false, error: PERSIST_RULE_SCOPE_REFUSED };
|
|
61
63
|
const rule = pr.rule;
|
|
62
|
-
if (typeof rule !== "string" || rule === "" || rule.length > MAX_RULE_TEXT_CHARS) {
|
|
63
|
-
return { ok: false, error: PERSIST_RULE_TEXT_ERROR };
|
|
64
|
-
}
|
|
65
64
|
const edited = pr.edited;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
const batchIdx = pr.batchOfferIndex;
|
|
66
|
+
if (batchIdx !== undefined) {
|
|
67
|
+
if (rule !== undefined || edited !== undefined)
|
|
68
|
+
return { ok: false, error: PERSIST_RULE_BATCH_EXCLUSIVE_ERROR };
|
|
69
|
+
if (typeof batchIdx !== "number" || !Number.isInteger(batchIdx) || batchIdx < 0) {
|
|
70
|
+
return { ok: false, error: PERSIST_RULE_BATCH_INDEX_ERROR };
|
|
71
|
+
}
|
|
72
|
+
persistRule = { kind: "batch", batchOfferIndex: batchIdx };
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (typeof rule !== "string" || rule === "" || rule.length > MAX_RULE_TEXT_CHARS) {
|
|
76
|
+
return { ok: false, error: PERSIST_RULE_TEXT_ERROR };
|
|
77
|
+
}
|
|
78
|
+
if (edited !== undefined && typeof edited !== "boolean")
|
|
79
|
+
return { ok: false, error: PERSIST_RULE_EDITED_FLAG_ERROR };
|
|
80
|
+
persistRule = { kind: "text", rule, edited: edited === true };
|
|
81
|
+
}
|
|
69
82
|
}
|
|
70
83
|
const rawNote = body.note;
|
|
71
84
|
let note;
|
|
@@ -244,12 +257,49 @@ export class ToolApprovalCoordinator {
|
|
|
244
257
|
this.admitByOwner.set(ownerKey, o);
|
|
245
258
|
};
|
|
246
259
|
}
|
|
247
|
-
static
|
|
260
|
+
static ruleOffersEqual(a, b) {
|
|
248
261
|
if (a === undefined || b === undefined)
|
|
249
262
|
return a === b;
|
|
250
263
|
if (a.length !== b.length)
|
|
251
264
|
return false;
|
|
252
|
-
return a.every((x, i) =>
|
|
265
|
+
return a.every((x, i) => {
|
|
266
|
+
const y = b[i];
|
|
267
|
+
if (x.kind !== y.kind)
|
|
268
|
+
return false;
|
|
269
|
+
if (x.kind === "single" && y.kind === "single")
|
|
270
|
+
return x.rule === y.rule && x.match === y.match && x.command === y.command;
|
|
271
|
+
if (x.kind === "batch" && y.kind === "batch") {
|
|
272
|
+
return (x.uncoveredSegments === y.uncoveredSegments &&
|
|
273
|
+
x.rules.length === y.rules.length &&
|
|
274
|
+
x.rules.every((r, j) => r.rule === y.rules[j].rule && r.match === y.rules[j].match && r.command === y.rules[j].command && r.segment === y.rules[j].segment));
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
static boundRuleOfferTexts(offers) {
|
|
280
|
+
const over = (s) => s.length > MAX_RULE_TEXT_CHARS;
|
|
281
|
+
const out = [];
|
|
282
|
+
for (const offer of offers) {
|
|
283
|
+
switch (offer.kind) {
|
|
284
|
+
case "single":
|
|
285
|
+
if (over(offer.rule) || over(offer.command))
|
|
286
|
+
return undefined;
|
|
287
|
+
out.push(offer);
|
|
288
|
+
break;
|
|
289
|
+
case "batch":
|
|
290
|
+
if (offer.rules.some((r) => over(r.rule) || over(r.command)))
|
|
291
|
+
return undefined;
|
|
292
|
+
out.push({
|
|
293
|
+
kind: "batch",
|
|
294
|
+
rules: offer.rules.map((r) => (over(r.segment) ? { ...r, segment: r.segment.slice(0, MAX_RULE_TEXT_CHARS) } : r)),
|
|
295
|
+
uncoveredSegments: offer.uncoveredSegments,
|
|
296
|
+
});
|
|
297
|
+
break;
|
|
298
|
+
default:
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
253
303
|
}
|
|
254
304
|
buildRuleLaneMaterial(req, owner, governanceForced, sessionId) {
|
|
255
305
|
if (this.ruleConsent === undefined)
|
|
@@ -258,10 +308,13 @@ export class ToolApprovalCoordinator {
|
|
|
258
308
|
return undefined;
|
|
259
309
|
if (owner === null)
|
|
260
310
|
return undefined;
|
|
261
|
-
const
|
|
262
|
-
if (!Array.isArray(
|
|
311
|
+
const rawOffers = req.ruleOffers;
|
|
312
|
+
if (!Array.isArray(rawOffers) || rawOffers.length === 0)
|
|
313
|
+
return undefined;
|
|
314
|
+
const capped = rawOffers.length > MAX_RULE_OFFERS ? rawOffers.slice(0, MAX_RULE_OFFERS) : rawOffers;
|
|
315
|
+
const offers = ToolApprovalCoordinator.boundRuleOfferTexts(capped);
|
|
316
|
+
if (offers === undefined)
|
|
263
317
|
return undefined;
|
|
264
|
-
const suggestions = rawSuggestions.length > MAX_RULE_SUGGESTIONS ? rawSuggestions.slice(0, MAX_RULE_SUGGESTIONS) : rawSuggestions;
|
|
265
318
|
const args = req.args;
|
|
266
319
|
if (args === null || typeof args !== "object" || Array.isArray(args))
|
|
267
320
|
return undefined;
|
|
@@ -272,7 +325,7 @@ export class ToolApprovalCoordinator {
|
|
|
272
325
|
const scopeRoot = this.ruleScopeRootFor?.(sessionId);
|
|
273
326
|
return {
|
|
274
327
|
command,
|
|
275
|
-
|
|
328
|
+
offers,
|
|
276
329
|
...(typeof req.toolCallId === "string" && req.toolCallId !== "" ? { toolCallId: req.toolCallId } : {}),
|
|
277
330
|
...(boundInputHash !== null ? { boundInputHash } : {}),
|
|
278
331
|
...(scopeRoot !== undefined ? { scopeRoot } : {}),
|
|
@@ -576,11 +629,11 @@ export class ToolApprovalCoordinator {
|
|
|
576
629
|
...(typeof req.persistedRuleShadowed === "string" && req.persistedRuleShadowed !== ""
|
|
577
630
|
? { persistedRuleShadowed: redactSecrets(req.persistedRuleShadowed).slice(0, MAX_RULE_TEXT_CHARS) }
|
|
578
631
|
: {}),
|
|
579
|
-
...(ruleLaneMaterial !== undefined ? {
|
|
632
|
+
...(ruleLaneMaterial !== undefined ? { ruleOffers: ruleLaneMaterial.offers } : {}),
|
|
580
633
|
...((c) => (c !== undefined ? { probeCause: c } : {}))(readProbeCause(req)),
|
|
581
634
|
...((c) => (c !== undefined ? { ruleEvidence: c } : {}))(readRuleEvidence(req)),
|
|
582
635
|
...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
|
|
583
|
-
...(bounded.hasBidi ? { inputHasBidi: true } : {}),
|
|
636
|
+
...(bounded.hasBidi || req.hasBidiControls === true ? { inputHasBidi: true } : {}),
|
|
584
637
|
};
|
|
585
638
|
const effectiveWindowMs = effectiveAskWindowMs(this.ttlMs, this.windowMarginMs, origin.legDeadlineMonotonic, performance.now());
|
|
586
639
|
const expiresAtMs = Date.now() + effectiveWindowMs;
|
|
@@ -662,18 +715,18 @@ export class ToolApprovalCoordinator {
|
|
|
662
715
|
if (cardForFrame.governanceForced === true && !effectiveGovernanceForced) {
|
|
663
716
|
effectiveGovernanceForced = true;
|
|
664
717
|
ruleLaneMaterial = undefined;
|
|
665
|
-
delete frame.
|
|
666
|
-
if (cardForFrame.
|
|
667
|
-
const {
|
|
718
|
+
delete frame.ruleOffers;
|
|
719
|
+
if (cardForFrame.ruleOffers !== undefined) {
|
|
720
|
+
const { ruleOffers: _dropped, ...rest } = cardForFrame;
|
|
668
721
|
cardForFrame = rest;
|
|
669
722
|
}
|
|
670
723
|
}
|
|
671
|
-
if (ruleLaneMaterial !== undefined && !ToolApprovalCoordinator.
|
|
672
|
-
this.noteStoreError(new Error("persisted approval row offers a different
|
|
724
|
+
if (ruleLaneMaterial !== undefined && !ToolApprovalCoordinator.ruleOffersEqual(cardForFrame.ruleOffers, ruleLaneMaterial.offers)) {
|
|
725
|
+
this.noteStoreError(new Error("persisted approval row offers a different ruleOffers set than this leg — withdrawing the rule lane for this delivery"), "ensureAsk(rule-offers-drift)");
|
|
673
726
|
ruleLaneMaterial = undefined;
|
|
674
|
-
delete frame.
|
|
675
|
-
if (cardForFrame.
|
|
676
|
-
const {
|
|
727
|
+
delete frame.ruleOffers;
|
|
728
|
+
if (cardForFrame.ruleOffers !== undefined) {
|
|
729
|
+
const { ruleOffers: _drifted, ...rest } = cardForFrame;
|
|
677
730
|
cardForFrame = rest;
|
|
678
731
|
}
|
|
679
732
|
}
|
|
@@ -1051,7 +1104,7 @@ export class ToolApprovalCoordinator {
|
|
|
1051
1104
|
}
|
|
1052
1105
|
precheckEditedRuleBeforeDecision(entry, parsed) {
|
|
1053
1106
|
const persistRule = parsed.persistRule;
|
|
1054
|
-
if (persistRule === undefined || !persistRule.edited)
|
|
1107
|
+
if (persistRule === undefined || persistRule.kind !== "text" || !persistRule.edited)
|
|
1055
1108
|
return undefined;
|
|
1056
1109
|
if (parsed.updatedInput !== undefined)
|
|
1057
1110
|
return undefined;
|
|
@@ -1073,7 +1126,7 @@ export class ToolApprovalCoordinator {
|
|
|
1073
1126
|
};
|
|
1074
1127
|
}
|
|
1075
1128
|
editedArmEcho(persistRule, persisted) {
|
|
1076
|
-
if (!persistRule.edited || !persisted.ok)
|
|
1129
|
+
if (persistRule.kind !== "text" || !persistRule.edited || !persisted.ok || persisted.kind !== "single")
|
|
1077
1130
|
return {};
|
|
1078
1131
|
return { persistedRule: persisted.rule };
|
|
1079
1132
|
}
|
|
@@ -1258,7 +1311,6 @@ export class ToolApprovalCoordinator {
|
|
|
1258
1311
|
const lane = this.ruleConsent;
|
|
1259
1312
|
const material = entry.ruleLane;
|
|
1260
1313
|
const owner = entry.owner;
|
|
1261
|
-
const ruleText = persistRule.rule;
|
|
1262
1314
|
const withFlag = (rulePersisted, ruleRefusal, echo) => ({
|
|
1263
1315
|
status: result.status,
|
|
1264
1316
|
body: { ...result.body, rulePersisted, ...(ruleRefusal !== undefined ? { ruleRefusal } : {}), ...(echo ?? {}) },
|
|
@@ -1271,7 +1323,33 @@ export class ToolApprovalCoordinator {
|
|
|
1271
1323
|
return withFlag(false, "rule_store_error");
|
|
1272
1324
|
if (inputWasEdited)
|
|
1273
1325
|
return withFlag(false, "rule_input_edited");
|
|
1274
|
-
if (
|
|
1326
|
+
if (persistRule.kind === "batch") {
|
|
1327
|
+
const offer = material.offers[persistRule.batchOfferIndex];
|
|
1328
|
+
if (offer === undefined || offer.kind !== "batch")
|
|
1329
|
+
return withFlag(false, "rule_not_offered");
|
|
1330
|
+
try {
|
|
1331
|
+
const persisted = await lane.persistCardRule({
|
|
1332
|
+
principal: owner,
|
|
1333
|
+
toolName: entry.toolName,
|
|
1334
|
+
command: material.command,
|
|
1335
|
+
redemption: { kind: "batch", offer },
|
|
1336
|
+
...(material.toolCallId !== undefined ? { toolCallId: material.toolCallId } : {}),
|
|
1337
|
+
...(material.boundInputHash !== undefined ? { boundInputHash: material.boundInputHash } : {}),
|
|
1338
|
+
...(material.scopeRoot !== undefined ? { scope: { kind: "project", root: material.scopeRoot } } : {}),
|
|
1339
|
+
});
|
|
1340
|
+
if (!persisted.ok) {
|
|
1341
|
+
defaultLogger.warn("permission rule batch was not persisted after an approval", { reason: persisted.reason, ...(persisted.detail !== undefined ? { detail: redactSecrets(persisted.detail) } : {}) });
|
|
1342
|
+
return withFlag(false, persisted.reason);
|
|
1343
|
+
}
|
|
1344
|
+
return withFlag(true, undefined, persisted.kind === "batch" ? { persistedRules: persisted.rules } : {});
|
|
1345
|
+
}
|
|
1346
|
+
catch (err) {
|
|
1347
|
+
this.noteStoreError(err, "persistCardRule(batch)");
|
|
1348
|
+
return withFlag(false, "rule_store_error");
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
const ruleText = persistRule.rule;
|
|
1352
|
+
if (!persistRule.edited && !material.offers.some((o) => o.kind === "single" && o.rule === ruleText))
|
|
1275
1353
|
return withFlag(false, "rule_not_offered");
|
|
1276
1354
|
try {
|
|
1277
1355
|
const persisted = await lane.persistCardRule({
|
|
@@ -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" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason";
|
|
29
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval";
|
|
30
|
+
type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls";
|
|
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
4
|
-
"description": "Sema Server
|
|
3
|
+
"version": "7.46.0-rc.2",
|
|
4
|
+
"description": "Sema Server \u2014 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",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -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.59.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.19.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|