@sema-agent/server 7.121.0 → 7.122.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/README.md +1 -1
- package/README.zh-CN.md +3 -1
- package/USAGE.md +35 -14
- package/dist/approval-reconciler.d.ts +8 -0
- package/dist/approval-reconciler.js +72 -52
- package/dist/auto-mode-face.d.ts +9 -2
- package/dist/auto-mode-face.js +5 -3
- package/dist/bench/s1/live-deps.js +1 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +2 -1
- package/dist/boot/resolve-spec.js +1 -4
- package/dist/boot/shutdown.js +6 -1
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/capabilities/memory-notice.d.ts +1 -1
- package/dist/config-catalog.js +3 -3
- package/dist/config-center/center-request.d.ts +6 -9
- package/dist/config-center/center-request.js +11 -27
- package/dist/config-types.d.ts +11 -8
- package/dist/config-types.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +23 -35
- package/dist/fleet/drain-entry.d.ts +28 -3
- package/dist/fleet/drain-entry.js +3 -0
- package/dist/fleet/serving-lease.js +13 -15
- package/dist/governance-overrides.d.ts +7 -2
- package/dist/http/admission.js +7 -5
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +60 -11
- package/dist/http/checkpoint-ctx.js +23 -9
- package/dist/http/dispatch.js +17 -5
- package/dist/http/resume-legs.js +8 -8
- package/dist/http/route-ctx.d.ts +19 -3
- package/dist/http/route-table.d.ts +3 -0
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-drain.d.ts +5 -2
- package/dist/http/routes/admin-drain.js +14 -2
- package/dist/http/routes/approvals-assistant.d.ts +1 -0
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/capabilities.js +5 -4
- package/dist/http/routes/fleet.d.ts +1 -0
- package/dist/http/routes/fleet.js +1 -1
- package/dist/http/routes/images.d.ts +1 -0
- package/dist/http/routes/images.js +1 -1
- package/dist/http/routes/leader.js +6 -0
- package/dist/http/routes/runs.d.ts +2 -0
- package/dist/http/routes/runs.js +5 -4
- package/dist/http/routes/sessions.d.ts +1 -0
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/trace-usage.d.ts +1 -0
- package/dist/http/routes/trace-usage.js +1 -1
- package/dist/http/routes/workflows.d.ts +1 -0
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/send.d.ts +13 -0
- package/dist/http/send.js +9 -0
- package/dist/http/server.d.ts +3 -0
- package/dist/http/stored-body-replay.d.ts +27 -0
- package/dist/http/stored-body-replay.js +23 -0
- package/dist/leader/leader.d.ts +22 -1
- package/dist/leader/leader.js +4 -1
- package/dist/leader/repair-wire.d.ts +1 -1
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +27 -0
- package/dist/leader/run-entry.js +29 -0
- package/dist/leader/wire.d.ts +10 -0
- package/dist/leader/wire.js +15 -10
- package/dist/model-route-url.d.ts +13 -20
- package/dist/model-route-url.js +28 -24
- package/dist/observability/fail-open.d.ts +23 -6
- package/dist/observability/fail-open.js +8 -4
- package/dist/plugins/approval-ask-store-file.d.ts +1 -0
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
- package/dist/plugins/approval-ask-store-memory.js +6 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -0
- package/dist/plugins/one-time-migrations.js +13 -0
- package/dist/plugins/park-commit-watch.d.ts +19 -5
- package/dist/plugins/park-commit-watch.js +24 -17
- package/dist/router/route-orchestration.d.ts +4 -2
- package/dist/router/route-orchestration.js +2 -1
- package/dist/run-local.js +3 -1
- package/dist/runtime-governance.d.ts +44 -32
- package/dist/runtime-governance.js +23 -9
- package/dist/sighup-idle.d.ts +7 -1
- package/dist/sighup-idle.js +4 -4
- package/dist/task-settings.d.ts +37 -14
- package/dist/task-settings.js +20 -20
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +13 -1
- package/dist/trace/redact.d.ts +3 -3
- package/dist/unattended-permission-mode.d.ts +17 -13
- package/dist/unattended-permission-mode.js +3 -1
- package/dist/url-shape.d.ts +31 -0
- package/dist/url-shape.js +41 -0
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +1 -1
package/dist/http/admission.js
CHANGED
|
@@ -2,7 +2,8 @@ import { APPROVAL_REACH, MCP_IMAGE_MAX_BASE64, isApprovalReach, isPermissionMode
|
|
|
2
2
|
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
|
|
3
3
|
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
4
4
|
import { HttpError } from "../security.js";
|
|
5
|
-
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
|
|
5
|
+
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, stampEntryPermissionMode, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
|
|
6
|
+
import { autonomyModeFloor } from "../runtime-governance.js";
|
|
6
7
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
7
8
|
import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
8
9
|
import * as facts from "../capability-facts.js";
|
|
@@ -623,19 +624,20 @@ export function createAdmission(ctx) {
|
|
|
623
624
|
sendError(res, 400, "request.precondition_unmet", "cascade requested but no ladder configured — set MODEL_CASCADE_LADDER (catalog model names, cheapest→strongest)");
|
|
624
625
|
return null;
|
|
625
626
|
}
|
|
627
|
+
const admitted = stampEntryPermissionMode(body, autonomyModeFloor(deps.config.autonomy));
|
|
626
628
|
try {
|
|
627
|
-
const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
|
|
629
|
+
const auth = deps.authorize ? await deps.authorize({ req, body: admitted }) : undefined;
|
|
628
630
|
const engineNotices = [];
|
|
629
|
-
const spec = await deps.resolveSpec(
|
|
631
|
+
const spec = await deps.resolveSpec(admitted, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}), onNotice: (n) => engineNotices.push(n) });
|
|
630
632
|
const bindingIntent = await resolveDevicePlacementIntent({
|
|
631
633
|
config: deps.config,
|
|
632
634
|
req,
|
|
633
635
|
rootSessionId: spec.sessionId,
|
|
634
|
-
deviceId:
|
|
636
|
+
deviceId: admitted.deviceId,
|
|
635
637
|
store: deps.deviceStore,
|
|
636
638
|
enrollment: deps.deviceEnrollment,
|
|
637
639
|
});
|
|
638
|
-
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId:
|
|
640
|
+
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: admitted.jobId, body: admitted, ...(bindingIntent === undefined ? {} : { bindingIntent }), ...(engineNotices.length > 0 ? { engineNotices } : {}) };
|
|
639
641
|
}
|
|
640
642
|
catch (err) {
|
|
641
643
|
if (err instanceof HttpError) {
|
|
@@ -8,7 +8,7 @@ import { hasServiceAuth } from "./dispatch.js";
|
|
|
8
8
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
9
9
|
import { admissionRefusalOf, readinessOf } from "./readiness.js";
|
|
10
10
|
import { readResumeCtx } from "./resume-ctx-read.js";
|
|
11
|
-
import { storedBodyReplayRefusal } from "./
|
|
11
|
+
import { storedBodyReplayRefusal } from "./stored-body-replay.js";
|
|
12
12
|
const BLOCKED_SCALAR_KEYS = ["errorCode", "blockedBy", "field", "reason", "count", "cap", "index"];
|
|
13
13
|
function blockedScalars(body) {
|
|
14
14
|
const out = {};
|
|
@@ -72,7 +72,7 @@ export function createApprovalSweeps(ctx) {
|
|
|
72
72
|
return undefined;
|
|
73
73
|
if (read.kind === "absent")
|
|
74
74
|
return undefined;
|
|
75
|
-
return storedBodyReplayRefusal(read.ctx);
|
|
75
|
+
return storedBodyReplayRefusal(read.ctx, deps.config.autonomy);
|
|
76
76
|
};
|
|
77
77
|
let parkedSkipped = 0;
|
|
78
78
|
const replaySkipped = [];
|
|
@@ -20,8 +20,19 @@
|
|
|
20
20
|
* 同一面上的第二个「新提交会被拒、存量体重放不能放过」:提交体里技能(顶层 / 子代)的 `disallowedTools` 有引擎读成空的条目(`"Bash "` 这类 7.41 去白后有效、
|
|
21
21
|
* 7.42 什么也不拒的条目)⇒ 提交 400(`resolveSpec`,拒点);存量停驻体重放 ⇒ 409 `resume_blocked_by_policy`。判据函数 = `rule-entry-list.ts` `skillDenyInertEntryOf`。
|
|
22
22
|
*
|
|
23
|
-
* ##
|
|
24
|
-
*
|
|
23
|
+
* ## 第三问(7.122.0):体里模式词的读法记号
|
|
24
|
+
* 7.122.0 起一条 run 的模式词只有一个源 —— 存储的提交体顶层 `permissionMode`(提交入口把运维 `AUTONOMY=plan` 的地板盖进去一次;`plan_review` 写回改它),
|
|
25
|
+
* 治理层不再在每一次续跑重钳。更早版本写下的行体里是**请求的原词**,运维的 plan 由治理层每段重新折上(7.121.x 及以前:只读手 + 写面 roots)。
|
|
26
|
+
* 那种行在本版上按体里的原词重放 = 静默放宽(`acceptEdits` / `bypassPermissions` 原词直接生效;无词 ⇒ 引擎读 checkpoint 记下的词)。
|
|
27
|
+
* ⇒ 同一条规则:**写模式词的人盖读法记号** —— 模式词的写者:每一个出生点(新的持久化写都经 {@link buildCheckpointCtx})与 `plan_review` 写回(经 {@link withPermissionModeRewritten}),各自写词的同一拍在 ctx 上写记号
|
|
28
|
+
`permissionModeReading`(闭集只有当前一词 {@link PERMISSION_MODE_READINGS});续跑重放之前,**本部署此刻有入口地板**
|
|
29
|
+
* (`runtime-governance.ts` `autonomyModeFloor`)而行上没有当前记号、且体的生效词被地板钳会变 ⇒ 拒(行字节不动)。没有地板的部署、体的词本就在地板之下(`plan`)的行
|
|
30
|
+
* 不受影响:那两种行在新旧两种读法下答同一个词。出路:作为新任务重提(仍停驻的先取消),或在它们收尾之前把 `AUTONOMY` 设成 `read-only`(按段的只读围栏,
|
|
31
|
+
* 与 7.121.x 的 `AUTONOMY=plan` 同一个结局,且没有入口地板 ⇒ 不拒)。删除条件登记在 `plugins/one-time-migrations.ts` 的 M12 行。
|
|
32
|
+
*
|
|
33
|
+
* ## 一只判据、五个问点(`http/stored-body-replay.ts` 的 `storedBodyReplayRefusal` / 本模块的 {@link storedBodyReplayAnswer})
|
|
34
|
+
* 「这条停驻行的持久化体在本版上能否重放」只有**一个**读法:上面三问合在 `stored-body-replay.ts` 的 `storedBodyReplayRefusal` 一处(前两问的判据在本模块、第三问读请求设置
|
|
35
|
+
* 与治理两个属主 —— 本模块的值依赖要留在 node 内建 / 引擎包根之内,随包排空脚本经编译产物读它),答复合在 {@link storedBodyReplayAnswer} 一处。
|
|
25
36
|
* 问点五个,同调同答:续跑驱动段(`resume-legs.ts` `resolveResumeSpecOrBlocked`,另四条腿的唯一判点)· plan_review 的 prepare 段(读类拒,在 nonce 消费之前)·
|
|
26
37
|
* SLA 拒扫腿的跳过臂(`approval-sweeps.ts`)· 终态 run 的 steer 铸唤醒卡之前(`routes/runs.ts`:铸卡前先证明它赎得回,证不出 ⇒ 不铸、当场同一个拒)·
|
|
27
38
|
* `resolveSpec` 本身留作真正的拒点(提交腿 400;续跑腿在到它之前已被本判据答掉)。再开第六个问点必须调这两只,不许各判各的 —— 此前铸卡前只判了
|
|
@@ -32,13 +43,19 @@
|
|
|
32
43
|
* 一次性迁移登记表(本条那一行):所有旧读法下停驻的行都已结束或被收割(停驻行的终局 = 落盘起 `TERMINAL_BACKSTOP_MS`,见
|
|
33
44
|
* `plugins/checkpoint-store-sql.ts`)之后的那个版本。删的时候记号的读者一起删。
|
|
34
45
|
*/
|
|
46
|
+
import type { PermissionMode } from "@sema-agent/core";
|
|
35
47
|
import type { CheckpointCtx } from "./route-ctx.js";
|
|
36
48
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
37
|
-
import {
|
|
49
|
+
import type { SkillDenyInertEntry } from "../rule-entry-list.js";
|
|
38
50
|
/** 读法记号的闭集(一词 = 当前读法):这一行体里的 `settings.permissions.allow` 是按「交给引擎的 allow 层」写下、也该按它读的
|
|
39
51
|
* (与能力位 `settings.permissions.allowLayer` 同一个说法)。 */
|
|
40
52
|
export declare const PERMISSIONS_ALLOW_READINGS: readonly ["allow_layer"];
|
|
41
53
|
export type PermissionsAllowReading = (typeof PERMISSIONS_ALLOW_READINGS)[number];
|
|
54
|
+
/** 体里模式词的读法记号闭集(一词 = 当前读法):这一行体的顶层 `permissionMode` 是这条 run 模式词**唯一的源**(提交入口盖过运维地板、`plan_review` 写回改它),
|
|
55
|
+
* 续跑照它读、不再由治理层按段重钳。写点两个 = {@link buildCheckpointCtx}(每一次新的持久化写都写)与 {@link withPermissionModeRewritten}(`plan_review` 写回改词的那一拍写);
|
|
56
|
+
* **不改词**的续跑腿写回传原 ctx ⇒ 记号随行。 */
|
|
57
|
+
export declare const PERMISSION_MODE_READINGS: readonly ["entry_word"];
|
|
58
|
+
export type PermissionModeReading = (typeof PERMISSION_MODE_READINGS)[number];
|
|
42
59
|
/** 被拒的行是在哪种读法下写的(闭集;拒句按它穷举):`list` = 无记号或记号认不得(≤7.114.0 的名单读法;认不得的记号不是任何一代的证据,
|
|
43
60
|
* 按最早那一代说话);`statement` = 7.115.0–7.116.x 写下的表态读法。 */
|
|
44
61
|
export declare const ALLOW_LIST_READING_ERAS: readonly ["list", "statement"];
|
|
@@ -53,13 +70,44 @@ export declare function bodyCarriesAllowList(body: TaskRequestBody): boolean;
|
|
|
53
70
|
*
|
|
54
71
|
* 调用方是「本进程刚按当前读法读过这份体」的写点:fresh 提交(受理门按当前读法读过 `allow`),或已经过了续跑拒臂的重放腿
|
|
55
72
|
* (`startFreshHostTurn` 把宿主的存量体交给 `POST /v1/runs` 同一条链 —— 它在那之前调过 `resolveResumeSpecOrBlocked`,没记号却带 `allow` 的体到不了这里)。
|
|
56
|
-
*
|
|
73
|
+
* 续跑腿的**写回**不经本函数:**不改词**的写回传读进来的 ctx(记号随行,不重新判 —— 在写回口重新判会把一条没记号的旧行「洗」成新行);
|
|
74
|
+
* **改词**的写回(只有 `plan_review` 的批准 / 编辑)经 {@link withPermissionModeRewritten}。
|
|
57
75
|
*/
|
|
58
76
|
export declare function buildCheckpointCtx(fields: {
|
|
59
77
|
body: TaskRequestBody;
|
|
60
78
|
memoryScope?: string | undefined;
|
|
61
79
|
unattendedLeg?: true;
|
|
62
80
|
}): CheckpointCtx;
|
|
81
|
+
/**
|
|
82
|
+
* 模式词的**第二个写者** —— `plan_review` 的批准 / 编辑把这条 run 的体词从 `plan` 写成批准的档(`resume-legs.ts` `drivePlanReview`,调用点只有那一处;
|
|
83
|
+
* `test/allow-list-reading-park-refusal.test.ts` 枚举调用点,多一处即红)。与 {@link buildCheckpointCtx} **同一条规则**:按当前读法写下的词,盖当前读法记号。
|
|
84
|
+
*
|
|
85
|
+
* 为什么记号必须随词一起写,而不是留给读的一侧去豁免:第三问问的是「体的词是不是按当前读法写下的」,记号就是这句话的证据。改写之后体里的词是本版的 `plan_review`
|
|
86
|
+
* 刚写下的(一个人刚批准的档),它确实是 `entry_word` 读法的词;若只改体、记号缺席,重建段的同一问把它读成「7.121.x 的请求原词」,被地板钳回 `plan` ≠ 所写词 ⇒ 误拒
|
|
87
|
+
* 一条刚被批准的 run。判据侧(`stored-body-replay.ts`)**不为「plan_review 之后」另开特判臂** —— 读的一侧只有一个读法,写的一侧各自负责盖章。
|
|
88
|
+
*
|
|
89
|
+
* 为什么这不是在「洗」旧行:走到写回口的行,其**原体**已过 prepare 段的同一问(词被地板钳不变:词已是 `plan`,或本部署没有地板);原体的词会被地板钳的旧行
|
|
90
|
+
* (`acceptEdits` / `bypassPermissions` / 无词)在 prepare 段就被拒,到不了这里。入参 `ctx` 的其余位(memoryScope / 无人值守标 / allow 记号)原样随行,只换体与模式词记号。
|
|
91
|
+
*
|
|
92
|
+
* 本模块零值依赖的闭包纪律不变(随包排空脚本经编译产物 import;`test/drain-check-local.test.ts`):本函数只用本模块内的东西。
|
|
93
|
+
*/
|
|
94
|
+
export declare function withPermissionModeRewritten(ctx: CheckpointCtx, body: TaskRequestBody): CheckpointCtx;
|
|
95
|
+
/** 行上带的是不是当前的模式词读法记号(第三问的记号那一半;另一半 —— 体的生效词被地板钳会不会变 —— 读请求设置的解析层,住在 `http/stored-body-replay.ts`)。
|
|
96
|
+
* 记号缺席或认不得 ⇒ 不是「按当前读法写下」的证据,与 allow 记号同一条纪律。 */
|
|
97
|
+
export declare function permissionModeReadingStated(ctx: CheckpointCtx): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* 第三问的拒答复 —— 码与落点同 allow 记号那一问(`checkpoint.unsupported_version`,409,`retriable:true` = 那张卡仍 pending,**不是**「原样重试会成功」)。
|
|
100
|
+
* 句子说原因(更早的版本在每次续跑重钳运维的 AUTONOMY、没把它记进任务;本版只在提交时记一次 ⇒ 续跑会按任务提交时的原词跑)与两条出路。
|
|
101
|
+
* 🔴 形是给冻结门看的:`status: <三位数>, body: { error: "<整句双引号字面>", errorCode: "<码>"`,中间只许有空白。
|
|
102
|
+
*/
|
|
103
|
+
export declare function permissionModeReadingRefusal(): {
|
|
104
|
+
status: number;
|
|
105
|
+
body: {
|
|
106
|
+
error: string;
|
|
107
|
+
errorCode: string;
|
|
108
|
+
retriable: true;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
63
111
|
/**
|
|
64
112
|
* 本行能否被按当前读法重放:体里 `allow` 在生效而行上的记号不是当前这一个词 ⇒ **不能**(`true` = 拒)。
|
|
65
113
|
* 记号缺席、是旧词(`"statement"`)或认不得(手改 / 将来的写者)⇒ 同样拒:只有当前这个词是「按当前读法写下」的证据。
|
|
@@ -89,12 +137,11 @@ export type StoredBodyReplayRefusal = {
|
|
|
89
137
|
} | {
|
|
90
138
|
readonly kind: "skill_deny_inert";
|
|
91
139
|
readonly entry: SkillDenyInertEntry;
|
|
140
|
+
} | {
|
|
141
|
+
readonly kind: "permission_mode_reading";
|
|
142
|
+
readonly stored: PermissionMode | undefined;
|
|
143
|
+
readonly floor: PermissionMode;
|
|
92
144
|
};
|
|
93
|
-
/**
|
|
94
|
-
* 「存量体能否重放」的**唯一**判据(文件头「一只判据、五个问点」):不能 ⇒ 返回拒的那条规则;能 ⇒ `undefined`。
|
|
95
|
-
* 顺序:先 allow 读法记号,再技能拒侧读成空(两问都中时答第一问 —— 任一问中都不重放,句子只需一句)。
|
|
96
|
-
*/
|
|
97
|
-
export declare function storedBodyReplayRefusal(ctx: CheckpointCtx): StoredBodyReplayRefusal | undefined;
|
|
98
145
|
/** 续跑家族交回的拒答复形(`{ status, body }`,由 `sendResumeOutcome` / 各路由发送;`body` 至少 `error` + `errorCode`)。 */
|
|
99
146
|
export interface ResumeRefusalAnswer {
|
|
100
147
|
readonly status: number;
|
|
@@ -110,17 +157,19 @@ export interface ResumeRefusalAnswer {
|
|
|
110
157
|
*/
|
|
111
158
|
export declare function resumeBlockedByPolicyAnswer(message: string, blockedBy: string, extra?: Record<string, unknown> | undefined): ResumeRefusalAnswer;
|
|
112
159
|
/**
|
|
113
|
-
*
|
|
160
|
+
* `stored-body-replay.ts` `storedBodyReplayRefusal` 的**日志字段**(三处问点同一形,各自只再加坐标键 `sessionId` / `taskId`):`kind` 判别,allow 臂带 `era`、技能臂带 `layer`,`note` 一句出路。
|
|
114
161
|
* 事件名三个(问点各一:`resume_refused_stored_body` / `steer_wake_park_refused_stored_body` / `deny_sweep_stored_body_skipped`),字段形只有这一只。
|
|
115
162
|
*/
|
|
116
163
|
export declare function storedBodyReplayLogFields(refusal: StoredBodyReplayRefusal): {
|
|
117
164
|
kind: StoredBodyReplayRefusal["kind"];
|
|
118
165
|
era?: AllowListReadingEra;
|
|
119
166
|
layer?: SkillDenyInertEntry["layer"];
|
|
167
|
+
storedMode?: PermissionMode | null;
|
|
168
|
+
floor?: PermissionMode;
|
|
120
169
|
note: string;
|
|
121
170
|
};
|
|
122
171
|
/**
|
|
123
|
-
*
|
|
172
|
+
* `stored-body-replay.ts` `storedBodyReplayRefusal` 的答复:每条规则一只既有铸点(allow 记号 ⇒ {@link allowListReadingRefusal};技能拒侧 ⇒ {@link resumeBlockedByPolicyAnswer},与提交腿的 400
|
|
124
173
|
* `request.field_invalid` 同码同句 —— 体上只有这一个问题时;存量体同时还踩中 `resolveSpec` 更早的拒〔三桶超帽 / 托管姿态〕时,提交侧答更早那条、续跑侧答本句,两边都是拒,指路不同)。
|
|
125
174
|
*/
|
|
126
175
|
export declare function storedBodyReplayAnswer(refusal: StoredBodyReplayRefusal): ResumeRefusalAnswer;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { skillDenyInertEntryOf } from "../rule-entry-list.js";
|
|
2
1
|
export const PERMISSIONS_ALLOW_READINGS = ["allow_layer"];
|
|
2
|
+
export const PERMISSION_MODE_READINGS = ["entry_word"];
|
|
3
3
|
export const ALLOW_LIST_READING_ERAS = ["list", "statement"];
|
|
4
4
|
export function bodyCarriesAllowList(body) {
|
|
5
5
|
const settings = body?.settings;
|
|
@@ -17,6 +17,24 @@ export function buildCheckpointCtx(fields) {
|
|
|
17
17
|
...(fields.memoryScope !== undefined ? { memoryScope: fields.memoryScope } : {}),
|
|
18
18
|
...(fields.unattendedLeg === true ? { unattendedLeg: true } : {}),
|
|
19
19
|
...(bodyCarriesAllowList(fields.body) ? { permissionsAllowReading: "allow_layer" } : {}),
|
|
20
|
+
permissionModeReading: "entry_word",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function withPermissionModeRewritten(ctx, body) {
|
|
24
|
+
return { ...ctx, body, permissionModeReading: "entry_word" };
|
|
25
|
+
}
|
|
26
|
+
export function permissionModeReadingStated(ctx) {
|
|
27
|
+
const reading = ctx.permissionModeReading;
|
|
28
|
+
return typeof reading === "string" && PERMISSION_MODE_READINGS.includes(reading);
|
|
29
|
+
}
|
|
30
|
+
export function permissionModeReadingRefusal() {
|
|
31
|
+
return {
|
|
32
|
+
status: 409,
|
|
33
|
+
body: {
|
|
34
|
+
error: "this task was suspended by an earlier server version that re-applied this deployment's AUTONOMY on every resume instead of recording it in the task; this version records the operator's plan floor once, when a task is submitted, so continuing would run this task under the permission mode it was submitted with rather than the one it ran under. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel); to let it finish with read-only hands instead, set AUTONOMY=read-only until it does.",
|
|
35
|
+
errorCode: "checkpoint.unsupported_version",
|
|
36
|
+
retriable: true,
|
|
37
|
+
},
|
|
20
38
|
};
|
|
21
39
|
}
|
|
22
40
|
export function allowListReadingUnstated(ctx) {
|
|
@@ -55,14 +73,6 @@ export function allowListReadingRefusal(era) {
|
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
|
-
export function storedBodyReplayRefusal(ctx) {
|
|
59
|
-
if (allowListReadingUnstated(ctx))
|
|
60
|
-
return { kind: "allow_list_reading", era: allowListReadingEraOf(ctx) };
|
|
61
|
-
const entry = skillDenyInertEntryOf(ctx.body);
|
|
62
|
-
if (entry !== undefined)
|
|
63
|
-
return { kind: "skill_deny_inert", entry };
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
76
|
export function resumeBlockedByPolicyAnswer(message, blockedBy, extra) {
|
|
67
77
|
return {
|
|
68
78
|
status: 409,
|
|
@@ -75,6 +85,8 @@ export function storedBodyReplayLogFields(refusal) {
|
|
|
75
85
|
return { kind: refusal.kind, era: refusal.era, note: "this session's stored request carries settings.permissions.allow written under an earlier reading (list era: a whitelist; statement era: a statement that cleared only the unstated question); it is not replayed under the current reading — the task stays suspended until someone cancels it" };
|
|
76
86
|
case "skill_deny_inert":
|
|
77
87
|
return { kind: refusal.kind, layer: refusal.entry.layer, note: "this session's stored request carries a skill disallowedTools entry the engine now reads as empty (an earlier engine trimmed it into a working deny); it is not replayed — the task stays suspended until someone cancels it and resubmits with the entry fixed" };
|
|
88
|
+
case "permission_mode_reading":
|
|
89
|
+
return { kind: refusal.kind, storedMode: refusal.stored ?? null, floor: refusal.floor, note: "this session's stored request was written before the operator's AUTONOMY plan floor was recorded in the task (earlier versions re-applied it on every resume); replaying it now would run under its submitted permission mode — the task stays suspended until someone cancels it, or AUTONOMY is set to read-only so it can finish with read-only hands" };
|
|
78
90
|
default: {
|
|
79
91
|
const unhandled = refusal;
|
|
80
92
|
throw new Error(`stored-body replay refusal ${JSON.stringify(unhandled)} has no log fields`);
|
|
@@ -87,6 +99,8 @@ export function storedBodyReplayAnswer(refusal) {
|
|
|
87
99
|
return allowListReadingRefusal(refusal.era);
|
|
88
100
|
case "skill_deny_inert":
|
|
89
101
|
return resumeBlockedByPolicyAnswer(refusal.entry.message, "request.field_invalid");
|
|
102
|
+
case "permission_mode_reading":
|
|
103
|
+
return permissionModeReadingRefusal();
|
|
90
104
|
default: {
|
|
91
105
|
const unhandled = refusal;
|
|
92
106
|
throw new Error(`stored-body replay refusal ${JSON.stringify(unhandled)} has no answer`);
|
package/dist/http/dispatch.js
CHANGED
|
@@ -3,9 +3,9 @@ import { setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isDestructiveSessio
|
|
|
3
3
|
import { publicStoreProbeError } from "../store-live-probe.js";
|
|
4
4
|
import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
|
|
5
5
|
import { ROUTE_METHOD_ORDER } from "./route-ctx.js";
|
|
6
|
-
import { READ_CREDENTIAL_ROUTES, PUBLIC_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, billableKindOf, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath } from "./route-table.js";
|
|
7
|
-
import { sendJson, sendError, isSseResponse } from "./send.js";
|
|
8
|
-
import { admissionRefusalOf, readinessOf, healthReadinessKeys, sendReadyz, sendBillableRefusal } from "./readiness.js";
|
|
6
|
+
import { READ_CREDENTIAL_ROUTES, PUBLIC_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, billableKindOf, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath, isSubscriptionRequest } from "./route-table.js";
|
|
7
|
+
import { sendJson, sendError, isSseResponse, endSseWithError } from "./send.js";
|
|
8
|
+
import { admissionRefusalOf, readinessOf, healthReadinessKeys, sendReadyz, sendBillableRefusal, billableRefusalOf } from "./readiness.js";
|
|
9
9
|
import { fleetMembershipKeys, mintEngineSignal, restartActionable } from "../fleet/engine-signal.js";
|
|
10
10
|
import { buildHealthSelfReport } from "./health-self-report.js";
|
|
11
11
|
import { authorized, systemFor, gatedPrincipal, sendPrincipalRefusal, sendUnauthorized, verifiedIdentitySource } from "./principal-gate.js";
|
|
@@ -43,6 +43,7 @@ export function createDispatch(ctx) {
|
|
|
43
43
|
const { deps, noteActivity, routeCtxBase, counters } = ctx;
|
|
44
44
|
const openResponses = new Set();
|
|
45
45
|
let lastAttachEndedAt = 0;
|
|
46
|
+
const openSubscriptions = new Set();
|
|
46
47
|
if (deps.drainState) {
|
|
47
48
|
deps.drainState.attachedStreams = () => {
|
|
48
49
|
let n = 0;
|
|
@@ -52,6 +53,12 @@ export function createDispatch(ctx) {
|
|
|
52
53
|
return n;
|
|
53
54
|
};
|
|
54
55
|
deps.drainState.lastAttachEndedAt = () => lastAttachEndedAt;
|
|
56
|
+
deps.drainState.endSubscriptions = () => {
|
|
57
|
+
const { errorCode, message } = billableRefusalOf("draining").body;
|
|
58
|
+
for (const r of [...openSubscriptions])
|
|
59
|
+
if (isSseResponse(r))
|
|
60
|
+
endSseWithError(r, { errorCode, message });
|
|
61
|
+
};
|
|
55
62
|
}
|
|
56
63
|
async function dispatchSegment(rows, req, res, url, search, method, allow, ctx, admit) {
|
|
57
64
|
for (const row of rows) {
|
|
@@ -82,10 +89,14 @@ export function createDispatch(ctx) {
|
|
|
82
89
|
const allow = { pre: [], post: [] };
|
|
83
90
|
noteActivity(url);
|
|
84
91
|
const admitted = method !== "OPTIONS" && !isProbePath(url);
|
|
85
|
-
|
|
92
|
+
const subscription = admitted && isSubscriptionRequest(method, url);
|
|
93
|
+
const countedInflight = admitted && !subscription;
|
|
94
|
+
if (countedInflight)
|
|
86
95
|
counters.admittedInflight++;
|
|
87
96
|
if (admitted)
|
|
88
97
|
openResponses.add(res);
|
|
98
|
+
if (subscription)
|
|
99
|
+
openSubscriptions.add(res);
|
|
89
100
|
let logged = false;
|
|
90
101
|
const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
|
|
91
102
|
const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, legs: routeCtxBase.legs, req: reqState };
|
|
@@ -93,8 +104,9 @@ export function createDispatch(ctx) {
|
|
|
93
104
|
if (logged)
|
|
94
105
|
return;
|
|
95
106
|
logged = true;
|
|
96
|
-
if (
|
|
107
|
+
if (countedInflight)
|
|
97
108
|
counters.admittedInflight--;
|
|
109
|
+
openSubscriptions.delete(res);
|
|
98
110
|
if (openResponses.delete(res) && isSseResponse(res))
|
|
99
111
|
lastAttachEndedAt = performance.now();
|
|
100
112
|
const route = routeLabel(method, url);
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -29,7 +29,7 @@ import { reasoningDeltaBytes, reasoningSegmentEventId } from "../trace/model-seg
|
|
|
29
29
|
import { errorText } from "../observability/err-text.js";
|
|
30
30
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
31
31
|
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
32
|
-
import { storedBodyPermissionMode } from "../task-settings.js";
|
|
32
|
+
import { planReviewDecisionExitsPlan, storedBodyPermissionMode } from "../task-settings.js";
|
|
33
33
|
import { acceptedResumingReceipt, buildHostLegOutcome } from "./route-ctx.js";
|
|
34
34
|
import { mintAndDriveDurableRun } from "./durable-run-leg.js";
|
|
35
35
|
import { createSessionsLocal } from "./routes/sessions.js";
|
|
@@ -43,7 +43,8 @@ import { commitSessionInheritEnv } from "./session-inherit-env.js";
|
|
|
43
43
|
import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
|
|
44
44
|
import { deploymentListAsks } from "../approval.js";
|
|
45
45
|
import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
|
|
46
|
-
import { resumeBlockedByPolicyAnswer, storedBodyReplayAnswer, storedBodyReplayLogFields,
|
|
46
|
+
import { resumeBlockedByPolicyAnswer, storedBodyReplayAnswer, storedBodyReplayLogFields, withPermissionModeRewritten } from "./checkpoint-ctx.js";
|
|
47
|
+
import { storedBodyReplayRefusal } from "./stored-body-replay.js";
|
|
47
48
|
import { httpErrorCode } from "./send.js";
|
|
48
49
|
function isCheckpointReopenedFailure(r) {
|
|
49
50
|
const code = failureCodeOf(r.terminal);
|
|
@@ -422,7 +423,7 @@ export function createResumeLegs(ctx) {
|
|
|
422
423
|
return storedBodyReplayAnswer(refusal);
|
|
423
424
|
};
|
|
424
425
|
const resolveResumeSpecOrBlocked = async (ctx, req, auth) => {
|
|
425
|
-
const replay = storedBodyReplayRefusal(ctx);
|
|
426
|
+
const replay = storedBodyReplayRefusal(ctx, deps.config.autonomy);
|
|
426
427
|
if (replay !== undefined)
|
|
427
428
|
return { ok: false, answer: refuseStoredBodyReplay(auth.sessionId, replay) };
|
|
428
429
|
try {
|
|
@@ -1352,7 +1353,7 @@ export function createResumeLegs(ctx) {
|
|
|
1352
1353
|
return refuse(resumeCtxCorruptAnswer(ctxRead.where));
|
|
1353
1354
|
if (ctxRead.kind === "absent")
|
|
1354
1355
|
return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
|
|
1355
|
-
const replay = storedBodyReplayRefusal(ctxRead.ctx);
|
|
1356
|
+
const replay = storedBodyReplayRefusal(ctxRead.ctx, deps.config.autonomy);
|
|
1356
1357
|
if (replay !== undefined)
|
|
1357
1358
|
return refuse(refuseStoredBodyReplay(sessionId, replay));
|
|
1358
1359
|
return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
|
|
@@ -1364,11 +1365,10 @@ export function createResumeLegs(ctx) {
|
|
|
1364
1365
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1365
1366
|
const effMode = storedBodyPermissionMode(ctx.body);
|
|
1366
1367
|
if (permissionModeAfter !== undefined && effMode !== "plan") {
|
|
1367
|
-
throw new Error("drivePlanReview: permissionModeAfter was passed for a task
|
|
1368
|
+
throw new Error("drivePlanReview: permissionModeAfter was passed for a task whose stored permission mode is not \"plan\" — the caller must refuse this before any state changes (see the plan_review route's pre-nonce gate)");
|
|
1368
1369
|
}
|
|
1369
|
-
const approvedMode = decision
|
|
1370
|
-
const
|
|
1371
|
-
const rebuildCtx = { ...ctx, body: resumeBody };
|
|
1370
|
+
const approvedMode = planReviewDecisionExitsPlan(decision) && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
|
|
1371
|
+
const rebuildCtx = approvedMode === undefined ? ctx : withPermissionModeRewritten(ctx, { ...ctx.body, permissionMode: approvedMode });
|
|
1372
1372
|
const resolved = await resolveResumeSpecOrBlocked(rebuildCtx, req, auth);
|
|
1373
1373
|
if (!resolved.ok)
|
|
1374
1374
|
return resolved.answer;
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -168,6 +168,14 @@ export interface CheckpointCtx {
|
|
|
168
168
|
* 写点只有 `buildCheckpointCtx`(同模块;新的持久化写);续跑腿的写回传原 ctx / 其展开 ⇒ 记号随行。体里没有 `allow` 的行不写它。
|
|
169
169
|
*/
|
|
170
170
|
permissionsAllowReading?: import("./checkpoint-ctx.js").PermissionsAllowReading;
|
|
171
|
+
/**
|
|
172
|
+
* 体里**模式词的读法记号**(闭集只有当前一词 `"entry_word"`):在场 = 这一行体的顶层 `permissionMode` 是这条 run 模式词唯一的源(提交入口盖过运维
|
|
173
|
+
* `AUTONOMY=plan` 的地板;`plan_review` 写回改它),续跑照它读。**服务端写、不来自请求体**。缺席(7.121.x 及以前写下的行 —— 那时运维的 plan 由治理层每段重钳)
|
|
174
|
+
* 而本部署此刻有入口地板、体的词被地板钳会变 ⇒ 续跑腿响亮拒、行字节不动(规则与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注第三问)。
|
|
175
|
+
* 写点两个(写模式词的人盖记号,同模块):`buildCheckpointCtx`(每一次新的持久化写都写)与 `withPermissionModeRewritten`(`plan_review` 改词的写回);
|
|
176
|
+
* 不改词的续跑腿写回传原 ctx ⇒ 记号随行。
|
|
177
|
+
*/
|
|
178
|
+
permissionModeReading?: import("./checkpoint-ctx.js").PermissionModeReading;
|
|
171
179
|
}
|
|
172
180
|
/** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
|
|
173
181
|
export interface DriveResumeArgs {
|
|
@@ -448,8 +456,8 @@ export interface RouteLegs {
|
|
|
448
456
|
drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined,
|
|
449
457
|
/** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
|
|
450
458
|
acceptEarly?: boolean,
|
|
451
|
-
/** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER
|
|
452
|
-
* 上有意义,缺席 ⇒ `"default"`)。语义与判据的唯一属主 = `resume-legs.ts` 的同名形参顶注。 */
|
|
459
|
+
/** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在把计划带出 plan 的决定
|
|
460
|
+
* —— `approve` / `edit` —— 上有意义,缺席 ⇒ `"default"`)。语义与判据的唯一属主 = `resume-legs.ts` 的同名形参顶注。 */
|
|
453
461
|
permissionModeAfter?: PlanApprovePermissionModeAfter): Promise<{
|
|
454
462
|
status: number;
|
|
455
463
|
body: object;
|
|
@@ -586,7 +594,7 @@ export interface RouteDeclFacts {
|
|
|
586
594
|
* 503 照旧罩着(它判的是「这台机能不能收写」,与方法无关,且必须在解析请求体之前)。 */
|
|
587
595
|
readonly methodDispatched?: true;
|
|
588
596
|
}
|
|
589
|
-
/**
|
|
597
|
+
/** **可分派行**多出来的列(S-146 第三刀):身份 + 它真认的方法(+ S-1281 的订阅类)。纯门行一列都没有(见下)。 */
|
|
590
598
|
interface RouteDeclIdentity {
|
|
591
599
|
/** **全表唯一**的行名(kebab 闭集词)。这是声明行的身份:域 handler 用
|
|
592
600
|
* `switch (match.decl.id)` 闭集分派(漏一口 = 编译红),所以它必须稳定、唯一、且与路径解耦
|
|
@@ -601,6 +609,14 @@ interface RouteDeclIdentity {
|
|
|
601
609
|
* ② 2 条「一条路径住着两只域」的跨域行按方法拆成两行各归各域(`/v1/tasks` = trace-usage 的 GET +
|
|
602
610
|
* tasks 的 POST;`/v1/memory/export` = memory-policy 的 GET + memory-bundle 的 POST)。 */
|
|
603
611
|
readonly methods: readonly RouteMethod[];
|
|
612
|
+
/**
|
|
613
|
+
* 「这条 GET 开的是一条**只读订阅**」—— 一条 SSE 读面(推送队列 / 账本 tail),它看着别人干活,自己不是活(S-1281)。
|
|
614
|
+
* 在场 ⇒ 分派器不把它记进在飞(`drainState.inflight()`:排空等的、`/health` 报的、SIGHUP 空闲窗与附着租约读的那一只),只记进附着
|
|
615
|
+
* (`attachedStreams()`:壳还挂在这只引擎上);排空进入那一拍(以及硬停起点)由分派器发一帧 `draining` 终帧后关掉它
|
|
616
|
+
* (`drainState.endSubscriptions`)。闭集 = 八条 GET SSE 读面,逐行理由见各行注;**唯一**一条「在飞腿的流」`POST /v1/tasks/stream`
|
|
617
|
+
* 不带它 —— 那条流就是 sync 腿本身(关流 = 断连语义),照旧算在飞。漏标一条订阅的方向是**多算在飞**(排空多等),不是少等。
|
|
618
|
+
*/
|
|
619
|
+
readonly subscription?: true;
|
|
604
620
|
}
|
|
605
621
|
/**
|
|
606
622
|
* **可分派行**:一条真路由。匹配式二选一 —— `path`(精确;标签**就是** path 自身,所以没有 label 列,
|
|
@@ -141,6 +141,9 @@ export declare function billableKindOf(url: string): BillableKind | undefined;
|
|
|
141
141
|
* `billable=false`(零模型工作),塞进那张名单会让它们连带吃 drain 与 roster-pending 两道 503 ——
|
|
142
142
|
* 而收编与规则读写在排空期/roster 未落时做完全无害。 */
|
|
143
143
|
export declare function isCredentialGatedRewrite(method: string, url: string): boolean;
|
|
144
|
+
/** S-1281:这次请求是不是一条**只读订阅**(`RouteDeclFacts.subscription` 那一列;方法也要对上 —— 订阅行恒只认 GET)。
|
|
145
|
+
* 分派器在 admission 那一拍(第一个 await 之前)同步判它:订阅不进在飞计数,只进附着集与订阅集。 */
|
|
146
|
+
export declare function isSubscriptionRequest(method: string, url: string): boolean;
|
|
144
147
|
/** 见 `RouteDeclFacts.methodDispatched` 的头注(单 URL 多方法端点)。 */
|
|
145
148
|
export declare function isMethodDispatchedSubmitPath(url: string): boolean;
|
|
146
149
|
/** 一次分派查找的结果:命中的行与它的参数,外加**这条路径上所有行**的方法并集(405 的 `Allow`)。 */
|
package/dist/http/route-table.js
CHANGED
|
@@ -111,6 +111,7 @@ const LABEL_BY_PATH = new Map(ROUTE_DECLARATIONS.flatMap((d) => ("path" in d ? [
|
|
|
111
111
|
const SHAPED_LABEL_DECLS = ROUTE_DECLARATIONS.filter((d) => !("path" in d) && typeof d.label === "string");
|
|
112
112
|
const BILLABLE_DECLS = ROUTE_DECLARATIONS.filter((d) => d.billable !== undefined);
|
|
113
113
|
const METHOD_DISPATCHED_DECLS = ROUTE_DECLARATIONS.filter((d) => d.methodDispatched === true);
|
|
114
|
+
const SUBSCRIPTION_DECLS = ROUTE_DECLARATIONS.filter(isDispatchDecl).filter((d) => d.subscription === true);
|
|
114
115
|
const CREDENTIAL_GATED_BY_METHOD = (() => {
|
|
115
116
|
const m = new Map();
|
|
116
117
|
for (const d of ROUTE_DECLARATIONS)
|
|
@@ -142,6 +143,9 @@ export function isCredentialGatedRewrite(method, url) {
|
|
|
142
143
|
const rows = CREDENTIAL_GATED_BY_METHOD.get(method.toUpperCase());
|
|
143
144
|
return rows !== undefined && rows.some((d) => declMatches(d, url));
|
|
144
145
|
}
|
|
146
|
+
export function isSubscriptionRequest(method, url) {
|
|
147
|
+
return SUBSCRIPTION_DECLS.some((d) => d.methods.includes(method) && matchDecl(d, url) !== null);
|
|
148
|
+
}
|
|
145
149
|
export function isMethodDispatchedSubmitPath(url) {
|
|
146
150
|
return METHOD_DISPATCHED_DECLS.some((d) => declMatches(d, url));
|
|
147
151
|
}
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
* 进入排空经 `drainState.enter`(收尾链装的唯一入口)。
|
|
7
7
|
* · 门序 = operator 面的既有口径(`routes/retention-ops.ts` 逐字):身份 → 授权 → 验型;billable=false,**刻意不吃排空 503**
|
|
8
8
|
* (排空中的重放必须能拿到 `already_draining` 回执,吃了就自锁)。
|
|
9
|
-
* ·
|
|
10
|
-
*
|
|
9
|
+
* · 判序:体坏 ⇒ 400 `request.field_invalid`(零状态变化)→ 没有收尾链(嵌入宿主 / 测试夹具)⇒ 501 `capability.drain_state_required`
|
|
10
|
+
* → 身份不符 ⇒ 409 `conflict.fleet_identity`(零状态变化;拒句点名不符的字段,不回显值)→ 入口:已在排空 / 硬停已在进行 ⇒ 200 回执
|
|
11
|
+
* (这一发的期限不会被采用);这一发会进入排空而 `deadlineAt` 离进入那一拍超过 `FLEET_LEASE_OUTAGE_MAX_MS`(`config.fleetLeaseOutageMaxMs`,
|
|
12
|
+
* 本口随入参声明的准入上界,`fleet/drain-entry.ts` 规则 2)⇒ 入口答拒、本口 400 `request.field_invalid`(零状态变化;拒句点名字段、上界的
|
|
13
|
+
* 席名与值,不回显请求带的期限)。
|
|
11
14
|
* · 首次 ⇒ 200 回执 `state:"draining"`;同一 `actionId`(或别的动作)再来 ⇒ 200 `already_draining`,回显第一次的 `since` / `deadlineAt`,不重置。
|
|
12
15
|
* · 人手形:`actionId` 自取(推荐 `manual:<uuid>`),身份照样四元比对 —— 非 fleet 成员不带 `expectedGeneration`(缺席对缺席);
|
|
13
16
|
* `instanceId` 在场才比(从本机 `/health` 读);不带 `scope` = 点名缺省 scope(多租引擎要带它自己的 `FLEET_SCOPE`)。
|
|
@@ -39,8 +39,20 @@ export async function handleAdminDrain(req, res, match, ctx) {
|
|
|
39
39
|
sendError(res, 409, "conflict.fleet_identity", `the drain request names a different instance than this engine (mismatched: ${mismatched.join(", ")}) — nothing was changed; compare with this engine's GET /health (worker comes from SEMA_REGISTRY_WORKER, generation from FLEET_GENERATION, scope from FLEET_SCOPE — unset, and a request without scope, mean "${fleetScopeOf(undefined)}")`);
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const answer = enter({ actionId: request.actionId, deadlineAt: request.deadlineAt, deadlineBoundMs: deps.config.fleetLeaseOutageMaxMs });
|
|
43
|
+
switch (answer.state) {
|
|
44
|
+
case "deadline_beyond_bound":
|
|
45
|
+
sendError(res, 400, "request.field_invalid", `the drain request's deadlineAt is further from now than FLEET_LEASE_OUTAGE_MAX_MS (${answer.boundMs} ms) on this engine's clock — the longest this engine stays up without serving; send a nearer deadline — nothing was changed`);
|
|
46
|
+
return;
|
|
47
|
+
case "draining":
|
|
48
|
+
case "already_draining":
|
|
49
|
+
sendJson(res, 200, answer);
|
|
50
|
+
return;
|
|
51
|
+
default: {
|
|
52
|
+
const unhandled = answer;
|
|
53
|
+
throw new Error(`drain entry: unknown answer ${JSON.stringify(unhandled)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
44
56
|
}
|
|
45
57
|
default:
|
|
46
58
|
return assertNever(match.decl.id);
|
|
@@ -151,6 +151,7 @@ export declare const APPROVALS_ASSISTANT_ROUTES: readonly [{
|
|
|
151
151
|
readonly id: "approvals-stream";
|
|
152
152
|
readonly path: "/v1/approvals/stream";
|
|
153
153
|
readonly methods: readonly ["GET"];
|
|
154
|
+
readonly subscription: true;
|
|
154
155
|
}, {
|
|
155
156
|
readonly id: "assistant-inbox";
|
|
156
157
|
readonly path: "/v1/assistant/inbox";
|
|
@@ -7,7 +7,7 @@ import { isParkedRunStatus } from "../../plugins/store-contracts.js";
|
|
|
7
7
|
import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
|
|
8
8
|
import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
|
|
9
9
|
import { MAX_APPROVAL_REASON_CHARS, projectQuestionAnswer, questionAnswerFromProjection } from "../../approval-hmac.js";
|
|
10
|
-
import { PLAN_APPROVE_PERMISSION_MODES_AFTER, isPlanApprovePermissionModeAfter, storedBodyPermissionMode } from "../../task-settings.js";
|
|
10
|
+
import { PLAN_APPROVE_PERMISSION_MODES_AFTER, isPlanApprovePermissionModeAfter, planReviewDecisionExitsPlan, storedBodyPermissionMode } from "../../task-settings.js";
|
|
11
11
|
import { MAX_APPROVAL_NONCE_CHARS } from "../../plugins/approval-nonce-store.js";
|
|
12
12
|
import { redactedPreview, redactSecrets } from "../../trace/redact.js";
|
|
13
13
|
import { redactRiskDescriptor } from "../../observability/run-terminal.js";
|
|
@@ -59,7 +59,7 @@ const DECISION_DOOR_FAMILY = {
|
|
|
59
59
|
"assistant-task-plan-review": {
|
|
60
60
|
accepted: PLAN_REVIEW_BODY_DECLARED_KEYS,
|
|
61
61
|
requestNoun: "plan-review-request",
|
|
62
|
-
costliestTypo: "a misspelled `permissionModeAfter` silently leaves this task
|
|
62
|
+
costliestTypo: "a misspelled `permissionModeAfter` silently leaves this task asking before every write after you approved its plan",
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
65
|
const SUBMIT_BODY_NEAR_MISS_KEYS = ["permissionMode"];
|
|
@@ -534,8 +534,8 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
534
534
|
sendError(res, 400, "request.field_invalid", `permissionModeAfter must be one of ${PLAN_APPROVE_PERMISSION_MODES_AFTER.map((m) => JSON.stringify(m)).join(" | ")} when present (the permission mode this task continues in once the plan is approved; absent = "default", which keeps asking before each write)`);
|
|
535
535
|
return;
|
|
536
536
|
}
|
|
537
|
-
if (decision
|
|
538
|
-
sendError(res, 400, "request.field_conflict", `permissionModeAfter is only valid with decision 'approve'
|
|
537
|
+
if (!planReviewDecisionExitsPlan(decision) && body.permissionModeAfter !== undefined) {
|
|
538
|
+
sendError(res, 400, "request.field_conflict", `permissionModeAfter is only valid with a decision that carries the plan out ('approve' or 'edit'; got '${decision}')`);
|
|
539
539
|
return;
|
|
540
540
|
}
|
|
541
541
|
if (body.reason !== undefined && typeof body.reason !== "string") {
|
|
@@ -610,7 +610,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
610
610
|
if (body.permissionModeAfter !== undefined) {
|
|
611
611
|
const storedCtx = plan.prepared.ctx;
|
|
612
612
|
if (storedCtx.body !== undefined && storedBodyPermissionMode(storedCtx.body) !== "plan") {
|
|
613
|
-
sendError(res, 400, "request.field_conflict", "permissionModeAfter does not apply to this task: it was not
|
|
613
|
+
sendError(res, 400, "request.field_conflict", "permissionModeAfter does not apply to this task: it was not started in plan mode (its stored permission mode is not \"plan\"), so approving the plan returns it to the mode it was in before planning");
|
|
614
614
|
return;
|
|
615
615
|
}
|
|
616
616
|
}
|
|
@@ -910,7 +910,7 @@ export async function streamApprovals(req, res, cs, live, scope, governance, pol
|
|
|
910
910
|
}
|
|
911
911
|
export const APPROVALS_ASSISTANT_ROUTES = [
|
|
912
912
|
{ id: "approvals-list", path: "/v1/approvals", methods: ["GET"] },
|
|
913
|
-
{ id: "approvals-stream", path: "/v1/approvals/stream", methods: ["GET"] },
|
|
913
|
+
{ id: "approvals-stream", path: "/v1/approvals/stream", methods: ["GET"], subscription: true },
|
|
914
914
|
{ id: "assistant-inbox", path: "/v1/assistant/inbox", methods: ["GET"] },
|
|
915
915
|
{ id: "assistant-tasks", path: "/v1/assistant/tasks", methods: ["GET"] },
|
|
916
916
|
{ id: "approval-decide", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/decide$/, label: "/v1/approvals/:id/decide", methods: ["POST"] },
|
|
@@ -4,7 +4,8 @@ import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
|
|
|
4
4
|
import { projectSealedKeyCapability } from "../../sealed-key.js";
|
|
5
5
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
6
6
|
import { taskWriteFaceRefusal } from "../../hosted-posture.js";
|
|
7
|
-
import {
|
|
7
|
+
import { leaderEntryRefusal, leaderRunEntry } from "../../leader/run-entry.js";
|
|
8
|
+
import { autonomyKeepsOpenWriteFace, autonomyModeFloor } from "../../runtime-governance.js";
|
|
8
9
|
import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
|
|
9
10
|
import { a2aInjectionHonored } from "../../task-a2a.js";
|
|
10
11
|
import { buildEmptyMcpProbeFace, buildMcpInjectionRefusal, buildMcpProbeCacheKey, buildMcpProbeInflightRefusal, buildMcpProbeRateKey, buildMcpProbeRateRefusal, createMcpProbeLocal, buildMcpProbeDeclarationRefusal, buildMcpProbeIncompleteRefusal, McpProbeIncomplete, MCP_PROBE_HTTP_STATUS, MCP_PROBE_MAX_INFLIGHT, MCP_PROBE_VERDICT_TIMEOUT_MS, createMcpProbeWalk, readMcpProbeBody, } from "../../capabilities/mcp-probe.js";
|
|
@@ -64,7 +65,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
67
|
const entitlementSource = centerEntitlementSourceWired(deps.config);
|
|
67
|
-
const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, principal: gatedPrincipal(req, deps.config), localDeny: deps.config.permissionsDisableAutoMode });
|
|
68
|
+
const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, floor: autonomyModeFloor(deps.config.autonomy), principal: gatedPrincipal(req, deps.config), localDeny: deps.config.permissionsDisableAutoMode });
|
|
68
69
|
if (autoModeFault !== undefined)
|
|
69
70
|
deps.logger?.warn("auto_mode_face_resolver_fault", { fault: autoModeFault });
|
|
70
71
|
const rulesLaneForCaller = facts.ruleLaneForCaller(deps, gatedPrincipal(req, deps.config)).available;
|
|
@@ -82,7 +83,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
82
83
|
? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "engine", hookFailureNotice: true, leaseClient: true, generationEcho: true }
|
|
83
84
|
: false,
|
|
84
85
|
workspace: facts.workspaceBrowse().available,
|
|
85
|
-
leader: facts.leaderLane(deps).available,
|
|
86
|
+
leader: facts.leaderLane(deps).available && leaderEntryRefusal(leaderRunEntry(deps.config.autonomy)) === undefined,
|
|
86
87
|
images: facts.imagePool(deps).available,
|
|
87
88
|
memory: false,
|
|
88
89
|
memoryWrite: false,
|
|
@@ -144,7 +145,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
144
145
|
skillToolLists: true,
|
|
145
146
|
projectContext: cwdHonored(deps.config),
|
|
146
147
|
callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
|
|
147
|
-
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined &&
|
|
148
|
+
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyKeepsOpenWriteFace(deps.config.autonomy),
|
|
148
149
|
executionLane: projectExecutionLaneCapability(deps.config),
|
|
149
150
|
mcpInjection: facts.mcpInjection(deps).available,
|
|
150
151
|
a2a: true,
|
|
@@ -14,6 +14,7 @@ export declare const FLEET_ROUTES: readonly [{
|
|
|
14
14
|
readonly id: "fleet-stream";
|
|
15
15
|
readonly path: "/v1/fleet/stream";
|
|
16
16
|
readonly methods: readonly ["GET"];
|
|
17
|
+
readonly subscription: true;
|
|
17
18
|
}];
|
|
18
19
|
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
19
20
|
export type FleetRouteId = RouteIdsOf<typeof FLEET_ROUTES>;
|
|
@@ -239,6 +239,6 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
|
|
|
239
239
|
});
|
|
240
240
|
}
|
|
241
241
|
export const FLEET_ROUTES = [
|
|
242
|
-
{ id: "fleet-stream", path: "/v1/fleet/stream", methods: ["GET"] },
|
|
242
|
+
{ id: "fleet-stream", path: "/v1/fleet/stream", methods: ["GET"], subscription: true },
|
|
243
243
|
];
|
|
244
244
|
//# sourceMappingURL=fleet.js.map
|
|
@@ -55,6 +55,7 @@ export declare const IMAGES_ROUTES: readonly [{
|
|
|
55
55
|
readonly pattern: RegExp;
|
|
56
56
|
readonly label: "/v1/images/bakes/:id/events";
|
|
57
57
|
readonly methods: readonly ["GET"];
|
|
58
|
+
readonly subscription: true;
|
|
58
59
|
}, {
|
|
59
60
|
readonly id: "image-bake-cancel";
|
|
60
61
|
readonly pattern: RegExp;
|