@sema-agent/server 7.95.1 → 7.97.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/USAGE.md +10 -5
- package/deploy/sema-up/README.md +8 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -0
- package/deploy/sema-up/chart/templates/server.yaml +3 -0
- package/deploy/sema-up/lib/lanes.json +56 -0
- package/deploy/sema-up/lib/lanes.py +179 -0
- package/deploy/sema-up/lib/parse-machines.py +7 -1
- package/deploy/sema-up/machines.example.yaml +5 -2
- package/deploy/sema-up/preflight.sh +7 -4
- package/deploy/sema-up/sema-up.sh +201 -24
- package/deploy/sema-up/stack/compose.yaml +4 -0
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +2 -0
- package/dist/approval.d.ts +28 -0
- package/dist/approval.js +24 -0
- package/dist/boot/budget-tracing.d.ts +1 -0
- package/dist/boot/budget-tracing.js +3 -2
- package/dist/boot/config-center.js +2 -5
- package/dist/boot/coordinators.js +2 -2
- package/dist/boot/deferred-sandbox-path-env.d.ts +5 -2
- package/dist/boot/deferred-sandbox-path-env.js +3 -1
- package/dist/boot/memory-boundary.d.ts +17 -2
- package/dist/boot/memory-boundary.js +11 -0
- package/dist/boot/reapers.d.ts +3 -1
- package/dist/boot/reapers.js +2 -2
- package/dist/boot/resolve-spec.js +6 -6
- package/dist/boot/runner-deps.d.ts +2 -1
- package/dist/boot/session-faces.d.ts +4 -1
- package/dist/boot/session-faces.js +5 -5
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-04-stores.js +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -0
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +10 -14
- package/dist/boot/stage-08-reapers.d.ts +2 -3
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stores.js +5 -11
- package/dist/capabilities/execution-lane.d.ts +5 -7
- package/dist/capabilities/execution-lane.js +2 -1
- package/dist/capabilities/memory-notice.d.ts +30 -12
- package/dist/capabilities/memory-notice.js +8 -1
- package/dist/capabilities/skills.d.ts +14 -0
- package/dist/capabilities/skills.js +4 -0
- package/dist/config-catalog.js +1 -0
- package/dist/config-types.d.ts +8 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.js +13 -12
- package/dist/env-facts.js +2 -4
- package/dist/execution-lane-caps.d.ts +36 -6
- package/dist/execution-lane-caps.js +4 -0
- package/dist/file-history-capture.d.ts +65 -0
- package/dist/file-history-capture.js +21 -0
- package/dist/hosted-posture.d.ts +1 -1
- package/dist/http/resume-legs.d.ts +14 -4
- package/dist/http/resume-legs.js +26 -17
- package/dist/http/route-ctx.d.ts +35 -9
- package/dist/http/routes/approvals-assistant.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +16 -6
- package/dist/http/routes/capabilities.js +4 -1
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/session-sync.js +6 -1
- package/dist/http/routes/tasks.js +2 -2
- package/dist/http/server.d.ts +4 -1
- package/dist/http/server.js +1 -1
- package/dist/http/verify-rounds.d.ts +1 -1
- package/dist/memory-scope.d.ts +6 -2
- package/dist/memory-scope.js +3 -2
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/observability/run-terminal.js +1 -0
- package/dist/run-local.js +4 -5
- package/dist/session-sync.d.ts +4 -1
- package/dist/session-sync.js +2 -2
- package/dist/task-cwd.d.ts +9 -3
- package/dist/task-cwd.js +3 -1
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/project.d.ts +11 -0
- package/dist/trace/project.js +1 -0
- package/package.json +4 -3
package/dist/config.js
CHANGED
|
@@ -18,7 +18,7 @@ import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat,
|
|
|
18
18
|
import {} from "./posture-source.js";
|
|
19
19
|
import { hostedPrincipalRefusal } from "./hosted-posture.js";
|
|
20
20
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
21
|
-
import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
|
|
21
|
+
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf } from "./execution-lane-caps.js";
|
|
22
22
|
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
23
23
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
24
24
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
@@ -197,7 +197,7 @@ let EXEC_LANE_NOTICE;
|
|
|
197
197
|
function buildExecLaneNotice(remoteExec) {
|
|
198
198
|
const explicit = process.env.REMOTE_EXEC !== undefined && process.env.REMOTE_EXEC !== "";
|
|
199
199
|
return {
|
|
200
|
-
lane: remoteExec?.provider
|
|
200
|
+
lane: executionLaneOf(remoteExec?.provider),
|
|
201
201
|
source: explicit ? "explicit-env" : process.env.CONFIG_PROVIDER === "local" ? "default-local-provider" : "default-unset",
|
|
202
202
|
};
|
|
203
203
|
}
|
|
@@ -450,6 +450,15 @@ export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
|
|
|
450
450
|
export const configLkgEnabled = () => boolEnvWithLegacyNegated("CONFIG_LKG_ENABLED", "CONFIG_LKG_DISABLED", true);
|
|
451
451
|
export const hostBackgroundShellEnabled = () => boolEnvWithLegacyNegated("HOST_BG_ENABLED", "HOST_BG_DISABLED", true);
|
|
452
452
|
export const hostExecSpoolEnabled = () => boolEnvWithLegacyNegated("HOST_EXEC_SPOOL_ENABLED", "HOST_EXEC_SPOOL_DISABLED", true);
|
|
453
|
+
export const REQUIRED_ENVS_BY_LANE = {
|
|
454
|
+
e2b: ["E2B_API_KEY"],
|
|
455
|
+
k8s: ["K8S_SANDBOX_IMAGE"],
|
|
456
|
+
ssh: ["SSH_HOST", "SSH_USER", "SSH_PRIVATE_KEY"],
|
|
457
|
+
adb: ["ADB_SERIAL"],
|
|
458
|
+
host: [],
|
|
459
|
+
"local-docker": ["DOCKER_IMAGE"],
|
|
460
|
+
device: [],
|
|
461
|
+
};
|
|
453
462
|
function parseReadDenyPatternsEnv(raw) {
|
|
454
463
|
let arr;
|
|
455
464
|
try {
|
|
@@ -767,6 +776,7 @@ function parseStoreDomain(ctx) {
|
|
|
767
776
|
snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
|
|
768
777
|
snapshotBlobAllowSql: boolEnv("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", false),
|
|
769
778
|
fileHistory: parseFileHistoryRetention(),
|
|
779
|
+
fileHistoryCapture: boolEnv("FILE_HISTORY_CAPTURE", true),
|
|
770
780
|
sendUserFile: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT) && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY
|
|
771
781
|
? {
|
|
772
782
|
endpoint: (process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT),
|
|
@@ -1618,15 +1628,6 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1618
1628
|
{
|
|
1619
1629
|
const lane = process.env.REMOTE_EXEC;
|
|
1620
1630
|
if (lane !== undefined && lane !== "") {
|
|
1621
|
-
const REQUIRED_ENVS_BY_LANE = {
|
|
1622
|
-
e2b: ["E2B_API_KEY"],
|
|
1623
|
-
k8s: ["K8S_SANDBOX_IMAGE"],
|
|
1624
|
-
ssh: ["SSH_HOST", "SSH_USER", "SSH_PRIVATE_KEY"],
|
|
1625
|
-
adb: ["ADB_SERIAL"],
|
|
1626
|
-
host: [],
|
|
1627
|
-
"local-docker": ["DOCKER_IMAGE"],
|
|
1628
|
-
device: [],
|
|
1629
|
-
};
|
|
1630
1631
|
const required = Object.entries(REQUIRED_ENVS_BY_LANE).find(([word]) => word === lane)?.[1];
|
|
1631
1632
|
if (required === undefined) {
|
|
1632
1633
|
throw new Error(`REMOTE_EXEC="${lane}" is not an execution lane — valid: ${Object.keys(REQUIRED_ENVS_BY_LANE).join(" | ")}. ` +
|
|
@@ -2038,7 +2039,7 @@ function parseA2aServeSkills(raw) {
|
|
|
2038
2039
|
const STORE_GROUP_KEYS = [
|
|
2039
2040
|
"sessionBackend", "sessionCacheTtlSec", "storeProbeIntervalMs", "storeLockWaitMs", "dbBackend", "dbBackendExplicit", "localDataRoot",
|
|
2040
2041
|
"runStoreStrictHydrate",
|
|
2041
|
-
"tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "sendUserFile",
|
|
2042
|
+
"tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "fileHistoryCapture", "sendUserFile",
|
|
2042
2043
|
];
|
|
2043
2044
|
const MODEL_PLANE_GROUP_KEYS = [
|
|
2044
2045
|
"gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "gatewayMaxRetries", "anthropic", "resilience", "model", "envModel", "models",
|
package/dist/env-facts.js
CHANGED
|
@@ -158,11 +158,9 @@ export async function sweepStaleScratchpads(localDataRoot, opts) {
|
|
|
158
158
|
return removed;
|
|
159
159
|
}
|
|
160
160
|
export function egressForRemoteExec(remoteExec) {
|
|
161
|
-
if (
|
|
162
|
-
return undefined;
|
|
163
|
-
if (remoteExec.provider === "e2b")
|
|
161
|
+
if (remoteExec?.provider === "e2b")
|
|
164
162
|
return remoteExec.allowInternetAccess === false ? "none" : "full";
|
|
165
|
-
if (remoteExec
|
|
163
|
+
if (remoteExec?.provider === "k8s")
|
|
166
164
|
return "full";
|
|
167
165
|
return undefined;
|
|
168
166
|
}
|
|
@@ -45,12 +45,18 @@ export interface ExecutionLaneCaps {
|
|
|
45
45
|
/** 记忆持久面的**自动**表态(部署可用 `MEMORY_PERSISTENCE_CAPABLE` 显式覆盖,那是旋钮不是车道位)。
|
|
46
46
|
* 判别式:`boot/memory-boundary.ts` 的 `effectiveMemoryPersistenceCapable`。 */
|
|
47
47
|
readonly memoryPersistenceCapable: LaneCapability;
|
|
48
|
-
/** 文件平面是否就在 worker 本机(= `hostSemanticsLane`)
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
/** 文件平面是否就在 worker 本机(= `hostSemanticsLane`)。🔴 S-602 起**消费点读本位**,不再各写判别式
|
|
49
|
+
* (改前 `resolve-spec` / `stores` / `memory-scope` / `deferred-sandbox-path-env` 各写一份
|
|
50
|
+
* `lane === undefined || lane === "host"` 的同义形,分叉过就是记忆边界判错盘)。读本位的真装配点:
|
|
51
|
+
* `boot/deferred-sandbox-path-env.ts` 的 `isSandboxPathAdjudicationLane`(resolve-spec 写门 / parked 赎回腿 /
|
|
52
|
+
* 执行环境装配三处的唯一属主)、`memory-scope.ts` 的 `memoryEngineRemoteLanePosture`、
|
|
53
|
+
* `boot/memory-boundary.ts` 的 `buildMemoryBoundaryLaneFacts`(stores)、`capabilities/memory-notice.ts` 的
|
|
54
|
+
* `buildNoticeLaneFacts`(resolve-spec)。⚠️ `in-process` 在本位上是 YES(hooks/进程内读确实在本机盘上),
|
|
55
|
+
* 但它**没有手** —— 问「手在不在本机盘上」要读 {@link handsOnWorkerFilePlane},不是只读本位。 */
|
|
51
56
|
readonly hostFilePlane: LaneCapability;
|
|
52
|
-
/** core 的 `isRemoteExecutionEnv` 对本车道的判决(
|
|
53
|
-
*
|
|
57
|
+
/** core 的 `isRemoteExecutionEnv` 对本车道的判决(鸭子类型,**含 host**)= core 拿没拿到执行环境 = 手带挂没挂。
|
|
58
|
+
* 5.26.0 起它决定 `# Memory` 写指令撤不撤。🔴 S-602 起消费点读本位(改前各写 `config.remoteExec !== undefined`);
|
|
59
|
+
* 对账测试拿 core 自己的 `isRemoteExecutionEnv` 问**真 adapter 实例**来核本位的值。 */
|
|
54
60
|
readonly coreRemoteExecutionEnv: LaneCapability;
|
|
55
61
|
/**
|
|
56
62
|
* S-354 / core 7.20.0([ref]):这条车道的适配器**声明**了 `capabilities.canonicalPathAuthoritative`
|
|
@@ -103,13 +109,37 @@ export declare const ALL_EXECUTION_LANES: readonly ExecutionLane[];
|
|
|
103
109
|
*/
|
|
104
110
|
export declare const REMOTE_EXEC_PROVIDER_WORDS: readonly RemoteExecProvider[];
|
|
105
111
|
/**
|
|
106
|
-
* `config.remoteExec?.provider` →
|
|
112
|
+
* `config.remoteExec?.provider` → 车道词。**全仓唯一**的缺席归一读口(S-602 门 D:`?? "in-process"` 只许住在这里,
|
|
113
|
+
* `?? "host"` 与 `lane === undefined || lane === "host"` 同义折零容忍)。
|
|
107
114
|
*
|
|
108
115
|
* · `undefined`(REMOTE_EXEC 未设)⇒ `"in-process"`,那是真车道不是缺席;
|
|
109
116
|
* · 认不出的词 ⇒ `undefined`,**调用方必须 fail-closed**(禁静默按某个默认车道处理)。生产链上这一臂
|
|
110
117
|
* 不可达:`loadConfig` 的 [ref] 拒启门先咬未知词;它存在是为了消费点自卫时有个响亮出口。
|
|
118
|
+
*
|
|
119
|
+
* 两个重载 = 类型收窄([ref] §2.2 A):喂**已装配的** config 形(`RemoteExecProvider | undefined`)⇒
|
|
120
|
+
* 返回型里没有 `undefined`,调用点不必为一条不可达的臂写兜底(兜底写多了就是下一处折词);喂开放字符串
|
|
121
|
+
* (测试、外部输入)⇒ 返回含 `undefined`,逼调用点表态。第一个重载的成立依据是**编译期**的:
|
|
122
|
+
* `RemoteExecProvider ⊂ ExecutionLane`(`ExecutionLane` 由它派生),而 {@link EXECUTION_LANE_WORDS} 是
|
|
123
|
+
* `Record<ExecutionLane, true>` —— 少一个词编译红,所以对 `RemoteExecProvider` 的每一个值 `isExecutionLane` 恒真。
|
|
111
124
|
*/
|
|
125
|
+
export declare function executionLaneOf(provider: RemoteExecProvider | undefined): ExecutionLane;
|
|
112
126
|
export declare function executionLaneOf(provider: string | undefined): ExecutionLane | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* S-602 —— 「这条车道的**手**就在本 worker 的盘上吗」。**两位合取,都读表**(不手抄):
|
|
129
|
+
* · `coreRemoteExecutionEnv` —— core 拿到了执行环境 = 手带真挂上(`in-process` 答 NO:core 落 `StubExecutionEnv`);
|
|
130
|
+
* · `hostFilePlane` —— 那套手的文件平面就是本 worker 的盘(沙箱 / 远端 / 设备车道答 NO)。
|
|
131
|
+
* 今天只有 `host` 两位同 YES。
|
|
132
|
+
*
|
|
133
|
+
* 🔴 **只读 `hostFilePlane` 是错的**:`in-process` 那一位是 YES(hooks / 进程内读确实在本机盘上),而它
|
|
134
|
+
* **没有手**。clay 裁 [ref] ③ 的字面写「判据读 `hostFilePlane`」,意图是同一句的「无手不挂」—— 照字面
|
|
135
|
+
* 实现会让无手部署**继续**挂 SendUserFile,与意图相反;本函数按意图实现(收车档 che-MA 写明)。
|
|
136
|
+
*
|
|
137
|
+
* 三个消费点问的都是这一句,**同一只函数**,不许各写一份 `=== "host"`:
|
|
138
|
+
* ① `task-cwd.ts` 的 `toolsRunOnThisHost` —— wire `executionLane.toolsOnThisHost` + 出厂技能 `baseDir`;
|
|
139
|
+
* ② `boot/coordinators.ts` 的 SendUserFile 本机腿(经 ①;[ref] ③「无手不挂」);
|
|
140
|
+
* ③ `approval.ts` 的 `buildNoResponderBootWarns` —— 写门的 ask 只在「有手写本机盘」时才会发生。
|
|
141
|
+
*/
|
|
142
|
+
export declare function handsOnWorkerFilePlane(lane: ExecutionLane): boolean;
|
|
113
143
|
/** 把一位能力在**具体部署**上落成布尔:租户面是唯一入参(旋钮由消费点自己再合取)。
|
|
114
144
|
* 穷举 switch,无 default 臂 —— {@link LaneCapability} 加词而此处不加臂 = 编译红。 */
|
|
115
145
|
export declare function laneCapabilityHolds(cap: LaneCapability, deployment: {
|
|
@@ -162,6 +162,10 @@ export function executionLaneOf(provider) {
|
|
|
162
162
|
return "in-process";
|
|
163
163
|
return isExecutionLane(provider) ? provider : undefined;
|
|
164
164
|
}
|
|
165
|
+
export function handsOnWorkerFilePlane(lane) {
|
|
166
|
+
const caps = executionLaneCaps(lane);
|
|
167
|
+
return caps.hostFilePlane === "supported" && caps.coreRemoteExecutionEnv === "supported";
|
|
168
|
+
}
|
|
165
169
|
export function laneCapabilityHolds(cap, deployment) {
|
|
166
170
|
switch (cap) {
|
|
167
171
|
case "supported":
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-589 —— 文件历史的**装配单点**与能力词 `fileHistoryCapture` 的闭集(叶模块:只 type-import store-backend)。
|
|
3
|
+
*
|
|
4
|
+
* ## 病(修前)
|
|
5
|
+
* `boot/budget-tracing.ts` 只要后端有文件历史店就无条件交给 core(`RunnerDeps.fileHistoryStore`)。core 的首次触碰
|
|
6
|
+
* 捕获是**进程级**的:接了店就恒捕获,没有任何单会话 / 单 run 的杠杆;全仓相关旋钮只有保留条数
|
|
7
|
+
* `FILE_HISTORY_RETENTION_KEEP`。于是用户在客户端关掉「文件检查点」关掉的只是提示,文件字节照旧进
|
|
8
|
+
* `file_history_*` —— 隐私 / 留存轴上的用户面语义谎言。
|
|
9
|
+
*
|
|
10
|
+
* ## 一处判定,两张脸(按动词类别切分,不是特例)
|
|
11
|
+
* 「这台部署有没有文件历史店」只在 {@link assembleFileHistory} 判一次,部署旋钮 `FILE_HISTORY_CAPTURE` 在这一处合取:
|
|
12
|
+
* · **捕获脸** `store` = 后端 ∧ 旋钮。喂 core 的首次触碰捕获(`RunnerDeps.fileHistoryStore`)、rewind 族能力位
|
|
13
|
+
* (`rewindFiles` / `restoreFiles` / `rewindFilesTo` / `sessionSync`)与本模块的能力词、2c sync 路由族。
|
|
14
|
+
* · **擦除脸** `erasure` = 后端在即在,类型上**只有删除类动词**。喂 E21 在线删除(`boot/session-faces.ts`)与
|
|
15
|
+
* 孤儿 blob GC(`boot/reapers.ts`)。
|
|
16
|
+
* 规则一句话:**旋钮只管写入,从不管擦除**。关掉捕获的部署照样要能删掉已经捕获的文件字节 —— 否则翻旋钮之后
|
|
17
|
+
* `DELETE /v1/sessions/:id` 会回成功却把存量文件内容留在库里,而留存车道(`plugins/retention-store-sql.ts` 的
|
|
18
|
+
* 原生 SQL 删六张 `file_history_*` 表,本来就不看店在不在)与在线删除从此不再等价(E21 两个执行体分家)。
|
|
19
|
+
* ⚠️ 关掉 ≠ 删掉:翻旋钮**不**做任何批量删除。已捕获的历史照既有的会话删除 / 留存规则清理;在旋钮关着的
|
|
20
|
+
* 部署上它也不会再被读用(rewind / restore / sync 都随捕获脸一起缺席)。
|
|
21
|
+
*
|
|
22
|
+
* ## 能力词为什么是闭集两词、没有 `on-optional`
|
|
23
|
+
* cli 要的是「`/config` 那个开关该不该出现」。present-iff 布尔在今天的**每一台**部署上都只能回答「关不掉」
|
|
24
|
+
* (core 的捕获是进程级的)⇒ 出一格三词闭集的前两词:`off`(这台部署根本不捕获,也没有 rewind)与
|
|
25
|
+
* `on-always`(捕获中,且无法按会话关闭)。第三词 `on-optional`(「本会话可以不捕获」)要等 core 收一个
|
|
26
|
+
* 单会话 / 单 run 的不捕获位,**在那之前刻意留空**:预先声明一个结构上不可达的词,等于给客户端一个永远等不到的分支。
|
|
27
|
+
*/
|
|
28
|
+
import type { ServiceFileHistoryStore, StoreBackend } from "./plugins/store-backend.js";
|
|
29
|
+
/** 能力词闭集的单点属主(`Record` 锁步:联合加词而这里不补 ⇒ 编译红;词表数组由它派生,不手抄)。 */
|
|
30
|
+
declare const FILE_HISTORY_CAPTURE_WORD_SET: {
|
|
31
|
+
readonly off: true;
|
|
32
|
+
readonly "on-always": true;
|
|
33
|
+
};
|
|
34
|
+
/** `GET /v1/capabilities` 的 `fileHistoryCapture` 词。 */
|
|
35
|
+
export type FileHistoryCapture = keyof typeof FILE_HISTORY_CAPTURE_WORD_SET;
|
|
36
|
+
/** 闭集的全部词(契约文档门 / 测试据此对表)。 */
|
|
37
|
+
export declare const FILE_HISTORY_CAPTURE_WORDS: readonly FileHistoryCapture[];
|
|
38
|
+
/** 擦除脸 —— 物理店上**只有删除类**的那几个动词(类型收窄 = 结构上拿它捕获不了、导出不了、还原不了)。
|
|
39
|
+
* `reap` 是 core 契约必填(local File 店没有整 scope 的 `deleteBySession`,E21 在那条车道上退回 `reap(scope, [])`);
|
|
40
|
+
* `deleteBySession` / `sweepOrphanBlobs` 是 SQL 双生的服务端额外动词。 */
|
|
41
|
+
export type FileHistoryErasureFace = Pick<ServiceFileHistoryStore, "reap" | "deleteBySession" | "sweepOrphanBlobs">;
|
|
42
|
+
/** 装配结果:两张脸。 */
|
|
43
|
+
export interface FileHistoryAssembly {
|
|
44
|
+
/** 捕获脸:在场 ⟺ 后端在 ∧ `FILE_HISTORY_CAPTURE`。 */
|
|
45
|
+
readonly store: ServiceFileHistoryStore | undefined;
|
|
46
|
+
/** 擦除脸:在场 ⟺ 后端在(与旋钮无关)。 */
|
|
47
|
+
readonly erasure: FileHistoryErasureFace | undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* **唯一**的装配判定。`capture` = 部署旋钮 `FILE_HISTORY_CAPTURE`(`config.fileHistoryCapture`)。
|
|
51
|
+
*
|
|
52
|
+
* 形照 `execution-lane-caps.ts` 的穷举 switch(无 default 臂):意图词加一个(将来的 `on-optional`)而这里不加臂
|
|
53
|
+
* ⇒ 编译红 —— 加臂的那一刻就是决定「这一词下捕获脸接不接、core 那一位单会话不捕获位怎么递」的地方。
|
|
54
|
+
*/
|
|
55
|
+
export declare function assembleFileHistory(backend: StoreBackend | undefined, capture: boolean): FileHistoryAssembly;
|
|
56
|
+
/**
|
|
57
|
+
* 能力词 = 捕获脸在不在(读**装配结果**,不照旋钮字面回声:后端本来就没有这只店的部署同样是 `off`)。
|
|
58
|
+
*
|
|
59
|
+
* 与 `rewindFiles` 读的是**同一个判定**(`deps.fileHistoryStore` 在场),今天两位同构。分叉条件(写在这里免得
|
|
60
|
+
* 将来有人把两位当成一回事):core 收了单会话不捕获位之后,本词在捕获脸在场时会变成 `on-optional`,而
|
|
61
|
+
* `rewindFiles` 仍是 true —— 它回答的是「这台部署能不能还原」,本词回答的是「捕获关不关得掉」。
|
|
62
|
+
*/
|
|
63
|
+
export declare function fileHistoryCaptureOf(captureFace: object | undefined): FileHistoryCapture;
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=file-history-capture.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const FILE_HISTORY_CAPTURE_WORD_SET = {
|
|
2
|
+
off: true,
|
|
3
|
+
"on-always": true,
|
|
4
|
+
};
|
|
5
|
+
export const FILE_HISTORY_CAPTURE_WORDS = Object.keys(FILE_HISTORY_CAPTURE_WORD_SET);
|
|
6
|
+
export function assembleFileHistory(backend, capture) {
|
|
7
|
+
if (backend === undefined)
|
|
8
|
+
return { store: undefined, erasure: undefined };
|
|
9
|
+
const physical = backend.fileHistory();
|
|
10
|
+
const intent = capture ? "on-always" : "off";
|
|
11
|
+
switch (intent) {
|
|
12
|
+
case "off":
|
|
13
|
+
return { store: undefined, erasure: physical };
|
|
14
|
+
case "on-always":
|
|
15
|
+
return { store: physical, erasure: physical };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function fileHistoryCaptureOf(captureFace) {
|
|
19
|
+
return captureFace === undefined ? "off" : "on-always";
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=file-history-capture.js.map
|
package/dist/hosted-posture.d.ts
CHANGED
|
@@ -151,7 +151,7 @@ export type TaskHostPostureScope = HostedPostureInput & Readonly<Pick<ServiceCon
|
|
|
151
151
|
*
|
|
152
152
|
* 🔴 **为什么这一条不拒**(初版给了 422,当批撤回):这个键**零授权** —— 引擎只把它渲给模型,没有任何
|
|
153
153
|
* 裁决腿读它(`http/wire-types.ts` 的字段注逐字,来源是 core 的 `ApproverPosture` 头注)。而续跑腿
|
|
154
|
-
* **逐字重放盘上的体**,调用方**删不掉**它 ⇒ 一条 park
|
|
154
|
+
* **逐字重放盘上的体**,调用方**删不掉**它 ⇒ 一条 park 会被永久砖死;plan_review 驱动段(`drivePlanReview`)上更贵:
|
|
155
155
|
* nonce 先烧、拒在其后才抛,运维重签再撞同一个拒 = 无限循环。本仓对这一类的成文结论逐字是
|
|
156
156
|
* 「**砖死 resume 比降级糟**」(`boot/resolve-spec.ts` 顶部那段列了三处先例)。
|
|
157
157
|
* ⇒ 把本批最硬的处置给族里**危害最小**的成员,方向是错的。
|
|
@@ -17,7 +17,7 @@ import type { DecideBinding } from "./wire-types.js";
|
|
|
17
17
|
import type { CheckpointStoreFull } from "../plugins/store-backend.js";
|
|
18
18
|
import { type HostDecision } from "../host-decision.js";
|
|
19
19
|
import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
|
|
20
|
-
import type { RouteCtxBase, DriveResumeArgs } from "./route-ctx.js";
|
|
20
|
+
import type { RouteCtxBase, DriveResumeArgs, PreparedPlanReview } from "./route-ctx.js";
|
|
21
21
|
import { type TrustedRunInternals } from "./durable-run-leg.js";
|
|
22
22
|
import type { RunRegistry, RouteHelpers, RouteLegs } from "./route-ctx.js";
|
|
23
23
|
import type { createRequestGuards } from "./request-guards.js";
|
|
@@ -67,13 +67,23 @@ export declare function createResumeLegs(ctx: {
|
|
|
67
67
|
status: number;
|
|
68
68
|
body: object;
|
|
69
69
|
}>;
|
|
70
|
-
|
|
70
|
+
preparePlanReview: (sessionId: string, log?: {
|
|
71
71
|
taskId?: string;
|
|
72
72
|
principalPresent?: boolean;
|
|
73
|
-
},
|
|
73
|
+
}, decider?: {
|
|
74
74
|
principal?: string;
|
|
75
75
|
explicitOperator: boolean;
|
|
76
|
-
}
|
|
76
|
+
}) => Promise<{
|
|
77
|
+
readonly ok: false;
|
|
78
|
+
readonly answer: {
|
|
79
|
+
status: number;
|
|
80
|
+
body: object;
|
|
81
|
+
};
|
|
82
|
+
} | {
|
|
83
|
+
readonly ok: true;
|
|
84
|
+
readonly prepared: PreparedPlanReview;
|
|
85
|
+
}>;
|
|
86
|
+
drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
|
|
77
87
|
status: number;
|
|
78
88
|
body: object;
|
|
79
89
|
}>;
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -64,6 +64,9 @@ function checkpointRowRedeemableElsewhere(reason) {
|
|
|
64
64
|
return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
|
|
65
65
|
}
|
|
66
66
|
const NON_BINDING_INVALID_OUTCOME_FIELDS = new Set(["hostDecision", "approver"]);
|
|
67
|
+
function notResumableAnswer() {
|
|
68
|
+
return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
|
|
69
|
+
}
|
|
67
70
|
export function createResumeLegs(ctx) {
|
|
68
71
|
const { deps, runnerFor, idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters, readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk, prepareSpec, finalizeTaskResult, ownerCostQuotaDenied, ownerFleetLeaseDenied, usageWindowDenialFor } = ctx;
|
|
69
72
|
const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
|
|
@@ -458,7 +461,7 @@ export function createResumeLegs(ctx) {
|
|
|
458
461
|
if (taskId && deps.runStore) {
|
|
459
462
|
claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId, deps.instanceId ?? "default");
|
|
460
463
|
if (!claimedRow)
|
|
461
|
-
return
|
|
464
|
+
return notResumableAnswer();
|
|
462
465
|
markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
|
|
463
466
|
fleetPub?.onStart();
|
|
464
467
|
const rs = deps.runStore;
|
|
@@ -1179,8 +1182,9 @@ export function createResumeLegs(ctx) {
|
|
|
1179
1182
|
...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
|
|
1180
1183
|
});
|
|
1181
1184
|
}
|
|
1182
|
-
async function
|
|
1185
|
+
async function preparePlanReview(sessionId, log, decider) {
|
|
1183
1186
|
const cs = deps.checkpointStore;
|
|
1187
|
+
const refuse = (answer) => ({ ok: false, answer });
|
|
1184
1188
|
const log404 = (r) => deps.logger?.info?.("decide_404", {
|
|
1185
1189
|
reason: r,
|
|
1186
1190
|
...(log?.taskId !== undefined ? { taskId: log.taskId } : { sessionId }),
|
|
@@ -1192,7 +1196,7 @@ export function createResumeLegs(ctx) {
|
|
|
1192
1196
|
const noPending = { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
|
|
1193
1197
|
if (!token) {
|
|
1194
1198
|
log404("no-pending-checkpoint");
|
|
1195
|
-
return noPending;
|
|
1199
|
+
return refuse(noPending);
|
|
1196
1200
|
}
|
|
1197
1201
|
if (decider !== undefined && !decider.explicitOperator) {
|
|
1198
1202
|
const peeked = await cs
|
|
@@ -1202,7 +1206,7 @@ export function createResumeLegs(ctx) {
|
|
|
1202
1206
|
const peekedOwner = peeked != null ? decodeCheckpointScope(peeked) : undefined;
|
|
1203
1207
|
if (peekedOwner !== undefined && peekedOwner !== decider.principal) {
|
|
1204
1208
|
deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "pending row is foreign to the verified caller — owner gate ahead of the row load" });
|
|
1205
|
-
return noPending;
|
|
1209
|
+
return refuse(noPending);
|
|
1206
1210
|
}
|
|
1207
1211
|
}
|
|
1208
1212
|
let cp;
|
|
@@ -1212,40 +1216,45 @@ export function createResumeLegs(ctx) {
|
|
|
1212
1216
|
catch (e) {
|
|
1213
1217
|
if (!(await mayDiscloseRowError(cs, token, decider))) {
|
|
1214
1218
|
deps.logger?.warn?.("plan_review_unreadable_row_withheld", { sessionId, note: "row unreadable and the caller's ownership of THAT token is unproven — folded into the uniform 404" });
|
|
1215
|
-
return noPending;
|
|
1219
|
+
return refuse(noPending);
|
|
1216
1220
|
}
|
|
1217
|
-
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
1221
|
+
return refuse({ status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } });
|
|
1218
1222
|
}
|
|
1219
1223
|
if (!cp) {
|
|
1220
1224
|
log404("checkpoint-missing");
|
|
1221
1225
|
if (!(await mayDiscloseRowError(cs, token, decider)))
|
|
1222
|
-
return noPending;
|
|
1223
|
-
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
1226
|
+
return refuse(noPending);
|
|
1227
|
+
return refuse({ status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } });
|
|
1224
1228
|
}
|
|
1225
1229
|
if (decider !== undefined && !decider.explicitOperator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== decider.principal) {
|
|
1226
1230
|
deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
|
|
1227
|
-
return noPending;
|
|
1231
|
+
return refuse(noPending);
|
|
1228
1232
|
}
|
|
1229
1233
|
const gateKind = cp.gate?.kind;
|
|
1230
1234
|
if (gateKind !== PLAN_REVIEW_GATE_KIND) {
|
|
1231
|
-
return {
|
|
1235
|
+
return refuse({
|
|
1232
1236
|
status: 409,
|
|
1233
1237
|
body: {
|
|
1234
1238
|
error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a plan_review — a tool approval must be decided via POST /v1/approvals/:id/decide`,
|
|
1235
1239
|
errorCode: "gate_not_plan_review",
|
|
1236
1240
|
},
|
|
1237
|
-
};
|
|
1241
|
+
});
|
|
1238
1242
|
}
|
|
1239
1243
|
const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
|
|
1240
1244
|
if (ctxRead.kind === "corrupt")
|
|
1241
|
-
return resumeCtxCorruptAnswer(ctxRead.where);
|
|
1245
|
+
return refuse(resumeCtxCorruptAnswer(ctxRead.where));
|
|
1242
1246
|
if (ctxRead.kind === "absent")
|
|
1243
|
-
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1244
|
-
|
|
1247
|
+
return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
|
|
1248
|
+
return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
|
|
1249
|
+
}
|
|
1250
|
+
async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
|
|
1251
|
+
const { sessionId, token, cp, ctx } = prepared;
|
|
1252
|
+
if ((await deps.checkpointStore.findPendingTokenBySession(sessionId, undefined, { gateKinds: ["plan_review"] })) !== token)
|
|
1253
|
+
return notResumableAnswer();
|
|
1245
1254
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1246
1255
|
const effMode = storedBodyPermissionMode(ctx.body);
|
|
1247
1256
|
if (permissionModeAfter !== undefined && effMode !== "plan") {
|
|
1248
|
-
throw new Error("
|
|
1257
|
+
throw new Error("drivePlanReview: permissionModeAfter was passed for a task that was not submitted with permissionMode \"plan\" — the caller must refuse this before any state changes (see the plan_review route's pre-nonce gate)");
|
|
1249
1258
|
}
|
|
1250
1259
|
const approvedMode = decision === "approve" && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
|
|
1251
1260
|
const resumeBody = approvedMode === undefined ? ctx.body : { ...ctx.body, permissionMode: approvedMode };
|
|
@@ -1269,8 +1278,8 @@ export function createResumeLegs(ctx) {
|
|
|
1269
1278
|
registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
|
|
1270
1279
|
helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
|
|
1271
1280
|
local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
|
|
1272
|
-
legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake,
|
|
1281
|
+
legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, preparePlanReview, drivePlanReview },
|
|
1273
1282
|
};
|
|
1274
|
-
return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake,
|
|
1283
|
+
return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, preparePlanReview, drivePlanReview };
|
|
1275
1284
|
}
|
|
1276
1285
|
//# sourceMappingURL=resume-legs.js.map
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
18
|
import type { HostDecision } from "../host-decision.js";
|
|
19
|
-
import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice } from "@sema-agent/core";
|
|
19
|
+
import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, Checkpoint, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice } from "@sema-agent/core";
|
|
20
20
|
import type { FlatServiceDeps, RequestAuth } from "./server.js";
|
|
21
21
|
import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
|
|
22
22
|
import type { IdempotencyCache, SubmitOutcome } from "./idempotency.js";
|
|
@@ -154,7 +154,7 @@ export interface DriveResumeArgs {
|
|
|
154
154
|
/**
|
|
155
155
|
* 🔴 **S-433 合并复审 F2 [high] —— 「本次续跑真正用的那个体」,由本腿在赢下 CAS 之后写回耐久层**
|
|
156
156
|
* (验真后修;必填 —— `driveResumeIntoRunLog` 今天恰有**四个**调用点:legacy `/decide`(`resumeCheckpoint`)、
|
|
157
|
-
* `resumePreempted`、`resumeWake`、`resumePlanReview
|
|
157
|
+
* `resumePreempted`、`resumeWake`、`drivePlanReview`(S-609 前名 `resumePlanReview`),四条一视同仁)。
|
|
158
158
|
*
|
|
159
159
|
* 病(修前):`resumePlanReview` 在 `driveResumeIntoRunLog` **受理(2xx)之后**才 `putCtx(sessionId,
|
|
160
160
|
* {...ctx, body})`,而 `ctx` 是请求**开头** `getCtx` 读到的那一份,两个店的 `putCtx` 又都是按
|
|
@@ -177,7 +177,7 @@ export interface DriveResumeArgs {
|
|
|
177
177
|
* 🔴 写失败**响亮但不致命**(codex r1 两条 [high] 验真后定):core 的 `resumeStream` 是 **EAGER** 的 ——
|
|
178
178
|
* 写点这一拍 run 已经在执行,裸抛会把一条活腿判成 failed + 释放 claim 而执行还在继续。⇒ 一个臂、
|
|
179
179
|
* 四条腿同守:计数 `resume_ctx_not_persisted_total` + 一行 warn,本次续跑照常。方向论证(不是 [ref] 的
|
|
180
|
-
* 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `
|
|
180
|
+
* 静默放宽):写失败 ⇒ 耐久体保持**上一次**的值,而这四条腿里只有 `drivePlanReview` 改写这个体、
|
|
181
181
|
* 且只会**放宽** ⇒ 落盘的恒不比本次续跑更宽。旧的 `plan_approve_mode_not_persisted`(plan 专用)随本批删除,
|
|
182
182
|
* 由上面这只家族级的取代。全文见 `resume-legs.ts` 的 `commitResumeCtx` 顶注。
|
|
183
183
|
*/
|
|
@@ -311,6 +311,17 @@ export interface DriveResumeArgs {
|
|
|
311
311
|
*/
|
|
312
312
|
ownerLeaseAdmitted?: boolean;
|
|
313
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* S-609(7.97.0):plan_review 纯读段全部过关后交给驱动段的那一份 —— **选中的那张卡**(token + 载入的行)与**它那一刻**
|
|
316
|
+
* 的续跑上下文。三者一起走:驱动段只认这张卡,ctx 读绑在它上面(读在选卡之后;之后任何 ctx 写都得先赢这张卡的
|
|
317
|
+
* CAS,于是一份过期的读只会让驱动段在 CAS 上败,不会被配给别的卡)。
|
|
318
|
+
*/
|
|
319
|
+
export interface PreparedPlanReview {
|
|
320
|
+
readonly sessionId: string;
|
|
321
|
+
readonly token: CheckpointToken;
|
|
322
|
+
readonly cp: Checkpoint;
|
|
323
|
+
readonly ctx: CheckpointCtx;
|
|
324
|
+
}
|
|
314
325
|
/** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
|
|
315
326
|
* run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
|
|
316
327
|
* `prepareSpec`,approvals/assistant/notify-wake 三域共用 resume 家族,把它们跟着任一域搬都会造出
|
|
@@ -395,21 +406,36 @@ export interface RouteLegs {
|
|
|
395
406
|
status: number;
|
|
396
407
|
body: object;
|
|
397
408
|
}>;
|
|
398
|
-
/**
|
|
399
|
-
|
|
409
|
+
/**
|
|
410
|
+
* assistant plan_review 三态腿的**纯读段**(S-609,7.97.0):选卡 → 属主轻读 → 载入 → 行级 scope → 门类 → 这条决断
|
|
411
|
+
* 唯一的一次 ctx 读。任何一步拒 ⇒ `{ok:false, answer}`(不改任何状态;端点在消费一次性 nonce **之前**把它答出去);
|
|
412
|
+
* 全部过关 ⇒ `{ok:true, prepared}`,交给 {@link RouteLegs.drivePlanReview}。语义属主 = `resume-legs.ts` 同名函数顶注。
|
|
413
|
+
*/
|
|
414
|
+
preparePlanReview(sessionId: string,
|
|
400
415
|
/** [ref]/[ref] 件5:`decide_404` 判别日志的附加位(只进日志,不上 wire;身份只记有无不记值)。 */
|
|
401
416
|
log?: {
|
|
402
417
|
taskId?: string;
|
|
403
418
|
principalPresent?: boolean;
|
|
404
419
|
},
|
|
405
|
-
/** [ref]
|
|
406
|
-
acceptEarly?: boolean,
|
|
407
|
-
/** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 腿内行级属主复核
|
|
420
|
+
/** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 属主轻读与行级属主复核
|
|
408
421
|
* ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
|
|
409
422
|
decider?: {
|
|
410
423
|
principal?: string;
|
|
411
424
|
explicitOperator: boolean;
|
|
412
|
-
}
|
|
425
|
+
}): Promise<{
|
|
426
|
+
readonly ok: false;
|
|
427
|
+
readonly answer: {
|
|
428
|
+
status: number;
|
|
429
|
+
body: object;
|
|
430
|
+
};
|
|
431
|
+
} | {
|
|
432
|
+
readonly ok: true;
|
|
433
|
+
readonly prepared: PreparedPlanReview;
|
|
434
|
+
}>;
|
|
435
|
+
/** plan_review 三态腿的**驱动段**(S-609):只吃 prepare 的结果,用它选定的 token,不再选卡、不再读 ctx。 */
|
|
436
|
+
drivePlanReview(prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined,
|
|
437
|
+
/** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
|
|
438
|
+
acceptEarly?: boolean,
|
|
413
439
|
/** S-433:批准之后这条会话继续用的 permission mode(闭集 `PLAN_APPROVE_PERMISSION_MODES_AFTER`;只在 `approve`
|
|
414
440
|
* 上有意义,缺席 ⇒ `"default"`)。语义与判据的唯一属主 = `resume-legs.ts` 的同名形参顶注。 */
|
|
415
441
|
permissionModeAfter?: PlanApprovePermissionModeAfter): Promise<{
|
|
@@ -36,7 +36,7 @@ export declare function planReviewBodyKeyIssue(raw: unknown): KeyClosureIssue |
|
|
|
36
36
|
* · **签名域**(直连门的 HMAC 信封第五位)—— 属主是**跨仓字节契约**(§6.2 / `approvalHmacMessage`,
|
|
37
37
|
* 签名器在 cli / SDK)。那份契约明写空串与 `null` 是两个不同的被签值。⇒ **逐字线值,永不折叠**;
|
|
38
38
|
* 在这里折一刀,一份合法签发的空理由证明就永远 401(合并复审 R1 [high] 真复现过)。
|
|
39
|
-
* · **上送域**(`
|
|
39
|
+
* · **上送域**(`drivePlanReview` / `resumeCheckpoint` 的 `reason` 尾参)—— 属主是**引擎的单铸点**
|
|
40
40
|
* (7.19.0 / [ref] 把「人的结算」收进一个铸点,空文本在那里折成缺席,live 与 durable decide 两条路径同形)。
|
|
41
41
|
* ⇒ **原样透传**;跨仓宪法「源头修复禁下游旁路」:在下游再折一次就是同一语义的第二个写者,
|
|
42
42
|
* 今天两者答案相同,上游哪天改法就变成一条谁都查不出来的分歧。
|
|
@@ -19,6 +19,8 @@ import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal }
|
|
|
19
19
|
import { governanceOriginOf } from "../active-run-conflict.js";
|
|
20
20
|
import { sendResumeOutcome } from "../route-ctx.js";
|
|
21
21
|
import { assertNever } from "../route-ctx.js";
|
|
22
|
+
import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
|
|
23
|
+
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
22
24
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
|
|
23
25
|
const DECIDE_BODY_DECLARED_KEYS = [
|
|
24
26
|
"answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "settledBy", "updatedInput",
|
|
@@ -199,7 +201,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
199
201
|
const { deps } = ctx;
|
|
200
202
|
const { preemptableRuns } = ctx.registry;
|
|
201
203
|
const { readJson, rateLimited, quotaExceeded, safeDecode } = ctx.helpers;
|
|
202
|
-
const { resumeCheckpoint, resumePreempted,
|
|
204
|
+
const { resumeCheckpoint, resumePreempted, preparePlanReview, drivePlanReview } = ctx.legs;
|
|
203
205
|
{
|
|
204
206
|
const cs = deps.checkpointStore;
|
|
205
207
|
const principal = gatedPrincipal(req, deps.config);
|
|
@@ -271,7 +273,10 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
271
273
|
const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
|
|
272
274
|
const summaries = await cs.listByScope(scope);
|
|
273
275
|
const inbox = (await Promise.all(summaries.map(async (s) => {
|
|
274
|
-
const
|
|
276
|
+
const ctxRead = await readResumeCtx(cs.getCtx(s.sessionId));
|
|
277
|
+
if (ctxRead.kind === "corrupt")
|
|
278
|
+
recordFailOpen("server.assistant-inbox.objective-ctx-corrupt", ctxRead.where);
|
|
279
|
+
const objective = ctxRead.kind === "ok" ? (ctxRead.ctx.body?.objective ?? null) : null;
|
|
275
280
|
const input = s.toolInput != null ? redactedPreview(s.toolInput) : null;
|
|
276
281
|
return {
|
|
277
282
|
sessionId: s.sessionId,
|
|
@@ -286,7 +291,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
286
291
|
...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
|
|
287
292
|
...(s.origin !== undefined ? { origin: s.origin } : {}),
|
|
288
293
|
...(s.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: s.ruleStoreUnreadable } : {}),
|
|
289
|
-
objective
|
|
294
|
+
objective,
|
|
290
295
|
input,
|
|
291
296
|
};
|
|
292
297
|
}))).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0) || (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
@@ -520,9 +525,14 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
520
525
|
sendError(res, 404, "not_found.run", "task not found");
|
|
521
526
|
return;
|
|
522
527
|
}
|
|
528
|
+
const plan = await preparePlanReview(run.sessionId, { taskId, principalPresent: principal !== undefined }, { principal: deciderPrincipal, explicitOperator });
|
|
529
|
+
if (!plan.ok) {
|
|
530
|
+
sendResumeOutcome(res, plan.answer, deps.logger);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
523
533
|
if (body.permissionModeAfter !== undefined) {
|
|
524
|
-
const storedCtx =
|
|
525
|
-
if (storedCtx
|
|
534
|
+
const storedCtx = plan.prepared.ctx;
|
|
535
|
+
if (storedCtx.body !== undefined && storedBodyPermissionMode(storedCtx.body) !== "plan") {
|
|
526
536
|
sendError(res, 400, "request.field_conflict", "permissionModeAfter does not apply to this task: it was not submitted with permissionMode \"plan\", so approving the plan lifts no read-only restriction");
|
|
527
537
|
return;
|
|
528
538
|
}
|
|
@@ -539,7 +549,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
539
549
|
return;
|
|
540
550
|
}
|
|
541
551
|
}
|
|
542
|
-
const out = await
|
|
552
|
+
const out = await drivePlanReview(plan.prepared, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, true, isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined);
|
|
543
553
|
sendResumeOutcome(res, out, deps.logger);
|
|
544
554
|
return;
|
|
545
555
|
}
|
|
@@ -4,6 +4,7 @@ import { resolveModelApiKey } from "../../key-resolver.js";
|
|
|
4
4
|
import { isSealedKeyPoison } from "../../sealed-key.js";
|
|
5
5
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
6
6
|
import { taskApproverPostureHonored, taskWriteFaceRefusal } from "../../hosted-posture.js";
|
|
7
|
+
import { autonomyOverrides } from "../../runtime-governance.js";
|
|
7
8
|
import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
|
|
8
9
|
import { a2aInjectionHonored } from "../../task-a2a.js";
|
|
9
10
|
import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
@@ -11,6 +12,7 @@ import { buildEmptyMcpProbeFace, buildMcpInjectionRefusal, buildMcpProbeCacheKey
|
|
|
11
12
|
import { mcpForScenario } from "../../config-center/facade.js";
|
|
12
13
|
import { sendJson, sendError } from "../send.js";
|
|
13
14
|
import { assertNever } from "../route-ctx.js";
|
|
15
|
+
import { fileHistoryCaptureOf } from "../../file-history-capture.js";
|
|
14
16
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
15
17
|
import { projectMemoryEngineCapability } from "../../memory-posture.js";
|
|
16
18
|
import { projectWriteProtectionCapability } from "../../write-protection.js";
|
|
@@ -136,7 +138,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
136
138
|
interactiveTools: true,
|
|
137
139
|
projectContext: cwdHonored(deps.config),
|
|
138
140
|
callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
|
|
139
|
-
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined,
|
|
141
|
+
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyOverrides(deps.config.autonomy).writeFace !== "roots",
|
|
140
142
|
taskApproverPosture: taskApproverPostureHonored(deps.config),
|
|
141
143
|
executionLane: projectExecutionLaneCapability(deps.config),
|
|
142
144
|
mcpInjection: mcpInjectionHonored(deps.config),
|
|
@@ -172,6 +174,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
172
174
|
rewindFiles: Boolean(deps.fileHistoryStore),
|
|
173
175
|
restoreFiles: Boolean(deps.fileHistoryStore),
|
|
174
176
|
rewindFilesTo: Boolean(deps.fileHistoryStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
|
|
177
|
+
fileHistoryCapture: fileHistoryCaptureOf(deps.fileHistoryStore),
|
|
175
178
|
scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
|
|
176
179
|
sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.sessionStorage?.beginImportStaging && deps.fileHistoryStore),
|
|
177
180
|
sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
|