@sema-agent/server 7.117.0 → 7.118.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 +97 -0
- package/USAGE.md +37 -18
- package/dist/approval-card.d.ts +40 -10
- package/dist/approval-card.js +11 -1
- package/dist/approval.d.ts +4 -12
- package/dist/approval.js +4 -26
- package/dist/boot/parked-revive-gate.d.ts +1 -7
- package/dist/boot/parked-revive-gate.js +2 -12
- package/dist/boot/resolve-spec.js +8 -13
- package/dist/boot/runner-deps.js +2 -2
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/capabilities/hands-lane.d.ts +25 -0
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +2 -2
- package/dist/config.d.ts +14 -18
- package/dist/config.js +13 -24
- package/dist/deployment-governance.d.ts +2 -7
- package/dist/deployment-governance.js +3 -5
- package/dist/host-decision.d.ts +83 -25
- package/dist/host-decision.js +31 -1
- package/dist/http/admission.js +7 -1
- package/dist/http/approval-sweeps.js +1 -1
- package/dist/http/mcp-dial-deadline.d.ts +35 -0
- package/dist/http/mcp-dial-deadline.js +21 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
- package/dist/http/memory-compliance-failure-reply.js +17 -8
- package/dist/http/memory-store-refusal-reply.d.ts +14 -11
- package/dist/http/memory-store-refusal-reply.js +16 -5
- package/dist/http/resume-legs.js +7 -3
- package/dist/http/routes/approvals-assistant.js +21 -24
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/routes/mcp-reconnect.d.ts +17 -0
- package/dist/http/routes/mcp-reconnect.js +6 -1
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/runs.js +41 -33
- package/dist/http/routes/sessions.d.ts +1 -1
- package/dist/http/routes/sessions.js +3 -6
- package/dist/http/routes/workflows.js +2 -0
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/memory-store-refusal.d.ts +42 -17
- package/dist/memory-store-refusal.js +25 -6
- package/dist/model-unreachable-notice.d.ts +1 -1
- package/dist/namespaced-rule-name.d.ts +45 -41
- package/dist/namespaced-rule-name.js +36 -22
- package/dist/observability/corrupt-read-seat.d.ts +4 -0
- package/dist/observability/corrupt-read-seat.js +1 -0
- package/dist/observability/fail-open.d.ts +14 -2
- package/dist/observability/fail-open.js +14 -2
- package/dist/operator-ask.d.ts +4 -1
- package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
- package/dist/orchestration/workflow-agent-session-index.js +80 -14
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
- package/dist/orchestration/workflow-completion-inbox.js +39 -17
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
- package/dist/orchestration/workflow-notify-journal.js +7 -2
- package/dist/outbound-dispatcher.d.ts +29 -20
- package/dist/outbound-dispatcher.js +105 -16
- package/dist/parked-decide.d.ts +43 -48
- package/dist/parked-decide.js +54 -16
- package/dist/permission-rule-vocab.d.ts +4 -3
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
- package/dist/plugins/approval-ask-store-sql.js +0 -3
- package/dist/plugins/one-time-migrations.d.ts +5 -0
- package/dist/plugins/one-time-migrations.js +110 -4
- package/dist/plugins/pg-pool.js +1 -1
- package/dist/plugins/tidb-pool.js +1 -1
- package/dist/plugins/web-search.d.ts +20 -1
- package/dist/plugins/web-search.js +39 -10
- package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
- package/dist/plugins/workflow-run-store-sql.js +5 -7
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +1 -1
- package/dist/rule-entry-list.d.ts +35 -46
- package/dist/rule-entry-list.js +17 -25
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.js +1 -0
- package/dist/runs.d.ts +1 -1
- package/dist/session-policy-wire.d.ts +13 -12
- package/dist/session-policy-wire.js +11 -6
- package/dist/spec-fields.d.ts +7 -9
- package/dist/spec-fields.js +4 -7
- package/dist/standing-answer-grant.d.ts +14 -2
- package/dist/standing-answer-grant.js +8 -1
- package/dist/task-settings.js +8 -4
- package/dist/tool-approval.d.ts +53 -112
- package/dist/tool-approval.js +75 -55
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/self-minted-wire.d.ts +16 -2
- package/dist/trace/self-minted-wire.js +7 -0
- package/dist/user-input-ledger.d.ts +24 -12
- package/dist/user-input-ledger.js +33 -14
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +74 -3
- package/scripts/drain-check.mysql.sql +43 -17
- package/scripts/drain-check.pg.sql +46 -20
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
/** 读口的入参:任何一种载着「这一问」的形(活的 `AskRequest`、`ToolCallRequest
|
|
1
|
+
/** 读口的入参:任何一种载着「这一问」的形(活的 `AskRequest`、`ToolCallRequest`、停驻行的 `tool_approval` 臂)都结构上满足。 */
|
|
2
2
|
export interface StandingAnswerGrantCarrier {
|
|
3
3
|
readonly toolName?: unknown;
|
|
4
4
|
readonly boundGrantDigest?: unknown;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
6
|
+
/** 一问的授予绑定(闭集):`none` = 应答只覆盖这一次调用;`bound` = 授予、绑定可读;`unreadable` = 授予、绑定读不出。 */
|
|
7
|
+
export type GrantBinding = {
|
|
8
|
+
readonly kind: "none";
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "bound";
|
|
11
|
+
readonly digest: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "unreadable";
|
|
14
|
+
};
|
|
15
|
+
/** 这一问的授予绑定(读法见文件头注)。 */
|
|
16
|
+
export declare function grantBindingOf(carrier: StandingAnswerGrantCarrier): GrantBinding;
|
|
17
|
+
/** 这一问的应答是否授予这一次调用之外的东西(= 绑定不是 `none`)。**按工具名记**的读者(耐久豁免店的读写口、豁免列表)据它不碰这类问:
|
|
18
|
+
* 它们的键没有绑定这一维,记下 / 代答都会越过绑定。 */
|
|
7
19
|
export declare function answerGrantsBeyondCall(carrier: StandingAnswerGrantCarrier): boolean;
|
|
8
20
|
//# sourceMappingURL=standing-answer-grant.d.ts.map
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { SKILL_TOOL_NAME } from "@sema-agent/core";
|
|
2
|
+
export function grantBindingOf(carrier) {
|
|
3
|
+
if (Object.hasOwn(carrier, "boundGrantDigest")) {
|
|
4
|
+
const d = carrier.boundGrantDigest;
|
|
5
|
+
return typeof d === "string" && d.length > 0 ? { kind: "bound", digest: d } : { kind: "unreadable" };
|
|
6
|
+
}
|
|
7
|
+
return carrier.toolName === SKILL_TOOL_NAME ? { kind: "unreadable" } : { kind: "none" };
|
|
8
|
+
}
|
|
2
9
|
export function answerGrantsBeyondCall(carrier) {
|
|
3
|
-
return
|
|
10
|
+
return grantBindingOf(carrier).kind !== "none";
|
|
4
11
|
}
|
|
5
12
|
//# sourceMappingURL=standing-answer-grant.js.map
|
package/dist/task-settings.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
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";
|
|
4
|
+
import { allowLayerEntryCountIssue, describeAllowLayerOverCap, describeEmptyToolSegmentEntry, emptyToolSegmentEntryOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
|
|
5
5
|
import { tightenGoverned } from "./governance-overrides.js";
|
|
6
6
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
|
|
7
7
|
import { parseHooksConfig } from "./hooks/hook-runner.js";
|
|
8
8
|
import { heldRuleName } from "./tool-rule-names.js";
|
|
9
|
+
import { policyAskStandingAnswerFacts, standingAnswerDurablyRecordable } from "./tool-approval.js";
|
|
9
10
|
export const PERMISSION_MODE_REQUEST_FACE = {
|
|
10
11
|
default: "request",
|
|
11
12
|
acceptEdits: "request",
|
|
@@ -304,7 +305,8 @@ function createWorkflowAskPolicy(isExempt) {
|
|
|
304
305
|
async check(req) {
|
|
305
306
|
if (heldRuleName(req, isWorkflowName) === undefined)
|
|
306
307
|
return { action: "allow" };
|
|
307
|
-
|
|
308
|
+
const ask = { action: "ask", message: `dynamic workflow "${req.toolName}" requires approval in this permission mode`, decisionReason: "rule" };
|
|
309
|
+
if (isExempt && standingAnswerDurablyRecordable(policyAskStandingAnswerFacts(req, ask))) {
|
|
308
310
|
try {
|
|
309
311
|
if (await isExempt(req.toolName))
|
|
310
312
|
return { action: "allow", decisionReason: "rule" };
|
|
@@ -312,7 +314,7 @@ function createWorkflowAskPolicy(isExempt) {
|
|
|
312
314
|
catch {
|
|
313
315
|
}
|
|
314
316
|
}
|
|
315
|
-
return
|
|
317
|
+
return ask;
|
|
316
318
|
},
|
|
317
319
|
nameSets: [{ ask: WORKFLOW_ASK_NAMES }],
|
|
318
320
|
};
|
|
@@ -346,7 +348,6 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
|
|
|
346
348
|
const judgments = [
|
|
347
349
|
["permissions.ask", perms.ask, { consumer: "rule-policy", behavior: "ask", ...(pathBases !== undefined ? { pathBases } : {}) }],
|
|
348
350
|
["permissions.deny", perms.deny, { consumer: "rule-policy", behavior: "deny", ...(pathBases !== undefined ? { pathBases } : {}) }],
|
|
349
|
-
["permissions.allow", perms.allow, { consumer: "allow-layer" }],
|
|
350
351
|
];
|
|
351
352
|
for (const [field, names, judgment] of judgments) {
|
|
352
353
|
if (!names)
|
|
@@ -355,6 +356,9 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
|
|
|
355
356
|
if (unmatchable !== undefined)
|
|
356
357
|
throw new Error(unmatchable.message);
|
|
357
358
|
}
|
|
359
|
+
const emptyAllow = perms.allow !== undefined ? emptyToolSegmentEntryOf(perms.allow) : undefined;
|
|
360
|
+
if (emptyAllow !== undefined)
|
|
361
|
+
throw new Error(describeEmptyToolSegmentEntry("permissions.allow", emptyAllow));
|
|
358
362
|
}
|
|
359
363
|
export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
360
364
|
const perms = settings.permissions;
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ApprovalDecidedBy, type HostSettledBy } from "./host-decision.js";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
-
import { type AskClass, type AskRequest, type AskOutcome, type CheckpointGate, type PendingAction, type PersistedRuleMandate, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
3
|
+
import { type AskClass, type AskRequest, type AskOutcome, type PermissionResult, type CheckpointGate, type PendingAction, type PersistedRuleMandate, type ReadRootGrantCandidate, type RuleOffer, type AutoModeFallbackCause, type BoundSkillGrants } from "@sema-agent/core";
|
|
4
4
|
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
5
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";
|
|
@@ -8,6 +8,7 @@ import type { ApprovalAskAuditSink } from "./approval-ask-audit-store.js";
|
|
|
8
8
|
import type { ParkCommitWatch } from "./plugins/park-commit-watch.js";
|
|
9
9
|
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
10
10
|
import { type GovernancePosture } from "./runtime-governance.js";
|
|
11
|
+
import { type GrantBinding, type StandingAnswerGrantCarrier } from "./standing-answer-grant.js";
|
|
11
12
|
/** [ref]:审批 gate kind 闭集(core gateMatch 的 `human`/`irreversible_ask` ↔ outcome.gate "policy_ask")。
|
|
12
13
|
* 此前 7 份手写副本散在两只 checkpoint store 的数组/SQL 字面与 /decide 守卫——core 加审批味 kind 时
|
|
13
14
|
* 全部静默漂移。core 无导出词表(全大写导出面零命中,2026-08-09 亲验),属主落此;SQL IN 片段从
|
|
@@ -56,13 +57,10 @@ export interface ParkedAskRedeemRequest {
|
|
|
56
57
|
decision: "approve" | "deny";
|
|
57
58
|
/** [ref] 同源的人写理由(deny 时 core 把它交给模型;allow 只进审计面)。 */
|
|
58
59
|
note?: string;
|
|
59
|
-
/**
|
|
60
|
-
* live 命中臂与本 PARKED 臂读**同一格** ——
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
* 只在 `policy` 上相交)—— 这正是这条 wire 键只开 `"policy"` 一个词的原因,也是它不能声明成其中任何
|
|
64
|
-
* 一只闭集的原因:同一个串要落到两个不同词表的座上。 */
|
|
65
|
-
settledBy?: string;
|
|
60
|
+
/** S-1154:决定体的**出处位**(已过 `readDecidedBy` 的 wire 词,缺席已落成 `"person"`),经 `hostDecisionFacts` 落到停驻座的
|
|
61
|
+
* `hostDecision.decidedBy`。live 命中臂与本 PARKED 臂读**同一格** —— 同一份体落在哪条臂上只取决于副本与时序;两臂各走自己座的
|
|
62
|
+
* 那一只映射(live = `liveSettledBy`,停驻 = 恒等),词表只有一份。 */
|
|
63
|
+
decidedBy: ApprovalDecidedBy;
|
|
66
64
|
/** [ref] ctrl+g 编辑放行的实参整体替换(approve 才有意义)。 */
|
|
67
65
|
updatedInput?: unknown;
|
|
68
66
|
/** 发起本次 respond 的**已验证**调用方(live 腿 `gatedPrincipal` 的那一把)。席在赎回之前拿它与
|
|
@@ -79,9 +77,21 @@ export type ParkedAskRedeem = (req: ParkedAskRedeemRequest) => Promise<{
|
|
|
79
77
|
body: Record<string, unknown>;
|
|
80
78
|
}>;
|
|
81
79
|
/**
|
|
82
|
-
*
|
|
80
|
+
* 本仓**策略层自己交出去的一问**的 {@link StandingAnswerFacts}。读者一个:workflow 问句门(`task-settings.ts`)—— 它在交出这一问之前先读按工具名记的
|
|
81
|
+
* 耐久豁免店(「本会话不再询问」在本层兑现,继承链上的同一问才够得着;审批席依约不退役继承问),而**能不能读**与审批席 / 回决写侧是同一只判据
|
|
82
|
+
* ({@link standingAnswerDurablyRecordable}):记得下的才代答,记不下的(规则车道清不掉的类、带授予绑定的问 ……)不代答,存量行也不例外 —— 一事一读。
|
|
83
|
+
* 部署名单门不读店(它的问带 `matchedAskRule` ⇒ 类 `ask_rule` ⇒ 规则车道清不掉,本函数对它也答这一类 —— `test/approval-exemption.test.ts` 的同判钉)。
|
|
84
|
+
* 事实取自**这一问本身**:类集经 core 包根唯一的类派生 `classifyAsk`;它的第二参是闸内的事实,本层看不到 ⇒ 给诚实的空(core 对「不持席位的站」的同一句;
|
|
85
|
+
* 只读决定半场的 `decisionAskClassesOf` 不在包根导出,所以空闸事实不是手造的第二种读法,而是同一只派生在本层能交出的全部输入)。
|
|
86
|
+
* `requiresRealApproval` / `mandated` 读这一问自报的成员;治理位恒假(本层不是治理层);授予绑定读调用(`grantBindingOf`)。
|
|
87
|
+
*/
|
|
88
|
+
export declare function policyAskStandingAnswerFacts(req: StandingAnswerGrantCarrier, ask: Extract<PermissionResult, {
|
|
89
|
+
action: "ask";
|
|
90
|
+
}>): StandingAnswerFacts;
|
|
91
|
+
/**
|
|
92
|
+
* 一问身上与「站着的答案」有关的事实位 —— {@link standingAnswerMayRetire} 的全部入参。
|
|
83
93
|
*
|
|
84
|
-
* 🔴
|
|
94
|
+
* 🔴 **全必填**,刻意不做 `?: true` 的「只记真」形:判据哪天多一位,每个读者都要当场编译红,
|
|
85
95
|
* 而不是漏传一格被静默读成 `false`(那是安全轴上的 fail-open)。读者各自从自己手里的形折成这三格,
|
|
86
96
|
* 然后交**同一只** {@link standingAnswerMayRetire}(判据只有一份,折法各归读者):
|
|
87
97
|
* · live 读侧取 `req` + 本地治理 peek;live 写侧取 pending 条目;
|
|
@@ -95,10 +105,13 @@ export interface StandingAnswerFacts {
|
|
|
95
105
|
readonly requiresRealApproval: boolean;
|
|
96
106
|
/** core 闸站盖的强制位([ref]):规则清不掉、站着的答案退不掉,只有对**这一次调用**的应答能答。 */
|
|
97
107
|
readonly mandated: boolean;
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
readonly
|
|
108
|
+
/** 这一问的应答**授不授予**这一次调用之外的东西、授予的话绑定是什么(批准引擎的 `Skill` = 激活技能 + 本 run 内清单免问)。
|
|
109
|
+
* `bound` ⇒ 站着的答案按「工具 + 绑定」记、只代答绑定相同的问(core 7.43.0 #1348 / #1357:活的问与停驻行同带 `boundGrantDigest`);
|
|
110
|
+
* `unreadable` ⇒ 不可记、不被代答(fail-closed)。读者一律经 `standing-answer-grant.ts` 的 `grantBindingOf`(唯一读口)折这一位。 */
|
|
111
|
+
readonly grantBinding: GrantBinding;
|
|
112
|
+
/** 引擎盖在这一问上的**类集**(`AskRequest.classes` / 停驻行孪生座;经 core 的屏 `screenAskClassMembers` 读)。`undefined` = 卡上没有类
|
|
113
|
+
* (老引擎 / 不盖类的站)或屏拒 —— 读不出的类不是「无类」。 */
|
|
114
|
+
readonly classes: readonly AskClass[] | undefined;
|
|
102
115
|
}
|
|
103
116
|
/**
|
|
104
117
|
* 🔴 **S-660 —— 会话级放行的唯一判据:站着的答案能不能退掉这一问。** 纯谓词,三个读者:
|
|
@@ -133,6 +146,14 @@ export interface StandingAnswerFacts {
|
|
|
133
146
|
* `mandated` 孪生座由**耐久** `/decide` 腿读,见 {@link parkRowStandingAnswerFacts}。)
|
|
134
147
|
*/
|
|
135
148
|
export declare function standingAnswerMayRetire(facts: StandingAnswerFacts): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* **耐久**豁免店能不能记下这一问的答案:先得退得掉({@link standingAnswerMayRetire}),再得没有授予绑定 —— 耐久店按 `(会话, 工具名)` 记,
|
|
151
|
+
* 没有绑定这一维,记下一张带绑定的答案就会代答同名工具的每一种绑定(`plugins/approval-exemption-store.ts`)。读者两处:耐久 `/decide` 腿的
|
|
152
|
+
* remember 可记性(`http/resume-legs.ts`)与运维队列行的 `rememberRefused`(`http/routes/approvals-assistant.ts`)—— 两处同答。
|
|
153
|
+
* 读侧同理:耐久店不代答带绑定的问(`standingGrantSettles` 经 `answerGrantsBeyondCall`;策略层的豁免探针 —— workflow 问句门 ——
|
|
154
|
+
* 经本判据 + {@link policyAskStandingAnswerFacts})。
|
|
155
|
+
*/
|
|
156
|
+
export declare function standingAnswerDurablyRecordable(facts: StandingAnswerFacts): boolean;
|
|
136
157
|
/**
|
|
137
158
|
* 🔴 **S-663 → NP-1(7.100.0 合并复审 [ref] 交叉表 #2)—— 耐久 `/decide` 腿的 {@link StandingAnswerFacts}:从 park 行铸三位。**
|
|
138
159
|
* 纯投影,**不判**:判据是同一只 {@link standingAnswerMayRetire}(唯一读点 = `http/resume-legs.ts` 的 remember 可记性,
|
|
@@ -311,6 +332,18 @@ export interface ToolApprovalFrame {
|
|
|
311
332
|
/** S-934 ②:这一问属于的**全部**类(core `ASK_CLASSES` 序,`classes[0] === askClass`,非空)—— 读整只集合,别只读 {@link askClass}(一问可同属
|
|
312
333
|
* 几类)。与 `askClass` 同在同缺、同一只窄读;逐字同序过境,不排序不去重。 */
|
|
313
334
|
classes?: AskClass[];
|
|
335
|
+
/**
|
|
336
|
+
* core 7.43.0 #1348 / #1357(**ADDITIVE**,`"tool_approval"` only)—— 对引擎 `Skill` 工具的调用,批准它**还授予**的东西的摘要
|
|
337
|
+
* (`AskRequest.boundGrantDigest`,技能名 + 两只清单规范拼写的哈希);别的卡缺席。与停驻行 / inbox 行的同名位同值(core:同一只工厂铸,
|
|
338
|
+
* 活卡与它的停驻孪生按 `(toolCallId, boundGrantDigest)` 对账)。逐字过境,本仓不重算;站着的答案按「工具 + 它」记(S-1115)。
|
|
339
|
+
*/
|
|
340
|
+
boundGrantDigest?: string;
|
|
341
|
+
/** 上一位哈希的那一份值:`{ skill, allowedTools, disallowedTools }`(`skill: null` = 调用没点名任何已载技能)。与摘要同在同缺;**部署文本**,
|
|
342
|
+
* 壳按上下文转义渲染(core:display metadata)。逐字过境。 */
|
|
343
|
+
boundGrants?: BoundSkillGrants;
|
|
344
|
+
/** core 7.43.0 #1353:分类器没给判词的成因词(`error` / `timeout` / `parse_error`;只在类 `classifier_no_ruling` 的卡上)。闭集经 core 的
|
|
345
|
+
* `readClosedWordSeat(req, "classifierNoRuling")` 窄读(筛 = core `isAutoModeFallbackCause`),集外 ⇒ 缺席 + 计一次。display:本仓不按它裁决。 */
|
|
346
|
+
classifierNoRuling?: AutoModeFallbackCause;
|
|
314
347
|
/**
|
|
315
348
|
* **[ref]**(core 7.19.0;**ADDITIVE**,`"tool_approval"` only)—— 这只**根外读** ask 的**清障目录**:
|
|
316
349
|
* 把 `dir` 加进本会话的读目录(壳侧既有的 `/add-dir` 口),同一条调用就不会再问。
|
|
@@ -713,83 +746,6 @@ export declare const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOffe
|
|
|
713
746
|
* 指定 scope = 让一次「在项目 A 里点的同意」写成一条覆盖项目 B(或全局)的常驻放行 —— 那是这条轴上
|
|
714
747
|
* 最贵的一次扩权。静默忽略同样不行:客户端会以为自己设上了。响亮拒,一句话说清谁拥有这一格。
|
|
715
748
|
*/
|
|
716
|
-
/**
|
|
717
|
-
* #1016 —— `settledBy` 与 allow 族**互斥**的那一句(两条回决腿共用一份文本:同一个体在两条口上必须
|
|
718
|
-
* 拿到同一句话)。判的是**键在不在**,不是它是什么词。
|
|
719
|
-
*/
|
|
720
|
-
/**
|
|
721
|
-
* #1016 —— `settledBy` 的**形**拒句(两条回决腿共用一份文本,与 {@link SETTLED_BY_DENY_ONLY} 同姿势)。
|
|
722
|
-
*
|
|
723
|
-
* 🔴 **判形 ≠ 判词**:本层证得出的只有「它是个串」;「它是不是词表里的那个串」由**引擎**判(两条腿各有
|
|
724
|
-
* 一处响亮拒,读数在收车档)。加这道形门的理由是**类型必须说真话**(wire 边界强类型宪法):
|
|
725
|
-
* 一个非串值往下会落进 `AskOutcome.settledBy`(core 的串闭集)与本仓审计账本的 `settledBy?: string` 席 ——
|
|
726
|
-
* 两处都声明成串,而运行期塞进去一个数字。引擎对它照样响亮拒(实测三形一致:`"bogus"` / `1` / `null`
|
|
727
|
-
* 都是 `approver_contract` 的 fail-closed deny),所以这不是在补一个 fail-open,而是**不让本层的类型撒谎**;
|
|
728
|
-
* 顺带把「一个非串永远不可能是闭集成员」这件必然的事拒在最早的地方,与同一只 parser 里 `note` /
|
|
729
|
-
* `persistRule.rule` / `persistRule.edited` 三道既有形门**同族**(那三道在,只有本格没有 = 本格才是特例)。
|
|
730
|
-
*/
|
|
731
|
-
export declare const SETTLED_BY_MUST_BE_STRING = "settledBy must be a string \u2014 the settlement source is one word from the engine's closed set, and this layer judges only the SHAPE (which word it is, the engine decides)";
|
|
732
|
-
/**
|
|
733
|
-
* #1016 / S-558 —— **live 座的那一次边界 cast,全仓只此一处**(耐久座的那一次在 `hostDecisionFacts` 里,
|
|
734
|
-
* 一座一次)。它说的是「我把一个**未判词**的串放到只收词的座上」:判词的是 core —— `resolveAsk` 对词表外
|
|
735
|
-
* 是 `approver_contract` 的响亮 fail-closed deny,不是静默当人拒(实测三形同判,特征钉在
|
|
736
|
-
* `test/tool-approval.test.ts` 的 #1016 组)。本层判过的只有**形**(是个串、只与 deny 同行,见
|
|
737
|
-
* {@link SETTLED_BY_MUST_BE_STRING} / {@link SETTLED_BY_DENY_ONLY}),而形不是词。
|
|
738
|
-
*
|
|
739
|
-
* 两个座位读它:这次回决的**即时结算**(`entry.settle` 的自报参)与**持久行**(`decideAsk` 的
|
|
740
|
-
* `settledBy` 列)。S-558 之前只有前者 —— 于是同一个事实在「当次」与「重放」上是两个答案。
|
|
741
|
-
* S-843:耐久 ask 回决口(`POST /v1/tasks/:taskId/asks/:askId/decision`)写的是**同一个座**(同一列,经
|
|
742
|
-
* `outcomeOfDecidedRow` 交给同一个 `AskOutcome.settledBy`),所以它也走这一只 —— cast 仍是一座一次。
|
|
743
|
-
*/
|
|
744
|
-
export declare function respondSettledBy(parsed: Pick<ParsedApprovalResponse, "settledBy">): HostSettledBy | undefined;
|
|
745
|
-
export declare const SETTLED_BY_DENY_ONLY = "settledBy is only meaningful with decision \"deny\" \u2014 only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door's own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision";
|
|
746
|
-
/**
|
|
747
|
-
* S-843 / 车QK F7 —— `settledBy` 的**宽**拒句(三条回决腿共用一份文本,与 {@link SETTLED_BY_DENY_ONLY} 同姿势)。句子逐字是 S-843 在
|
|
748
|
-
* 耐久 ask 回决口路由里写下的那一句(车QK F7 起搬到判官旁,路由不再自己写),尺 = 店的同一个常量({@link MAX_SETTLED_BY_CHARS})。
|
|
749
|
-
*/
|
|
750
|
-
export declare const SETTLED_BY_TOO_WIDE = "settledBy must be 1-16 characters \u2014 the settlement source is one word from the engine's closed set and lands in a fixed-width column on the decision row";
|
|
751
|
-
/**
|
|
752
|
-
* S-843 —— 决议体上 `settledBy` 的判据,**一处成形**。三条 HTTP 回决腿共读:
|
|
753
|
-
* `POST /v1/approvals/:sessionId/decide`(耐久 park 腿)、`POST /v1/tasks/:taskId/asks/:askId/decision`(耐久 ask 腿)与
|
|
754
|
-
* live `POST /v1/tool-approvals/:id/respond`(经 {@link parseToolApprovalResponse};车QK F7 起 —— 此前那只 parser 手写了 ②③ 两臂、
|
|
755
|
-
* 没有 ④,是这条判据的第二个写者)。
|
|
756
|
-
*
|
|
757
|
-
* 次序即契约(①–③ = decide 腿修前那三道 if 的原序,逐字搬来,不是新判据):
|
|
758
|
-
* ① 直连门在场 ⇒ `not_in_proof` —— 键不在决策证明的签名载荷里(§6.3 同族第四条,理由在 approvals-assistant 的
|
|
759
|
-
* `DECIDE_NOT_IN_PROOF_KEYS` 注);
|
|
760
|
-
* ② 不是 deny ⇒ `deny_only` —— {@link SETTLED_BY_DENY_ONLY};
|
|
761
|
-
* ③ 不是串 ⇒ `not_string` —— {@link SETTLED_BY_MUST_BE_STRING};
|
|
762
|
-
* ④ 长度不在 1..{@link MAX_SETTLED_BY_CHARS} ⇒ `too_wide` —— {@link SETTLED_BY_TOO_WIDE}(车QK F7 / A3,7.104.0 合并复审)。
|
|
763
|
-
* 尺与店门口的形门同一只谓词({@link settledByFitsColumn}):`/respond` 与耐久 ask 口写的是**同一列**
|
|
764
|
-
* (`approval_ask.settled_by`,`VARCHAR(16)`),列宽是列的事实,不是某一条腿的事实。修前这一道只长在耐久 ask 口的路由里:
|
|
765
|
-
* `/respond` 上一只超宽 / 空串的来源位要到 CAS 里才被店门抛,落进 D5「店抖动不挡真实回决」臂 —— 行没决,决议照样交给了引擎,200。
|
|
766
|
-
* `/decide` 的来源位不落这一列(交给 core 的 checkpoint,core 在 CAS 之前按词表拒),这一道对它同样成立:core 的闭集词
|
|
767
|
-
* (`human` / `timeout` / `policy`)都远短于这把尺,尺外的串不可能是一个词 ⇒ 三条口对同一个体同判同句,不为一条腿开特例。
|
|
768
|
-
* 判的是**键在不在 / 配不配这个决议 / 形**,**从不判词**(词表属主是 core;三条腿的词都由引擎响亮拒)。
|
|
769
|
-
*
|
|
770
|
-
* 返回判决而不是直接回 400:各条腿在调用点按自己的码约定发出(错误文案冻结门按字面站点采集,码是 wire 契约);
|
|
771
|
-
* 调用点对判决做穷举 `switch` + `never` 兜底(加一臂 = 编译红,[ref])。`accepted` 臂带着已证形的串 —— 调用点拿它往下传,
|
|
772
|
-
* 不再对 `body.settledBy` 做第二次窄化。第三参给字面 `false` 的调用点(live `/respond`:那条口没有直连门证明)拿到的判决型里
|
|
773
|
-
* **没有** `not_in_proof` 臂(重载),不必为一条结构上不可达的臂写兜底。
|
|
774
|
-
*/
|
|
775
|
-
export type SettledByBodyVerdict = {
|
|
776
|
-
kind: "absent";
|
|
777
|
-
} | {
|
|
778
|
-
kind: "accepted";
|
|
779
|
-
settledBy: string;
|
|
780
|
-
} | {
|
|
781
|
-
kind: "not_in_proof";
|
|
782
|
-
} | {
|
|
783
|
-
kind: "deny_only";
|
|
784
|
-
} | {
|
|
785
|
-
kind: "not_string";
|
|
786
|
-
} | {
|
|
787
|
-
kind: "too_wide";
|
|
788
|
-
};
|
|
789
|
-
export declare function judgeSettledByBody(settledBy: unknown, decision: string, directDoorActive: false): Exclude<SettledByBodyVerdict, {
|
|
790
|
-
kind: "not_in_proof";
|
|
791
|
-
}>;
|
|
792
|
-
export declare function judgeSettledByBody(settledBy: unknown, decision: string, directDoorActive: boolean): SettledByBodyVerdict;
|
|
793
749
|
/** S-720(core 7.32.0 #1093)—— `readRootGrant` 只随 `decision:"allow"`(CC 卡的第三选项「Yes, and add directory」= 放行这一次
|
|
794
750
|
* + 加目录;`allow_session` 不是那一个选项,`deny` 上它没有意义)。裁决之前响亮拒,不静默丢。 */
|
|
795
751
|
export declare const READ_ROOT_GRANT_ALLOW_ONLY = "readRootGrant is only meaningful with decision \"allow\" \u2014 it is the card's \"Yes, and add directory\" answer (allow this call and add the card's readRootCandidate.dir to this run's read directories)";
|
|
@@ -803,27 +759,12 @@ export declare function parseToolApprovalResponse(body: unknown): {
|
|
|
803
759
|
updatedInput?: unknown;
|
|
804
760
|
persistRule?: ParsedPersistRule;
|
|
805
761
|
note?: string;
|
|
806
|
-
|
|
762
|
+
decidedBy: ApprovalDecidedBy;
|
|
807
763
|
readRootGrant?: string;
|
|
808
764
|
} | {
|
|
809
765
|
ok: false;
|
|
810
766
|
error: string;
|
|
811
767
|
};
|
|
812
|
-
/** [ref] 车2:`parseToolApprovalResponse` 的成功分支形——`respond()` 拆分出的私有收尾方法(`finishRespond`/
|
|
813
|
-
* `respondWithCas`)复用,避免裸内联匿名类型重复三处。 */
|
|
814
|
-
type ParsedApprovalResponse = Extract<ReturnType<typeof parseToolApprovalResponse>, {
|
|
815
|
-
ok: true;
|
|
816
|
-
}>;
|
|
817
|
-
/**
|
|
818
|
-
* #1016 —— wire 上**开给调用方**的那一个词,从 core 的两只闭集里取**交集**。
|
|
819
|
-
*
|
|
820
|
-
* 🔴 它是一条**文档钉**,不是过境值的型(过境值是 `string`,理由在 `parseToolApprovalResponse` 那处注):
|
|
821
|
-
* `"human"` 与缺席同义、`"timeout"` 是本仓自己观察到的窗(客户端说出来就是替服务端伪证一件只有服务端
|
|
822
|
-
* 看得见的事),而耐久座那边的 `"person"` / `"sla_timeout"` 同理。两只词表**只在 `"policy"` 上相交** ——
|
|
823
|
-
* 这条 wire 键因此恰好只开那一个词。core 哪天退役它,这里**编译期先红**,而不是在 wire 上静默留一个
|
|
824
|
-
* 两个座都已经不收的词;`test/tool-approval.test.ts` 的 #1016 那组按它写判据。
|
|
825
|
-
*/
|
|
826
|
-
export type RespondSettledBy = Extract<HostSettledBy, HostDecidedBy>;
|
|
827
768
|
/**
|
|
828
769
|
* [ref] R-13 C —— `UNATTENDED_APPROVAL_POLICY` 的**闭集**词表([ref]Q1「裁2:A+C」/[ref] 裁②)。
|
|
829
770
|
*
|
|
@@ -1038,9 +979,9 @@ type DecidedRowFacts = Pick<AskRow, "decision" | "settledBy"> & {
|
|
|
1038
979
|
* · **结算来源**(S-558)—— 行上非 null 即宿主自报的那个词(`null` = 人决,与「不铸这个键」同义;
|
|
1039
980
|
* 语义属主 = `AskRow.settledBy` 顶注)。
|
|
1040
981
|
*
|
|
1041
|
-
* 🔴
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
982
|
+
* 🔴 S-1154:来源位**两向都读**。决定体的出处位 `decidedBy:"policy"` 在 allow 与 deny 上都会落列(引擎 7.39.0 起铸
|
|
983
|
+
* `policy_allowed`),所以 approve 行带来源位是一条合法的行,它的每条读回路径交还 `{allow:true, settledBy:"policy", updatedInput?}`
|
|
984
|
+
* —— 与当次回决交给引擎的那一只逐字同形。
|
|
1044
985
|
*/
|
|
1045
986
|
export declare function outcomeOfDecidedRow(row: DecidedRowFacts): DecidedAskOutcome;
|
|
1046
987
|
/**
|
|
@@ -1085,7 +1026,7 @@ export declare class ToolApprovalCoordinator {
|
|
|
1085
1026
|
* askId 下全部本地条目(自己 + 兄弟 + 重复注册),直接调用它们各自的 `settle`,而不是被动等一个永远
|
|
1086
1027
|
* 不会来的本地信号。 */
|
|
1087
1028
|
private readonly pendingByAskId;
|
|
1088
|
-
/** Per-capability session grants — keys = sessionAllowKey(owner, sessionId, category) (修2; bounded). */
|
|
1029
|
+
/** Per-capability session grants — keys = sessionAllowKey(owner, sessionId, category, grant binding) (修2; bounded). */
|
|
1089
1030
|
private readonly allowAllSessions;
|
|
1090
1031
|
/** [ref] HIGH-1(broker)→[ref]四 core 产品裁定「多活集合+广播+首决胜出」:per-(owner, host-session)
|
|
1091
1032
|
* 的**当前活跃连接集合**——boundAsk 闭包只携身份,emit 时广播给该 key 下**全部**活连接(宿主重连/
|
package/dist/tool-approval.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { contentKindOf } from "./approval-content-kind.js";
|
|
2
|
+
import { liveSettledBy, policyCannotAllow, readDecidedBy, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK, SETTLED_BY_RETIRED } from "./host-decision.js";
|
|
2
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import { uuidv7, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
|
|
4
|
+
import { uuidv7, classifyAsk, foldArbitrationRow, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
|
|
4
5
|
import { precheckCardRuleText } from "./rules-consent.js";
|
|
5
6
|
import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
|
|
6
7
|
import { hasBidiControls } from "./text-bidi.js";
|
|
@@ -9,11 +10,11 @@ import { createLogger } from "./observability/logger.js";
|
|
|
9
10
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
10
11
|
import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
|
|
11
12
|
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
|
-
import {
|
|
13
|
+
import { POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
|
|
13
14
|
import { settleByPolicy } from "./approval-policy-settlement.js";
|
|
14
15
|
import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
|
|
15
16
|
import { governanceOriginOf } from "./runtime-governance.js";
|
|
16
|
-
import { answerGrantsBeyondCall } from "./standing-answer-grant.js";
|
|
17
|
+
import { answerGrantsBeyondCall, grantBindingOf } from "./standing-answer-grant.js";
|
|
17
18
|
const defaultLogger = createLogger();
|
|
18
19
|
export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
|
|
19
20
|
export function isApprovalGateKind(k) {
|
|
@@ -81,11 +82,37 @@ const FS_WRITE_TOOLS = pathConfinableWriteToolNames();
|
|
|
81
82
|
function sessionAllowCategory(toolName) {
|
|
82
83
|
return FS_WRITE_TOOLS.has(toolName) ? "fs-write" : `tool:${toolName}`;
|
|
83
84
|
}
|
|
84
|
-
function sessionAllowKey(owner, sessionId, category) {
|
|
85
|
-
return JSON.stringify([owner, sessionId, category]);
|
|
85
|
+
function sessionAllowKey(owner, sessionId, category, binding) {
|
|
86
|
+
return JSON.stringify([owner, sessionId, category, binding.kind === "bound" ? binding.digest : null]);
|
|
87
|
+
}
|
|
88
|
+
function askStandingAnswerFacts(req, governanceForced) {
|
|
89
|
+
return {
|
|
90
|
+
governanceForced,
|
|
91
|
+
requiresRealApproval: req.requiresRealApproval === true,
|
|
92
|
+
mandated: req.mandated === true,
|
|
93
|
+
grantBinding: grantBindingOf(req),
|
|
94
|
+
classes: screenAskClassMembers(req.askClass, req.classes)?.classes,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function policyAskStandingAnswerFacts(req, ask) {
|
|
98
|
+
return {
|
|
99
|
+
governanceForced: false,
|
|
100
|
+
requiresRealApproval: ask.requiresRealApproval === true,
|
|
101
|
+
mandated: ask.mandated === true,
|
|
102
|
+
grantBinding: grantBindingOf(req),
|
|
103
|
+
classes: [...classifyAsk(ask, { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} }).classes],
|
|
104
|
+
};
|
|
86
105
|
}
|
|
87
106
|
export function standingAnswerMayRetire(facts) {
|
|
88
|
-
return !facts.governanceForced &&
|
|
107
|
+
return (!facts.governanceForced &&
|
|
108
|
+
!facts.requiresRealApproval &&
|
|
109
|
+
!facts.mandated &&
|
|
110
|
+
facts.grantBinding.kind !== "unreadable" &&
|
|
111
|
+
facts.classes !== undefined &&
|
|
112
|
+
foldArbitrationRow(new Set(facts.classes)).ruleMayClear);
|
|
113
|
+
}
|
|
114
|
+
export function standingAnswerDurablyRecordable(facts) {
|
|
115
|
+
return standingAnswerMayRetire(facts) && facts.grantBinding.kind === "none";
|
|
89
116
|
}
|
|
90
117
|
export function parkRowStandingAnswerFacts(pending, gate, governance) {
|
|
91
118
|
const riskDescriptor = "riskDescriptor" in gate ? gate.riskDescriptor : undefined;
|
|
@@ -93,7 +120,8 @@ export function parkRowStandingAnswerFacts(pending, gate, governance) {
|
|
|
93
120
|
governanceForced: governanceOriginOf({ riskDescriptor }, governance) === true,
|
|
94
121
|
requiresRealApproval: readParkRowTrueSeat(pending, "requiresRealApproval"),
|
|
95
122
|
mandated: readParkRowTrueSeat(pending, "mandated") || mandatedFromSeat(readRuleOffersAbsenceSeat(pending)),
|
|
96
|
-
|
|
123
|
+
grantBinding: grantBindingOf(pending),
|
|
124
|
+
classes: screenAskClassMembers(pending.askClass, pending.classes)?.classes,
|
|
97
125
|
};
|
|
98
126
|
}
|
|
99
127
|
function pendingEntryStandingAnswerFacts(entry) {
|
|
@@ -101,7 +129,8 @@ function pendingEntryStandingAnswerFacts(entry) {
|
|
|
101
129
|
governanceForced: entry.governanceForced === true,
|
|
102
130
|
requiresRealApproval: entry.requiresRealApproval === true,
|
|
103
131
|
mandated: entry.mandated === true,
|
|
104
|
-
|
|
132
|
+
grantBinding: entry.grantBinding,
|
|
133
|
+
classes: entry.classes,
|
|
105
134
|
};
|
|
106
135
|
}
|
|
107
136
|
function mandatedFromSeat(seat) {
|
|
@@ -118,25 +147,6 @@ export const PERSIST_RULE_TEXT_ERROR = `persistRule.rule must be a non-empty str
|
|
|
118
147
|
export const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present";
|
|
119
148
|
export const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present";
|
|
120
149
|
export const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited — send exactly one arm";
|
|
121
|
-
export const SETTLED_BY_MUST_BE_STRING = "settledBy must be a string — the settlement source is one word from the engine's closed set, and this layer judges only the SHAPE (which word it is, the engine decides)";
|
|
122
|
-
export function respondSettledBy(parsed) {
|
|
123
|
-
return parsed.settledBy;
|
|
124
|
-
}
|
|
125
|
-
export const SETTLED_BY_DENY_ONLY = 'settledBy is only meaningful with decision "deny" — only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door\'s own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision';
|
|
126
|
-
export const SETTLED_BY_TOO_WIDE = `settledBy must be 1-${MAX_SETTLED_BY_CHARS} characters — the settlement source is one word from the engine's closed set and lands in a fixed-width column on the decision row`;
|
|
127
|
-
export function judgeSettledByBody(settledBy, decision, directDoorActive) {
|
|
128
|
-
if (settledBy === undefined)
|
|
129
|
-
return { kind: "absent" };
|
|
130
|
-
if (directDoorActive)
|
|
131
|
-
return { kind: "not_in_proof" };
|
|
132
|
-
if (decision !== "deny")
|
|
133
|
-
return { kind: "deny_only" };
|
|
134
|
-
if (typeof settledBy !== "string")
|
|
135
|
-
return { kind: "not_string" };
|
|
136
|
-
if (!settledByFitsColumn(settledBy))
|
|
137
|
-
return { kind: "too_wide" };
|
|
138
|
-
return { kind: "accepted", settledBy };
|
|
139
|
-
}
|
|
140
150
|
export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with decision "allow" — it is the card\'s "Yes, and add directory" answer (allow this call and add the card\'s readRootCandidate.dir to this run\'s read directories)';
|
|
141
151
|
export const READ_ROOT_GRANT_SHAPE = `readRootGrant must be the card's readRootCandidate.dir sent back byte for byte — a non-empty string of at most ${READ_ROOT_CANDIDATE_DIR_MAX} characters`;
|
|
142
152
|
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`)";
|
|
@@ -183,23 +193,19 @@ export function parseToolApprovalResponse(body) {
|
|
|
183
193
|
}
|
|
184
194
|
note = rawNote;
|
|
185
195
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
case "
|
|
192
|
-
return { ok: false, error:
|
|
193
|
-
case "too_wide":
|
|
194
|
-
return { ok: false, error: SETTLED_BY_TOO_WIDE };
|
|
195
|
-
case "absent":
|
|
196
|
-
break;
|
|
196
|
+
if (body.settledBy !== undefined)
|
|
197
|
+
return { ok: false, error: SETTLED_BY_RETIRED };
|
|
198
|
+
const decidedByReading = readDecidedBy(body.decidedBy, false);
|
|
199
|
+
let decidedBy;
|
|
200
|
+
switch (decidedByReading.kind) {
|
|
201
|
+
case "outside_closed_set":
|
|
202
|
+
return { ok: false, error: DECIDED_BY_OUTSIDE_CLOSED_SET };
|
|
197
203
|
case "accepted":
|
|
198
|
-
|
|
204
|
+
decidedBy = decidedByReading.decidedBy;
|
|
199
205
|
break;
|
|
200
206
|
default: {
|
|
201
|
-
const unreachable =
|
|
202
|
-
throw new Error(`unhandled
|
|
207
|
+
const unreachable = decidedByReading;
|
|
208
|
+
throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
211
|
const rawGrant = body.readRootGrant;
|
|
@@ -214,7 +220,7 @@ export function parseToolApprovalResponse(body) {
|
|
|
214
220
|
...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}),
|
|
215
221
|
...(persistRule !== undefined ? { persistRule } : {}),
|
|
216
222
|
...(note !== undefined ? { note } : {}),
|
|
217
|
-
|
|
223
|
+
decidedBy,
|
|
218
224
|
...(readRootGrant !== undefined ? { readRootGrant } : {}),
|
|
219
225
|
};
|
|
220
226
|
}
|
|
@@ -310,9 +316,10 @@ function replayTerminalAskRow(row) {
|
|
|
310
316
|
return false;
|
|
311
317
|
}
|
|
312
318
|
export function outcomeOfDecidedRow(row) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
319
|
+
const allow = row.decision === "approve";
|
|
320
|
+
const source = row.settledBy !== null ? { settledBy: row.settledBy } : {};
|
|
321
|
+
const edit = allow && row.updatedInput !== undefined ? { updatedInput: row.updatedInput } : {};
|
|
322
|
+
return row.settledBy === null && !("updatedInput" in edit) ? allow : { allow, ...edit, ...source };
|
|
316
323
|
}
|
|
317
324
|
function settleArgsOfDecided(outcome) {
|
|
318
325
|
if (typeof outcome === "boolean")
|
|
@@ -751,13 +758,14 @@ export class ToolApprovalCoordinator {
|
|
|
751
758
|
return (this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(origin.owner, origin.sessionId, origin.taskId))).isMarked(req.toolCallId);
|
|
752
759
|
}
|
|
753
760
|
async standingGrantSettles(origin, req, governanceForced) {
|
|
754
|
-
|
|
761
|
+
const facts = askStandingAnswerFacts(req, governanceForced);
|
|
762
|
+
if (!standingAnswerMayRetire(facts))
|
|
755
763
|
return false;
|
|
756
764
|
const sessionId = origin.sessionId;
|
|
757
765
|
if (!sessionId)
|
|
758
766
|
return false;
|
|
759
|
-
const live = this.allowAllSessions.has(sessionAllowKey(origin.owner, sessionId, sessionAllowCategory(req.toolName)));
|
|
760
|
-
const durable = !live && (await this.durableGrantHas(sessionId, req.toolName));
|
|
767
|
+
const live = this.allowAllSessions.has(sessionAllowKey(origin.owner, sessionId, sessionAllowCategory(req.toolName), facts.grantBinding));
|
|
768
|
+
const durable = !live && !answerGrantsBeyondCall(req) && (await this.durableGrantHas(sessionId, req.toolName));
|
|
761
769
|
if (!live && !durable)
|
|
762
770
|
return false;
|
|
763
771
|
if (!(await this.sessionGrantUncontestedByRow(origin, req)))
|
|
@@ -1071,6 +1079,7 @@ export class ToolApprovalCoordinator {
|
|
|
1071
1079
|
let effectiveGovernanceForced = governanceForced;
|
|
1072
1080
|
let ruleLaneMaterial = this.buildRuleLaneMaterial(req, origin.owner, governanceForced, origin.sessionId);
|
|
1073
1081
|
const mandateWord = readClosedWordSeat(req, "mandate");
|
|
1082
|
+
const noRulingCause = readClosedWordSeat(req, "classifierNoRuling");
|
|
1074
1083
|
const originWord = typeof req.origin === "string" && req.origin.length > 0 ? req.origin : undefined;
|
|
1075
1084
|
const askClassPair = readAskClassPair(req);
|
|
1076
1085
|
const frame = {
|
|
@@ -1098,7 +1107,9 @@ export class ToolApprovalCoordinator {
|
|
|
1098
1107
|
...(governanceForced ? { governanceForced: true } : {}),
|
|
1099
1108
|
...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
1100
1109
|
...(req.mandated === true ? { mandated: true } : {}),
|
|
1101
|
-
...(standingAnswerMayRetire(
|
|
1110
|
+
...(standingAnswerMayRetire(askStandingAnswerFacts(req, governanceForced)) ? {} : { rememberRefused: true }),
|
|
1111
|
+
...(typeof req.boundGrantDigest === "string" ? { boundGrantDigest: req.boundGrantDigest, ...(req.boundGrants !== undefined ? { boundGrants: req.boundGrants } : {}) } : {}),
|
|
1112
|
+
...(noRulingCause !== undefined ? { classifierNoRuling: noRulingCause } : {}),
|
|
1102
1113
|
...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
|
|
1103
1114
|
...(originWord !== undefined ? { origin: originWord } : {}),
|
|
1104
1115
|
...(askClassPair ?? {}),
|
|
@@ -1306,11 +1317,12 @@ export class ToolApprovalCoordinator {
|
|
|
1306
1317
|
return unattended === "unavailable" ? unattended : shapeDeny(unattended);
|
|
1307
1318
|
}
|
|
1308
1319
|
if (settled.allowed) {
|
|
1309
|
-
if (settled.updatedInput === undefined && settled.readRootGrant === undefined)
|
|
1320
|
+
if (hostSettled === undefined && settled.updatedInput === undefined && settled.readRootGrant === undefined)
|
|
1310
1321
|
return true;
|
|
1311
1322
|
return {
|
|
1312
1323
|
allow: true,
|
|
1313
1324
|
...(settled.updatedInput !== undefined ? { updatedInput: settled.updatedInput } : {}),
|
|
1325
|
+
...(hostSettled !== undefined ? { settledBy: hostSettled } : {}),
|
|
1314
1326
|
...(settled.readRootGrant !== undefined ? { readRootGrant: settled.readRootGrant } : {}),
|
|
1315
1327
|
};
|
|
1316
1328
|
}
|
|
@@ -1623,11 +1635,14 @@ export class ToolApprovalCoordinator {
|
|
|
1623
1635
|
return shapeOutcome(await allowedP);
|
|
1624
1636
|
}
|
|
1625
1637
|
}
|
|
1638
|
+
const entryGrantBinding = grantBindingOf(req);
|
|
1639
|
+
const entryClasses = askClassPair?.classes;
|
|
1626
1640
|
const rememberRefused = !standingAnswerMayRetire(pendingEntryStandingAnswerFacts({
|
|
1627
1641
|
...(effectiveGovernanceForced ? { governanceForced: true } : {}),
|
|
1628
1642
|
...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
1629
1643
|
...(req.mandated === true ? { mandated: true } : {}),
|
|
1630
|
-
|
|
1644
|
+
grantBinding: entryGrantBinding,
|
|
1645
|
+
classes: entryClasses,
|
|
1631
1646
|
}));
|
|
1632
1647
|
if (rememberRefused)
|
|
1633
1648
|
frame.rememberRefused = true;
|
|
@@ -1648,6 +1663,8 @@ export class ToolApprovalCoordinator {
|
|
|
1648
1663
|
...(origin.sessionId ? { sessionId: origin.sessionId } : {}),
|
|
1649
1664
|
frame,
|
|
1650
1665
|
toolName: req.toolName,
|
|
1666
|
+
grantBinding: entryGrantBinding,
|
|
1667
|
+
...(entryClasses !== undefined ? { classes: entryClasses } : {}),
|
|
1651
1668
|
registeredAtMs: Date.now(),
|
|
1652
1669
|
expiresAtMs: persistedExpiresAtMs,
|
|
1653
1670
|
...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
|
|
@@ -1785,6 +1802,9 @@ export class ToolApprovalCoordinator {
|
|
|
1785
1802
|
const preRule = this.precheckEditedRuleBeforeDecision(entry, parsed);
|
|
1786
1803
|
if (preRule !== undefined)
|
|
1787
1804
|
return preRule;
|
|
1805
|
+
if (policyCannotAllow(parsed.decidedBy, parsed.value !== "deny", entry.requiresRealApproval === true)) {
|
|
1806
|
+
return { status: 400, body: { error: POLICY_CANNOT_ALLOW_PERSON_ASK, errorCode: "request.body_shape" } };
|
|
1807
|
+
}
|
|
1788
1808
|
const ruleEligibility = { eligible: true };
|
|
1789
1809
|
const settled = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
|
|
1790
1810
|
? this.respondWithCas(this.askStore, entry.askId, entry.batchId, id, entry, parsed, ruleEligibility)
|
|
@@ -1945,7 +1965,7 @@ export class ToolApprovalCoordinator {
|
|
|
1945
1965
|
gateBoundInputHash: row.gateBoundInputHash,
|
|
1946
1966
|
decision,
|
|
1947
1967
|
...(parsed.note !== undefined ? { note: parsed.note } : {}),
|
|
1948
|
-
|
|
1968
|
+
decidedBy: parsed.decidedBy,
|
|
1949
1969
|
...(parsed.updatedInput !== undefined && parsed.value !== "deny" ? { updatedInput: parsed.updatedInput } : {}),
|
|
1950
1970
|
...(principal !== undefined ? { principal } : {}),
|
|
1951
1971
|
...(httpReq !== undefined ? { httpReq } : {}),
|
|
@@ -2183,7 +2203,7 @@ export class ToolApprovalCoordinator {
|
|
|
2183
2203
|
decision: parsed.value === "deny" ? "deny" : "approve",
|
|
2184
2204
|
...(parsed.note !== undefined ? { decisionNote: parsed.note } : {}),
|
|
2185
2205
|
...(parsed.value !== "deny" && parsed.updatedInput !== undefined ? { updatedInput: parsed.updatedInput } : {}),
|
|
2186
|
-
...(
|
|
2206
|
+
...((sb) => (sb !== undefined ? { settledBy: sb } : {}))(liveSettledBy(parsed.decidedBy)),
|
|
2187
2207
|
}), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
2188
2208
|
if (late.ok)
|
|
2189
2209
|
this.notifyExternalDecision(askId, outcomeOfDecidedRow(late.row));
|
|
@@ -2244,10 +2264,10 @@ export class ToolApprovalCoordinator {
|
|
|
2244
2264
|
if (oldest !== undefined)
|
|
2245
2265
|
this.allowAllSessions.delete(oldest);
|
|
2246
2266
|
}
|
|
2247
|
-
this.allowAllSessions.add(sessionAllowKey(entry.owner, entry.sessionId, sessionAllowCategory(entry.toolName)));
|
|
2267
|
+
this.allowAllSessions.add(sessionAllowKey(entry.owner, entry.sessionId, sessionAllowCategory(entry.toolName), entry.grantBinding));
|
|
2248
2268
|
}
|
|
2249
2269
|
const allowed = parsed.value !== "deny";
|
|
2250
|
-
const settleArgs = opts?.settleOutcome !== undefined ? settleArgsOfDecided(opts.settleOutcome) : { allowed, updatedInput: parsed.updatedInput, settledBy:
|
|
2270
|
+
const settleArgs = opts?.settleOutcome !== undefined ? settleArgsOfDecided(opts.settleOutcome) : { allowed, updatedInput: parsed.updatedInput, settledBy: liveSettledBy(parsed.decidedBy) };
|
|
2251
2271
|
const readRootGrant = opts?.settleOutcome === undefined || opts.settleOutcomeIsThisRequest === true ? parsed.readRootGrant : undefined;
|
|
2252
2272
|
entry.settle(settleArgs.allowed, settleArgs.allowed ? "allowed" : "denied", settleArgs.updatedInput, settleArgs.settledBy, settleArgs.allowed ? undefined : parsed.note, readRootGrant);
|
|
2253
2273
|
const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
|