@sema-agent/server 7.116.0 → 7.117.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/MIGRATION.md +98 -0
- package/USAGE.md +32 -27
- package/dist/approval-card.d.ts +43 -9
- package/dist/approval-card.js +36 -4
- package/dist/boot/budget-tracing.js +2 -3
- package/dist/boot/config-center.d.ts +2 -2
- package/dist/boot/config-center.js +5 -7
- package/dist/boot/resolve-spec.js +10 -3
- package/dist/boot/runner-deps.d.ts +5 -3
- package/dist/boot/runner-deps.js +4 -2
- package/dist/brain.d.ts +17 -8
- package/dist/brain.js +10 -7
- package/dist/config-catalog.js +3 -2
- package/dist/config-center/apply-effective.d.ts +6 -16
- package/dist/config-center/apply-effective.js +0 -6
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-types.d.ts +12 -16
- package/dist/config.d.ts +34 -27
- package/dist/config.js +23 -19
- package/dist/deployment-governance.d.ts +11 -12
- package/dist/deployment-governance.js +4 -9
- package/dist/governance-overrides.d.ts +20 -0
- package/dist/governance-overrides.js +8 -0
- package/dist/http/admission.js +7 -7
- package/dist/http/approval-sweeps.js +15 -12
- package/dist/http/checkpoint-ctx.d.ts +83 -24
- package/dist/http/checkpoint-ctx.js +68 -8
- package/dist/http/resume-legs.js +11 -15
- package/dist/http/retired-skill-manifest.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/capabilities.js +3 -3
- package/dist/http/routes/runs.js +12 -0
- package/dist/model-select.d.ts +16 -5
- package/dist/model-select.js +7 -3
- package/dist/namespaced-rule-name.d.ts +7 -5
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/observability/run-terminal.js +21 -1
- package/dist/outbound-dispatcher.d.ts +18 -0
- package/dist/outbound-dispatcher.js +96 -10
- package/dist/permission-rule-list.d.ts +20 -31
- package/dist/plugins/one-time-migrations.d.ts +20 -0
- package/dist/plugins/one-time-migrations.js +84 -4
- package/dist/router/route-orchestration.d.ts +2 -23
- package/dist/rule-entry-list.d.ts +99 -0
- package/dist/rule-entry-list.js +110 -0
- package/dist/run-local.d.ts +4 -2
- package/dist/run-local.js +3 -3
- package/dist/runs.js +1 -0
- package/dist/runtime-governance.d.ts +5 -21
- package/dist/runtime-governance.js +4 -8
- package/dist/spec-fields.d.ts +15 -24
- package/dist/spec-fields.js +16 -17
- package/dist/task-settings.d.ts +18 -18
- package/dist/task-settings.js +57 -30
- package/dist/tool-approval.d.ts +24 -33
- package/dist/tool-approval.js +5 -7
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +4 -6
- package/dist/trace/engine-notice-wire.js +14 -1
- package/package.json +3 -3
- package/scripts/drain-check-local.mjs +31 -6
- package/scripts/drain-check.mysql.sql +31 -6
- package/scripts/drain-check.pg.sql +38 -8
package/dist/task-settings.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { combinePolicies, createFsWriteGatePolicy, parseRuleText,
|
|
1
|
+
import { combinePolicies, createFsWriteGatePolicy, parseRuleText, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, PERMISSION_MODES, ruleToolGrammarOf, } from "@sema-agent/core";
|
|
2
2
|
import { unmatchableToolNamesRefusal } from "./config.js";
|
|
3
3
|
import { MAX_PERMISSION_RULES_PER_LIST, createRuleListPolicy } from "./permission-rule-list.js";
|
|
4
|
+
import { allowLayerEntryCountIssue, describeAllowLayerOverCap, ruleEntryListShapeOk } from "./rule-entry-list.js";
|
|
5
|
+
import { tightenGoverned } from "./governance-overrides.js";
|
|
4
6
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
|
|
5
7
|
import { parseHooksConfig } from "./hooks/hook-runner.js";
|
|
6
8
|
import { heldRuleName } from "./tool-rule-names.js";
|
|
@@ -83,15 +85,28 @@ function cleanEnvMap(v) {
|
|
|
83
85
|
}
|
|
84
86
|
export const SETTINGS_PERMISSION_BUCKETS = ["allow", "deny", "ask"];
|
|
85
87
|
export const SETTINGS_PERMISSION_BUCKET_REFUSALS = ["over_cap", "bad_entry"];
|
|
86
|
-
|
|
88
|
+
const BUCKET_ENTRY_RULE = { allow: "allow_layer", deny: "rule_policy", ask: "rule_policy" };
|
|
89
|
+
function bucketIssueMessage(f, rule) {
|
|
87
90
|
switch (f.reason) {
|
|
88
91
|
case "over_cap":
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
switch (rule) {
|
|
93
|
+
case "rule_policy":
|
|
94
|
+
return (`${f.field} has ${f.count} entries — at most ${f.cap} are accepted. Refused rather than truncated: a truncated list silently drops the rules past the cap, ` +
|
|
95
|
+
`and a dropped deny / ask rule is a tightening that never happened. Merge rules into broader patterns or trim the list`);
|
|
96
|
+
case "allow_layer":
|
|
97
|
+
return describeAllowLayerOverCap(f.field, f.count);
|
|
98
|
+
}
|
|
91
99
|
case "bad_entry":
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
switch (rule) {
|
|
101
|
+
case "rule_policy":
|
|
102
|
+
return f.index === undefined
|
|
103
|
+
? `${f.field} must be an array of non-empty rule strings — refused rather than ignored, because an ignored list reads as "in effect" to the caller`
|
|
104
|
+
: `${f.field}[${f.index}] is not a non-empty rule string — every entry must be one. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`;
|
|
105
|
+
case "allow_layer":
|
|
106
|
+
return f.index === undefined
|
|
107
|
+
? `${f.field} must be an array of permission-rule strings — refused rather than ignored, because an ignored list reads as "in effect" to the caller`
|
|
108
|
+
: `${f.field}[${f.index}] is not a string — every entry must be a permission-rule string. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`;
|
|
109
|
+
}
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
export function settingsPermissionBucketIssue(perms) {
|
|
@@ -102,18 +117,34 @@ export function settingsPermissionBucketIssue(perms) {
|
|
|
102
117
|
if (v === undefined || v === null)
|
|
103
118
|
continue;
|
|
104
119
|
const field = `settings.permissions.${bucket}`;
|
|
120
|
+
const rule = BUCKET_ENTRY_RULE[bucket];
|
|
105
121
|
let fields;
|
|
106
122
|
if (!Array.isArray(v))
|
|
107
123
|
fields = { field, reason: "bad_entry" };
|
|
108
|
-
else if (v.length > MAX_PERMISSION_RULES_PER_LIST)
|
|
109
|
-
fields = { field, reason: "over_cap", count: v.length, cap: MAX_PERMISSION_RULES_PER_LIST };
|
|
110
124
|
else {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
switch (rule) {
|
|
126
|
+
case "rule_policy": {
|
|
127
|
+
if (v.length > MAX_PERMISSION_RULES_PER_LIST)
|
|
128
|
+
fields = { field, reason: "over_cap", count: v.length, cap: MAX_PERMISSION_RULES_PER_LIST };
|
|
129
|
+
else {
|
|
130
|
+
const index = v.findIndex((x) => typeof x !== "string" || x.length === 0);
|
|
131
|
+
if (index >= 0)
|
|
132
|
+
fields = { field, reason: "bad_entry", index };
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "allow_layer": {
|
|
137
|
+
const over = allowLayerEntryCountIssue(field, v);
|
|
138
|
+
if (over !== undefined)
|
|
139
|
+
fields = { field, reason: "over_cap", count: over.count, cap: over.cap };
|
|
140
|
+
else if (!ruleEntryListShapeOk(v))
|
|
141
|
+
fields = { field, reason: "bad_entry", index: v.findIndex((x) => typeof x !== "string") };
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
114
145
|
}
|
|
115
146
|
if (fields !== undefined)
|
|
116
|
-
return { message: bucketIssueMessage(fields), fields };
|
|
147
|
+
return { message: bucketIssueMessage(fields, rule), fields };
|
|
117
148
|
}
|
|
118
149
|
return null;
|
|
119
150
|
}
|
|
@@ -265,7 +296,7 @@ export function acceptAppendSystemPrompt(v, warn, packDropsAppend) {
|
|
|
265
296
|
}
|
|
266
297
|
return v;
|
|
267
298
|
}
|
|
268
|
-
const RULE_SOURCE_BY_BEHAVIOR = {
|
|
299
|
+
const RULE_SOURCE_BY_BEHAVIOR = { ask: "settings.permissions.ask", deny: "settings.permissions.deny" };
|
|
269
300
|
const WORKFLOW_ASK_NAMES = [RUN_WORKFLOW_TOOL_NAME];
|
|
270
301
|
const isWorkflowName = (name) => name === RUN_WORKFLOW_TOOL_NAME;
|
|
271
302
|
function createWorkflowAskPolicy(isExempt) {
|
|
@@ -301,7 +332,6 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
|
|
|
301
332
|
for (const [field, names, behavior] of [
|
|
302
333
|
["permissions.ask", perms.ask, "ask"],
|
|
303
334
|
["permissions.deny", perms.deny, "deny"],
|
|
304
|
-
["permissions.allow", perms.allow, "allow"],
|
|
305
335
|
]) {
|
|
306
336
|
for (const name of names ?? []) {
|
|
307
337
|
const parsed = parseRuleText(name, behavior);
|
|
@@ -313,14 +343,15 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
|
|
|
313
343
|
}
|
|
314
344
|
}
|
|
315
345
|
}
|
|
316
|
-
|
|
317
|
-
["permissions.ask", perms.ask, "ask"],
|
|
318
|
-
["permissions.deny", perms.deny, "deny"],
|
|
319
|
-
["permissions.allow", perms.allow, "allow"],
|
|
320
|
-
]
|
|
346
|
+
const judgments = [
|
|
347
|
+
["permissions.ask", perms.ask, { consumer: "rule-policy", behavior: "ask", ...(pathBases !== undefined ? { pathBases } : {}) }],
|
|
348
|
+
["permissions.deny", perms.deny, { consumer: "rule-policy", behavior: "deny", ...(pathBases !== undefined ? { pathBases } : {}) }],
|
|
349
|
+
["permissions.allow", perms.allow, { consumer: "allow-layer" }],
|
|
350
|
+
];
|
|
351
|
+
for (const [field, names, judgment] of judgments) {
|
|
321
352
|
if (!names)
|
|
322
353
|
continue;
|
|
323
|
-
const unmatchable = unmatchableToolNamesRefusal(field, names,
|
|
354
|
+
const unmatchable = unmatchableToolNamesRefusal(field, names, judgment);
|
|
324
355
|
if (unmatchable !== undefined)
|
|
325
356
|
throw new Error(unmatchable.message);
|
|
326
357
|
}
|
|
@@ -329,14 +360,12 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
|
329
360
|
const perms = settings.permissions;
|
|
330
361
|
if (!perms)
|
|
331
362
|
return {};
|
|
332
|
-
const {
|
|
363
|
+
const { deny, ask, defaultMode } = perms;
|
|
333
364
|
const pathBases = gate?.taskRoot !== undefined || gate?.taskHome !== undefined
|
|
334
365
|
? { ...(gate.taskRoot !== undefined ? { root: gate.taskRoot } : {}), ...(gate.taskHome !== undefined ? { home: gate.taskHome } : {}) }
|
|
335
366
|
: undefined;
|
|
336
367
|
rejectUnmatchableSettingsNames(perms, pathBases);
|
|
337
368
|
const policies = [];
|
|
338
|
-
if (allow)
|
|
339
|
-
policies.push(createRuleListPolicy(allow, "allow", RULE_SOURCE_BY_BEHAVIOR.allow, pathBases));
|
|
340
369
|
if (deny)
|
|
341
370
|
policies.push(createRuleListPolicy(deny, "deny", RULE_SOURCE_BY_BEHAVIOR.deny, pathBases));
|
|
342
371
|
if (ask)
|
|
@@ -380,15 +409,13 @@ export function applyTaskSettings(base, settings, gate, workflowGate) {
|
|
|
380
409
|
const { toolPolicy, handsReadOnly, enablePlanMode } = deriveSettingsPolicy(settings, gate, workflowGate);
|
|
381
410
|
let next = base;
|
|
382
411
|
if (toolPolicy !== undefined || handsReadOnly !== undefined) {
|
|
383
|
-
|
|
384
|
-
if (toolPolicy !== undefined)
|
|
385
|
-
overrides.toolPolicy = toolPolicy;
|
|
386
|
-
if (handsReadOnly !== undefined)
|
|
387
|
-
overrides.handsReadOnly = handsReadOnly;
|
|
388
|
-
next = tightenTaskSpec(next, overrides);
|
|
412
|
+
next = tightenGoverned(next, { ...(toolPolicy !== undefined ? { toolPolicy } : {}), ...(handsReadOnly !== undefined ? { handsReadOnly } : {}) });
|
|
389
413
|
}
|
|
390
414
|
if (enablePlanMode)
|
|
391
415
|
next = { ...next, enablePlanMode: true };
|
|
416
|
+
const allow = settings.permissions?.allow;
|
|
417
|
+
if (allow !== undefined)
|
|
418
|
+
next = { ...next, allowedTools: allow };
|
|
392
419
|
if (settings.outputStyle) {
|
|
393
420
|
const appended = next.appendSystemPrompt ? `${next.appendSystemPrompt}\n\n${settings.outputStyle}` : settings.outputStyle;
|
|
394
421
|
next = { ...next, appendSystemPrompt: appended };
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { HostDecidedBy, HostSettledBy } from "./host-decision.js";
|
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
3
|
import { type AskClass, type AskRequest, type AskOutcome, type CheckpointGate, type PendingAction, type PersistedRuleMandate, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
4
4
|
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
5
|
-
import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
5
|
+
import { type AllowRuleShadowProjection, type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
6
6
|
import { type ApprovalAskStore, type AskRow } from "./plugins/approval-ask-store-sql.js";
|
|
7
7
|
import type { ApprovalAskAuditSink } from "./approval-ask-audit-store.js";
|
|
8
8
|
import type { ParkCommitWatch } from "./plugins/park-commit-watch.js";
|
|
@@ -372,37 +372,28 @@ export interface ToolApprovalFrame {
|
|
|
372
372
|
expiresInMs?: number;
|
|
373
373
|
serverNowMs?: number;
|
|
374
374
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
* 🔴 **缺席
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* `
|
|
394
|
-
*
|
|
395
|
-
* 值是**用户内容族**(规则原文是人写的文本)⇒ 与 `message`/`sourceAgentName` 同待遇:`redactSecrets`
|
|
396
|
-
* 后上帧,UNTRUSTED-for-display。空串**不铸键**(core 只在真有命中时带它,空串是坏值不是「空规则」)。
|
|
397
|
-
*
|
|
398
|
-
* 耐久路(park 行)的对偶是 `gate.riskDescriptor.shadowedRule` —— 我方对 `riskDescriptor` 是**整体透传**
|
|
399
|
-
* (SQL 双生落整只 JSON 列、`listPending` 整只回读、两条 durable 读面整行上 wire),所以那一路**键集**零施工;
|
|
400
|
-
* 唯一的施工是**内容**:那两条读面(`GET /v1/approvals` + `/v1/approvals/stream`)在读边界对这一格补
|
|
401
|
-
* `redactSecrets`(core 只中和不脱敏,而运维队列是跨租户可见的那一条 —— 见
|
|
402
|
-
* `http/routes/approvals-assistant.ts` 的 `redactPendingDisclosures` 顶注)。
|
|
403
|
-
* 两路的钉见 `test/approval-shadowed-rule-wire.test.ts` 与 db-integration 的「[ref] 件4」格。
|
|
375
|
+
* core 7.42.0 #1327(**硬破坏换名**:取代 7.41.x 的 `persistedRuleShadowed`,零别名;`"tool_approval"` only)——
|
|
376
|
+
* allow 层里**命中了这次调用、却没能清掉这只 ask** 的成员,每个成员一条 `{ member, entries, omitted? }`:`member` ∈ core `ALLOW_RULE_MEMBERS`
|
|
377
|
+
* (`hook` 部署 PreToolUse hook 的 allow / `deployment` 运维的 `APPROVAL_GRANT` / `persisted` 人存的 allow 规则 / `skill` 被激活技能的清单 /
|
|
378
|
+
* `request` 请求的 `settings.permissions.allow`);`entries` = 那个成员命中的条目原文(hook 恒空表);`omitted` = 没列出的条目数。
|
|
379
|
+
*
|
|
380
|
+
* 🔴 **它存在的理由**:allow 层只清「普通问 / 没人表态」两类问;运维的强制类(`APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` / shell 地板 /
|
|
381
|
+
* 写保护 / 治理)、hook 的问、plan 模式的问照样到人。没有这个键,人看到的是「我明明写了 allow,它怎么又问」,唯一合理的结论是「我的规则坏了」;
|
|
382
|
+
* 有了它,壳能渲「你的规则在、命中了,只是这次调用必须逐次确认」。卡上那句话引擎已经写进 `message`(每个呈卡站的 `cardDisclosureOf`),
|
|
383
|
+
* 本键是同一事实的结构化一面,本仓不另写句子。
|
|
384
|
+
*
|
|
385
|
+
* 🔴 **缺席 ≠「你没有规则」**:本键只在「有成员命中 ∧ 清不掉这只 ask」时在场;命中且清掉了的根本不会变成 ask。
|
|
386
|
+
*
|
|
387
|
+
* 与 {@link ToolApprovalFrame.governanceForced} **刻意分列**:那个键回答「门是谁下的」,本键回答「你那条规则怎么了」。
|
|
388
|
+
*
|
|
389
|
+
* 值是**内容族**(人 / 运维写的条目)⇒ 唯一窄读点 `approval-card.ts` 的 `readAllowRuleShadowed`(卡也用它 ⇒ 帧与 `card_json` 同值;
|
|
390
|
+
* `redactSecrets` 后截长一次算定)。形不合 / 空表 ⇒ 不铸键。
|
|
391
|
+
*
|
|
392
|
+
* 耐久路(park 行)的对偶是 `gate.riskDescriptor.allowRuleShadowed`(整体透传,两条 durable 读面在读边界对条目补 `redactSecrets`,
|
|
393
|
+
* 叶表属主 `observability/run-terminal.ts` 的 `redactRiskDescriptor`)。升级前停驻的行带旧键 `shadowedRule`,起服一次性迁移改写
|
|
394
|
+
* (`plugins/one-time-migrations.ts` 登记表)。
|
|
404
395
|
*/
|
|
405
|
-
|
|
396
|
+
allowRuleShadowed?: AllowRuleShadowProjection[];
|
|
406
397
|
/**
|
|
407
398
|
* [ref] 车二(core 5.18.0 [ref]):`"tool_approval"` only —— 引擎为这次 ask 铸的**规则候选**
|
|
408
399
|
* (`AskRequest.ruleOffers`,core 5.58.0 判别联合逐字透传:闭词表 `kind`/`match` + 引擎铸的文本,
|
|
@@ -506,9 +497,9 @@ export interface ToolApprovalFrame {
|
|
|
506
497
|
* 🔴 **展示/对账元数据,永不是裁决输入**(core 契约逐字:removing it would leave every decision
|
|
507
498
|
* byte-identical);server 逐字转录**引擎盖章**的那一份,**永不自铸缺席词**(server 铸词 = 谎报
|
|
508
499
|
* 「某一层没报」)。窄读走 `approval-card.ts` 的 `readRuleEvidence`(卡也用它 ⇒ 帧与 `card_json`
|
|
509
|
-
* 同值;redact
|
|
500
|
+
* 同值;redact+截长在函数内一次算定,allow 层披露 R2-[medium] 同款)。
|
|
510
501
|
*
|
|
511
|
-
* 与 `
|
|
502
|
+
* 与 `allowRuleShadowed` **刻意分列**:那是**人读的**命中条目原文(display 值,非身份通道),
|
|
512
503
|
* 本键的 dots 是那条规则的**机器身份**(a rule is a set of adds;数组即身份,消费端禁 join 成标量)。
|
|
513
504
|
*/
|
|
514
505
|
ruleEvidence?: RuleEvidenceProjection;
|
package/dist/tool-approval.js
CHANGED
|
@@ -8,7 +8,7 @@ import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerpri
|
|
|
8
8
|
import { createLogger } from "./observability/logger.js";
|
|
9
9
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
10
10
|
import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
|
|
11
|
-
import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readProbeReason, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readClosedWordSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
|
|
11
|
+
import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readProbeReason, readReadRootCandidate, readRuleEvidence, readAllowRuleShadowed, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readAskClassPair, readClosedWordSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
|
|
12
12
|
import { MAX_SETTLED_BY_CHARS, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth, settledByFitsColumn } from "./plugins/approval-ask-store-sql.js";
|
|
13
13
|
import { settleByPolicy } from "./approval-policy-settlement.js";
|
|
14
14
|
import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
|
|
@@ -1102,9 +1102,7 @@ export class ToolApprovalCoordinator {
|
|
|
1102
1102
|
...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
|
|
1103
1103
|
...(originWord !== undefined ? { origin: originWord } : {}),
|
|
1104
1104
|
...(askClassPair ?? {}),
|
|
1105
|
-
...(
|
|
1106
|
-
? { persistedRuleShadowed: redactSecrets(req.persistedRuleShadowed).slice(0, MAX_RULE_TEXT_CHARS) }
|
|
1107
|
-
: {}),
|
|
1105
|
+
...((d) => (d !== undefined ? { allowRuleShadowed: d } : {}))(readAllowRuleShadowed(req)),
|
|
1108
1106
|
...(ruleLaneMaterial !== undefined ? { ruleOffers: ruleLaneMaterial.offers } : {}),
|
|
1109
1107
|
...((a) => (a !== undefined ? { ruleOffersAbsence: a } : {}))(readRuleOffersAbsence(req)),
|
|
1110
1108
|
...((f) => (f !== undefined ? { denialLimitFallback: f } : {}))(readDenialLimitFallback(req)),
|
|
@@ -1206,10 +1204,10 @@ export class ToolApprovalCoordinator {
|
|
|
1206
1204
|
frame.governanceForced = true;
|
|
1207
1205
|
else
|
|
1208
1206
|
delete frame.governanceForced;
|
|
1209
|
-
if (
|
|
1210
|
-
frame.
|
|
1207
|
+
if (cardForFrame.allowRuleShadowed !== undefined)
|
|
1208
|
+
frame.allowRuleShadowed = cardForFrame.allowRuleShadowed;
|
|
1211
1209
|
else
|
|
1212
|
-
delete frame.
|
|
1210
|
+
delete frame.allowRuleShadowed;
|
|
1213
1211
|
if (cardForFrame.inputHasBidi === true)
|
|
1214
1212
|
frame.inputHasBidi = true;
|
|
1215
1213
|
else
|
|
@@ -43,7 +43,7 @@ type _GuardBgNotifReverse = AssertAllKeysHandled<Exclude<BgNotifProjected | BgNo
|
|
|
43
43
|
type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
|
|
44
44
|
type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
|
|
45
45
|
type _GuardRosterReverse = AssertAllKeysHandled<Exclude<RosterProjected, keyof RosterEntry>>;
|
|
46
|
-
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "
|
|
46
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "allowRuleShadowed" | "probeCause" | "probeReason" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "readRootCandidate" | "mandated" | "mandate" | "askClass" | "classes";
|
|
47
47
|
type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "hasBidiControls" | "previewWithheld" | "execCwd" | "authoredBy";
|
|
48
48
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
49
49
|
type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
|
|
@@ -129,7 +129,7 @@ type WiringGovernanceHandled = "audience" | "lockedConfig" | "compliance" | "mem
|
|
|
129
129
|
type _GuardWiringGovernance = AssertAllKeysHandled<Exclude<keyof WiringSection<"governance">, WiringGovernanceHandled>>;
|
|
130
130
|
type _GuardWiringGovernanceReverse = AssertAllKeysHandled<Exclude<WiringGovernanceHandled, keyof WiringSection<"governance">>>;
|
|
131
131
|
type PermissionTraceKind = Extract<TraceEvent["kind"], `permission.${string}`>;
|
|
132
|
-
export type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable" | "permission.read_only_allowed" | "permission.skill_allowed";
|
|
132
|
+
export type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable" | "permission.read_only_allowed" | "permission.skill_allowed" | "permission.allowed_tools_allowed";
|
|
133
133
|
type PermissionTraceDropped = "permission.sandbox_admitted" | "permission.rule_sync_resurrected" | "permission.rule_sync_dropped" | "permission.org_snapshot_unavailable";
|
|
134
134
|
type _GuardPermissionTrace = AssertAllKeysHandled<Exclude<PermissionTraceKind, PermissionTraceProjected | PermissionTraceDropped>>;
|
|
135
135
|
type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTraceProjected | PermissionTraceDropped, PermissionTraceKind>>;
|
|
@@ -148,7 +148,7 @@ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCal
|
|
|
148
148
|
type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
|
|
149
149
|
type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
|
|
150
150
|
type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
|
|
151
|
-
type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills";
|
|
151
|
+
type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "allowedTools";
|
|
152
152
|
type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "outputStyle";
|
|
153
153
|
type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
|
|
154
154
|
type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
|
|
17
17
|
* 刚立,立完就在下一次加码时自己咬住了)。
|
|
18
18
|
*/
|
|
19
|
-
export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "server.model_unreachable", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "task.tool_turn_incomplete"];
|
|
19
|
+
export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "server.model_unreachable", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "permission.allowed_tool_not_granted", "task.tool_turn_incomplete"];
|
|
20
20
|
export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
|
|
21
21
|
/** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
|
|
22
22
|
export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
|
|
@@ -63,12 +63,10 @@ export interface EngineNoticeWireRow {
|
|
|
63
63
|
*/
|
|
64
64
|
export declare function sanitizeNoticeDetail(detail: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
65
65
|
/**
|
|
66
|
-
* `
|
|
67
|
-
*
|
|
68
|
-
* 与 `trace/project.ts` 的 `*EventData` 族同纪律(白名单挑键、禁 `{...notice}`),但**不住在那个文件里**:
|
|
69
|
-
* 那一族的入参全是 core `TaskEvent` 的臂(有 `wire-whitelist-exhaustiveness` 的臂集编译门看着),
|
|
70
|
-
* 而 `EngineNotice` 不是 `TaskEvent` —— 混进去会让那道门的语义变成"两个不同来源的联合",判别力反而降。
|
|
66
|
+
* 通告 `detail` 里的**规则条目原串**过呈现口(`presentRuleEntry`:剥 C0 / 双向控制符、超长截断)—— 会话流行(经 {@link engineNoticeEventData})
|
|
67
|
+
* 与运维日志行(`boot/runner-deps.ts` 的转发席)同调这一只。其余字段、其余码原样(多行文本不受影响)。
|
|
71
68
|
*/
|
|
69
|
+
export declare function presentNoticeDetail(code: string, detail: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
72
70
|
export declare function engineNoticeEventData(notice: {
|
|
73
71
|
code: string;
|
|
74
72
|
message: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { redactSecrets } from "./redact.js";
|
|
2
|
+
import { presentRuleEntry } from "../rule-entry-list.js";
|
|
2
3
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
3
4
|
export const ENGINE_NOTICE_WIRE_CODES = [
|
|
4
5
|
"memory.session_polluted",
|
|
@@ -33,6 +34,7 @@ export const ENGINE_NOTICE_WIRE_CODES = [
|
|
|
33
34
|
"permission.ask_released_by_mode",
|
|
34
35
|
"permission.read_deny_unapplied",
|
|
35
36
|
"permission.rule_store_unavailable",
|
|
37
|
+
"permission.allowed_tool_not_granted",
|
|
36
38
|
"task.tool_turn_incomplete",
|
|
37
39
|
];
|
|
38
40
|
const WIRE_CODES = new Set(ENGINE_NOTICE_WIRE_CODES);
|
|
@@ -90,11 +92,22 @@ function sanitizeValue(v, depth) {
|
|
|
90
92
|
}
|
|
91
93
|
return undefined;
|
|
92
94
|
}
|
|
95
|
+
const RULE_ENTRY_DETAIL_ARRAYS = { "permission.allowed_tool_not_granted": "entries" };
|
|
96
|
+
export function presentNoticeDetail(code, detail) {
|
|
97
|
+
const key = Object.hasOwn(RULE_ENTRY_DETAIL_ARRAYS, code) ? RULE_ENTRY_DETAIL_ARRAYS[code] : undefined;
|
|
98
|
+
if (key === undefined || detail === undefined)
|
|
99
|
+
return detail;
|
|
100
|
+
const list = detail[key];
|
|
101
|
+
if (!Array.isArray(list))
|
|
102
|
+
return detail;
|
|
103
|
+
const presented = list.map((item) => item !== null && typeof item === "object" && typeof item.entry === "string" ? { ...item, entry: presentRuleEntry(item.entry) } : item);
|
|
104
|
+
return { ...detail, [key]: presented };
|
|
105
|
+
}
|
|
93
106
|
export function engineNoticeEventData(notice, sessionId, nowMs) {
|
|
94
107
|
return {
|
|
95
108
|
code: notice.code,
|
|
96
109
|
message: redactSecrets(String(notice.message)).slice(0, NOTICE_TEXT_MAX),
|
|
97
|
-
detail: sanitizeNoticeDetail(notice.detail),
|
|
110
|
+
detail: sanitizeNoticeDetail(presentNoticeDetail(notice.code, notice.detail)),
|
|
98
111
|
sessionId,
|
|
99
112
|
ts: nowMs,
|
|
100
113
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.117.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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"node": "^20.19.0 || >=22.12.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
|
-
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
47
|
+
"typecheck": "node --max-old-space-size=4096 node_modules/typescript/bin/tsc --noEmit -p tsconfig.typecheck.json",
|
|
48
48
|
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
|
49
49
|
"build": "rm -rf dist && tsc -p tsconfig.build-js.json && tsc -p tsconfig.build-dts.json && node scripts/scrub-dist-refs.mjs dist",
|
|
50
50
|
"prepublishOnly": "npm run build && vitest run test/pack-hygiene.test.ts",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@sema-agent/core": "7.
|
|
67
|
+
"@sema-agent/core": "7.42.1",
|
|
68
68
|
"@sema-agent/settings-schema": "8.0.0",
|
|
69
69
|
"e2b": "^2.28.0",
|
|
70
70
|
"libsodium-wrappers": "^0.8.4",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* 换钉前的**排空检查 —— `DB_BACKEND=local`(File 店)车道**,只读。随 npm 包出(`scripts/drain-check-local.mjs`)。
|
|
4
|
-
*
|
|
4
|
+
* 一次跑**五条判据**(判据表 {@link DRAIN_CRITERIA},每条一行末行、各自的命中行;SQL 车道的同一判据是同目录随包的 `drain-check.mysql.sql` / `drain-check.pg.sql`,
|
|
5
5
|
* 每条判据一段、段首标记 `-- drain-check:<判据名>:<后端>`,判据名与本表的 `name` 同一个词):
|
|
6
6
|
* · `skill-grants-rollback`(**回滚**到 7.115.x 前,即引擎从 7.41 退回 7.40):四样任一 ——
|
|
7
7
|
* ① pending 行带 `state.skillGrants`(本次 run 冻结在行上的技能授予行:模型激活 / 子代捕获写下的放行与禁止);
|
|
@@ -16,11 +16,16 @@
|
|
|
16
16
|
* · `retired-skill-manifest`(升级到 7.116.0 前,core 7.41.0 退役技能上的 `manifest` 键):pending 行所在会话的续跑上下文(`checkpoint-ctx/*.json`)里,提交体顶层 `skills[]` 或某只子代定义的
|
|
17
17
|
* `agents[].skills[]` 仍带 `manifest`(任何值)—— 升级后续跑顶层那种会被引擎按名拒(行仍 pending,只能取消重提),子代那种不被拒、而是那只子代定义被整只丢掉并留一条警告日志(任务照跑、少一只子代)。命中行点名是哪一层(`member=skills` / `member=agents.skills`)。
|
|
18
18
|
* 「带 `manifest`」的判据是服务里**一只**函数(`src/http/retired-skill-manifest.ts`,提交口的顶层技能门读同一只),经包里脚本隔壁的编译产物 `dist/http/retired-skill-manifest.js` 取 —— 本脚本不写第二份;
|
|
19
|
+
* · `skill-deny-inert`(换钉 7.117.0 前,引擎 7.42 逐字读规则条目):pending 行所在会话的续跑上下文里,提交体顶层 `skills[]` 或某只子代的 `agents[].skills[]`
|
|
20
|
+
* 某只技能的 `disallowedTools` 有引擎读成空的条目(例:带首尾空白的 `"Bash "` —— 7.41 去白后是一条有效的拒,7.42 什么也不拒)—— 这样的行在 7.117.0 上续跑会被拒
|
|
21
|
+
* (行字节不动;出路 = 取消、改好条目重提,见随包 `MIGRATION.md` 7.117.0 节)。命中行点名是哪一层(`member=skills` / `member=agents.skills`)。
|
|
22
|
+
* 判据**就是**服务提交与续跑两腿同经的那一只函数(`src/rule-entry-list.ts` 的 `skillDenyInertEntryOf`,它调引擎包根的 `compileAllowList`),经编译产物
|
|
23
|
+
* `dist/rule-entry-list.js` 取 —— 本脚本不写第二份;
|
|
19
24
|
* · `retired-mode-seed`(换钉 7.113.0 前,core 7.39.0 删掉退役 mode-seed 的一次性迁移):pending 行的 `state.inheritedGate` 仍带退役成员;
|
|
20
|
-
* · `permissions-allow-list`(换钉 7.
|
|
21
|
-
* (`checkpoint-ctx/*.json` 的信封 `{ sessionId, updatedAt, ctx }`)里,`ctx.body.settings.permissions.allow` 是非空数组,且 `ctx.permissionsAllowReading` 不是 `"
|
|
22
|
-
* —— 这样的行在 7.
|
|
23
|
-
* 经包里脚本隔壁的编译产物 `dist/http/checkpoint-ctx.js` 取(与缺省数据根经 `dist/local-data-root.js`
|
|
25
|
+
* · `permissions-allow-list`(换钉 7.117.0 前,本服务把 `settings.permissions.allow` 整份交给引擎的 allow 层):pending 行所在会话的续跑上下文
|
|
26
|
+
* (`checkpoint-ctx/*.json` 的信封 `{ sessionId, updatedAt, ctx }`)里,`ctx.body.settings.permissions.allow` 是非空数组,且 `ctx.permissionsAllowReading` 不是 `"allow_layer"`
|
|
27
|
+
* (无记号 = 名单时代写下的;`"statement"` = 表态时代写下的)—— 这样的行在 7.117.0 上续跑会被拒(行字节不动;出路按停驻种类分,见随包 `MIGRATION.md` 7.117.0 节)。判据**就是**服务续跑腿拒臂的那一只函数(`src/http/checkpoint-ctx.ts` 的 `allowListReadingUnstated`),
|
|
28
|
+
* 经包里脚本隔壁的编译产物 `dist/http/checkpoint-ctx.js` 取(与缺省数据根经 `dist/local-data-root.js` 取同一姿势;7.117.0 起该模块经 `dist/rule-entry-list.js` 依赖引擎包根 `@sema-agent/core` —— 随包安装时它就是包的依赖,解开的 tarball 没装依赖时本判据与 `skill-deny-inert` 同读成 criterion-unavailable、rc 2)—— 本脚本不写第二份。
|
|
24
29
|
* 扫描骨架(快照 + 账本折成当前行、一致性重读、数据根判定)各条判据共用;判据只回答「这一条 pending 行算不算命中」。
|
|
25
30
|
*
|
|
26
31
|
* ## 为什么是一只脚本,不是一行 grep
|
|
@@ -56,10 +61,12 @@
|
|
|
56
61
|
*
|
|
57
62
|
* ## 读数(机读末行 + 退出码)
|
|
58
63
|
* · 每条命中一行:`SKILL-GRANTS-ROW token=<token> sessionId=<id> createdAt=<ms> member=<skillGrants|boundGrantDigest|userInvokedSkills|disallowedTools>` / `SKILL-MANIFEST-ROW token=<token> sessionId=<id> createdAt=<ms> member=<skills|agents.skills>` /
|
|
64
|
+
* `SKILL-DENY-INERT-ROW token=<token> sessionId=<id> createdAt=<ms> member=<skills|agents.skills>` /
|
|
59
65
|
* `ALLOW-LIST-ROW token=<token> sessionId=<id> createdAt=<ms>` / `RETIRED-ROW token=<token> sessionId=<id> createdAt=<ms> member=<成员>`;
|
|
60
66
|
* · 每条判据一行末行(表序;`RETIRED-MODE-SEED` 那一行恒在最后,7.113.0 起读「最后一行」的下游不受影响;`PERMISSIONS-ALLOW-LIST` 仍是倒数第二行):
|
|
61
67
|
* `SKILL-GRANTS rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
|
|
62
68
|
* `RETIRED-SKILL-MANIFEST rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
|
|
69
|
+
* `SKILL-DENY-INERT rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
|
|
63
70
|
* `PERMISSIONS-ALLOW-LIST rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
|
|
64
71
|
* `RETIRED-MODE-SEED rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`;
|
|
65
72
|
* `corrupt` = 共用扫描的坏记录 + 这条判据自己读不懂的旁路输入(`permissions-allow-list`:读不懂的 ctx 档,文件名进 stderr —— 一份读不懂的 ctx 可能正是某条 pending 行带 `allow` 的那一份);
|
|
@@ -74,7 +81,7 @@ import { join } from "node:path";
|
|
|
74
81
|
import { fileURLToPath } from "node:url";
|
|
75
82
|
|
|
76
83
|
/** 从脚本隔壁的编译产物取来的服务判据(脚本不持第二份实现)。
|
|
77
|
-
* @typedef {{ allowListReadingUnstated: (ctx: Record<string, unknown>) => boolean, retiredSkillManifestLayerOf: (body: unknown) => "skills" | "agents.skills" | undefined }} ServiceCriteria */
|
|
84
|
+
* @typedef {{ allowListReadingUnstated: (ctx: Record<string, unknown>) => boolean, retiredSkillManifestLayerOf: (body: unknown) => "skills" | "agents.skills" | undefined, skillDenyInertEntryOf: (body: unknown) => { layer: "skills" | "agents.skills", message: string } | undefined }} ServiceCriteria */
|
|
78
85
|
|
|
79
86
|
/** 读期间文件在变时的有界重读次数(含第一次)。 */
|
|
80
87
|
const STABLE_READ_ATTEMPTS = 3;
|
|
@@ -332,6 +339,23 @@ export const DRAIN_CRITERIA = [
|
|
|
332
339
|
return layer === undefined ? undefined : ` member=${layer}`;
|
|
333
340
|
},
|
|
334
341
|
},
|
|
342
|
+
{
|
|
343
|
+
// 换钉 7.117.0 之前:停驻、持久化体里某只技能的拒侧清单有引擎读成空的条目(头注里那一条)。判据函数是服务提交与续跑两腿同经的那一只(`skillDenyInertEntryOf`,经编译产物取);
|
|
344
|
+
// 命中行点名层。表序在 `permissions-allow-list` 之前 —— 那一条仍倒数第二、`RETIRED-MODE-SEED` 仍最后。
|
|
345
|
+
name: "skill-deny-inert",
|
|
346
|
+
tail: "SKILL-DENY-INERT",
|
|
347
|
+
hit: "SKILL-DENY-INERT-ROW",
|
|
348
|
+
load: (root, service) => {
|
|
349
|
+
const idx = readCtxIndex(root);
|
|
350
|
+
return { input: { bySession: idx.bySession, entryOf: service.skillDenyInertEntryOf }, corrupt: idx.corrupt, unreadable: idx.unreadable };
|
|
351
|
+
},
|
|
352
|
+
judge: (row, input) => {
|
|
353
|
+
const { bySession, entryOf } = /** @type {{ bySession: Map<string, Record<string, unknown>>, entryOf: ServiceCriteria["skillDenyInertEntryOf"] }} */ (input);
|
|
354
|
+
const ctx = typeof row.sessionId === "string" ? bySession.get(row.sessionId) : undefined;
|
|
355
|
+
const hit = ctx === undefined ? undefined : entryOf(ctx.body);
|
|
356
|
+
return hit === undefined ? undefined : ` member=${hit.layer}`;
|
|
357
|
+
},
|
|
358
|
+
},
|
|
335
359
|
{
|
|
336
360
|
name: "permissions-allow-list",
|
|
337
361
|
tail: "PERMISSIONS-ALLOW-LIST",
|
|
@@ -376,6 +400,7 @@ async function loadServiceCriteria(rootEcho) {
|
|
|
376
400
|
const wanted = [
|
|
377
401
|
["dist/http/checkpoint-ctx.js", () => import("../dist/http/checkpoint-ctx.js"), "allowListReadingUnstated"],
|
|
378
402
|
["dist/http/retired-skill-manifest.js", () => import("../dist/http/retired-skill-manifest.js"), "retiredSkillManifestLayerOf"],
|
|
403
|
+
["dist/rule-entry-list.js", () => import("../dist/rule-entry-list.js"), "skillDenyInertEntryOf"],
|
|
379
404
|
];
|
|
380
405
|
/** @type {Record<string, unknown>} */
|
|
381
406
|
const fns = {};
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
-- sessions 命中行所在的会话 id,逗号连接,新的在前。以 hit_rows 为准:这一列被会话变量 group_concat_max_len(缺省 1024 字节)截断时,列出的会话数会少于 hit_rows ——
|
|
9
9
|
-- 先处理列出的再跑一次,或先在同一个会话里 SET SESSION group_concat_max_len = 1048576; 再跑本文件(本文件自己只含 SELECT)。
|
|
10
10
|
-- 每段只数 pending 行(已决 / 已过期的行不数);判据只看**键在不在 / 值形**,不读行的其余内容。
|
|
11
|
-
-- DB_BACKEND=local(File 店)没有 SQL:用同目录的 drain-check-local.mjs
|
|
11
|
+
-- DB_BACKEND=local(File 店)没有 SQL:用同目录的 drain-check-local.mjs,五条判据同名。
|
|
12
12
|
--
|
|
13
|
-
--
|
|
13
|
+
-- 五条判据(顺序同 drain-check-local.mjs 的判据表):
|
|
14
14
|
-- skill-grants-rollback 回滚到 7.115.x(引擎 7.40)之前:7.116.0 停驻的行带 7.40 不读的技能授予节 / 卡绑定 / 会话上下文里的人点名技能 / 还没被激活的技能声明的禁止清单。
|
|
15
15
|
-- retired-skill-manifest 升级到 7.116.0 之前:停驻、持久化提交体里技能(顶层 skills[] 或子代 agents[].skills[])还带已退役的 manifest 键。
|
|
16
|
-
--
|
|
16
|
+
-- skill-deny-inert 换钉 7.117.0 之前:停驻、提交体里技能(顶层 skills[] 或子代 agents[].skills[])的 disallowedTools 有引擎会读成空的条目的**候选**(见该段注)。
|
|
17
|
+
-- permissions-allow-list 换钉 7.117.0 之前:停驻、提交体 settings.permissions.allow 是非空数组且读法记号不是当前的 "allow_layer"(无记号 = 名单时代;"statement" = 表态时代)。
|
|
17
18
|
-- retired-mode-seed 换钉 7.113.0 之前:检查点的 inheritedGate 里仍带退役 mode-seed 成员。
|
|
18
19
|
|
|
19
20
|
-- drain-check:skill-grants-rollback:mysql —— 期望 hit_rows = 0;每一行 = 一条回滚到 7.115.x 后续跑会丢技能禁止行 / 不核卡绑定的停驻行。
|
|
@@ -45,16 +46,40 @@ WHERE k.status = 'pending'
|
|
|
45
46
|
OR (JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.agents')) = 'ARRAY'
|
|
46
47
|
AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.agents[*].skills[*].manifest')));
|
|
47
48
|
|
|
49
|
+
-- drain-check:skill-deny-inert:mysql —— 期望 hit_rows = 0;每一行 = 一条**候选**:升级后续跑可能被拒的停驻行(拒的是提交体里技能的一条 disallowedTools 条目 ——
|
|
50
|
+
-- 读哪一份续跑上下文:与续跑腿同 —— workflow 子代的停驻行(在 workflow_agent_session 里登记过)续跑时读的是**发起这条 workflow 的会话**那一份
|
|
51
|
+
-- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。
|
|
52
|
+
-- 引擎 7.42 逐字读规则条目,读成空的拒什么也不拒,服务对这种体在提交与续跑上同拒;7.41 去首尾空白,所以 7.116.x 上它可能还是一条有效的拒)。
|
|
53
|
+
-- SQL 不能逐条照引擎判读(引擎的文法 —— 非 ASCII 空白、内容形不受支持的工具、括号不配对 —— 在 SQL 里复刻不全,逐形枚举的表必漏),所以这里数的是**真超集**:
|
|
54
|
+
-- 顶层 skills[*] 或子代 agents[*].skills[*] 的 disallowedTools **非空**的停驻行(与回滚判据 skill-grants-rollback 的第四样同一只形)。两方言都 ⊇ 运行期判据;拒侧清单不是数组的形(受理口 400,盘上不该有)两边数法不同 —— pg 只认数组,MySQL 的 [0] 把非数组当一元数组读。
|
|
55
|
+
-- 命中行逐条按运行期的拒核:续跑时服务按引擎的读法拒(409 resume_blocked_by_policy,blockedBy = request.field_invalid,句子点名条目);拒侧清单全部有效的行放过。
|
|
56
|
+
-- local 车道的同名判据调的就是运行期那一只,是精确的。
|
|
57
|
+
SELECT 'skill-deny-inert' AS criterion, COUNT(*) AS hit_rows,
|
|
58
|
+
COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
|
|
59
|
+
FROM checkpoint k
|
|
60
|
+
LEFT JOIN workflow_agent_session w ON w.session_id = k.session_id
|
|
61
|
+
LEFT JOIN workflow_run r ON r.id = w.run_id
|
|
62
|
+
JOIN checkpoint_ctx c ON c.session_id = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(r.run, '$.originatingSessionId')), k.session_id)
|
|
63
|
+
WHERE k.status = 'pending'
|
|
64
|
+
AND ((JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.skills')) = 'ARRAY'
|
|
65
|
+
AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.skills[*].disallowedTools[0]'))
|
|
66
|
+
OR (JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.agents')) = 'ARRAY'
|
|
67
|
+
AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.agents[*].skills[*].disallowedTools[0]')));
|
|
68
|
+
|
|
48
69
|
-- drain-check:permissions-allow-list:mysql —— 期望 hit_rows = 0;每一行 = 一条升级后续跑会被拒的停驻行。
|
|
49
|
-
--
|
|
70
|
+
-- 读哪一份续跑上下文:与续跑腿同 —— workflow 子代的停驻行(在 workflow_agent_session 里登记过)续跑时读的是**发起这条 workflow 的会话**那一份
|
|
71
|
+
-- (workflow_run.run 的 originatingSessionId);其余行读自己会话那一份。
|
|
72
|
+
-- 判据 = 服务续跑腿拒臂的同一句话:提交体 settings.permissions.allow 是非空数组,且上下文的读法记号 permissionsAllowReading 不是当前的 "allow_layer"。
|
|
50
73
|
SELECT 'permissions-allow-list' AS criterion, COUNT(*) AS hit_rows,
|
|
51
74
|
COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
|
|
52
75
|
FROM checkpoint k
|
|
53
|
-
JOIN
|
|
76
|
+
LEFT JOIN workflow_agent_session w ON w.session_id = k.session_id
|
|
77
|
+
LEFT JOIN workflow_run r ON r.id = w.run_id
|
|
78
|
+
JOIN checkpoint_ctx c ON c.session_id = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(r.run, '$.originatingSessionId')), k.session_id)
|
|
54
79
|
WHERE k.status = 'pending'
|
|
55
80
|
AND JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.settings.permissions.allow')) = 'ARRAY'
|
|
56
81
|
AND JSON_LENGTH(JSON_EXTRACT(c.ctx, '$.body.settings.permissions.allow')) > 0
|
|
57
|
-
AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(c.ctx, '$.permissionsAllowReading')), '') <> '
|
|
82
|
+
AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(c.ctx, '$.permissionsAllowReading')), '') <> 'allow_layer';
|
|
58
83
|
|
|
59
84
|
-- drain-check:retired-mode-seed:mysql —— 期望 hit_rows = 0;每一行 = 一条升级后会被拒的停放行。
|
|
60
85
|
-- 判据逐字照引擎 refuseRetiredModeSeed:inheritedGate 带 autoModeRequested / approverPosture(任何值,含 JSON null),或 shellGate 不是 "always"(含 JSON null)。
|