@sema-agent/server 7.108.0 → 7.109.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 +43 -1
- package/USAGE.md +18 -28
- package/dist/approval-ask-machine.d.ts +9 -25
- package/dist/approval-ask-machine.js +5 -2
- package/dist/approval-card.d.ts +32 -3
- package/dist/approval-card.js +29 -2
- package/dist/bench/s1/live-deps.d.ts +4 -5
- package/dist/bench/s1/live-deps.js +9 -10
- package/dist/boot/leader.js +0 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +3 -8
- package/dist/capabilities/scenarios.d.ts +3 -2
- package/dist/config-catalog.js +1 -3
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +2 -9
- package/dist/config.js +2 -5
- package/dist/http/admission.js +8 -2
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/server.d.ts +2 -2
- package/dist/http/wire-types.d.ts +8 -0
- package/dist/leader/diffup.d.ts +53 -0
- package/dist/leader/diffup.js +95 -1
- package/dist/leader/leader.d.ts +0 -6
- package/dist/leader/leader.js +1 -1
- package/dist/leader/repair-wire.js +1 -1
- package/dist/leader/wire.d.ts +3 -10
- package/dist/leader/wire.js +39 -13
- package/dist/observability/fail-open.d.ts +1 -5
- package/dist/observability/fail-open.js +1 -5
- package/dist/plugins/approval-ask-store-file.d.ts +2 -1
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +7 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +18 -0
- package/dist/plugins/approval-ask-store-sql.js +7 -0
- package/dist/request-key-closure.js +1 -1
- package/dist/run-local.js +2 -3
- package/dist/runtime-governance.d.ts +3 -3
- package/dist/tool-approval.d.ts +37 -15
- package/dist/tool-approval.js +8 -7
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/project.js +2 -3
- package/dist/unattended-permission-mode.d.ts +54 -93
- package/dist/unattended-permission-mode.js +8 -61
- package/package.json +3 -3
|
@@ -253,6 +253,9 @@ export class FileApprovalAskStore {
|
|
|
253
253
|
listPendingBySession(sessionId, owner, signal) {
|
|
254
254
|
return this.core.listPendingBySession(sessionId, owner, signal);
|
|
255
255
|
}
|
|
256
|
+
listByCall(call) {
|
|
257
|
+
return this.core.listByCall(call);
|
|
258
|
+
}
|
|
256
259
|
getAsk(askId) {
|
|
257
260
|
return this.core.getAsk(askId);
|
|
258
261
|
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* - resolveProvisional 故意不经 canAskTransition(同 SQL twin 头注:这是版本化补偿的例外通道)。
|
|
26
26
|
*/
|
|
27
27
|
import { type AskState, type BatchState } from "../approval-ask-machine.js";
|
|
28
|
-
import type { AskDecision, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
|
|
28
|
+
import type { AskCallCoordinate, AskDecision, AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
|
|
29
29
|
/**
|
|
30
30
|
* [ref] 的 `updatedInput` **入店编码**——两 twin 共用的那一只(本函数是唯一真源)。
|
|
31
31
|
*
|
|
@@ -98,6 +98,7 @@ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
|
|
|
98
98
|
* 已 abort 就一条都不读、如实抛,与 SQL twin 的第①条语义逐字一致(第②条在这里天然平凡成立)。 */
|
|
99
99
|
listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
|
|
100
100
|
listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
|
|
101
|
+
listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
|
|
101
102
|
getAsk(askId: string): Promise<AskRow | null>;
|
|
102
103
|
getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
|
|
103
104
|
resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canAskTransition } from "../approval-ask-machine.js";
|
|
2
|
-
import { assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
|
|
2
|
+
import { askRowOnCall, assertDecideSettledBy, assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
|
|
3
3
|
function applyPatch(row, patch) {
|
|
4
4
|
if (patch.decision !== undefined)
|
|
5
5
|
row.decision = patch.decision;
|
|
@@ -304,6 +304,12 @@ export class InMemoryApprovalAskStore {
|
|
|
304
304
|
.sort((a, b) => a.createdAtMs - b.createdAtMs)
|
|
305
305
|
.map((r) => ({ ...r }));
|
|
306
306
|
}
|
|
307
|
+
async listByCall(call) {
|
|
308
|
+
return [...this.asks.values()]
|
|
309
|
+
.filter((r) => askRowOnCall(r, call))
|
|
310
|
+
.sort((a, b) => a.createdAtMs - b.createdAtMs || (a.askId < b.askId ? -1 : a.askId > b.askId ? 1 : 0))
|
|
311
|
+
.map((r) => ({ ...r }));
|
|
312
|
+
}
|
|
307
313
|
async getAsk(askId) {
|
|
308
314
|
const r = this.asks.get(askId);
|
|
309
315
|
return r ? { ...r } : null;
|
|
@@ -410,6 +410,10 @@ export interface BatchRow {
|
|
|
410
410
|
updatedAtMs: number;
|
|
411
411
|
}
|
|
412
412
|
/** design 定稿 §4 的持久层接口。协调器接线(车2)、恢复扫描消费(车5)不在本车范围——本车只落这些方法。 */
|
|
413
|
+
/** 一次调用在一条腿上的坐标({@link ApprovalAskStore.listByCall} 的四列;型从 {@link AskRow} 派生,不手抄)。 */
|
|
414
|
+
export type AskCallCoordinate = Pick<AskRow, "taskId" | "legKey" | "toolCallId" | "sourceTaskId">;
|
|
415
|
+
/** 「这一行在这次调用上」的唯一判据(四列逐字相等):InMemory twin 的筛子与 SQL twin 的出口对齐读同一只。 */
|
|
416
|
+
export declare function askRowOnCall(row: AskRow, call: AskCallCoordinate): boolean;
|
|
413
417
|
export interface ApprovalAskStore {
|
|
414
418
|
/** 幂等 upsert。返回**行 + 本次是否真插入**(判别位语义见 {@link EnsureAskResult})。 */
|
|
415
419
|
ensureAsk(row: NewAskRow): Promise<EnsureAskResult>;
|
|
@@ -464,6 +468,19 @@ export interface ApprovalAskStore {
|
|
|
464
468
|
* 等待」只被**部分**缓解(不再叠加调用方侧的等待,池侧仍会堆)。真正的取消要 driver 级 seam,登记为
|
|
465
469
|
* 后续件。 */
|
|
466
470
|
listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
|
|
471
|
+
/**
|
|
472
|
+
* 🔴 S-944 / SN-C1:**这次调用在这条腿上的全部行** —— 任意状态、任意 ask 身份(同一调用不同类集 = 不同 askId 的几只问、升级前按旧派生
|
|
473
|
+
* 写的行,都在其中)。坐标 = {@link AskCallCoordinate} 四列**精确**匹配;序 = `created_at_ms ASC, ask_id ASC`(确定性)。
|
|
474
|
+
*
|
|
475
|
+
* 唯一消费者 = 会话授权短路的争议判据(`tool-approval.ts` `sessionGrantUncontestedByRow`):「这次调用在腿内有行 ⇒ 一揽子放行不得代答」。
|
|
476
|
+
* 为什么按调用而不是按 askId 点读:争议的真源是**调用**(人对这次调用说过什么 / 这次调用还有一张卡挂着),ask 身份只是其中一问的主键;
|
|
477
|
+
* 身份可以加轴(S-944 加了类集),争议判据不得依赖身份 —— 按身份点读在身份加轴的那一刻就把同调用的别的问、旧派生的行读成「不存在」。
|
|
478
|
+
*
|
|
479
|
+
* 三 twin 同答:SQL twin 读后按四列逐字再滤一遍 —— MySQL 协议的 VARCHAR 等值带 PAD SPACE(`'tc'` = `'tc '`),PG(`COLLATE "C"`)与
|
|
480
|
+
* InMemory 严格比较;不在入口拒(调用 id 是引擎铸的原值,首尾空白不归本店判),在出口对齐。索引:走 `idx_ask_task_state`
|
|
481
|
+
* `(task_id, state)` 的 `task_id` 前缀(一条 run 的行数 = 真出过卡的问数;grant 代答的问不落行),不另加索引。
|
|
482
|
+
*/
|
|
483
|
+
listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
|
|
467
484
|
getAsk(askId: string): Promise<AskRow | null>;
|
|
468
485
|
/** 回决幂等回放的读口。🔴 `taskId` **必填**且是键的第一维——UNIQUE 已改 `(task_id, idempotency_key)`
|
|
469
486
|
* (车4 §12-E),裸按 key 全局查在多租户库上既可能回**别人 task** 的行(存在性外泄),也可能在同 key
|
|
@@ -621,6 +638,7 @@ export declare class SqlApprovalAskStore implements ApprovalAskStore {
|
|
|
621
638
|
listByState(state: AskState, limit: number): Promise<AskRow[]>;
|
|
622
639
|
listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
|
|
623
640
|
listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
|
|
641
|
+
listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
|
|
624
642
|
getAsk(askId: string): Promise<AskRow | null>;
|
|
625
643
|
getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
|
|
626
644
|
deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
|
|
@@ -81,6 +81,9 @@ export function projectAskTerminalClaimTruth(row) {
|
|
|
81
81
|
settledBy: row.settledBy,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
+
export function askRowOnCall(row, call) {
|
|
85
|
+
return row.taskId === call.taskId && row.legKey === call.legKey && row.toolCallId === call.toolCallId && row.sourceTaskId === call.sourceTaskId;
|
|
86
|
+
}
|
|
84
87
|
export const APPROVAL_ASK_TABLE = "approval_ask";
|
|
85
88
|
export const APPROVAL_BATCH_TABLE = "approval_batch";
|
|
86
89
|
export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
@@ -675,6 +678,10 @@ export class SqlApprovalAskStore {
|
|
|
675
678
|
: this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner = ? ORDER BY created_at_ms ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner = $2 ORDER BY created_at_ms ASC`), owner === null ? [sessionId] : [sessionId, owner]), signal, "listPendingBySession");
|
|
676
679
|
return rows.map(mapAskRow);
|
|
677
680
|
}
|
|
681
|
+
async listByCall(call) {
|
|
682
|
+
const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceTaskId]);
|
|
683
|
+
return rows.map(mapAskRow).filter((r) => askRowOnCall(r, call));
|
|
684
|
+
}
|
|
678
685
|
async getAsk(askId) {
|
|
679
686
|
const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [
|
|
680
687
|
askId,
|
|
@@ -20,7 +20,7 @@ export function keyClosureClause(names, label) {
|
|
|
20
20
|
const TASK_BODY_DECLARED_KEYS = [
|
|
21
21
|
"a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents",
|
|
22
22
|
"appendSystemPrompt", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
|
|
23
|
-
"compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "enableFork",
|
|
23
|
+
"compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "disableSkills", "enableFork",
|
|
24
24
|
"excludeAllTools", "excludeTools", "fileHistory",
|
|
25
25
|
"finalVerification", "forwardSubagentEvents", "images", "inheritEnv", "interactiveTools", "jobId", "limits", "maxCostUsd",
|
|
26
26
|
"maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
|
package/dist/run-local.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
|
|
11
11
|
import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
|
|
12
|
-
import {
|
|
12
|
+
import { withUnattendedLeg } from "./unattended-permission-mode.js";
|
|
13
13
|
import { createBrain } from "./brain.js";
|
|
14
14
|
import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
|
|
15
15
|
import { applyRuntimeGovernance } from "./runtime-governance.js";
|
|
@@ -568,7 +568,6 @@ export async function runLocal(argv, deps = {}) {
|
|
|
568
568
|
...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
|
|
569
569
|
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
570
570
|
};
|
|
571
|
-
const unattendedLeg = createUnattendedLeg(unattendedPermissionMode(config), runnerDeps);
|
|
572
571
|
const spec = withUnattendedLeg(applyLocalGovernance({
|
|
573
572
|
objective: mention.cleanedObjective,
|
|
574
573
|
sessionId,
|
|
@@ -585,7 +584,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
585
584
|
...(mcp ? { mcp } : {}),
|
|
586
585
|
...(a2a ? { a2a } : {}),
|
|
587
586
|
...(Object.keys(limits).length > 0 ? { limits } : {}),
|
|
588
|
-
}, config, workspaceDir, memoryEngine?.root),
|
|
587
|
+
}, config, workspaceDir, memoryEngine?.root), onAsk);
|
|
589
588
|
logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: executionLaneOf(config.remoteExec?.provider) });
|
|
590
589
|
const settleSessionBackground = async () => {
|
|
591
590
|
const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, scope);
|
|
@@ -188,9 +188,9 @@ export interface GovernancePosture {
|
|
|
188
188
|
* (checkpoint 行没有这一列,core 的 `CheckpointGate`/`PendingAction`/`Checkpoint` 顶层都不带它)。反向
|
|
189
189
|
* (checkpoint → ask)今天**没有读口**:两条 list 读口都硬过滤 `state='STREAM_PENDING'`,而 park 完的行是
|
|
190
190
|
* `PARKED`;唯一精确的连接键是 ask 行上的 `gate_token`,要按它反查得给店加一个新读口(SQL 面 = 双库集成
|
|
191
|
-
* 门)。派生 `deriveAskId(...)` 也不行:它吃 `legKey`(= 上一腿 resume token 的摘要)
|
|
192
|
-
*
|
|
193
|
-
* 比缺席坏得多(`http/active-run-conflict.ts` 的 `resumeEntryForGate` 的 default 臂是同一条纪律)。故诚实缺席。
|
|
191
|
+
* 门)。派生 `deriveAskId(...)` 也不行:它吃 `legKey`(= 上一腿 resume token 的摘要)、`parentToolCallId` 与这一问的
|
|
192
|
+
* 类集(S-944);前两维从 checkpoint 行推不出来,类集在 park 行上老行缺席、续跑不补 —— 二腿/子代/老行形上会算出一个
|
|
193
|
+
* **错**的 id —— 在「等的谁」这条通道上,错 id 比缺席坏得多(`http/active-run-conflict.ts` 的 `resumeEntryForGate` 的 default 臂是同一条纪律)。故诚实缺席。
|
|
194
194
|
*/
|
|
195
195
|
export declare function governanceOriginOf(gate: {
|
|
196
196
|
riskDescriptor?: {
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HostDecidedBy, HostSettledBy } from "./host-decision.js";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
-
import { 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 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
5
|
import { 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";
|
|
@@ -285,6 +285,19 @@ export interface ToolApprovalFrame {
|
|
|
285
285
|
* ask」,治理策略产的与普通策略产的在这一个词上同形 ⇒ 顶替不了(逐字论证见 `governance-ask-marks.ts` 顶注)。
|
|
286
286
|
*/
|
|
287
287
|
origin?: string;
|
|
288
|
+
/**
|
|
289
|
+
* **S-934 ②(core 7.37.0 #1214;ADDITIVE,`"tool_approval"` only)** —— 这一问的**类**:`classes` 的首员(core `ASK_CLASSES` 序),
|
|
290
|
+
* `MODE_ARBITRATION` 的行键(权限模式读的就是它 / 它们)。引擎在每一个铸 `origin` 的站盖章(策略自报的值在盖章处被替换),本仓经
|
|
291
|
+
* `approval-card.ts` 的 `readAskClassPair(req)` 结构窄读(成员筛 = core `isAskClass`,本仓不抄词表;在场却读不出 ⇒ 两键都不投 + 计一次
|
|
292
|
+
* S-249 集外 tag)。**与 {@link classes} 同在同缺**;缺席 = 老引擎 / 不盖类的站(共享制品读确认,core KL-1214-a),不是「无类」。
|
|
293
|
+
* display:壳按类渲卡(cli 卡面、client-core CC-272),durable 审批席按类答;**本仓不按它裁决**(无人值守腿的按类作答读 core 交来的
|
|
294
|
+
* `AskRequest.classes` 本身)。同一对的其余面:`card_json` 卡面 / `approval_request` 呈卡帧 / 断连重放帧(同一份素材)、运维队列行
|
|
295
|
+
* (`parkRowFacts`,读停车行孪生座 `PendingAction.tool_approval.askClass`);inbox 行不带(core `CheckpointSummary` 不回显)。
|
|
296
|
+
*/
|
|
297
|
+
askClass?: AskClass;
|
|
298
|
+
/** S-934 ②:这一问属于的**全部**类(core `ASK_CLASSES` 序,`classes[0] === askClass`,非空)—— 读整只集合,别只读 {@link askClass}(一问可同属
|
|
299
|
+
* 几类)。与 `askClass` 同在同缺、同一只窄读;逐字同序过境,不排序不去重。 */
|
|
300
|
+
classes?: AskClass[];
|
|
288
301
|
/**
|
|
289
302
|
* **[ref]**(core 7.19.0;**ADDITIVE**,`"tool_approval"` only)—— 这只**根外读** ask 的**清障目录**:
|
|
290
303
|
* 把 `dir` 加进本会话的读目录(壳侧既有的 `/add-dir` 口),同一条调用就不会再问。
|
|
@@ -1440,23 +1453,31 @@ export declare class ToolApprovalCoordinator {
|
|
|
1440
1453
|
sessionId?: string;
|
|
1441
1454
|
}) => ((req: AskRequest) => Promise<AskOutcome>);
|
|
1442
1455
|
/**
|
|
1443
|
-
* 🔴 session grant 短路前的**持久出处复核**([ref] 的 codex R1-[high] 补丁;[ref]
|
|
1456
|
+
* 🔴 session grant 短路前的**持久出处复核**([ref] 的 codex R1-[high] 补丁;[ref] 换判据;S-944 / SN-C1 换读口)。
|
|
1457
|
+
*
|
|
1458
|
+
* 回 `true` = 「这次调用在这条腿上从来没有过持久出处」⇒ 一揽子放行可以短路;
|
|
1459
|
+
* 回 `false` = 这次调用在腿内有行(任何状态、任何 ask 身份),**或**读不出来(不知道 = 不放宽)⇒ 调用方必须往下走真出卡。
|
|
1460
|
+
*
|
|
1461
|
+
* 为什么只读不写:短路的全部价值就是「不铸卡、不落行、不占 pending」,做一次按调用坐标的读保住这个价值,
|
|
1462
|
+
* 而 `ensureAsk` 会写行。为什么一行都没有就放行:这次调用在这条腿上的行只可能由**这次调用的某一问**铸出来,
|
|
1463
|
+
* 一行都没有 ⇒ 从来没有过一份能反驳本地判据的持久出处(这也是常见形:grant 命中的调用多半是头一次到)。
|
|
1444
1464
|
*
|
|
1445
|
-
*
|
|
1446
|
-
* 回 `false` = 行在(任何状态),**或**读不出来(不知道 = 不放宽)⇒ 调用方必须往下走真出卡。
|
|
1465
|
+
* ## 争议按**调用**判,不按 ask 身份判(S-944 / SN-C1)
|
|
1447
1466
|
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1467
|
+
* 争议的真源是调用:人对这次调用说过什么、这次调用还有没有一张卡挂着。ask 身份(`deriveAskId`)只是其中**一问**的主键,而身份会加轴 ——
|
|
1468
|
+
* S-944 把这一问的类集并进身份后,按身份点读会把两类行读成「不存在」:① 同一调用另一类集的那一问(core #1218 的委派第二张卡与首卡);
|
|
1469
|
+
* ② 升级前按旧派生写的行(同一 resume token 重投 = 同一 `legKey`,跨版本重入同一调用)。两形都让一揽子放行代答、人拒 / 挂着的卡被绕过
|
|
1470
|
+
* (codex 车内 r1 [high] + [ref] gpt-6 腿 A 同指)。⇒ 读口 = 店的 `listByCall`(四列 = 这次调用在这条腿上的坐标,精确匹配),本函数不派生 askId。
|
|
1471
|
+
* **身份可以加轴,争议判据不得依赖身份。**
|
|
1451
1472
|
*
|
|
1452
1473
|
* ## 判据的单一原则:**短路不得产生与「没有 grant 时那条路」不同的终局**
|
|
1453
1474
|
*
|
|
1454
|
-
* 没有 grant
|
|
1455
|
-
* 走 `replayTerminalAskRow`(deny 回放 `false`、park 回放 `"unavailable"`)
|
|
1456
|
-
*
|
|
1457
|
-
* ——
|
|
1475
|
+
* 没有 grant 时,这次调用在腿内的任何一行都让下面 `askBroadcast` 有确定去处:同身份的终局行(DECIDED/PARKED/DENIED/VOID)
|
|
1476
|
+
* 走 `replayTerminalAskRow`(deny 回放 `false`、park 回放 `"unavailable"`);同身份的活 STREAM_PENDING 行走 `ensureAsk` 的幂等命中
|
|
1477
|
+
* (采信行上的卡与 deadline,等真决议);别的身份的行(另一类集 / 旧派生)⇒ 本问的身份无行 ⇒ 出卡再问(fail-closed)。短路一旦 return,
|
|
1478
|
+
* 这些腿结构上都够不着 —— 所以只要这次调用在腿内有行,短路就是在用一个**更宽**的答案顶替那条路。⇒ 判据 = `rows.length === 0`。
|
|
1458
1479
|
*
|
|
1459
|
-
*
|
|
1480
|
+
* 这条原则是分几次到位的,每次漏的都是「行 = 真源」的一部分:
|
|
1460
1481
|
* · **[ref]**(codex R1-[high]):首版只查**本地** peek,行上标治理的那一半漏了 —— 本地标表是
|
|
1461
1482
|
* 进程内表,failover / 重启后幂等命中既有行时它恒空。
|
|
1462
1483
|
* · **[ref]**(2026-08-19 三轴组复审,refuter 真运行复现三形):补丁把行读到手里,却只看
|
|
@@ -1466,12 +1487,13 @@ export declare class ToolApprovalCoordinator {
|
|
|
1466
1487
|
* · **codex 交叉复审**([ref] 同批,验真后采纳):只补终态判据仍漏「卡还在别处挂着」——
|
|
1467
1488
|
* STREAM_PENDING 行意味着已有一张卡在等人,凭 grant 抢跑会让行上的终局与真实执行长期矛盾
|
|
1468
1489
|
* (那张卡随后可被人/另一副本决成相反答案,或 TTL 到点被收敛成幻影 gate)。
|
|
1469
|
-
*
|
|
1490
|
+
* · **S-944 / SN-C1**(见上节):读口按身份点读,身份加轴后漏掉同调用别的问与旧派生的行。
|
|
1491
|
+
* 几次都不是各自独立的 bug,是同一句话没说全,故最终判据收成一条而不是几个分支。
|
|
1470
1492
|
*
|
|
1471
|
-
* 零额外 IO(
|
|
1493
|
+
* 零额外 IO(一次读),且 fail-closed **by construction**:将来 `AskState` 加词、ask 身份再加轴,都不需要动这里。
|
|
1472
1494
|
*
|
|
1473
1495
|
* 🔴 **已登记的残余([ref],PARTIAL,本批不修)**:这一步是一次**非原子的 check-then-act** ——
|
|
1474
|
-
*
|
|
1496
|
+
* 另一副本正在为同一调用落行、而本次按调用坐标的读(`listByCall`)抢在其提交前完成时,本副本仍会短路。真解 = 把出处
|
|
1475
1497
|
* 判定与 grant 消费合并成**同一次**原子存储操作(带条件 upsert / CAS),属店语义级改动,须本模块属主
|
|
1476
1498
|
* 立件(与上面 R4-F1/[ref] 两条同源残余同一处置)。「行不在 ⇒ 不短路」不是修法:grant 命中的 ask
|
|
1477
1499
|
* 多半头一次到,那等于把一揽子放行整只废掉。窗的上界由本判据定死:行**一提交**窗即关。
|
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, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readClosedWordSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
|
|
11
|
+
import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readReadRootCandidate, readRuleEvidence, 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";
|
|
@@ -776,14 +776,13 @@ export class ToolApprovalCoordinator {
|
|
|
776
776
|
async sessionGrantUncontestedByRow(origin, req) {
|
|
777
777
|
if (!this.askStore)
|
|
778
778
|
return true;
|
|
779
|
-
const
|
|
780
|
-
const askId = deriveAskId(sourceTaskId, origin.taskId, req.toolCallId, origin.legKey, req.delegation?.parentToolCallId);
|
|
779
|
+
const call = { taskId: origin.taskId, legKey: origin.legKey, toolCallId: req.toolCallId, sourceTaskId: req.sourceTaskId ?? origin.taskId };
|
|
781
780
|
try {
|
|
782
|
-
const
|
|
783
|
-
return
|
|
781
|
+
const rows = await this.withStoreDeadline(this.askStore.listByCall(call), "listByCall(session-grant-provenance)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
782
|
+
return rows.length === 0;
|
|
784
783
|
}
|
|
785
784
|
catch (err) {
|
|
786
|
-
this.noteStoreError(err, "
|
|
785
|
+
this.noteStoreError(err, "listByCall(session-grant-provenance)");
|
|
787
786
|
return false;
|
|
788
787
|
}
|
|
789
788
|
}
|
|
@@ -1060,6 +1059,7 @@ export class ToolApprovalCoordinator {
|
|
|
1060
1059
|
let ruleLaneMaterial = this.buildRuleLaneMaterial(req, origin.owner, governanceForced, origin.sessionId);
|
|
1061
1060
|
const mandateWord = readClosedWordSeat(req, "mandate");
|
|
1062
1061
|
const originWord = typeof req.origin === "string" && req.origin.length > 0 ? req.origin : undefined;
|
|
1062
|
+
const askClassPair = readAskClassPair(req);
|
|
1063
1063
|
const frame = {
|
|
1064
1064
|
type: "tool_approval",
|
|
1065
1065
|
approvalId: id,
|
|
@@ -1087,6 +1087,7 @@ export class ToolApprovalCoordinator {
|
|
|
1087
1087
|
...(req.mandated === true ? { mandated: true } : {}),
|
|
1088
1088
|
...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
|
|
1089
1089
|
...(originWord !== undefined ? { origin: originWord } : {}),
|
|
1090
|
+
...(askClassPair ?? {}),
|
|
1090
1091
|
...(typeof req.persistedRuleShadowed === "string" && req.persistedRuleShadowed !== ""
|
|
1091
1092
|
? { persistedRuleShadowed: redactSecrets(req.persistedRuleShadowed).slice(0, MAX_RULE_TEXT_CHARS) }
|
|
1092
1093
|
: {}),
|
|
@@ -1138,7 +1139,7 @@ export class ToolApprovalCoordinator {
|
|
|
1138
1139
|
const sourceTaskId = req.sourceTaskId ?? origin.taskId;
|
|
1139
1140
|
const runId = origin.taskId;
|
|
1140
1141
|
const legKey = origin.legKey;
|
|
1141
|
-
askId = deriveAskId(sourceTaskId, runId,
|
|
1142
|
+
askId = deriveAskId(sourceTaskId, runId, legKey, req);
|
|
1142
1143
|
batchId = this.batchIdFor(askId, sourceTaskId, runId, legKey);
|
|
1143
1144
|
try {
|
|
1144
1145
|
const existingOrCreated = await this.withStoreDeadline(this.askStore.ensureAsk({
|
|
@@ -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" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "readRootCandidate" | "mandated" | "mandate";
|
|
46
|
+
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "readRootCandidate" | "mandated" | "mandate" | "askClass" | "classes";
|
|
47
47
|
type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd" | "authoredBy";
|
|
48
48
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
49
49
|
type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
|
package/dist/trace/project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { admitProviderEcho, redactErrorMessage, redactModelText } from "../observability/run-terminal.js";
|
|
2
|
-
import { isAskClass, isAskOrigin, isPermissionMode, isPersistedRuleMandate, MCP_DELIVERY_VERDICTS, MCP_LIVENESS_STATES, screenGateOutcome, TEAM_ROLES, ToolRoster, ToolRosterDelta, WRITE_PROTECTION_TARGET_VIEWS } from "@sema-agent/core";
|
|
2
|
+
import { isAskClass, isAskOrigin, isModeReleaseReason, isPermissionMode, isPersistedRuleMandate, MCP_DELIVERY_VERDICTS, MCP_LIVENESS_STATES, screenGateOutcome, TEAM_ROLES, ToolRoster, ToolRosterDelta, WRITE_PROTECTION_TARGET_VIEWS } from "@sema-agent/core";
|
|
3
3
|
import { Value } from "typebox/value";
|
|
4
4
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
5
5
|
import { redactSecrets, redactDeep } from "./redact.js";
|
|
@@ -126,11 +126,10 @@ function modeLedgerDefect(d) {
|
|
|
126
126
|
return "disposition.released.origin is outside the closed set";
|
|
127
127
|
if (o.mandate !== undefined && !isPersistedRuleMandate(o.mandate))
|
|
128
128
|
return "disposition.released.mandate is outside the closed set";
|
|
129
|
-
if (o.reason !== undefined && !(
|
|
129
|
+
if (o.reason !== undefined && !isModeReleaseReason(o.reason))
|
|
130
130
|
return "disposition.released.reason is outside the closed set";
|
|
131
131
|
return false;
|
|
132
132
|
}
|
|
133
|
-
const RELEASE_REASON_WORDS = { rule_unreadable: true };
|
|
134
133
|
function screenedGateOrThrow(v) {
|
|
135
134
|
if (v === null || typeof v !== "object") {
|
|
136
135
|
if (v !== undefined)
|
|
@@ -1,110 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* S-
|
|
2
|
+
* S-934 ①(S-906 / clay 裁 [ref];core 7.37.0 #1214,`docs/INTEGRATION-CORE.md` §4.6e)—— **无人值守腿:`default` 模式 + 按类作答的 onAsk**。
|
|
3
3
|
*
|
|
4
4
|
* ## 规则(一句)
|
|
5
|
-
* 本仓**自己铸** spec
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* 本仓**自己铸** spec、**没有调用方**能在请求上表态模式的腿 —— leader worker(`src/leader/wire.ts` provisionWorker 两车道)、leader 的
|
|
6
|
+
* 修复腿(集成沙箱修复 `mkRepair`、模块门修复 `leader.ts` `runModuleGate`、单代理修复环 `repair-wire.ts` `attachRepairLoopDeps`)、run-local
|
|
7
|
+
* (`src/run-local.ts`,一次性 CLI)与这些腿的 durable 续跑重挂(`CheckpointCtx.unattendedLeg`)—— 经 {@link withUnattendedLeg} 这**一只**
|
|
8
|
+
* 合并口铸两键:`permissionMode: "default"`(显式:续跑时盖过 checkpoint 记下的旧模式)与 `onAsk` = {@link createUnattendedOnAsk}。
|
|
9
|
+
* 那只 onAsk 读 `AskRequest.classes` **整体**:集非空且每一类都在 {@link UNATTENDED_ANSWERED_CLASSES}(= `{ordinary}`,常量,不是旋钮)⇒ 答 yes;
|
|
10
|
+
* 否则不答 —— 本腿**有席**(run-local 的 `createLocalApprover`:有 TTY 问人、无 TTY 当场拒;续跑腿的协调器席)⇒ 交给席;**无席** ⇒ 答
|
|
11
|
+
* `"unavailable"` = 交回 core 的缺席路(本腿武装了 durable park ⇒ park,等人经 `/decide` 答;否则 headless 拒 —— fail-closed)。
|
|
9
12
|
*
|
|
10
|
-
* ##
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* ## 为什么是这一形(core §4.6e 推荐形逐条)
|
|
14
|
+
* · **读整体,不读 `askClass`**:一问可以同属几类(探针问与写保护并存、组织裁决与 hook 并存),`askClass` 只是表序第一个;集里任一类
|
|
15
|
+
* 不答 ⇒ 整问不答。
|
|
16
|
+
* · **缺席 = 不答的类**:没盖 `origin` 的站(共享制品读确认,KL-1214-a)、部署自铸的请求、7.37.0 之前 park 的行续跑(KL-1214-b,续跑不补)
|
|
17
|
+
* 都不带类 ⇒ 不答。
|
|
18
|
+
* · **答集只 `ordinary`**:七个保护类(`probe` / `write_protection` / `rule_unreadable` / `tool_marks_mcp` / `tool_marks_a2a` /
|
|
19
|
+
* `transcript_write` / `artifact_shared_read`)与 `mode`(plan 的写问)留给人;`RELEASABLE_ASK_CLASSES` 之外的类(hook / ask_rule /
|
|
20
|
+
* org_rule / operator_forced / …,权威为人提的问)恒不答。function approver 的 yes 是「这一次调用的答」—— 它连 `requiresRealApproval`
|
|
21
|
+
* 都清得掉(引擎分不出函数与人),所以带那一位的类(`transcript_write` / `rule_unreadable` / `artifact_shared_read` / `policy_rra`)不进答集
|
|
22
|
+
* (钉:test/unattended-permission-mode.test.ts「答集 ⊆ RELEASABLE_ASK_CLASSES,且与 RRA_EXPLAINERS ∪ {policy_rra} 不交」)。
|
|
23
|
+
* · **`ordinary` 覆盖什么**(core 原句):`Read` 工具的根外读、shell 的根外**写**(`echo x > <outside>` —— 写守卫只判保护表)、模式写门对每一次
|
|
24
|
+
* covered write 的问、形状层对非只读 shell 的问;shell 的根外**读**(`cat <outside>`)是 `probe`,不答。
|
|
17
25
|
*
|
|
18
26
|
* ## 三问
|
|
19
|
-
* · **谁需要**:用 leader / run-local 的部署
|
|
20
|
-
* ·
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* 候裁 18 → D:缺省 bypass 会释放写保护 ⇒ 本腿把写保护从问改成拒({@link createUnattendedWriteProtectionPolicy});另六类释放 = 相对 7.105.0 的宽化,
|
|
25
|
-
* 如实披露(见那只函数的「射程外」)。**过渡形**:core 7.37.0(S-906)后退成 `default` + 按类作答的 onAsk,旋钮硬删(拒启点名)。
|
|
27
|
+
* · **谁需要**:用 leader / run-local 的部署 —— 普通写与非只读命令照跑(7.105.0 行为;7.106.0–7.108.0 过渡形靠 `bypassPermissions` 宽放给出的同一结果)。
|
|
28
|
+
* · **谁受伤**:① 设了 `UNATTENDED_PERMISSION_MODE` 的部署 —— 起服拒启(`config.ts` 墓碑表,`removed` 族);② 靠过渡形缺省 bypass 放行
|
|
29
|
+
* `probe` / MCP·A2A 标记工具 / 转录写 / 规则店读不出 / 共享制品读这六类的无人值守流程 —— 改成不答(有席交席、无席 park 或拒)。安全轴收紧。
|
|
30
|
+
* · **什么补偿**:有人的腿照问人(run-local 在终端里跑;leader 配 durable 审批块 ⇒ park 到 `/decide`);答集是 core 推荐形的常量 ——
|
|
31
|
+
* 要放更多类的部署先立案候裁,不开旋钮。
|
|
26
32
|
*
|
|
27
|
-
* ##
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* **逐字**同形,运维把旋钮设成任何词也不会顺手拿到根外写面(那是 HTTP 腿上 `TASK_WRITE_FACE_ESCALATION` 管的另一件事,
|
|
32
|
-
* S-591;这几条腿不是请求,不经那道门)。⚠️ 这一格是按裁定的**意图**(「= 7.105.0 行为」)施工、比裁定字面多出的一半,
|
|
33
|
-
* 收车档 ⑩ 单列,主会话可翻。
|
|
33
|
+
* ## 退役(硬破坏,零别名 / 零双读 / 零过渡臂)
|
|
34
|
+
* `UNATTENDED_PERMISSION_MODE` 旋钮(`config-catalog` 行 / `config` 解析 / `ServiceConfig.unattendedPermissionMode`)、`createUnattendedWriteProtectionPolicy`
|
|
35
|
+
* (#1211 的 `pathDenyRules` 过渡形)、`createUnattendedLeg` 的模式 / Runner 座参数、写面钉 `writeFace: "roots"`(那一钉只为不拿 bypass 的开放写面;
|
|
36
|
+
* `default` 模式行不给写面 ⇒ 引擎缺省 roots,与 7.105.0 同)、P-DEBT 臂 `server.unattended-leg.mode-releases-protective-asks`(FAIL-OPEN-CENSUS 第 101 行终态)。
|
|
34
37
|
*
|
|
35
38
|
* ## 射程外(成文)
|
|
36
|
-
* · HTTP 腿(`POST /v1/tasks|runs`)
|
|
37
|
-
*
|
|
38
|
-
* ·
|
|
39
|
-
*
|
|
40
|
-
* —— 组织级拒优先。本仓今天不写这一位([ref] 未闭环),故这条今天只在 core 侧可达。
|
|
39
|
+
* · HTTP 腿(`POST /v1/tasks|runs`):模式在请求上,有调用方能表态,不经本合并口。
|
|
40
|
+
* · leader 的 planner 路由(无手 Runner,没有写 / shell 可问)不经本合并口。
|
|
41
|
+
* · 续跑腿上的协调器席:`http/resume-legs.ts` 只在「run 行在场 ∧ 活卡协调器在场」时给 spec 另装活席(`deps.toolApproval && taskId`)——
|
|
42
|
+
* leader worker 的会话没有 run 行,那一装今天够不着本腿;哪天够着了,活席会盖掉本层(方向 = 多问人,fail-closed),见收车档 ⑤。
|
|
41
43
|
*/
|
|
42
|
-
import {
|
|
43
|
-
/** 旋钮的出厂值(主会话裁定候裁 15 B / 18 D)。写面由 {@link buildUnattendedLegSpec} 钉 roots;写保护由 {@link createUnattendedWriteProtectionPolicy} 改拒。 */
|
|
44
|
-
export declare const UNATTENDED_PERMISSION_MODE_DEFAULT: "bypassPermissions";
|
|
45
|
-
/** 解析结果:值 + 它从哪来(boot 一行 `unattended_permission_mode` 的两个字段)。 */
|
|
46
|
-
export interface UnattendedPermissionModeSetting {
|
|
47
|
-
readonly value: PermissionMode;
|
|
48
|
-
readonly source: "env" | "default";
|
|
49
|
-
}
|
|
44
|
+
import type { AskClass, AskRequest, OnAsk, TaskSpec } from "@sema-agent/core";
|
|
50
45
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* 未设 / 空串 ⇒ 出厂值,来源 `default`;词表外 ⇒ **拒启**,拒因点名旋钮与全部合法词([ref]:安全轴旋钮不许「以为设了其实没设」)。
|
|
46
|
+
* 按类作答的**答集** —— core §4.6e 推荐形:机器只答 `ordinary`。**常量,不是旋钮**([ref]):要放更多类是部署级的安全决定,先立案候裁。
|
|
47
|
+
* 不变量(测试钉):答集 ⊆ core `RELEASABLE_ASK_CLASSES`,且与带 `requiresRealApproval` 的类(core `RRA_EXPLAINERS` ∪ `policy_rra`)不交;`mode` 不在其中。
|
|
54
48
|
*/
|
|
55
|
-
export declare
|
|
56
|
-
/**
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/** 无人值守腿铸进 spec 的两键(纯数据 ⇒ `build*`):模式 = 旋钮值;写面 = `"roots"`(理由见模块头注「写面」)。 */
|
|
61
|
-
export declare function buildUnattendedLegSpec(mode: PermissionMode): Required<Pick<TaskSpec, "permissionMode" | "writeFace">>;
|
|
49
|
+
export declare const UNATTENDED_ANSWERED_CLASSES: ReadonlySet<AskClass>;
|
|
50
|
+
/** onAsk 的函数臂(`"allow"` / `"deny"` 两个字面臂是姿态设置,不是席)—— 本腿的活审批席与本层自己都是这一形。 */
|
|
51
|
+
export type UnattendedSeat = Extract<OnAsk, (req: AskRequest, signal?: AbortSignal) => unknown>;
|
|
52
|
+
/** 这一问是不是机器答得了的:读 `classes` **整体** —— 在场、非空、且每一类都在答集;缺席 / 空 / 任一类在答集外 ⇒ 否。 */
|
|
53
|
+
export declare function unattendedAnswers(req: Pick<AskRequest, "classes">): boolean;
|
|
62
54
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* 目标零询问放行;7.105.0 这些腿无模式 ⇒ 写保护是一问 ⇒ 无人可答 ⇒ 拒。core 7.35 没有「放普通问、留保护问」的模式,也没有限定模式可释放类的座。
|
|
67
|
-
*
|
|
68
|
-
* **规则(一条)**:本腿的模式**会释放**写保护一问时(读 core 仲裁本尊 `arbitrateByMode`,不手抄模式词),把**本腿引擎判官用的同一张写保护表**
|
|
69
|
-
* (`runnerSeats` = 构造本腿 Runner 的那份 deps 的两只座 {@link UnattendedLegRunnerSeats};`writeProtectedPaths` 缺席 = core 源表 `WRITE_PROTECTED_DEFAULT_TABLE`)经 core 的
|
|
70
|
-
* `compileWriteProtection` 编成判官,每一行铸成一条 **core 路径拒行**(`ToolPolicy.pathDenyRules`,`faces:["write"]`)—— 引擎的写面把它们并进本腿的拒表:
|
|
71
|
-
* 写工具的路径解析与 shell 站(重定向 / `tee` / `sed -i` / `cp`·`mv`·`touch` 操作数)都按拒表拒,模式**不释放拒**。**路径匹配全在 core**:每行的
|
|
72
|
-
* `reaches` 就是判官自己的 `matchPath`(同一只折叠、同一只数据根放行 —— `dataRoot` 与引擎判官同源:本腿 Runner 的 `memoryEngineDir`),本仓不写一个字的
|
|
73
|
-
* 路径比较。模式不释放写保护(`default` / `acceptEdits` / `plan` / `auto` / `dontAsk`)⇒ 不铸(那一问照旧问,有席的腿上人能答 —— run-local 有 TTY 时)。
|
|
74
|
-
*
|
|
75
|
-
* **射程外**(如实,收车档 ⑩ 候裁 18):缺省 bypass 还释放 `probe` / `tool_marks_mcp` / `tool_marks_a2a` / `transcript_write` / `rule_unreadable` /
|
|
76
|
-
* `artifact_shared_read` 六类(core 对它们没有「拒」的座),相对 7.105.0 是宽化 —— CHANGELOG「谁受伤」与 MIGRATION 写明,补偿 = 旋钮 `acceptEdits` / `default`。
|
|
77
|
-
* **过渡形**:core 7.37.0(S-906,`AskRequest.askClass?` / `classes?` 到货)后本仓改成 `default` 模式 + fail-closed 按类作答的 `onAsk`,本座与旋钮一并硬删。
|
|
78
|
-
* **续跑**(codex r3 F1):本层是活闭包、进不了 checkpoint,而 core 续跑在 spec 不说模式时取 checkpoint 记下的模式 ⇒ durable 续跑必须重挂整条腿 ——
|
|
79
|
-
* leader worker 的续跑上下文带 `CheckpointCtx.unattendedLeg`,续跑家族据此让 `resolveSpec` 在全部折叠之后调 {@link createUnattendedLeg} / {@link withUnattendedLeg}。
|
|
80
|
-
* **已知过拒**(codex r3 F2,fail-closed 方向,随过渡形退役):core 的路径拒座在 shell 站把 `cp` 的**源**操作数也按写目标判(CC 同形,cli280.js:216993
|
|
81
|
-
* `cp:"write"`),引擎写保护判官只判真写目标 ⇒ 从写保护目标复制出来在本层下被拒、7.105.0 上照跑。本仓不补路径比较(那是第二个判官),特征化钉在
|
|
82
|
-
* test/run-local.test.ts「🚧 codex r3 F2」。
|
|
83
|
-
* **附带效果**(S-877 四续 ⑫b,随过渡形退役):本层只能是一只 `ToolPolicy`(路径拒座挂在策略上,`check` 必填 —— 这里恒 `allow`),原先没有 toolPolicy
|
|
84
|
-
* 的腿因此被引擎读成「有策略层」:系统提示多 `# Execution environment` 一块、UNGATED 告警不再发。7.35.2 上表达不了「只供拒行、不算策略层」;
|
|
85
|
-
* 特征化钉在 test/unattended-permission-mode.test.ts「🚧 四续 ⑫b」。
|
|
86
|
-
*
|
|
87
|
-
* 返回 `undefined` = 本腿不需要这一层(模式不释放写保护,或运维把表清空 `[]`)。活对象(带判官闭包)⇒ `create*`。
|
|
88
|
-
*/
|
|
89
|
-
export declare function createUnattendedWriteProtectionPolicy(mode: PermissionMode, runnerSeats: UnattendedLegRunnerSeats): ToolPolicy | undefined;
|
|
90
|
-
/** 无人值守腿的全部铸件:模式 + 写面(纯数据)与写保护拒层(活对象,可缺席)。由 {@link createUnattendedLeg} 铸、{@link withUnattendedLeg} 并进 spec。 */
|
|
91
|
-
export interface UnattendedLeg {
|
|
92
|
-
readonly spec: Required<Pick<TaskSpec, "permissionMode" | "writeFace">>;
|
|
93
|
-
readonly writeProtection: ToolPolicy | undefined;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* 引擎写保护判官读的两只 Runner 座(`RunnerDeps` 的同名切片):表(`writeProtectedPaths`,缺席 = core 源表)与数据根放行(`memoryEngineDir`,缺席 = 引擎缺省)。
|
|
97
|
-
* 调用方递**构造本腿 Runner 的那一份** deps(或它的展开源)—— 表与根从哪来、根要不要在,都由那份 deps 说了算,腿的接线处不再单独读任何一只坐标
|
|
98
|
-
* (本模块是唯一读者;S-877 四续:接线处各读一份就是「两边同一张表、同一个根」这句话的第二、三个写者)。
|
|
55
|
+
* 按类作答的 onAsk(活对象 ⇒ `create*`)。答得了 ⇒ `true`(这一次调用的答);答不了 ⇒ 有席交席(席的答原样返回,席自己的
|
|
56
|
+
* fail-closed 语义不动),无席 ⇒ `"unavailable"`(core G1:这一问此刻没有人够得着 ⇒ 改道 durable park;本腿没武装 park ⇒ headless 拒)。
|
|
57
|
+
* 不答的臂**从不**自己拒成 `false`:那会把「交给 park 的人」折成「机器拒了」,durable 审批块的 leader worker 就再也 park 不下来。
|
|
99
58
|
*/
|
|
100
|
-
export
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
59
|
+
export declare function createUnattendedOnAsk(seat: UnattendedSeat | undefined): UnattendedSeat;
|
|
60
|
+
/** {@link withUnattendedLeg} 铸进 spec 的两键。 */
|
|
61
|
+
export type UnattendedLegSpec = {
|
|
62
|
+
readonly permissionMode: "default";
|
|
63
|
+
readonly onAsk: UnattendedSeat;
|
|
64
|
+
};
|
|
104
65
|
/**
|
|
105
|
-
*
|
|
106
|
-
* (
|
|
107
|
-
* 调用点必须在 spec 其余键(含治理折叠)
|
|
66
|
+
* 无人值守腿并进一条 spec —— 各腿的**唯一**合并口:`permissionMode: "default"` + `onAsk: createUnattendedOnAsk(seat)`。`seat` = 本腿的
|
|
67
|
+
* 活审批席(run-local 递它的 `createLocalApprover`;leader 各腿与续跑重挂无席,不递)—— 席只经这个参数进来,从不从 spec 上读。
|
|
68
|
+
* 调用点必须在 spec 其余键(含治理折叠)**之后**调它;本口不碰 `toolPolicy`(没有策略层要并:过渡形的写保护拒层已退役)。
|
|
108
69
|
*/
|
|
109
|
-
export declare function withUnattendedLeg<S extends Partial<TaskSpec>>(spec: S,
|
|
70
|
+
export declare function withUnattendedLeg<S extends Partial<TaskSpec>>(spec: S, seat?: UnattendedSeat): S & UnattendedLegSpec;
|
|
110
71
|
//# sourceMappingURL=unattended-permission-mode.d.ts.map
|
|
@@ -1,65 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const trimmed = (raw ?? "").trim();
|
|
6
|
-
if (trimmed === "")
|
|
7
|
-
return { value: UNATTENDED_PERMISSION_MODE_DEFAULT, source: "default" };
|
|
8
|
-
const word = PERMISSION_MODES.find((m) => m === trimmed);
|
|
9
|
-
if (word === undefined) {
|
|
10
|
-
throw new Error(`env UNATTENDED_PERMISSION_MODE="${trimmed.slice(0, 120)}" is not a permission mode — use one of: ${PERMISSION_MODES.join(", ")} ` +
|
|
11
|
-
`(or leave it unset for the default "${UNATTENDED_PERMISSION_MODE_DEFAULT}", which keeps the unattended legs — leader workers / repairs and run-local — acting as they did before server 7.106.0). ` +
|
|
12
|
-
`The word is matched EXACTLY (surrounding whitespace trimmed, no case folding). ` +
|
|
13
|
-
`Refusing to boot rather than treating it as unset: that would run those legs under a mode the operator did not choose`);
|
|
14
|
-
}
|
|
15
|
-
return { value: word, source: "env" };
|
|
1
|
+
export const UNATTENDED_ANSWERED_CLASSES = new Set(["ordinary"]);
|
|
2
|
+
export function unattendedAnswers(req) {
|
|
3
|
+
const classes = req.classes;
|
|
4
|
+
return classes !== undefined && classes.length > 0 && classes.every((c) => UNATTENDED_ANSWERED_CLASSES.has(c));
|
|
16
5
|
}
|
|
17
|
-
export function
|
|
18
|
-
return
|
|
6
|
+
export function createUnattendedOnAsk(seat) {
|
|
7
|
+
return (req, signal) => (unattendedAnswers(req) ? true : seat !== undefined ? seat(req, signal) : "unavailable");
|
|
19
8
|
}
|
|
20
|
-
export function
|
|
21
|
-
return { permissionMode:
|
|
22
|
-
}
|
|
23
|
-
export function createUnattendedWriteProtectionPolicy(mode, runnerSeats) {
|
|
24
|
-
if (!modeReleases(mode, "write_protection"))
|
|
25
|
-
return undefined;
|
|
26
|
-
const dataRoot = runnerSeats.memoryEngineDir;
|
|
27
|
-
const judge = compileWriteProtection(runnerSeats.writeProtectedPaths, dataRoot !== undefined ? { dataRoot } : undefined);
|
|
28
|
-
if (judge === undefined)
|
|
29
|
-
return undefined;
|
|
30
|
-
const pathDenyRules = judge.rows.map((row) => ({
|
|
31
|
-
rule: `WriteProtected(${row.name})`,
|
|
32
|
-
tool: "Edit",
|
|
33
|
-
pattern: row.name,
|
|
34
|
-
faces: ["write"],
|
|
35
|
-
reaches: (absolutePath) => judge.matchPath(absolutePath)?.name === row.name,
|
|
36
|
-
}));
|
|
37
|
-
return {
|
|
38
|
-
check: () => ({ action: "allow" }),
|
|
39
|
-
pathDenyRules,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function modeReleases(mode, askClass) {
|
|
43
|
-
const classes = new Set([askClass]);
|
|
44
|
-
return arbitrateByMode(mode, { classes, askClass, row: foldArbitrationRow(classes) }).kind === "released";
|
|
45
|
-
}
|
|
46
|
-
const INTENDED_RELEASES = new Set(["ordinary", "mode"]);
|
|
47
|
-
function releasedProtectiveClasses(mode) {
|
|
48
|
-
return ASK_CLASSES.filter((c) => !INTENDED_RELEASES.has(c) && c !== "write_protection" && modeReleases(mode, c));
|
|
49
|
-
}
|
|
50
|
-
const RELEASED_PROTECTIVE_ASKS_TAG = "server.unattended-leg.mode-releases-protective-asks";
|
|
51
|
-
export function createUnattendedLeg(mode, runnerSeats) {
|
|
52
|
-
const debt = releasedProtectiveClasses(mode);
|
|
53
|
-
if (debt.length > 0)
|
|
54
|
-
recordFailOpen(RELEASED_PROTECTIVE_ASKS_TAG, `mode=${mode} released=${debt.join(",")}`);
|
|
55
|
-
return { spec: buildUnattendedLegSpec(mode), writeProtection: createUnattendedWriteProtectionPolicy(mode, runnerSeats) };
|
|
56
|
-
}
|
|
57
|
-
export function withUnattendedLeg(spec, leg) {
|
|
58
|
-
const wp = leg.writeProtection;
|
|
59
|
-
return {
|
|
60
|
-
...spec,
|
|
61
|
-
...leg.spec,
|
|
62
|
-
...(wp !== undefined ? { toolPolicy: spec.toolPolicy !== undefined ? combinePolicies(spec.toolPolicy, wp) : wp } : {}),
|
|
63
|
-
};
|
|
9
|
+
export function withUnattendedLeg(spec, seat) {
|
|
10
|
+
return { ...spec, permissionMode: "default", onAsk: createUnattendedOnAsk(seat) };
|
|
64
11
|
}
|
|
65
12
|
//# sourceMappingURL=unattended-permission-mode.js.map
|