@sema-agent/server 7.138.0 → 7.140.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 +457 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +91 -41
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/runner.js +1 -1
- package/dist/adoption/sql.js +1 -1
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +26 -26
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/live-deps.js +3 -2
- package/dist/bench/s1/row.d.ts +5 -2
- package/dist/bench/s1/row.js +2 -22
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/adoption.js +1 -1
- 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 +28 -34
- package/dist/boot/execution-lane.d.ts +9 -1
- package/dist/boot/execution-lane.js +6 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-boundary.d.ts +14 -51
- package/dist/boot/memory-boundary.js +10 -20
- package/dist/boot/memory-consolidation.d.ts +15 -11
- package/dist/boot/memory-consolidation.js +25 -14
- package/dist/boot/resolve-spec.d.ts +4 -6
- package/dist/boot/resolve-spec.js +10 -13
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +6 -4
- package/dist/boot/runner-deps.js +3 -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-03-store-backend.d.ts +2 -6
- package/dist/boot/stage-03-store-backend.js +2 -3
- package/dist/boot/stage-04-stores.d.ts +2 -6
- package/dist/boot/stage-04-stores.js +5 -5
- package/dist/boot/stage-05-execution-env.d.ts +2 -6
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +2 -6
- package/dist/boot/stage-06-runners.js +2 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -6
- package/dist/boot/stage-08-reapers.d.ts +2 -6
- package/dist/boot/stage-09-leader.d.ts +2 -6
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.d.ts +2 -6
- package/dist/boot/stage-10-http-server.js +7 -6
- package/dist/boot/stores.d.ts +3 -6
- package/dist/boot/stores.js +27 -40
- package/dist/boot/task-list-lane.d.ts +1 -1
- package/dist/boot/task-list-lane.js +1 -1
- package/dist/boot/web-admin.js +37 -11
- package/dist/capabilities/execution-lane.d.ts +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 +9 -8
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +33 -37
- package/dist/config-provider.js +51 -30
- package/dist/config-types.d.ts +21 -7
- package/dist/config.d.ts +33 -0
- package/dist/config.js +46 -16
- package/dist/delegated-session-owner.d.ts +4 -3
- package/dist/delegated-session-owner.js +1 -1
- package/dist/deployment-shape.d.ts +10 -9
- package/dist/deployment-shape.js +4 -4
- 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 +115 -105
- package/dist/execution-lane-caps.js +76 -82
- 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/memory-compliance-failure-reply.d.ts +3 -1
- package/dist/http/memory-compliance-failure-reply.js +5 -0
- package/dist/http/memory-layer-reply.d.ts +30 -0
- package/dist/http/memory-layer-reply.js +24 -0
- package/dist/http/resume-legs.js +63 -36
- package/dist/http/retired-service-deps.d.ts +1 -1
- package/dist/http/retired-service-deps.js +1 -1
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/adoption.js +1 -1
- 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/memory-bundle.js +3 -0
- package/dist/http/routes/memory-compliance.js +8 -1
- package/dist/http/routes/memory-consolidation.js +12 -1
- package/dist/http/routes/memory-origin.js +14 -2
- package/dist/http/routes/memory-policy.js +5 -0
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +143 -22
- 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 +41 -33
- package/dist/http/server.d.ts +40 -44
- package/dist/http/server.js +13 -2
- 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/identity-source.d.ts +1 -1
- package/dist/identity-source.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/lane-home-probe.d.ts +68 -42
- package/dist/lane-home-probe.js +53 -30
- package/dist/leader/wire.js +5 -4
- package/dist/memory-bundle-engine.d.ts +16 -9
- package/dist/memory-bundle-engine.js +29 -6
- package/dist/memory-operator-faces.d.ts +85 -30
- package/dist/memory-operator-faces.js +136 -30
- package/dist/memory-project-layer.d.ts +3 -21
- package/dist/memory-project-layer.js +11 -8
- package/dist/memory-scope-stores.d.ts +73 -0
- package/dist/memory-scope-stores.js +86 -0
- package/dist/memory-scope.d.ts +29 -5
- package/dist/memory-scope.js +16 -4
- package/dist/memory-sync-client.d.ts +70 -18
- package/dist/memory-sync-client.js +215 -117
- 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/adoption-log-sql.js +4 -4
- 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 +44 -26
- package/dist/plugins/approval-nonce-store.js +4 -4
- package/dist/plugins/blob-backend.d.ts +4 -4
- package/dist/plugins/blob-backend.js +7 -7
- package/dist/plugins/breaker-state-sql.js +2 -2
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-index-columns.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +21 -23
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +68 -122
- package/dist/plugins/ensure-index.d.ts +1 -2
- 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 +9 -8
- package/dist/plugins/file-history-store-sql.js +9 -9
- 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/image-bake-store-sql.js +9 -9
- package/dist/plugins/image-index-sql.js +11 -11
- package/dist/plugins/leader-run-store-sql.js +4 -4
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/mailbox-store-sql.d.ts +2 -1
- package/dist/plugins/mailbox-store-sql.js +4 -4
- package/dist/plugins/memory-engine-tidb.js +5 -5
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/memory-sync-store-tidb.js +3 -3
- package/dist/plugins/one-time-migrations.d.ts +117 -8
- package/dist/plugins/one-time-migrations.js +408 -38
- package/dist/plugins/org-delivery-quota-store.js +2 -2
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/outcome-ledger-sql.js +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +17 -19
- package/dist/plugins/permission-rule-store-sql.js +29 -32
- 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 +18 -19
- package/dist/plugins/remote-env-adb.js +91 -66
- package/dist/plugins/remote-env-device.d.ts +23 -31
- package/dist/plugins/remote-env-device.js +19 -17
- package/dist/plugins/remote-env-e2b.d.ts +27 -21
- package/dist/plugins/remote-env-e2b.js +91 -45
- package/dist/plugins/remote-env-file-error.d.ts +3 -1
- package/dist/plugins/remote-env-file-error.js +9 -3
- package/dist/plugins/remote-env-host.d.ts +31 -34
- package/dist/plugins/remote-env-host.js +91 -72
- package/dist/plugins/remote-env-k8s.d.ts +13 -29
- package/dist/plugins/remote-env-k8s.js +63 -55
- package/dist/plugins/remote-env-local-docker.d.ts +24 -12
- package/dist/plugins/remote-env-local-docker.js +49 -61
- package/dist/plugins/remote-env-ssh.d.ts +11 -19
- package/dist/plugins/remote-env-ssh.js +74 -57
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/resume-anchor-store-sql.js +2 -2
- package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
- package/dist/plugins/retention-lane-store-sql.js +2 -2
- package/dist/plugins/retention-store-sql.d.ts +10 -7
- package/dist/plugins/retention-store-sql.js +13 -7
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +20 -15
- package/dist/plugins/schema-consistency.d.ts +21 -11
- package/dist/plugins/schema-consistency.js +15 -9
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/schema-ensure-legs.d.ts +2 -3
- package/dist/plugins/schema-ensure-legs.js +2 -2
- package/dist/plugins/schema-live-columns.d.ts +1 -1
- package/dist/plugins/session-capture-record-store-sql.js +1 -1
- 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-rules-column.d.ts +1 -1
- package/dist/plugins/session-rules-column.js +1 -1
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/session-workspace-cell.d.ts +1 -1
- package/dist/plugins/session-workspace-cell.js +1 -1
- package/dist/plugins/shared-memory-store-sql.js +2 -2
- package/dist/plugins/sql-driver.d.ts +10 -5
- package/dist/plugins/sql-driver.js +6 -6
- package/dist/plugins/sql-errors.js +1 -1
- package/dist/plugins/store-backend.d.ts +5 -7
- package/dist/plugins/store-backend.js +23 -11
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/task-list-store-sql.js +11 -11
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +13 -7
- package/dist/plugins/tidb-session-storage.js +4 -4
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +127 -59
- package/dist/plugins/tool-result-store-sql.d.ts +5 -4
- package/dist/plugins/tool-result-store-sql.js +21 -13
- package/dist/plugins/user-input-ledger-sql.js +2 -2
- package/dist/plugins/workflow-journal-store-sql.js +5 -5
- package/dist/plugins/workflow-run-store-sql.js +19 -19
- package/dist/plugins/write-behind-counter.d.ts +2 -3
- package/dist/plugins/write-behind-counter.js +5 -5
- 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/resource-suspend.d.ts +14 -12
- package/dist/resource-suspend.js +1 -1
- package/dist/resource-window.d.ts +4 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +34 -35
- package/dist/runs.d.ts +10 -5
- package/dist/runs.js +52 -66
- 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 +20 -10
- package/dist/session-incarnation.js +5 -3
- package/dist/session-policy-wire.d.ts +1 -1
- 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/sql-engine-posture.d.ts +1 -1
- package/dist/sql-engine-posture.js +1 -1
- package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
- package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
- package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
- package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
- package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
- package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-a2a.js +5 -6
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +22 -9
- package/dist/task-mcp.js +17 -9
- package/dist/terminal.d.ts +27 -0
- package/dist/terminal.js +6 -0
- 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/redact.d.ts +3 -1
- package/dist/trace/redact.js +80 -28
- package/dist/trace/self-minted-wire.d.ts +169 -2
- package/dist/trace/self-minted-wire.js +54 -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/candidate-gate.d.ts +11 -7
- package/dist/web-admin-client/candidate-gate.js +1 -1
- package/dist/web-admin-client/http-client.d.ts +5 -4
- package/dist/web-admin-client/http-client.js +2 -2
- package/dist/web-admin-client/read-warnings.d.ts +7 -17
- package/dist/web-admin-client/read-warnings.js +5 -9
- 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/wire-changes/server@7.140.0.json +184 -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
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* core 的另外三只 409 与 `ControlPlaneCorruptError` 全落 500 `internal.error` —— 「同一个 requestId 拿去抹另一个会话」这种**调用方自己
|
|
9
9
|
* 能改**的错被说成服务器坏了。两份分类 = 两个判官,少的那份恒是静默的一份。现在**码与状态**只在这里判一次,两面同读。
|
|
10
10
|
*
|
|
11
|
-
* ## 规则(三层,与 operator
|
|
11
|
+
* ## 规则(三层,与 operator 面既有层序逐字同序;另有本服务自铸的一只,先判)
|
|
12
|
+
* ⓪ 本服务的抹除店护栏拒 {@link MemoryErasureStoreUnjudged} —— 409 `memory.erasure_store_unjudged`(抹除落到部署的店之外的那只店:本部署此刻判不了
|
|
13
|
+
* 同一个 `requestId` 跨店的幂等;什么都没删)。文案按受众,同下一节的规则;
|
|
12
14
|
* ① core 的**本口专属**码(闭集穷举 `switch`,见 {@link sentCoreCode})—— 码与状态两面同;文案按受众(下一节,一条规则);
|
|
13
15
|
* ② **店形拒**两码 `memory.target_outside_store`(core 7.31.1 [ref])/ `memory.scope_home_not_directory`(core 7.44.0 [ref])—— 跨面 store 状态码,409、码原样
|
|
14
16
|
* (判据 `memory-store-refusal.ts` 一只;运维行 `memory_store_form_refused { code, path?, form? }` 两受众同一只铸点,路径只在那里);
|
|
@@ -3,6 +3,7 @@ import { logStoreFormRefusal, storeFormRefusalOf } from "../memory-store-refusal
|
|
|
3
3
|
import { sentMemoryStoreRefusal } from "./memory-store-refusal-reply.js";
|
|
4
4
|
import { sendError } from "./send.js";
|
|
5
5
|
import { assertNever } from "./route-ctx.js";
|
|
6
|
+
import { MemoryErasureStoreUnjudged } from "../memory-operator-faces.js";
|
|
6
7
|
function sentCoreCode(res, coreCode, coreMessage, audience) {
|
|
7
8
|
switch (coreCode) {
|
|
8
9
|
case "config.memory_erasure_request":
|
|
@@ -42,6 +43,10 @@ function codeOf(err) {
|
|
|
42
43
|
return typeof code === "string" ? code : undefined;
|
|
43
44
|
}
|
|
44
45
|
export function sentMemoryComplianceFailure(res, err, audience, logger) {
|
|
46
|
+
if (err instanceof MemoryErasureStoreUnjudged) {
|
|
47
|
+
sendError(res, 409, "memory.erasure_store_unjudged", textFor(audience, err.message, "session memory erasure refused — this erasure reaches memory kept in this deployment's second memory store, which this build does not erase from (an erasure requestId is judged on the deployment's main store only); nothing was erased — it becomes available with the next engine update, or ask the operator"));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
45
50
|
const coreCode = codeOf(err);
|
|
46
51
|
if (coreCode !== undefined && sentCoreCode(res, coreCode, err instanceof Error ? err.message : String(err), audience))
|
|
47
52
|
return true;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆动词的**层座**与「这次请求落不到一只记忆店」的拒 —— 两件的 HTTP 半场,唯一一处。
|
|
3
|
+
*
|
|
4
|
+
* ## 规则(店的那一半在 `src/memory-scope-stores.ts`)
|
|
5
|
+
* 一次记忆请求只落一只店:请求点名的层住的那一只;不点名层的动词(条目 id / 店级账本)答在本部署点名的层与部署的店共处的那一只上,
|
|
6
|
+
* 两只店分住时不猜、拒。会话 id 的两口不点名层时不拒:记忆状态读本部署的每一只店再折成一份,按会话抹除落部署的店(`memory-operator-faces.ts`)。
|
|
7
|
+
* 不点名层的动词因此多一个可选的**层座** `scope`:GET 口在查询串(`?scope=<层键>`),POST 口在体里(`"scope": "<层键>"`);
|
|
8
|
+
* 点名的层只用来定店,动词本身的语义不变。点名层的动词(导出 / 导入 / 外源列表 / 整理 / operator 抹除的 `select.scope` 与 `expectedScopes`)
|
|
9
|
+
* 本来就点名,不加座。
|
|
10
|
+
*
|
|
11
|
+
* ## 层座的读法({@link readLayerSeat})
|
|
12
|
+
* 缺席 ⇒ 没点名;在场 ⇒ 必须是非空串,否则 400 `request.field_invalid`、面一次都不调(空串与非串不是「没点名」—— 静默当缺席会把请求答在
|
|
13
|
+
* 另一只店上)。
|
|
14
|
+
*
|
|
15
|
+
* ## 拒的映射({@link sentMemoryLayerRefusal})
|
|
16
|
+
* `MemoryLayerRefusal` ⇒ 400 `request.memory_layer`:请求可以改对(点名层 / 按店拆成几次),服务器没坏,也不是盘形问题。
|
|
17
|
+
* 文案按受众分(与 `memory-compliance-failure-reply.ts` 同一条规则):operator ⇒ 原句(点名部署配置的层、哪几层住在哪只店 —— 运维要的就是它);
|
|
18
|
+
* 会话所有者 ⇒ 固定一句(只说怎么办),原句交 `withheld` 落运维行 —— 部署配置的层键不是这个受众的材料。
|
|
19
|
+
*/
|
|
20
|
+
import type { ServerResponse } from "node:http";
|
|
21
|
+
import type { MemoryLayerSeat } from "../memory-operator-faces.js";
|
|
22
|
+
import type { MemoryComplianceAudience } from "./memory-compliance-failure-reply.js";
|
|
23
|
+
/**
|
|
24
|
+
* 层座:`present` = 键在不在(查询串 `has` / 体 `in`),`value` = 它的值。缺席 ⇒ `undefined`;非空串 ⇒ `{ layer }`;
|
|
25
|
+
* 其余 ⇒ 已答 400 `request.field_invalid`,返回 `null`(调用方 `return`)。
|
|
26
|
+
*/
|
|
27
|
+
export declare function readLayerSeat(res: ServerResponse, present: boolean, value: unknown): MemoryLayerSeat | undefined | null;
|
|
28
|
+
/** `err` 是本服务的层拒 ⇒ 400 `request.memory_layer`(文案按受众),返回 true;否则 false,一个字节都不写。 */
|
|
29
|
+
export declare function sentMemoryLayerRefusal(res: ServerResponse, err: unknown, audience: MemoryComplianceAudience): boolean;
|
|
30
|
+
//# sourceMappingURL=memory-layer-reply.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MemoryLayerRefusal } from "../memory-scope-stores.js";
|
|
2
|
+
import { sendError } from "./send.js";
|
|
3
|
+
export function readLayerSeat(res, present, value) {
|
|
4
|
+
if (!present)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (typeof value === "string" && value.length > 0)
|
|
7
|
+
return { layer: value };
|
|
8
|
+
sendError(res, 400, "request.field_invalid", "scope must be a non-empty memory layer key when present — it names the layer whose memory store this request is answered on");
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
export function sentMemoryLayerRefusal(res, err, audience) {
|
|
12
|
+
if (!(err instanceof MemoryLayerRefusal))
|
|
13
|
+
return false;
|
|
14
|
+
switch (audience.kind) {
|
|
15
|
+
case "operator":
|
|
16
|
+
sendError(res, 400, "request.memory_layer", err.message);
|
|
17
|
+
return true;
|
|
18
|
+
case "session_owner":
|
|
19
|
+
audience.withheld(err.message);
|
|
20
|
+
sendError(res, 400, "request.memory_layer", "the memory store this request reaches cannot be told — the `scope` in the request is not a memory layer this deployment can read, or this deployment's own memory layer configuration cannot be read; resend without `scope` (or with a readable layer), or ask the operator");
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=memory-layer-reply.js.map
|