@sema-agent/server 7.138.0 → 7.139.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 +276 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +73 -29
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +20 -20
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +16 -24
- package/dist/boot/execution-lane.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -9
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +3 -1
- package/dist/boot/runner-deps.js +2 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.js +0 -1
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.js +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +7 -7
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +19 -24
- package/dist/config-provider.js +27 -12
- package/dist/config-types.d.ts +10 -5
- package/dist/config.d.ts +33 -0
- package/dist/config.js +44 -15
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +19 -0
- package/dist/execution-lane-caps.js +10 -0
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/resume-legs.js +50 -20
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +115 -11
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +37 -31
- package/dist/http/server.d.ts +35 -19
- package/dist/http/server.js +11 -0
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/index.d.ts +1 -0
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +196 -116
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +30 -12
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +5 -7
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +49 -103
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +7 -6
- package/dist/plugins/file-history-store-sql.js +5 -5
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +111 -3
- package/dist/plugins/one-time-migrations.js +380 -10
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
- package/dist/plugins/permission-rule-store-sql.js +13 -16
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +20 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +30 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +18 -4
- package/dist/plugins/remote-env-e2b.js +41 -42
- package/dist/plugins/remote-env-file-error.d.ts +2 -0
- package/dist/plugins/remote-env-file-error.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +21 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +11 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +29 -6
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +16 -4
- package/dist/plugins/remote-env-ssh.js +72 -56
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/retention-store-sql.d.ts +9 -6
- package/dist/plugins/retention-store-sql.js +7 -1
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/schema-consistency.d.ts +15 -5
- package/dist/plugins/schema-consistency.js +14 -8
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +19 -7
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +9 -3
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +118 -50
- package/dist/plugins/tool-result-store-sql.d.ts +3 -2
- package/dist/plugins/tool-result-store-sql.js +18 -10
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +24 -20
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +3 -1
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -9
- package/dist/session-incarnation.js +4 -2
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/self-minted-wire.d.ts +111 -2
- package/dist/trace/self-minted-wire.js +38 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **执行上下文** —— 每条执行起点从这一只解析器取同一型上下文:根会话是谁、这一位住客在根会话行上的执行放置是什么、这条执行走哪条车道、
|
|
3
|
+
* 主体能不能用这条车道;认领之后的初始放置只有一只写口 {@link commitExecutionContext}。
|
|
4
|
+
*
|
|
5
|
+
* ## 规则(一句)
|
|
6
|
+
* 根只有一个来源 = authorizer(`ServiceCoreDeps.authorize` 必填席,唯一铸点即登记点),根进 spec 只有一个写者 = {@link stampSpecRoot};
|
|
7
|
+
* 执行放置**读只有一只** —— 会话店执行席 `seat.read(root)` 一次行读(住客 + 格),解析器读它、提交腿的门交来的 `judged` 比它、设备准入链判的也是这一读;
|
|
8
|
+
* **写只有一只** —— 认领之后的 {@link commitExecutionContext}(按解析器那一读的住客定址,0 行按住客复读分类,永不对现住客重试)。
|
|
9
|
+
*
|
|
10
|
+
* ## 解析步序(设计 §3.2 表 ①–④;⑤ 取 rig 不在本版)
|
|
11
|
+
* ① 读:会话店不带执行席 ⇒ absent、`occupant` 缺(只一元车道表的部署可能);否则一次行读 —— 行不在 ⇒ 404 `resume.session_not_found`;`judged`(门判过的那一读)
|
|
12
|
+
* 在场而与行头不是同一位住客(化身 ∧ 属主,写口同一只判官 `sameOccupant`)⇒ 同 404(他人在门与解析之间删掉并同 id 重登记,或同一化身的属主变了 ——
|
|
13
|
+
* local M10 认领 / identical 重戳 / SQL 导入提交点改属主 —— 判到新住客的车道就是错的);local 格是 `forking`
|
|
14
|
+
* ⇒ 409 `conflict.fork_in_progress`;读故障抛(不吞成 absent)。解析器不建行、不铸根。
|
|
15
|
+
* ② 定车道:`mirror` ⇒ 409 `conflict.session_mirror`(体带 `forkPath`);`writer(L)` 而 L 不在本部署的车道表 ⇒ 409 `execution_lane.not_served{boundLane}`;absent ⇒ 表首。
|
|
16
|
+
* ③ 授权:主体的执行车道裁定(`gateExecutionLane`,判据与码同 `runtime-caps-resolver.ts`);服务形不判。
|
|
17
|
+
* ④ 只 fresh:设备准入链(体 `deviceId` 只在 device 车道收;准入链三步 + 首绑目标三读,全读;判的是步 ① 那一读)。
|
|
18
|
+
*
|
|
19
|
+
* 入口三形(判别联合,{@link ExecutionEntry}):`fresh`(提交腿:根 = authorizer 交出的 `auth.sessionId`,`judged` = authorizer 判过的头)·
|
|
20
|
+
* `continuation`(续跑家族与停驻赎回:根 = checkpoint / 停驻行的根;不带 `judged` —— 步 ① 那一读就是这条执行对根的唯一一读,认领之后由写口按它判)·
|
|
21
|
+
* `service`(服务 run:没有会话根)。
|
|
22
|
+
* `fresh` / `continuation` 两形型上恒带 `root: string`,`service` 形没有 `root` 键 —— 「按一只缺席的根去读行」在型上不可表达。
|
|
23
|
+
*
|
|
24
|
+
* ## 提交({@link commitExecutionContext};设计 §3.2「提交」)
|
|
25
|
+
* 只在认领之后(四个 `createRun` 点、续跑家族 `markResuming` 之后、停驻赎回 `claimParkedAgent` 之后)调,是认领之后改执行放置的唯一写口:
|
|
26
|
+
* · 有设备意图(fresh ∧ device 车道)⇒ 首绑(设备店一只事务,按住客定址)或已绑臂的再断言(按住客再读 + 准入链再断言设备);
|
|
27
|
+
* · absent ⇒ place(非 device 车道);命中 = 成立;0 行按住客复读分类:同值 = 成立 · 异车道 ⇒ `execution_lane.tree_bound` · mirror ⇒ `conflict.session_mirror` ·
|
|
28
|
+
* gone(行不在或住客不是解析器那一位)⇒ 「会话不在」(提交腿 404 `resume.session_not_found`;停驻赎回答它自己的 `decide.not_parked`)—— **不对现住客重试**;
|
|
29
|
+
* · writer ⇒ 按住客再读断言仍是 writer(L);
|
|
30
|
+
* · 拒 ⇒ 先释放本次认领(run:终结 + 放认领 + 放幂等键;停驻:回滚认领)再交回拒答;店故障 ⇒ 释放后原样上抛(响亮,不当答案)。
|
|
31
|
+
* claim 之后每一次店往返都带 {@link placementWriteDeadlineMs} 硬时限。
|
|
32
|
+
*
|
|
33
|
+
* ## 便利度不变(clay [ref] 硬约束)
|
|
34
|
+
* 本文件**不产生任何审批**:只做「这条会话放在哪条车道 / 哪台设备上」的读、判与认领之后的一次写。device 车道上 `permissionMode: "bypassPermissions"` 的 run
|
|
35
|
+
* 与 host 车道同构造同读数(判据钉在 device-first-bind 测试的「零额外审批」格)。
|
|
36
|
+
*/
|
|
37
|
+
import type { IncomingMessage } from "node:http";
|
|
38
|
+
import type { SessionHead, TaskSpec } from "@sema-agent/core";
|
|
39
|
+
import { type DeviceOwner, type DeviceStore } from "../device-store.js";
|
|
40
|
+
import { type DeviceEnrollment, type DeviceRejection } from "../device-enrollment.js";
|
|
41
|
+
import type { ServiceConfig } from "../config.js";
|
|
42
|
+
import type { RunStore } from "../plugins/store-backend.js";
|
|
43
|
+
import { type ExecutionPlacementSeat } from "../execution-placement.js";
|
|
44
|
+
import { type ExecutionLane } from "../execution-lane-caps.js";
|
|
45
|
+
import type { ExecutionRuling } from "../web-admin-client/facade.js";
|
|
46
|
+
import type { Logger } from "../observability/logger.js";
|
|
47
|
+
import type { SessionFace } from "../session-incarnation.js";
|
|
48
|
+
import { type RefusalBody } from "./refusal-body.js";
|
|
49
|
+
/** 门判过的那一读(authorizer 的头;只 `fresh` 入口带):解析器拿它的住客(化身 ∧ 属主)与行头比 —— 判官 = 写口同一只 `sameOccupant`。 */
|
|
50
|
+
export type JudgedOccupant = Pick<SessionHead, "owner" | "incarnation">;
|
|
51
|
+
/** 执行起点交给解析器的入口。 */
|
|
52
|
+
export type ExecutionEntry = {
|
|
53
|
+
readonly kind: "fresh";
|
|
54
|
+
readonly root: string;
|
|
55
|
+
/** 主体(授权步 ③ 按它问执行车道裁定)。 */
|
|
56
|
+
readonly principal: string | undefined;
|
|
57
|
+
/** authorizer 判过的头(属主门那一读;现铸臂是登记后那一读)。缺 = 店没有 `headOf`(只一元表无席形可达)。 */
|
|
58
|
+
readonly judged: JudgedOccupant | undefined;
|
|
59
|
+
/** 设备准入(步 ④)的两样输入:调用方的请求(判调用方域)与体里的 `deviceId`(原值,形在步 ④ 判)。 */
|
|
60
|
+
readonly req: IncomingMessage | undefined;
|
|
61
|
+
readonly deviceId: unknown;
|
|
62
|
+
} | {
|
|
63
|
+
readonly kind: "continuation";
|
|
64
|
+
readonly root: string;
|
|
65
|
+
readonly principal: string | undefined;
|
|
66
|
+
} | {
|
|
67
|
+
readonly kind: "service";
|
|
68
|
+
};
|
|
69
|
+
/** 解析器的产出:有会话根的两形与没有根的服务形。 */
|
|
70
|
+
export type ExecutionContext = {
|
|
71
|
+
readonly kind: "fresh" | "continuation";
|
|
72
|
+
readonly root: string;
|
|
73
|
+
/** 步 ① 那一读的住客;会话店不带执行席 ⇒ 缺(提交无事可做)。 */
|
|
74
|
+
readonly occupant: SessionFace | undefined;
|
|
75
|
+
readonly placement: "absent" | "writer";
|
|
76
|
+
readonly lane: ExecutionLane;
|
|
77
|
+
/** 只 fresh ∧ device 车道:准入链判完的设备意图(认领之后由 {@link commitExecutionContext} 写或再断言)。 */
|
|
78
|
+
readonly device?: DevicePlacementIntent;
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: "service";
|
|
81
|
+
readonly lane: ExecutionLane;
|
|
82
|
+
readonly placement: "absent";
|
|
83
|
+
};
|
|
84
|
+
/** 带根的上下文 —— 认领之后的提交、审批席的出处车道读这一形(服务 run 不认领)。 */
|
|
85
|
+
export type RootedExecutionContext = Extract<ExecutionContext, {
|
|
86
|
+
readonly root: string;
|
|
87
|
+
}>;
|
|
88
|
+
/** 装配器(`resolveSpec`)收的那一半:根与入口形(它不判放置、不选车道)。 */
|
|
89
|
+
export type ExecutionRoot = Pick<RootedExecutionContext, "kind" | "root">;
|
|
90
|
+
/** 解析器读的部署件(全部取自同一只 `ServiceDeps`)。 */
|
|
91
|
+
export interface ExecutionContextDeps {
|
|
92
|
+
readonly config: ServiceConfig;
|
|
93
|
+
/** 会话店:执行席 `executionPlacement` 是放置的唯一载体与唯一读。 */
|
|
94
|
+
readonly sessionStorage?: {
|
|
95
|
+
readonly executionPlacement?: ExecutionPlacementSeat;
|
|
96
|
+
} | undefined;
|
|
97
|
+
/** 主体的执行车道裁定(caps client;缺 = 无中心 ⇒ 不判,与已发同)。 */
|
|
98
|
+
readonly executionRuling?: ((principal: string | undefined) => Promise<ExecutionRuling | undefined>) | undefined;
|
|
99
|
+
readonly logger?: Logger | undefined;
|
|
100
|
+
readonly deviceStore?: DeviceStore | undefined;
|
|
101
|
+
readonly deviceEnrollment?: DeviceEnrollment | undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* **唯一的解析器**(设计 §3.2 步 ①–④)。同一入口、同一行、同一裁定恒得同一上下文;判别是穷举的(新入口形 = 编译错)。
|
|
105
|
+
* 拒一律抛 typed `HttpError`(提交腿原样答;续跑家族照它自己的折叠律答,`http/resume-legs.ts`)。
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveExecutionContext(deps: ExecutionContextDeps, entry: Extract<ExecutionEntry, {
|
|
108
|
+
readonly root: string;
|
|
109
|
+
}>): Promise<RootedExecutionContext>;
|
|
110
|
+
export declare function resolveExecutionContext(deps: ExecutionContextDeps, entry: ExecutionEntry): Promise<ExecutionContext>;
|
|
111
|
+
/** 拒体里「继续在本处执行」的下一步路径(fork 口)—— 镜像拒体的 `forkPath` 唯一铸法(执行面与会话行写口 `PATCH /v1/sessions/:id` 同读)。 */
|
|
112
|
+
export declare function forkPathOf(root: string): string;
|
|
113
|
+
/** 解析器的拒是不是「会话不在」(停驻赎回腿把它答成自己既有的 `decide.not_parked`;判据只在这里认那只答复的码)。 */
|
|
114
|
+
export declare function isRootSessionGoneError(err: unknown): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* 调用方 device 域的**唯一铸造点**。`undefined` = 判不出域(无 principal,或凭据不带 tenant claim)—— 消费点必须响亮拒,禁静默空视图。
|
|
117
|
+
*
|
|
118
|
+
* 🔴 位置即契约:`{owner_tenant, owner_subject}` 是 O11 的 device-lane-local 复合身份(全局 principal 一个字节不动)。subject = `gatedPrincipal`(verified 链),
|
|
119
|
+
* tenant = `ssoVerifiedScope`(registry auth-bridge JWT 的 `scope` claim)—— **两者都是签名真源,不是自报头**。管理面三读三写与首绑写协议全部从这一只函数取域;
|
|
120
|
+
* 派生第二份就是把 O11 的判据本体复制成一份会漂的孪生。
|
|
121
|
+
*/
|
|
122
|
+
export declare function deviceOwnerDomainOf(req: IncomingMessage, config: ServiceConfig): DeviceOwner | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* **已校验、未写库**的设备放置意图(device 车道的 fresh 上下文才带)。解析那一刻还不知道哪个并发请求会赢下会话的认领,所以它不写 ——
|
|
125
|
+
* 只把「如果我赢了,该写什么 / 该断言什么」算好交给 {@link commitExecutionContext}。
|
|
126
|
+
*/
|
|
127
|
+
export interface DevicePlacementIntent {
|
|
128
|
+
/** 绑定键 = **根**会话(后代无条件继承)。 */
|
|
129
|
+
readonly rootSessionId: string;
|
|
130
|
+
/** 步 ① 那一读的住客:首绑写按住客谓词;已绑臂的再断言先比住客。 */
|
|
131
|
+
readonly occupant: SessionFace;
|
|
132
|
+
/** 验证过的复合身份(恒取 verified 链,不取自可伪造头)。 */
|
|
133
|
+
readonly owner: DeviceOwner;
|
|
134
|
+
/**
|
|
135
|
+
* **准入链批准的那台设备** —— 提交的断言对象。身份留在意图里,已绑臂才有东西可断言(一条写了 `deviceId: A` 的提交在「准入过后、认领之前」被换绑到 B
|
|
136
|
+
* 时不许静默在 B 上跑完 —— 执行环境工厂读的是**当下**的绑定)。
|
|
137
|
+
*/
|
|
138
|
+
readonly admittedDeviceId: string;
|
|
139
|
+
/** `true` = 解析那一读里还没有设备绑定 ⇒ 提交做那一次首绑(写);`false` = 已绑 ⇒ 提交不写,只再断言。 */
|
|
140
|
+
readonly firstBind: boolean;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 装配器的产物型:根不归装配器写。`{ sessionId?: never }` 让任何型上带 `string` 的 `sessionId`(字面量、变量、展开、`TaskSpec` 型值)都不可赋;
|
|
144
|
+
* 擦型(`any` / `as unknown as`)与 JS 嵌入方不归型管,由 {@link stampSpecRoot} 的运行时判兜。
|
|
145
|
+
*/
|
|
146
|
+
export type ResolvedTaskSpec = Omit<TaskSpec, "sessionId"> & {
|
|
147
|
+
sessionId?: never;
|
|
148
|
+
};
|
|
149
|
+
/** 盖过根的 spec:执行点、认领、续跑输入读的都是这一形(`sessionId` 恒在)。 */
|
|
150
|
+
export type RootedTaskSpec = TaskSpec & {
|
|
151
|
+
readonly sessionId: string;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* **根进 spec 的唯一写者。** 装配器交回的 spec 上 `sessionId` 缺席(或为 `undefined`)⇒ 盖上这条上下文的根;
|
|
155
|
+
* 在场且等于根 ⇒ 同值,照盖;在场而**不等** ⇒ 400 `request.precondition_unmet`(嵌入方的装配器另点了一只会话 —— 它在型上不能这么写,
|
|
156
|
+
* 走到这里只能是擦型或 JS 形;拒在建任何行之前)。续跑家族照它自己的折叠律把这只拒答成 409 `resume_blocked_by_policy`(`blockedBy` = 这只码,
|
|
157
|
+
* `http/resume-legs.ts` 的 `resolveResumeSpecOrBlocked`:装配期的每一只 `HttpError` 在续跑腿上都这么折),同样什么都没续。
|
|
158
|
+
*/
|
|
159
|
+
export declare function stampSpecRoot(spec: ResolvedTaskSpec, ctx: ExecutionRoot): RootedTaskSpec;
|
|
160
|
+
/**
|
|
161
|
+
* run 店建行时根会话行已经不在(`CreateRunOutcome.sessionAbsent`:受理之后、认领之前会话被删)的**唯一**答复 ——
|
|
162
|
+
* 四个建行点(同步 / 流式 / durable / wake)、解析期的「行不在 / 住客换了」与提交期的 `gone` 同答 404 `resume.session_not_found`,什么都没建、什么都没跑。
|
|
163
|
+
*/
|
|
164
|
+
export declare function rootSessionAbsentAnswer(): {
|
|
165
|
+
status: 404;
|
|
166
|
+
body: RefusalBody;
|
|
167
|
+
};
|
|
168
|
+
/** 提交点的拒答(各建行点用自己协议的发送姿势,状态与体只有这一份)。 */
|
|
169
|
+
export interface ExecutionRefusal {
|
|
170
|
+
readonly status: number;
|
|
171
|
+
readonly body: RefusalBody;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 本次认领 —— 提交拒时要释放的东西。`run` = 四个建行点与续跑家族认领的 run 行(释放 = 终结 + 放认领 + 放幂等键,唯一写法 `releaseClaimedRun`);
|
|
175
|
+
* `parked` = 停驻赎回的认领(释放 = 回滚认领;「会话不在」答那条腿既有的 `decide.not_parked`,不是提交腿的 404)。
|
|
176
|
+
*/
|
|
177
|
+
export type ExecutionClaim = {
|
|
178
|
+
readonly kind: "run";
|
|
179
|
+
readonly runStore: RunStore;
|
|
180
|
+
readonly taskId: string;
|
|
181
|
+
} | {
|
|
182
|
+
readonly kind: "parked";
|
|
183
|
+
readonly taskId: string;
|
|
184
|
+
readonly rollback: (why: string) => Promise<unknown>;
|
|
185
|
+
readonly goneAnswer: ExecutionRefusal;
|
|
186
|
+
};
|
|
187
|
+
/** {@link commitExecutionContext} 的依赖 —— 逐字取自 `ServiceDeps` 的同名键(同一只 deps 对象喂进来)。 */
|
|
188
|
+
export interface ExecutionCommitDeps {
|
|
189
|
+
readonly sessionStorage?: {
|
|
190
|
+
readonly executionPlacement?: ExecutionPlacementSeat;
|
|
191
|
+
} | undefined;
|
|
192
|
+
readonly deviceEnrollment?: DeviceEnrollment | undefined;
|
|
193
|
+
readonly config?: {
|
|
194
|
+
readonly runStaleSec?: number;
|
|
195
|
+
} | undefined;
|
|
196
|
+
readonly logger?: {
|
|
197
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
198
|
+
} | undefined;
|
|
199
|
+
readonly metrics?: {
|
|
200
|
+
inc(name: string, labels?: Record<string, string>): void;
|
|
201
|
+
} | undefined;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* claim 之后那一次写 / 再读的**硬时限**:从 `runStaleSec` 派生(不新铸旋钮)—— 它只回答「这次店往返会不会挂到 stale 回收把认领收走」。
|
|
205
|
+
* 取四分之一并夹在 `[1s, 15s]`。
|
|
206
|
+
*/
|
|
207
|
+
export declare function placementWriteDeadlineMs(runStaleSec: number | undefined): number;
|
|
208
|
+
/** 拒绝的**唯一** wire 体铸点(各消费点各自用自己协议的发送姿势,但体的键集只有这一份)。 */
|
|
209
|
+
export declare function devicePlacementErrorBody(rej: DeviceRejection): RefusalBody;
|
|
210
|
+
/**
|
|
211
|
+
* **认领之后的唯一写口**(四个 `createRun` 点、续跑家族 `markResuming` 之后、停驻赎回 `claimParkedAgent` 之后)。返回 `null` = 放行;
|
|
212
|
+
* 否则本次认领已释放,调用方交回拒答。店故障 / 超时 ⇒ 释放后原样上抛(响亮,不当答案);设备意图那一支的注册簿故障答 {@link registryUnavailable}(设备闭集,同准入阶段)。
|
|
213
|
+
*
|
|
214
|
+
* 不变量(一句):**离开本函数的那一刻,这条会话(解析器那一位住客)的格就是这条执行的车道(device 车道:准入批准的那台设备);否则本次认领已释放、调用方拿到拒答。**
|
|
215
|
+
* 它不说「此后不变」:认领持有期间根行住客与放置不被换绑 / 导入改动,那是各写者写事务内锁根行后读认领的保证(规则句),不在本函数重复判。
|
|
216
|
+
*/
|
|
217
|
+
export declare function commitExecutionContext(deps: ExecutionCommitDeps, claim: ExecutionClaim, ctx: RootedExecutionContext): Promise<ExecutionRefusal | null>;
|
|
218
|
+
//# sourceMappingURL=execution-context.d.ts.map
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { HttpError, createCodedHttpError, ssoVerifiedScope } from "../security.js";
|
|
2
|
+
import { gatedPrincipal } from "./principal-gate.js";
|
|
3
|
+
import { DEVICE_ERROR_CODES, assertDeviceIdShape } from "../device-store.js";
|
|
4
|
+
import { buildDeviceRejection } from "../device-enrollment.js";
|
|
5
|
+
import { sameOccupant } from "../execution-placement.js";
|
|
6
|
+
import { deployedExecutionLanes } from "../execution-lane-caps.js";
|
|
7
|
+
import { gateExecutionLane } from "../runtime-caps-resolver.js";
|
|
8
|
+
import { releaseClaimedRun } from "./claimed-run.js";
|
|
9
|
+
import { refusalBody } from "./refusal-body.js";
|
|
10
|
+
export async function resolveExecutionContext(deps, entry) {
|
|
11
|
+
const lanes = deployedExecutionLanes(deps.config);
|
|
12
|
+
switch (entry.kind) {
|
|
13
|
+
case "service":
|
|
14
|
+
return { kind: "service", lane: lanes[0], placement: "absent" };
|
|
15
|
+
case "fresh":
|
|
16
|
+
case "continuation": {
|
|
17
|
+
const seat = deps.sessionStorage?.executionPlacement;
|
|
18
|
+
const reading = seat === undefined ? undefined : await readRow(seat, entry.root, entry.kind === "fresh" ? entry.judged : undefined);
|
|
19
|
+
const { lane, placement } = laneOf(entry.root, reading?.cell ?? { role: "absent" }, lanes);
|
|
20
|
+
gateExecutionLane(await deps.executionRuling?.(entry.principal), lane, deps.logger);
|
|
21
|
+
const ctx = { kind: entry.kind, root: entry.root, occupant: reading?.occupant, placement, lane };
|
|
22
|
+
if (entry.kind === "continuation")
|
|
23
|
+
return ctx;
|
|
24
|
+
const device = await admitDevicePlacement(deps, entry, lane, seat, reading);
|
|
25
|
+
return device === undefined ? ctx : { ...ctx, device };
|
|
26
|
+
}
|
|
27
|
+
default: {
|
|
28
|
+
const unhandled = entry;
|
|
29
|
+
throw new Error(`resolveExecutionContext: unhandled entry ${JSON.stringify(unhandled)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function readRow(seat, root, judged) {
|
|
34
|
+
const reading = await seat.read(root);
|
|
35
|
+
if (reading === undefined || (judged !== undefined && !sameOccupant(judged, reading.occupant)))
|
|
36
|
+
throw rootSessionGoneError();
|
|
37
|
+
if (reading.cell.role === "forking") {
|
|
38
|
+
throw createCodedHttpError(409, "conflict.fork_in_progress", "this session is the target of a fork that has not finished copying — retry that fork with the same forkId and forkNonce, or delete the session; nothing was started");
|
|
39
|
+
}
|
|
40
|
+
return reading;
|
|
41
|
+
}
|
|
42
|
+
function laneOf(root, cell, lanes) {
|
|
43
|
+
switch (cell.role) {
|
|
44
|
+
case "absent":
|
|
45
|
+
return { lane: lanes[0], placement: "absent" };
|
|
46
|
+
case "writer":
|
|
47
|
+
if (!lanes.includes(cell.lane)) {
|
|
48
|
+
throw createCodedHttpError(409, "execution_lane.not_served", "this session is placed where this deployment no longer runs work (see boundLane) — nothing was started", { boundLane: cell.lane });
|
|
49
|
+
}
|
|
50
|
+
return { lane: cell.lane, placement: "writer" };
|
|
51
|
+
case "mirror":
|
|
52
|
+
throw sessionMirrorError(root);
|
|
53
|
+
case "forking":
|
|
54
|
+
throw new Error(`resolveExecutionContext: session ${root} reads as forking after the read step refused it`);
|
|
55
|
+
default: {
|
|
56
|
+
const unknownRole = cell;
|
|
57
|
+
throw new Error(`resolveExecutionContext: no lane for ${JSON.stringify(unknownRole)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function sessionMirrorError(root) {
|
|
62
|
+
const answer = sessionMirrorAnswer(root);
|
|
63
|
+
return createCodedHttpError(answer.status, answer.body.errorCode, answer.body.error, { forkPath: forkPathOf(root) });
|
|
64
|
+
}
|
|
65
|
+
export function forkPathOf(root) {
|
|
66
|
+
return `/v1/sessions/${encodeURIComponent(root)}/fork`;
|
|
67
|
+
}
|
|
68
|
+
function sessionMirrorAnswer(root) {
|
|
69
|
+
return {
|
|
70
|
+
status: 409,
|
|
71
|
+
body: refusalBody({
|
|
72
|
+
errorCode: "conflict.session_mirror",
|
|
73
|
+
message: "this session is a read-only mirror of a session whose writer is elsewhere — it cannot run here; fork it (forkPath) to continue here; nothing was started",
|
|
74
|
+
facts: { forkPath: forkPathOf(root) },
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function rootSessionGoneError() {
|
|
79
|
+
const absent = rootSessionAbsentAnswer();
|
|
80
|
+
return createCodedHttpError(absent.status, absent.body.errorCode, absent.body.error);
|
|
81
|
+
}
|
|
82
|
+
export function isRootSessionGoneError(err) {
|
|
83
|
+
return err instanceof HttpError && err.code === rootSessionAbsentAnswer().body.errorCode;
|
|
84
|
+
}
|
|
85
|
+
export function deviceOwnerDomainOf(req, config) {
|
|
86
|
+
const subject = gatedPrincipal(req, config);
|
|
87
|
+
if (subject === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
const tenant = ssoVerifiedScope(req);
|
|
90
|
+
if (tenant === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
return { tenant, subject };
|
|
93
|
+
}
|
|
94
|
+
function asHttpError(rej) {
|
|
95
|
+
return createCodedHttpError(rej.status, rej.code, rej.message);
|
|
96
|
+
}
|
|
97
|
+
function registryUnavailable() {
|
|
98
|
+
return buildDeviceRejection(DEVICE_ERROR_CODES.NOT_BOUND, "the device binding for this session could not be resolved (the device registry is unavailable) — refusing rather than running on an unverified device");
|
|
99
|
+
}
|
|
100
|
+
async function admitDevicePlacement(deps, entry, lane, seat, reading) {
|
|
101
|
+
const requested = entry.deviceId;
|
|
102
|
+
if (lane !== "device") {
|
|
103
|
+
if (requested !== undefined) {
|
|
104
|
+
throw createCodedHttpError(400, "request.precondition_unmet", "deviceId is only accepted on a deployment that executes on the user's own device (REMOTE_EXEC=device) — this deployment does not, so binding this session to a device id would be a declaration that never took effect");
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
if (!deps.deviceStore || !deps.deviceEnrollment || seat === undefined || reading === undefined) {
|
|
109
|
+
throw createCodedHttpError(501, "capability.device_lane_required", "device execution is selected for this deployment but its stores are not wired on this replica — refusing to admit a submit that would execute nowhere");
|
|
110
|
+
}
|
|
111
|
+
if (entry.req === undefined) {
|
|
112
|
+
throw createCodedHttpError(403, "auth.forbidden", "device placement needs a verified caller domain and this leg carries no request context");
|
|
113
|
+
}
|
|
114
|
+
const owner = deviceOwnerDomainOf(entry.req, deps.config);
|
|
115
|
+
if (owner === undefined) {
|
|
116
|
+
throw createCodedHttpError(403, "auth.forbidden", "device placement needs a caller domain: this credential carries no verified tenant scope claim (registry auth-bridge JWT `scope`) — connect through the registry auth bridge");
|
|
117
|
+
}
|
|
118
|
+
if (requested !== undefined) {
|
|
119
|
+
if (typeof requested !== "string") {
|
|
120
|
+
throw createCodedHttpError(400, "request.field_invalid", "deviceId must be a string (the device id returned by POST /v1/devices/enroll)");
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
assertDeviceIdShape(requested);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throw createCodedHttpError(400, "request.field_invalid", "deviceId is not a valid device id (byte-exact key: non-empty, no control characters or surrounding whitespace, at most 64 bytes)");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const requestedDeviceId = requested;
|
|
130
|
+
let admitted;
|
|
131
|
+
try {
|
|
132
|
+
admitted = await deps.deviceEnrollment.admitSession({ rootSessionId: entry.root, placement: reading, requester: owner, ...(requestedDeviceId === undefined ? {} : { requestedDeviceId }) });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw asHttpError(registryUnavailable());
|
|
136
|
+
}
|
|
137
|
+
if (admitted.ok)
|
|
138
|
+
return { rootSessionId: entry.root, occupant: reading.occupant, owner, admittedDeviceId: admitted.value.deviceId, firstBind: false };
|
|
139
|
+
if (admitted.reject.code !== DEVICE_ERROR_CODES.NOT_BOUND || requestedDeviceId === undefined)
|
|
140
|
+
throw asHttpError(admitted.reject);
|
|
141
|
+
let view;
|
|
142
|
+
try {
|
|
143
|
+
view = await deps.deviceStore.readDeviceForAdmission(requestedDeviceId, owner);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw asHttpError(registryUnavailable());
|
|
147
|
+
}
|
|
148
|
+
if (!view)
|
|
149
|
+
throw asHttpError(buildDeviceRejection(DEVICE_ERROR_CODES.NOT_FOUND, "device not found"));
|
|
150
|
+
if (view.row.status === "revoked")
|
|
151
|
+
throw asHttpError(buildDeviceRejection(DEVICE_ERROR_CODES.REVOKED, "device has been revoked"));
|
|
152
|
+
if (!view.online)
|
|
153
|
+
throw asHttpError(buildDeviceRejection(DEVICE_ERROR_CODES.OFFLINE, "device is not connected"));
|
|
154
|
+
return { rootSessionId: entry.root, occupant: reading.occupant, owner, admittedDeviceId: requestedDeviceId, firstBind: true };
|
|
155
|
+
}
|
|
156
|
+
export function stampSpecRoot(spec, ctx) {
|
|
157
|
+
const named = spec.sessionId;
|
|
158
|
+
if (named !== undefined && named !== ctx.root) {
|
|
159
|
+
throw createCodedHttpError(400, "request.precondition_unmet", "the deployment's spec resolver named a session other than the one this submission was authorized for — a submission's root session comes only from the authorizer, so resolveSpec must not set sessionId (read opts.execution.root instead)");
|
|
160
|
+
}
|
|
161
|
+
return { ...spec, sessionId: ctx.root };
|
|
162
|
+
}
|
|
163
|
+
export function rootSessionAbsentAnswer() {
|
|
164
|
+
return {
|
|
165
|
+
status: 404,
|
|
166
|
+
body: refusalBody({
|
|
167
|
+
errorCode: "resume.session_not_found",
|
|
168
|
+
message: "the session this run was to start on no longer exists (it was deleted after the request was admitted) — nothing was started",
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function placementWriteDeadlineMs(runStaleSec) {
|
|
173
|
+
const stale = typeof runStaleSec === "number" && Number.isFinite(runStaleSec) && runStaleSec > 0 ? runStaleSec : 120;
|
|
174
|
+
return Math.min(15_000, Math.max(1_000, Math.floor((stale * 1000) / 4)));
|
|
175
|
+
}
|
|
176
|
+
function deviceRefusal(rej) {
|
|
177
|
+
return { status: rej.status, body: devicePlacementErrorBody(rej) };
|
|
178
|
+
}
|
|
179
|
+
export function devicePlacementErrorBody(rej) {
|
|
180
|
+
return refusalBody({ errorCode: rej.code, message: rej.message });
|
|
181
|
+
}
|
|
182
|
+
function treeBoundAnswer(boundLane) {
|
|
183
|
+
return {
|
|
184
|
+
status: 409,
|
|
185
|
+
body: refusalBody({
|
|
186
|
+
errorCode: "execution_lane.tree_bound",
|
|
187
|
+
message: "this session is already placed elsewhere on this deployment (see boundLane) — this run cannot be placed somewhere else; nothing was started",
|
|
188
|
+
facts: { boundLane },
|
|
189
|
+
}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export async function commitExecutionContext(deps, claim, ctx) {
|
|
193
|
+
const seat = deps.sessionStorage?.executionPlacement;
|
|
194
|
+
if (ctx.occupant === undefined || seat === undefined)
|
|
195
|
+
return null;
|
|
196
|
+
const occupant = ctx.occupant;
|
|
197
|
+
const release = (why) => releaseClaim(deps, claim, why);
|
|
198
|
+
const deadlineMs = placementWriteDeadlineMs(deps.config?.runStaleSec);
|
|
199
|
+
const withDeadline = async (work) => {
|
|
200
|
+
let timer;
|
|
201
|
+
try {
|
|
202
|
+
return await Promise.race([
|
|
203
|
+
work,
|
|
204
|
+
new Promise((_resolve, reject) => {
|
|
205
|
+
timer = setTimeout(() => reject(new Error(`execution placement store call exceeded ${deadlineMs}ms`)), deadlineMs);
|
|
206
|
+
timer.unref?.();
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
if (timer)
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const goneAnswer = () => (claim.kind === "parked" ? claim.goneAnswer : rootSessionAbsentAnswer());
|
|
216
|
+
let refusal;
|
|
217
|
+
try {
|
|
218
|
+
refusal =
|
|
219
|
+
ctx.device !== undefined
|
|
220
|
+
? await commitDevice(deps, seat, ctx.device, withDeadline, goneAnswer)
|
|
221
|
+
: ctx.placement === "absent"
|
|
222
|
+
? await commitPlace(seat, ctx, occupant, withDeadline, goneAnswer)
|
|
223
|
+
: await reassertWriter(seat, ctx, occupant, withDeadline, goneAnswer);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
deps.logger?.warn("execution_placement_commit_failed", { root: ctx.root, lane: ctx.lane, placement: ctx.placement, deadlineMs, err: err instanceof Error ? err.message : String(err) });
|
|
227
|
+
await release(`execution placement could not be committed: ${err instanceof Error ? err.message : String(err)}`);
|
|
228
|
+
if (ctx.device !== undefined) {
|
|
229
|
+
refusal = deviceRefusal(registryUnavailable());
|
|
230
|
+
deps.metrics?.inc("execution_placement_rejects_total", { errorCode: refusal.body.errorCode });
|
|
231
|
+
return refusal;
|
|
232
|
+
}
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
if (refusal === null)
|
|
236
|
+
return null;
|
|
237
|
+
deps.metrics?.inc("execution_placement_rejects_total", { errorCode: refusal.body.errorCode });
|
|
238
|
+
await release(`execution placement rejected: [${refusal.body.errorCode}] ${refusal.body.error}`);
|
|
239
|
+
return refusal;
|
|
240
|
+
}
|
|
241
|
+
async function releaseClaim(deps, claim, why) {
|
|
242
|
+
switch (claim.kind) {
|
|
243
|
+
case "run":
|
|
244
|
+
await releaseClaimedRun(claim.runStore, claim.taskId, why, (err, step) => {
|
|
245
|
+
deps.logger?.warn("execution_placement_run_release_failed", { taskId: claim.taskId, step, err: err instanceof Error ? err.message : String(err) });
|
|
246
|
+
deps.metrics?.inc("execution_placement_run_release_failed_total");
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
case "parked":
|
|
250
|
+
await claim.rollback(why);
|
|
251
|
+
return;
|
|
252
|
+
default: {
|
|
253
|
+
const unhandled = claim;
|
|
254
|
+
throw new Error(`commitExecutionContext: unknown claim ${JSON.stringify(unhandled)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function commitPlace(seat, ctx, occupant, withDeadline, goneAnswer) {
|
|
259
|
+
if (ctx.lane === "device") {
|
|
260
|
+
return deviceRefusal(buildDeviceRejection(DEVICE_ERROR_CODES.NOT_BOUND, "this session has no device binding — nothing was started"));
|
|
261
|
+
}
|
|
262
|
+
return placeOutcomeAnswer(ctx.root, await withDeadline(seat.place(ctx.root, occupant, { role: "writer", lane: ctx.lane })), goneAnswer);
|
|
263
|
+
}
|
|
264
|
+
function placeOutcomeAnswer(root, outcome, goneAnswer) {
|
|
265
|
+
switch (outcome.outcome) {
|
|
266
|
+
case "placed":
|
|
267
|
+
case "same":
|
|
268
|
+
return null;
|
|
269
|
+
case "tree_bound":
|
|
270
|
+
return treeBoundAnswer(outcome.boundLane);
|
|
271
|
+
case "mirror":
|
|
272
|
+
return sessionMirrorAnswer(root);
|
|
273
|
+
case "gone":
|
|
274
|
+
return goneAnswer();
|
|
275
|
+
default: {
|
|
276
|
+
const unhandled = outcome;
|
|
277
|
+
throw new Error(`commitExecutionContext: no answer for ${JSON.stringify(unhandled)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function reassertWriter(seat, ctx, occupant, withDeadline, goneAnswer) {
|
|
282
|
+
const now = await withDeadline(seat.read(ctx.root));
|
|
283
|
+
if (now === undefined || !sameOccupant(now.occupant, occupant))
|
|
284
|
+
return goneAnswer();
|
|
285
|
+
const cell = now.cell;
|
|
286
|
+
switch (cell.role) {
|
|
287
|
+
case "writer":
|
|
288
|
+
return cell.lane === ctx.lane ? null : treeBoundAnswer(cell.lane);
|
|
289
|
+
case "mirror":
|
|
290
|
+
return sessionMirrorAnswer(ctx.root);
|
|
291
|
+
case "absent":
|
|
292
|
+
case "forking":
|
|
293
|
+
throw new Error(`commitExecutionContext: session ${ctx.root} was resolved as a writer and now reads as ${cell.role} for the same occupant`);
|
|
294
|
+
default: {
|
|
295
|
+
const unknownRole = cell;
|
|
296
|
+
throw new Error(`commitExecutionContext: no arm for ${JSON.stringify(unknownRole)}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function commitDevice(deps, seat, intent, withDeadline, goneAnswer) {
|
|
301
|
+
const enrollment = deps.deviceEnrollment;
|
|
302
|
+
if (!enrollment) {
|
|
303
|
+
throw new Error("commitExecutionContext: a device placement intent was minted without a DeviceEnrollment — assembly bug (both are read from the same ServiceDeps)");
|
|
304
|
+
}
|
|
305
|
+
if (intent.firstBind) {
|
|
306
|
+
const bound = await withDeadline(enrollment.bindPlacement({ rootSessionId: intent.rootSessionId, occupant: intent.occupant, deviceId: intent.admittedDeviceId, requester: intent.owner }));
|
|
307
|
+
if (bound.ok)
|
|
308
|
+
return null;
|
|
309
|
+
if ("reject" in bound)
|
|
310
|
+
return deviceRefusal(bound.reject);
|
|
311
|
+
switch (bound.placement) {
|
|
312
|
+
case "gone":
|
|
313
|
+
return goneAnswer();
|
|
314
|
+
case "mirror":
|
|
315
|
+
return sessionMirrorAnswer(intent.rootSessionId);
|
|
316
|
+
case "tree_bound":
|
|
317
|
+
return treeBoundAnswer(bound.boundLane);
|
|
318
|
+
default: {
|
|
319
|
+
const unhandled = bound;
|
|
320
|
+
throw new Error(`commitExecutionContext: no answer for ${JSON.stringify(unhandled)}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const placement = await withDeadline(seat.read(intent.rootSessionId));
|
|
325
|
+
if (placement === undefined || !sameOccupant(placement.occupant, intent.occupant))
|
|
326
|
+
return goneAnswer();
|
|
327
|
+
const readmitted = await withDeadline(enrollment.admitSession({ rootSessionId: intent.rootSessionId, placement, requester: intent.owner, requestedDeviceId: intent.admittedDeviceId }));
|
|
328
|
+
return readmitted.ok ? null : deviceRefusal(readmitted.reject);
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=execution-context.js.map
|