@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.
Files changed (215) hide show
  1. package/MIGRATION.md +276 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +73 -29
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/wire.d.ts +3 -3
  8. package/dist/approval-card.d.ts +20 -20
  9. package/dist/bench/s1/arms.d.ts +1 -1
  10. package/dist/bench/s1/arms.js +1 -3
  11. package/dist/bench/s1/run-firm.js +2 -2
  12. package/dist/boot/device-lane.d.ts +8 -3
  13. package/dist/boot/device-lane.js +7 -2
  14. package/dist/boot/execution-env.d.ts +4 -2
  15. package/dist/boot/execution-env.js +16 -24
  16. package/dist/boot/execution-lane.d.ts +1 -1
  17. package/dist/boot/governance-seams.d.ts +3 -2
  18. package/dist/boot/governance-seams.js +9 -11
  19. package/dist/boot/memory-consolidation.js +2 -2
  20. package/dist/boot/resolve-spec.d.ts +1 -1
  21. package/dist/boot/resolve-spec.js +8 -9
  22. package/dist/boot/retention-lane.d.ts +5 -5
  23. package/dist/boot/retention-lane.js +0 -6
  24. package/dist/boot/runner-deps.d.ts +3 -1
  25. package/dist/boot/runner-deps.js +2 -1
  26. package/dist/boot/session-faces.d.ts +0 -3
  27. package/dist/boot/session-faces.js +15 -8
  28. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  29. package/dist/boot/stage-05-execution-env.js +2 -2
  30. package/dist/boot/stage-06-runners.js +0 -1
  31. package/dist/boot/stage-09-leader.js +1 -2
  32. package/dist/boot/stage-10-http-server.js +2 -2
  33. package/dist/boot/stores.js +4 -4
  34. package/dist/boot/web-admin.js +2 -2
  35. package/dist/capabilities/mcp-probe.js +1 -1
  36. package/dist/capabilities/scenarios.d.ts +1 -1
  37. package/dist/capability-facts.d.ts +15 -7
  38. package/dist/capability-facts.js +8 -3
  39. package/dist/config-catalog.js +7 -7
  40. package/dist/config-invariants.d.ts +23 -3
  41. package/dist/config-invariants.js +42 -0
  42. package/dist/config-provider.d.ts +19 -24
  43. package/dist/config-provider.js +27 -12
  44. package/dist/config-types.d.ts +10 -5
  45. package/dist/config.d.ts +33 -0
  46. package/dist/config.js +44 -15
  47. package/dist/delegated-session-owner.d.ts +2 -2
  48. package/dist/deployment-shape.d.ts +1 -1
  49. package/dist/device-enrollment.d.ts +41 -2
  50. package/dist/device-enrollment.js +27 -12
  51. package/dist/device-store.d.ts +58 -85
  52. package/dist/device-store.js +19 -49
  53. package/dist/device-ws-hub.js +2 -1
  54. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  55. package/dist/exec-cut.d.ts +60 -0
  56. package/dist/exec-cut.js +36 -0
  57. package/dist/execution-lane-caps.d.ts +19 -0
  58. package/dist/execution-lane-caps.js +10 -0
  59. package/dist/execution-placement.d.ts +259 -0
  60. package/dist/execution-placement.js +199 -0
  61. package/dist/field-conflict.d.ts +3 -2
  62. package/dist/field-conflict.js +1 -0
  63. package/dist/hosted-posture.d.ts +2 -2
  64. package/dist/http/admission.js +8 -19
  65. package/dist/http/checkpoint-ctx.d.ts +21 -12
  66. package/dist/http/checkpoint-ctx.js +4 -2
  67. package/dist/http/claimed-run.d.ts +1 -1
  68. package/dist/http/durable-run-leg.js +9 -5
  69. package/dist/http/execution-context.d.ts +218 -0
  70. package/dist/http/execution-context.js +330 -0
  71. package/dist/http/resume-legs.js +50 -20
  72. package/dist/http/route-ctx.d.ts +14 -8
  73. package/dist/http/routes/background.d.ts +29 -1
  74. package/dist/http/routes/background.js +29 -2
  75. package/dist/http/routes/devices.d.ts +13 -19
  76. package/dist/http/routes/devices.js +21 -26
  77. package/dist/http/routes/session-sync.js +110 -15
  78. package/dist/http/routes/sessions.d.ts +6 -0
  79. package/dist/http/routes/sessions.js +115 -11
  80. package/dist/http/routes/side-query.d.ts +12 -4
  81. package/dist/http/routes/side-query.js +4 -5
  82. package/dist/http/routes/tasks.js +37 -31
  83. package/dist/http/server.d.ts +35 -19
  84. package/dist/http/server.js +11 -0
  85. package/dist/http/session-inherit-env.d.ts +3 -4
  86. package/dist/http/session-inherit-env.js +1 -1
  87. package/dist/http/wire-types.d.ts +8 -12
  88. package/dist/index.d.ts +1 -0
  89. package/dist/memory-sync-client.d.ts +53 -14
  90. package/dist/memory-sync-client.js +196 -116
  91. package/dist/memory-sync.d.ts +1 -1
  92. package/dist/memory-sync.js +3 -2
  93. package/dist/observability/cost-taxonomy.d.ts +2 -2
  94. package/dist/observability/fail-open.d.ts +12 -0
  95. package/dist/observability/fail-open.js +12 -0
  96. package/dist/observability/run-terminal.js +0 -1
  97. package/dist/orchestration/workflow-notify-journal.js +4 -4
  98. package/dist/org-memory-admission.d.ts +4 -3
  99. package/dist/org-memory-admission.js +6 -6
  100. package/dist/parked-decide.d.ts +34 -9
  101. package/dist/parked-decide.js +12 -19
  102. package/dist/parked-revive-surface.d.ts +12 -15
  103. package/dist/parked-revive-surface.js +3 -6
  104. package/dist/peer-seat.d.ts +13 -7
  105. package/dist/peer-seat.js +37 -3
  106. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  107. package/dist/plugins/approval-ask-store-file.js +11 -0
  108. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  109. package/dist/plugins/approval-ask-store-memory.js +12 -0
  110. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  111. package/dist/plugins/approval-ask-store-sql.js +30 -12
  112. package/dist/plugins/caching-session-store.d.ts +6 -2
  113. package/dist/plugins/caching-session-store.js +9 -3
  114. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  115. package/dist/plugins/checkpoint-store-sql.js +5 -7
  116. package/dist/plugins/device-store-sql.d.ts +25 -51
  117. package/dist/plugins/device-store-sql.js +49 -103
  118. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  119. package/dist/plugins/execution-placement-sql.js +223 -0
  120. package/dist/plugins/file-history-store-sql.d.ts +7 -6
  121. package/dist/plugins/file-history-store-sql.js +5 -5
  122. package/dist/plugins/file-run-store.d.ts +6 -1
  123. package/dist/plugins/file-run-store.js +4 -0
  124. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  125. package/dist/plugins/fork-routing-session-store.js +16 -0
  126. package/dist/plugins/local-session-store.d.ts +149 -50
  127. package/dist/plugins/local-session-store.js +382 -107
  128. package/dist/plugins/memory-key-guards.d.ts +4 -0
  129. package/dist/plugins/memory-key-guards.js +1 -0
  130. package/dist/plugins/memory-run-store.d.ts +1 -1
  131. package/dist/plugins/one-time-migrations.d.ts +111 -3
  132. package/dist/plugins/one-time-migrations.js +380 -10
  133. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  134. package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
  135. package/dist/plugins/permission-rule-store-sql.js +13 -16
  136. package/dist/plugins/pg-pool.d.ts +7 -3
  137. package/dist/plugins/pg-session-storage.d.ts +31 -20
  138. package/dist/plugins/pg-session-storage.js +120 -52
  139. package/dist/plugins/remote-env-adb.d.ts +20 -4
  140. package/dist/plugins/remote-env-adb.js +70 -62
  141. package/dist/plugins/remote-env-device.d.ts +30 -12
  142. package/dist/plugins/remote-env-device.js +18 -17
  143. package/dist/plugins/remote-env-e2b.d.ts +18 -4
  144. package/dist/plugins/remote-env-e2b.js +41 -42
  145. package/dist/plugins/remote-env-file-error.d.ts +2 -0
  146. package/dist/plugins/remote-env-file-error.js +1 -1
  147. package/dist/plugins/remote-env-host.d.ts +21 -8
  148. package/dist/plugins/remote-env-host.js +66 -70
  149. package/dist/plugins/remote-env-k8s.d.ts +11 -16
  150. package/dist/plugins/remote-env-k8s.js +61 -54
  151. package/dist/plugins/remote-env-local-docker.d.ts +29 -6
  152. package/dist/plugins/remote-env-local-docker.js +46 -59
  153. package/dist/plugins/remote-env-ssh.d.ts +16 -4
  154. package/dist/plugins/remote-env-ssh.js +72 -56
  155. package/dist/plugins/remote-shell.d.ts +100 -3
  156. package/dist/plugins/remote-shell.js +166 -2
  157. package/dist/plugins/retention-store-sql.d.ts +9 -6
  158. package/dist/plugins/retention-store-sql.js +7 -1
  159. package/dist/plugins/run-store-sql.d.ts +13 -3
  160. package/dist/plugins/run-store-sql.js +5 -0
  161. package/dist/plugins/schema-consistency.d.ts +15 -5
  162. package/dist/plugins/schema-consistency.js +14 -8
  163. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  164. package/dist/plugins/schema-drift-sentence.js +38 -0
  165. package/dist/plugins/session-fork.d.ts +54 -0
  166. package/dist/plugins/session-fork.js +38 -1
  167. package/dist/plugins/session-row-sql.d.ts +22 -0
  168. package/dist/plugins/session-row-sql.js +81 -0
  169. package/dist/plugins/session-store.d.ts +1 -1
  170. package/dist/plugins/store-backend.d.ts +1 -1
  171. package/dist/plugins/store-backend.js +19 -7
  172. package/dist/plugins/store-contracts.d.ts +25 -5
  173. package/dist/plugins/tidb-pool.d.ts +12 -7
  174. package/dist/plugins/tidb-pool.js +9 -3
  175. package/dist/plugins/tidb-session-store.d.ts +57 -26
  176. package/dist/plugins/tidb-session-store.js +118 -50
  177. package/dist/plugins/tool-result-store-sql.d.ts +3 -2
  178. package/dist/plugins/tool-result-store-sql.js +18 -10
  179. package/dist/project-memory.d.ts +9 -15
  180. package/dist/project-memory.js +5 -4
  181. package/dist/request-key-closure.js +1 -1
  182. package/dist/request-value-closure.d.ts +6 -7
  183. package/dist/request-value-closure.js +1 -3
  184. package/dist/run-local.d.ts +6 -0
  185. package/dist/run-local.js +24 -20
  186. package/dist/runs.d.ts +2 -1
  187. package/dist/runs.js +3 -1
  188. package/dist/security.d.ts +38 -12
  189. package/dist/security.js +8 -2
  190. package/dist/server-secret-env.d.ts +9 -5
  191. package/dist/session-children-retire.d.ts +27 -9
  192. package/dist/session-children-retire.js +9 -5
  193. package/dist/session-incarnation.d.ts +19 -9
  194. package/dist/session-incarnation.js +4 -2
  195. package/dist/session-row.d.ts +114 -5
  196. package/dist/session-row.js +55 -5
  197. package/dist/session-titler.d.ts +2 -2
  198. package/dist/task-a2a.d.ts +1 -1
  199. package/dist/task-cwd.d.ts +6 -1
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/tool-approval.d.ts +6 -0
  202. package/dist/tool-approval.js +8 -1
  203. package/dist/trace/project.d.ts +1 -0
  204. package/dist/trace/project.js +1 -0
  205. package/dist/trace/self-minted-wire.d.ts +111 -2
  206. package/dist/trace/self-minted-wire.js +38 -1
  207. package/dist/url-refusal.d.ts +1 -1
  208. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  209. package/dist/web-admin-client/apply-effective.js +20 -10
  210. package/dist/web-admin-client/skills-mcp.js +3 -2
  211. package/dist/web-admin-client/types.d.ts +6 -0
  212. package/package.json +3 -3
  213. package/wire-changes/server@7.139.0.json +348 -0
  214. package/dist/http/device-placement.d.ts +0 -175
  215. package/dist/http/device-placement.js +0 -145
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Model, type PathRuleBases, type PermissionRule } from "@sema-agent/core";
2
+ import { type ConfigProviderWord } from "./config-types.js";
2
3
  import { type CenterEndpoint } from "./web-admin-client/center-request.js";
3
4
  import { type Autonomy } from "./runtime-governance.js";
4
5
  import { type RunnerEnvNameAllowlistKnob, type ServerEnvNameAllowlistKnob } from "./env-name-allowlist-knobs.js";
@@ -106,6 +107,19 @@ export interface ConfigNotice {
106
107
  fields: Record<string, string>;
107
108
  }
108
109
  export declare function drainConfigNotices(): ConfigNotice[];
110
+ /**
111
+ * [ref] —— `CONFIG_PROVIDER` 的**唯一**读点。闭集 {@link CONFIG_PROVIDER_WORDS};缺席 / 空串 ⇒ 未表态(`undefined`);
112
+ * 其余一律**拒启**,拒因句列合法词。逐字比较(不 trim、不折大小写)—— 与本仓其余闭集 env 同律([ref]:认不出的词
113
+ * 不许被悄悄猜成某一种意思)。
114
+ *
115
+ * 修前这根旋钮是开放词,而一个认不出的词在同一进程里被读成两种意思:配置源判官(`configProviderKind`)把它当 `local`
116
+ * (读 `config.d/`、执行车道合成步照读 `remote-exec.json`),这里的隐式 host 臂与车道落点披露却按字面 `=== "local"`
117
+ * 判它**不是** local;web-admin 工厂同样按字面只在 `"local"` 上递本地根,于是配置源的根落回 `process.cwd()`、
118
+ * 合成步读的是 `localRoot` —— 两个根分家。收成闭集 + 一个读点之后,这三处读的是同一个值。
119
+ * 调用方两处、读的是同一张闭集:`loadConfig` 开头一次;`run-local` 在 `loadConfig` 之前一次(本入口只装本地配置源,词是 `remote` 就在这里拒 ——
120
+ * 配没配中心地址都一样)。
121
+ */
122
+ export declare function parseConfigProvider(): ConfigProviderWord | undefined;
109
123
  /** F10 (codex audit): drain + log BOTH pre-logger diagnostic collectors (S20 soft-knob warnings + [ref]④ boot
110
124
  * notices) in one place. EVERY entrypoint that calls loadConfig() then builds a logger must call this right
111
125
  * after (main.ts, run-local.ts) — a new entry that forgets it would silently drop operator-facing diagnostics
@@ -212,6 +226,25 @@ export declare function configKnobTable(): ConfigKnobRecord[];
212
226
  export declare const hostBackgroundShellEnabled: () => boolean;
213
227
  /** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
214
228
  export declare const hostExecSpoolEnabled: () => boolean;
229
+ /** 名单在活配置里的落点(服务进程四只旋钮;bake-runner 那只在它自己的进程里,不在 `ServiceConfig`)。 */
230
+ export type EnvNameAllowlistSeats = Pick<ServiceConfig, "lspEnvAllow" | "remoteExec">;
231
+ /**
232
+ * 沙箱名单(表列 `channel: "sandbox-grant"` 的服务侧旋钮,表派生)此刻交出去的名 —— 起服行 `remote_exec_enabled.sandboxEnvKeys` 读它(`boot/execution-env.ts`,
233
+ * 排序后只出键名)。只有被选中车道的那一只有落点;e2b / k8s 报的是名单点到的键,不含 `SANDBOX_PKG_SOURCE` 推导出的包源变量(那一组是区域常量,装配点另并)。
234
+ */
235
+ export declare function sandboxEnvNamesInForce(seats: EnvNameAllowlistSeats): readonly string[];
236
+ /** 声明增长后重判的一处命中:哪只旋钮、它交出去的哪个名、判官的拒因({@link ownCredentialNameProblem} 那一句,只回显名)。 */
237
+ export interface OwnCredentialInForce {
238
+ readonly knob: ServerEnvNameAllowlistKnob;
239
+ readonly name: string;
240
+ readonly problem: string;
241
+ }
242
+ /**
243
+ * 解析器头注 ② 那条不变量在**声明增长**那一刻的半边:对活配置里每只名单此刻交出去的名问**同一只**判官({@link ownCredentialNameProblem} → 引擎
244
+ * `inheritEnvPolicyProblem`,读引擎此刻的声明)。读活配置的落点,不重读 env(名单是起服输入;解析那一刻已判过形与当时的声明)。返回每一处命中,空 = 不相交。
245
+ * 调用点只有一处 = 声明增长的那一处(`apply-effective.ts` 的换代声明,真声明之后、席位换装之前)。
246
+ */
247
+ export declare function ownCredentialHitsInForce(seats: EnvNameAllowlistSeats): OwnCredentialInForce[];
215
248
  /** 名单型 env 旋钮的**唯一**解析器(规则全文见上方 `ENV_NAME_ALLOWLIST_KNOBS` 头注)。`undefined` = 没配(未设 / 空串)。
216
249
  * 服务侧旋钮要交本进程的姿态(③);runner 旋钮没有姿态,签名上就不收 —— 读者与行形由叶表的类型一起定,传错是编译红。 */
217
250
  export declare function parseEnvNameAllowlist(knob: ServerEnvNameAllowlistKnob, singleUserTurnkey: boolean): readonly string[] | undefined;
package/dist/config.js CHANGED
@@ -9,6 +9,7 @@ import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
9
9
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
10
  import { buildRuleListJudgeOptions } from "./permission-rule-list.js";
11
11
  import { isV2ScopeKey, memoryReplicationCoverage, replicatesProjectLayersOf } from "./memory-scope.js";
12
+ import { memorySyncPeerOf } from "./memory-sync-client.js";
12
13
  import { assertProjectLayerOwnable } from "./project-identity.js";
13
14
  import { projectLayerOwnerOf } from "./project-layer-key.js";
14
15
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
@@ -26,7 +27,7 @@ import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat,
26
27
  import {} from "./posture-source.js";
27
28
  import { COST_QUOTA_WINDOW_SEC_RANGE, costQuotaWindowSecInRange } from "./observability/cost-quota.js";
28
29
  import { NAME_TABLE_BEHAVIOR, TOOL_NAME_ROSTER, describeUnmatchableRuleName, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
29
- import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal, operatorSingleUserRefusal, workflowParkDecisionDomainRefusal } from "./config-invariants.js";
30
+ import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal, operatorSingleUserRefusal, remoteConfigProviderUrlRefusal, retentionLaneRefusal, workflowParkDecisionDomainRefusal } from "./config-invariants.js";
30
31
  import { declaredSemaConfigDir } from "./sema-config-dir.js";
31
32
  import { lkgPathOverrideOf } from "./config-lkg.js";
32
33
  import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE, parseWorkspaceSnapshotExclude, parseWorkspaceSnapshotPrefix } from "./workspace-space.js";
@@ -35,7 +36,7 @@ import { laneLivenessOf } from "./exec-bounds.js";
35
36
  import { SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS } from "./sandbox-egress-policy.js";
36
37
  import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE_MOUNT_RESERVED, WORKSPACE_CACHE_SIZE_DEFAULT, WORKSPACE_CACHE_WORDS, isK8sQuantity, mountsOverlap, reservedMountCollision } from "./workspace-cache.js";
37
38
  import { DEPLOYMENT_SHAPE_PREDICATES } from "./deployment-shape.js";
38
- import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, laneRequiredEnv } from "./execution-lane-caps.js";
39
+ import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, laneMissingEnv } from "./execution-lane-caps.js";
39
40
  import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
40
41
  import { AUTONOMY_MODES } from "./runtime-governance.js";
41
42
  import { MAX_NAMED_INERT_ENTRIES, allowLayerEntryCountIssue, describeInertRuleEntries, inertRuleEntriesOf, inertRuleEntryGuidance, ruleEntryListShapeOk } from "./rule-entry-list.js";
@@ -195,7 +196,7 @@ function buildExecLaneNotice(remoteExec) {
195
196
  };
196
197
  }
197
198
  const ENV_VALUE_ECHO_CHARS = 120;
198
- function parseConfigProvider() {
199
+ export function parseConfigProvider() {
199
200
  const raw = process.env.CONFIG_PROVIDER;
200
201
  if (raw === undefined || raw === "")
201
202
  return undefined;
@@ -509,11 +510,32 @@ function parseNameListEnv(name, raw, opts) {
509
510
  }
510
511
  const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
511
512
  const ENV_NAME_ALLOWLIST_WITHHELD = "<withheld: entries are not echoed — a value pasted in place of a name can pass the name check>";
512
- function ownCredentialPutBackProblem(name) {
513
+ const OWN_CREDENTIAL_HANDOFF = {
514
+ "host-put-back": { where: "a put-back list hands the named variable back, as written, to the child the engine withheld it from", whom: "that child" },
515
+ "sandbox-grant": { where: "a sandbox list puts the named variable, as written, into a sandbox the model can read", whom: "the sandbox" },
516
+ };
517
+ function ownCredentialNameProblem(name, channel) {
513
518
  const engine = inheritEnvPolicyProblem([name]);
514
- return engine === undefined
515
- ? undefined
516
- : `this deployment's own credential — a put-back list hands the named variable back, as written, to the child the engine withheld it from (the engine refuses it on any allow list: ${engine})`;
519
+ if (engine === undefined)
520
+ return undefined;
521
+ const { where, whom } = OWN_CREDENTIAL_HANDOFF[channel];
522
+ return `this deployment's own credential — ${where}; give ${whom} a credential of its own (another name and another value) and list that name instead (the engine refuses it on any allow list: ${engine})`;
523
+ }
524
+ const NAMES_IN_FORCE = {
525
+ LSP_ENV_ALLOW: (c) => c.lspEnvAllow,
526
+ E2B_SANDBOX_ENV: (c) => (c.remoteExec?.provider === "e2b" ? Object.keys(c.remoteExec.sandboxEnv ?? {}) : []),
527
+ K8S_SANDBOX_ENV: (c) => (c.remoteExec?.provider === "k8s" ? Object.keys(c.remoteExec.sandboxEnv ?? {}) : []),
528
+ DOCKER_SANDBOX_ENV: (c) => (c.remoteExec?.provider === "local-docker" ? Object.keys(c.remoteExec.env ?? {}) : []),
529
+ };
530
+ const SERVER_ENV_NAME_ALLOWLIST_KNOBS = Object.keys(NAMES_IN_FORCE);
531
+ export function sandboxEnvNamesInForce(seats) {
532
+ return SERVER_ENV_NAME_ALLOWLIST_KNOBS.filter((knob) => ENV_NAME_ALLOWLIST_KNOBS[knob].channel === "sandbox-grant").flatMap((knob) => NAMES_IN_FORCE[knob](seats));
533
+ }
534
+ export function ownCredentialHitsInForce(seats) {
535
+ return SERVER_ENV_NAME_ALLOWLIST_KNOBS.flatMap((knob) => NAMES_IN_FORCE[knob](seats).flatMap((name) => {
536
+ const problem = ownCredentialNameProblem(name, ENV_NAME_ALLOWLIST_KNOBS[knob].channel);
537
+ return problem === undefined ? [] : [{ knob, name, problem }];
538
+ }));
517
539
  }
518
540
  export function parseEnvNameAllowlist(knob, singleUserTurnkey) {
519
541
  const raw = process.env[knob];
@@ -528,9 +550,7 @@ export function parseEnvNameAllowlist(knob, singleUserTurnkey) {
528
550
  ? 'not an environment-variable name (letters, digits and "_", not starting with a digit)'
529
551
  : ENV_NAME_ALLOWLIST_KNOB_NAMES.has(seg.toUpperCase())
530
552
  ? `the name of a name-list knob (${[...ENV_NAME_ALLOWLIST_KNOB_NAMES].join(" / ")}: their raw text is withheld from every child process and sandbox; naming one would put that text back)`
531
- : spec.channel === "host-put-back"
532
- ? ownCredentialPutBackProblem(seg)
533
- : undefined;
553
+ : ownCredentialNameProblem(seg, spec.channel);
534
554
  if (problem === undefined)
535
555
  return;
536
556
  throw new Error(`env ${knob}: entry #${i + 1} of ${segments.length} is ${problem}` +
@@ -1704,6 +1724,7 @@ function parseMemoryDomain(ctx) {
1704
1724
  if (memorySyncToken === undefined) {
1705
1725
  throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
1706
1726
  }
1727
+ memorySyncPeerOf(memorySyncUrl);
1707
1728
  if (memorySyncScopeRaw === undefined && memoryScope !== undefined && !isV2ScopeKey(memoryScope)) {
1708
1729
  throw new Error(`MEMORY_SYNC_URL is set but MEMORY_SYNC_SCOPE is not set and MEMORY_SCOPE (${JSON.stringify(memoryScope)}) is not a typed scope key — the sync leg replicates exactly ONE scope by name ` +
1709
1730
  "(core syncMemoryScope takes the entries whose scope === the sync scope and POSTs them under that same name), so deriving `user:<MEMORY_SCOPE>` would name a scope this deployment never writes and nothing would ever be synced (S-964). " +
@@ -1902,10 +1923,10 @@ function parseOrchestrationDomain(ctx) {
1902
1923
  throw new Error(`REMOTE_EXEC="${lane}" is not an execution lane — valid: ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")}. ` +
1903
1924
  `An unrecognized value used to fall through to the host/in-process lane silently; refusing to start instead (#157).`);
1904
1925
  }
1905
- const missing = laneRequiredEnv(word).filter((name) => !process.env[name]);
1926
+ const missing = laneMissingEnv(word, process.env);
1906
1927
  if (missing.length > 0) {
1907
- throw new Error(`REMOTE_EXEC=${lane} but required env ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} missing/empty — ` +
1908
- `refusing to start rather than silently downgrading to the host/in-process lane (#157; unset REMOTE_EXEC if you meant no sandbox lane).`);
1928
+ throw new Error(`REMOTE_EXEC=${lane} but required env ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} unset, empty or whitespace-only — it counts as absent: ` +
1929
+ `refusing to start rather than silently downgrading to the host/in-process lane (#157; set ${missing.length === 1 ? "it" : "them"}, or unset REMOTE_EXEC if you meant no sandbox lane).`);
1909
1930
  }
1910
1931
  }
1911
1932
  }
@@ -1951,7 +1972,7 @@ function parseOrchestrationDomain(ctx) {
1951
1972
  orphanReclaim: boolEnv("E2B_ORPHAN_RECLAIM", true),
1952
1973
  }
1953
1974
  :
1954
- process.env.REMOTE_EXEC === "k8s" && process.env.K8S_SANDBOX_IMAGE
1975
+ process.env.REMOTE_EXEC === "k8s" && process.env.K8S_SANDBOX_IMAGE && process.env.K8S_NAMESPACE
1955
1976
  ? {
1956
1977
  provider: "k8s",
1957
1978
  image: process.env.K8S_SANDBOX_IMAGE,
@@ -1959,7 +1980,7 @@ function parseOrchestrationDomain(ctx) {
1959
1980
  ...(process.env.K8S_TOKEN ? { token: process.env.K8S_TOKEN } : {}),
1960
1981
  ...(process.env.K8S_CA_CERT_B64 ? { caCert: Buffer.from(process.env.K8S_CA_CERT_B64, "base64").toString("utf8") } : {}),
1961
1982
  ...(boolEnv("K8S_INSECURE_TLS", false) ? { insecureTls: true } : {}),
1962
- ...(process.env.K8S_NAMESPACE ? { namespace: process.env.K8S_NAMESPACE } : {}),
1983
+ namespace: process.env.K8S_NAMESPACE,
1963
1984
  ...(process.env.K8S_RUNTIME_CLASS != null ? { runtimeClass: process.env.K8S_RUNTIME_CLASS } : {}),
1964
1985
  ...(process.env.K8S_MOUNT_PATH ? { mountPath: process.env.K8S_MOUNT_PATH } : {}),
1965
1986
  ...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("K8S_TIMEOUT_MS")),
@@ -2602,6 +2623,14 @@ export function loadConfig() {
2602
2623
  if (centerIdentity !== undefined) {
2603
2624
  throw new Error(centerIdentity);
2604
2625
  }
2626
+ const remoteSourceRefusal = remoteConfigProviderUrlRefusal({ configProvider: integrations.configProvider, webAdmin: integrations.webAdmin });
2627
+ if (remoteSourceRefusal !== undefined) {
2628
+ throw new Error(remoteSourceRefusal);
2629
+ }
2630
+ const retentionRefusal = retentionLaneRefusal({ retentionPolicy: auth.retentionPolicy, retentionSweep: auth.retentionSweep });
2631
+ if (retentionRefusal !== undefined) {
2632
+ throw new Error(retentionRefusal);
2633
+ }
2605
2634
  const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
2606
2635
  const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, operatorSingleUser, authTokens } = auth;
2607
2636
  const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
@@ -38,7 +38,7 @@
38
38
  * M23 回填了无主的父)⇒ 写下的仍是判断时的读数 —— 按律如此;那一行若因此无主,照上一节处置。
39
39
  * · **停驻挂点**(`plugins/session-store.ts` 的 `ensureChildSessionDurableWithPromotion`;快路 + 晋升):来源 = **调用方交出的委派树根,只有这一只**,连同
40
40
  * 调用方判断那一刻读到的根行住客({@link DelegationRootWitness};见证值,判断用,不进 CAS):请求腿 = 本请求经鉴权解析的会话,住客 = 受理时那一读
41
- * (`boot/resolve-spec.ts`);`/decide` 的赎回腿 = 行上的 `rootSessionId`,住客 = 赎回腿 claim 前后两读的那一位(`parked-revive-surface.ts`,每一行都交)。
41
+ * (`boot/resolve-spec.ts`);`/decide` 的赎回腿 = 行上的 `rootSessionId`,住客 = 赎回腿写口放行的那一位(认领之前解析器那一读;`parked-revive-surface.ts`,每一行都交)。
42
42
  * 挂点先读一次根行:它在而住客不是见证的那一位 ⇒ 根被删、同 id 换了住客 ⇒ **拒**(不按新住客晋升 / 认领,{@link assertRootStillWitnessed});它不在 ⇒ 律给哨兵
43
43
  * (根真没了)。不回落父:core 的挂点 `ensureChildSessionDurable(sessionId)` 只交子代 id,父 id 在这一写点拿不到;两条候选读法都读不到东西 ——
44
44
  * ① 读 `session_meta(子代).placement_parent_session_id`:晋升的拷贝分支恰恰只在子代**没有** placed 行时才走(有 placed 行 ⇒ 快路),这一列按构造为空;
@@ -134,7 +134,7 @@ export interface PlacedRowWriter {
134
134
  export declare function createPlacedSessionRow(sessionId: string, placement: PlacementSources, io: PlacedRowWriter, log: OrphanOwnerLog): Promise<void>;
135
135
  /**
136
136
  * 停驻挂点的**见证**:调用方交出的委派树根 + 调用方判断那一刻读到的根行住客(化身;`null` = 那一读里根行不在)。请求腿 = 鉴权解析的会话与受理时那一读;
137
- * 赎回腿 = 行上的根与 claim 前后两读的那一位住客。
137
+ * 赎回腿 = 行上的根与写口放行的那一位住客(认领之前解析器那一读)。
138
138
  */
139
139
  export interface DelegationRootWitness {
140
140
  readonly sessionId: string;
@@ -92,7 +92,7 @@ export declare const DEPLOYMENT_SHAPE_PREDICATES: Readonly<{
92
92
  * 签发方配置是身份验签轴、不算租户证据)—— 两个问题、一根轴、一个读口;「鉴权必需」与「多租形」拆成两个声明时只改这一行的判官。
93
93
  */
94
94
  multiTenant: DeploymentShapePredicate<"principal">;
95
- /** 读能力表 `callerCwd` 位(host = 单主才开;device = 路径落在发起者自己的设备上,归属由 `device_session` 绑定验过,不要求单主)。 */
95
+ /** 读能力表 `callerCwd` 位(host = 单主才开;device = 路径落在发起者自己的设备上,归属由根会话行执行格上的设备绑定验过,不要求单主)。 */
96
96
  callerCwd: DeploymentShapePredicate<"lane" | "principal">;
97
97
  /** 调用方给的宿主绝对路径(附加目录 / 项目上下文 / 本机 SendUserFile / 指令文件探针)只在「手就在本 worker 的盘上 ∧ 单主」时指向调用方自己的东西。 */
98
98
  callerOwnsWorkerDisk: DeploymentShapePredicate<"lane" | "principal">;
@@ -18,12 +18,21 @@
18
18
  * 上是**同一个** `device.enrollment_invalid`;设备读面的「不存在」与「不是你的」是同一个
19
19
  * `not_found.device`。细分只进审计。
20
20
  *
21
+ * ── 绑定从哪读(一只读)─────────────────────────────────────────────────────────────────────────────
22
+ * 根会话 ↔ 设备的绑定是根会话行上这一位住客的执行格 writer(device, D)(`execution-placement.ts` 头注)。准入链**不另读**:
23
+ * 读只有会话店的执行席 `seat.read(root)` 一只({@link DeviceEnrollment.readPlacement},装配点把会话店的执行席交进来 ——
24
+ * device 车道只在 SQL 后端 ⇒ 席恒在,缺 ⇒ 装配期响亮抛);{@link DeviceEnrollment.admitSession} 判的是调用方交进来的**那一读**,
25
+ * 于是提交意图「判已绑 / 未绑」与它记下的住客出自同一次行读。「这一格放在哪台设备上」只有一种读法 `boundDeviceOf`。
26
+ *
21
27
  * ── 吊销在途语义(§8-R4)──────────────────────────────────────────────────────────────────────────
22
28
  * `revoked` 是终态:①绑定的 session 后续提交 = `device.revoked`(admitSession 步 2,**先于** presence
23
29
  * 判定);②吊销同事务把连接租约作废并把 `conn_generation` 推进一格 ⇒ 在途连接的每一次带世代谓词的写
24
30
  * 都会影响 0 行,设备侧因此立刻失权(§4.6 步 2);③重新 enrollment = 新 deviceId,不复活旧行。
25
31
  */
26
- import { type DeviceErrorCode, type DeviceOwner, type DeviceRejectCode, type DeviceEnrollTokenRow, type DeviceRow, type DeviceSessionRow, type DeviceStore, type NewDeviceInput } from "./device-store.js";
32
+ import { type DeviceErrorCode, type DeviceOwner, type DeviceRejectCode, type DeviceEnrollTokenRow, type DeviceRow, type DeviceStore, type NewDeviceInput } from "./device-store.js";
33
+ import { type ExecutionDeviceCell, type ExecutionPlacementReading, type ExecutionPlacementSeat } from "./execution-placement.js";
34
+ import type { ExecutionLane } from "./execution-lane-caps.js";
35
+ import type { SessionFace } from "./session-incarnation.js";
27
36
  /** typed 拒绝 —— 端点层只负责把它翻成 HTTP(status 已在体内,不许调用方另算)。 */
28
37
  export interface DeviceRejection {
29
38
  /** [ref]:含两个借用位(`limit.rate_exceeded` 429 / `draining` 503);enrollment 本文件只铸前者,
@@ -68,10 +77,34 @@ export interface EnrollDeviceValue {
68
77
  }
69
78
  export interface AdmitSessionInput {
70
79
  rootSessionId: string;
80
+ /** 调用方读的**那一读**(会话店执行席 `seat.read(rootSessionId)` 的答案,经 {@link DeviceEnrollment.readPlacement})—— 准入链只判它,不另读。 */
81
+ placement: ExecutionPlacementReading | undefined;
71
82
  requester: DeviceOwner;
72
83
  /** 提交体携带的 deviceId(§4.3.2:仅根任务携;缺席 = 沿用绑定)。 */
73
84
  requestedDeviceId?: string;
74
85
  }
86
+ /**
87
+ * 首绑({@link DeviceEnrollment.bindPlacement})的结果:成立(`device` = 落下的格)、设备闭集的 typed 拒(异设备 `device.claim_conflict` /
88
+ * 设备 404 / 已吊销),或会话行那一侧的三种结局 —— 住客不是判过的那一位 / 行不在(`gone`)、写者在别处(`mirror`)、格在别的车道上(`tree_bound`)。
89
+ * 后三者不在设备闭集里(它们说的是会话,不是设备),由提交点(`http/execution-context.ts` 的 `commitExecutionContext`)各答各的码。
90
+ */
91
+ export type BindPlacementOutcome = {
92
+ readonly ok: true;
93
+ readonly device: ExecutionDeviceCell;
94
+ } | {
95
+ readonly ok: false;
96
+ readonly reject: DeviceRejection;
97
+ } | {
98
+ readonly ok: false;
99
+ readonly placement: "gone";
100
+ } | {
101
+ readonly ok: false;
102
+ readonly placement: "mirror";
103
+ } | {
104
+ readonly ok: false;
105
+ readonly placement: "tree_bound";
106
+ readonly boundLane: ExecutionLane;
107
+ };
75
108
  export interface AdmitSessionValue {
76
109
  deviceId: string;
77
110
  device: DeviceRow;
@@ -110,11 +143,15 @@ export interface DeviceEnrollment {
110
143
  /** §7-T5 五件的第 ①②③ 件的**写入侧**:绑定复合身份 + TTL + 一次性摘要落库(明文一现)。 */
111
144
  issueEnrollToken(input: IssueEnrollTokenRequest): Promise<DeviceOutcome<IssuedEnrollToken>>;
112
145
  enroll(input: EnrollDeviceInput): Promise<DeviceOutcome<EnrollDeviceValue>>;
146
+ /** 绑定的唯一读:会话店执行席的 `read(root)`(一次行读:住客 + 格;行不在 ⇒ `undefined`;读故障抛,不吞成「未绑」)。 */
147
+ readPlacement(rootSessionId: string): Promise<ExecutionPlacementReading | undefined>;
148
+ /** 首绑(认领之后的初始放置写):按意图那一读的住客定址,设备店一只事务。 */
113
149
  bindPlacement(input: {
114
150
  rootSessionId: string;
151
+ occupant: SessionFace;
115
152
  deviceId: string;
116
153
  requester: DeviceOwner;
117
- }): Promise<DeviceOutcome<DeviceSessionRow>>;
154
+ }): Promise<BindPlacementOutcome>;
118
155
  admitSession(input: AdmitSessionInput): Promise<DeviceOutcome<AdmitSessionValue>>;
119
156
  revoke(input: {
120
157
  deviceId: string;
@@ -125,6 +162,8 @@ export interface DeviceEnrollment {
125
162
  }
126
163
  export interface DeviceEnrollmentDeps {
127
164
  store: DeviceStore;
165
+ /** 会话店的执行席(绑定的唯一载体与唯一读;装配点交会话店的 `executionPlacement`)。 */
166
+ seat: ExecutionPlacementSeat;
128
167
  /** 缺席 = 不限速(端点车必须传;见文件头注 ⑤)。 */
129
168
  rateLimiter?: DeviceEnrollRateLimiter;
130
169
  /** 测试可注入确定性铸造;默认 `mintDeviceId()`。 */
@@ -1,4 +1,5 @@
1
1
  import { DEVICE_ERROR_CODES, DEVICE_ERROR_STATUS, DEVICE_RATE_LIMIT_CODE, assertDeviceIdShape, assertDeviceOwnerShape, assertDevicePubkeyShape, assertEnrollTokenHashShape, assertRootSessionIdShape, hashEnrollToken, mintDeviceId, mintEnrollToken, } from "./device-store.js";
2
+ import { boundDeviceOf } from "./execution-placement.js";
2
3
  export const DEFAULT_ENROLL_TOKEN_TTL_SEC = 900;
3
4
  export function buildDeviceRejection(errorCode, message) {
4
5
  return { code: errorCode, status: DEVICE_ERROR_STATUS[errorCode], message };
@@ -13,7 +14,7 @@ function rejectNotFound() {
13
14
  return reject(DEVICE_ERROR_CODES.NOT_FOUND, "device not found");
14
15
  }
15
16
  export function createDeviceEnrollment(deps) {
16
- const { store } = deps;
17
+ const { store, seat } = deps;
17
18
  const mintId = deps.mintId ?? mintDeviceId;
18
19
  async function rateGate(key) {
19
20
  if (!deps.rateLimiter)
@@ -87,21 +88,35 @@ export function createDeviceEnrollment(deps) {
87
88
  return rejectEnrollment();
88
89
  }
89
90
  }
91
+ async function readPlacement(rootSessionId) {
92
+ assertRootSessionIdShape(rootSessionId);
93
+ return seat.read(rootSessionId);
94
+ }
90
95
  async function bindPlacement(input) {
91
96
  assertRootSessionIdShape(input.rootSessionId);
92
97
  assertDeviceIdShape(input.deviceId);
93
98
  assertDeviceOwnerShape(input.requester);
94
- const result = await store.bindSession({ rootSessionId: input.rootSessionId, deviceId: input.deviceId, owner: input.requester });
99
+ const result = await store.placeDeviceExecution({ root: input.rootSessionId, occupant: input.occupant, deviceId: input.deviceId, owner: input.requester });
95
100
  switch (result.outcome) {
96
101
  case "bound":
97
- case "idempotent":
98
- return { ok: true, value: result.row };
99
- case "conflict":
102
+ case "same":
103
+ return { ok: true, device: result.device };
104
+ case "device_conflict":
100
105
  return reject(DEVICE_ERROR_CODES.CLAIM_CONFLICT, "this session is already bound to a different device");
101
106
  case "device_not_found":
102
107
  return rejectNotFound();
103
108
  case "device_revoked":
104
109
  return reject(DEVICE_ERROR_CODES.REVOKED, "device has been revoked");
110
+ case "gone":
111
+ return { ok: false, placement: "gone" };
112
+ case "mirror":
113
+ return { ok: false, placement: "mirror" };
114
+ case "tree_bound":
115
+ return { ok: false, placement: "tree_bound", boundLane: result.boundLane };
116
+ default: {
117
+ const unhandled = result;
118
+ throw new Error(`bindPlacement: unknown store outcome ${JSON.stringify(unhandled)}`);
119
+ }
105
120
  }
106
121
  }
107
122
  async function admitSession(input) {
@@ -109,22 +124,22 @@ export function createDeviceEnrollment(deps) {
109
124
  assertDeviceOwnerShape(input.requester);
110
125
  if (input.requestedDeviceId !== undefined)
111
126
  assertDeviceIdShape(input.requestedDeviceId);
112
- const binding = await store.getSessionBinding(input.rootSessionId);
113
- if (!binding)
127
+ const bound = input.placement === undefined ? undefined : boundDeviceOf(input.placement.cell);
128
+ if (!bound)
114
129
  return reject(DEVICE_ERROR_CODES.NOT_BOUND, "this session has no device binding");
115
- if (binding.owner.tenant !== input.requester.tenant || binding.owner.subject !== input.requester.subject)
130
+ if (bound.owner.tenant !== input.requester.tenant || bound.owner.subject !== input.requester.subject)
116
131
  return rejectNotFound();
117
- if (input.requestedDeviceId !== undefined && input.requestedDeviceId !== binding.deviceId) {
132
+ if (input.requestedDeviceId !== undefined && input.requestedDeviceId !== bound.id) {
118
133
  return reject(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, "the requested device is not the one bound to this session");
119
134
  }
120
- const view = await store.readDeviceForAdmission(binding.deviceId, input.requester);
135
+ const view = await store.readDeviceForAdmission(bound.id, input.requester);
121
136
  if (!view)
122
137
  return rejectNotFound();
123
138
  if (view.row.status === "revoked")
124
139
  return reject(DEVICE_ERROR_CODES.REVOKED, "device has been revoked");
125
140
  if (!view.online)
126
141
  return reject(DEVICE_ERROR_CODES.OFFLINE, "device is not connected");
127
- return { ok: true, value: { deviceId: binding.deviceId, device: view.row, connGeneration: view.row.connGeneration } };
142
+ return { ok: true, value: { deviceId: bound.id, device: view.row, connGeneration: view.row.connGeneration } };
128
143
  }
129
144
  async function revoke(input) {
130
145
  assertDeviceIdShape(input.deviceId);
@@ -138,7 +153,7 @@ export function createDeviceEnrollment(deps) {
138
153
  return rejectNotFound();
139
154
  }
140
155
  }
141
- return { issueEnrollToken, enroll, bindPlacement, admitSession, revoke };
156
+ return { issueEnrollToken, enroll, readPlacement, bindPlacement, admitSession, revoke };
142
157
  }
143
158
  export function createFixedWindowRateLimiter(opts) {
144
159
  const now = opts.now ?? (() => Date.now());
@@ -1,3 +1,5 @@
1
+ import type { DeviceBindOutcome, DeviceRebindOutcome, InMemoryExecutionPlacementSeat } from "./execution-placement.js";
2
+ import type { SessionFace } from "./session-incarnation.js";
1
3
  /**
2
4
  * device lane 的 HTTP 错误码闭集(v2 §4.8 逐字)。**唯一导出点** —— 端点车(A-3/车C)从这里取,
3
5
  * 禁在路由里手抄字面量(§4.8 task-R1-12:「下表即闭集全集,禁四份手抄」)。
@@ -33,7 +35,7 @@ export declare const DEVICE_ERROR_CODES: {
33
35
  * (管理面,`http/routes/devices.ts`);adapter/准入链结构上不产它。 */
34
36
  readonly REBIND_REV_CONFLICT: "device.rebind_rev_conflict";
35
37
  /** O4 换绑动词(v2 稿 §2 步 4):该根会话名下有非终态 run(durable 判据 = run store 的 session
36
- * claim;hub 内存在途计数**不作门**——跨副本不可见)—— 体带 `activeTaskId`。铸造点同上一条。 */
38
+ * claim;hub 内存在途计数**不作门**——跨副本不可见;保证 = 设备店换绑事务内锁根行后那一读,池级读只作早拒)—— 体带 `activeTaskId`。铸造点同上一条。 */
37
39
  readonly REBIND_ACTIVE_RUN: "device.rebind_active_run";
38
40
  };
39
41
  /** {@link DEVICE_ERROR_CODES} 的值联合 —— 闭集(未知码 = 编译错误,[ref] 词表纪律)。 */
@@ -113,14 +115,6 @@ export interface DeviceRow {
113
115
  createdAtMs: number;
114
116
  rev: number;
115
117
  }
116
- export interface DeviceSessionRow {
117
- /** 键 = **根**会话(task-R1-3:后代继承不另绑)。 */
118
- rootSessionId: string;
119
- deviceId: string;
120
- owner: DeviceOwner;
121
- boundAtMs: number;
122
- rev: number;
123
- }
124
118
  export interface DeviceAuditRow {
125
119
  id: number;
126
120
  atMs: number;
@@ -147,7 +141,7 @@ export interface DeviceEnrollTokenRow {
147
141
  export declare const DEVICE_KEY_MAX_BYTES = 190;
148
142
  /** `device_id` 列宽(`dev_` + 26 位 Crockford base32 ULID = 30 字节,64 是三倍余量)。 */
149
143
  export declare const DEVICE_ID_MAX_BYTES = 64;
150
- /** `root_session_id` 列宽(提交入口硬拒 >64 的 sessionId,128 留一倍余量)。 */
144
+ /** 根会话 id 的形门上界(换绑路由的路径段;会话行 `session_id` 宽 64,提交入口硬拒 >64 的 sessionId,128 留一倍余量 —— 超宽的 id 等值读不到任何行)。 */
151
145
  export declare const DEVICE_SESSION_ID_MAX_BYTES = 128;
152
146
  /** `pubkey` / `token_hash` 列宽(Ed25519 b64 = 44,sha256 hex = 64)。 */
153
147
  export declare const DEVICE_PUBKEY_MAX_BYTES = 128;
@@ -176,10 +170,12 @@ export declare function assertDevicePubkeyShape(pubkey: string): void;
176
170
  export declare function assertNewDeviceShape(input: NewDeviceInput): void;
177
171
  export declare function assertIssueTokenShape(input: IssueEnrollTokenInput): void;
178
172
  export declare function assertRevokeShape(input: RevokeDeviceInput): void;
173
+ /** device 首绑入参的整形门(三形共用:内存参照实现与两方言从同一处过)。 */
174
+ export declare function assertPlaceDeviceShape(input: PlaceDeviceExecutionInput): void;
179
175
  /** O4 换绑入参的整形门(三形共用)。`expectedRev` 收窄为非负整数:它要进 SQL 等值谓词,小数/负数/NaN
180
176
  * 在两方言的 BIGINT 比对下不是「不匹配」而是各自的隐式转换歧义面 —— 在门口拒,方言分歧面消失。
181
177
  * ⚠️ HTTP 层对坏形先答 400(业务拒绝);这里的抛是**编程/装配错误**的兜底,不是 wire 分支。 */
182
- export declare function assertRebindShape(input: RebindSessionInput): void;
178
+ export declare function assertRebindShape(input: RebindDeviceExecutionInput): void;
183
179
  export declare function assertAcquireShape(input: AcquireConnectionInput): void;
184
180
  /** token 摘要必须是**规范形**(小写 sha256 hex):大小写混形在 VARBINARY/COLLATE "C" 下是两个键,
185
181
  * 于是同一个明文 token 能被消费两次 —— 这是 §7-T5 ③「一次性 CAS」的直接跳过面。 */
@@ -268,73 +264,53 @@ export type ConsumeEnrollTokenResult =
268
264
  | {
269
265
  outcome: "pubkey_taken";
270
266
  };
271
- export interface BindSessionInput {
272
- rootSessionId: string;
273
- deviceId: string;
274
- /** 验证过的复合身份;与 devices 行的 owner 在**同一事务**内比对(§4.3.2 首绑)。 */
275
- owner: DeviceOwner;
276
- }
277
- /** §4.3.2 首绑协议的判别式结果 —— 并发争抢**单赢家,无未定义态**。 */
278
- export type BindSessionResult = {
279
- outcome: "bound";
280
- row: DeviceSessionRow;
281
- }
282
- /** 并发输家或重试,但绑的是**同一台**设备 ⇒ 幂等成功。 */
283
- | {
284
- outcome: "idempotent";
285
- row: DeviceSessionRow;
286
- }
287
- /** 根会话已绑**另一台**设备(或另一个 owner)⇒ `device.claim_conflict`。 */
288
- | {
289
- outcome: "conflict";
290
- row: DeviceSessionRow;
267
+ /**
268
+ * device 车道的**首绑**(§4.3.2;认领之后的初始放置写,§3.3 转换表 place / bind 两行):根会话行上这一位住客的执行格
269
+ * absent → writer(device, D),或迁移回填的 writer(device, ∅) → writer(device, D)。绑定**住在会话行上**(唯一载体;
270
+ * 列与语句的属主 = `plugins/execution-placement-sql.ts`),本店只负责「设备行判 + 写格」同一事务(锁序:设备行 → 根 `session_meta`)。
271
+ */
272
+ export interface PlaceDeviceExecutionInput {
273
+ /** 根会话(放置键;后代继承,不另绑)。 */
274
+ readonly root: string;
275
+ /** 意图那一读的住客(会话店执行席 `seat.read(root)` 交出的那一位)—— 写按住客谓词定址:住客换了 ⇒ `gone`,永不写到新住客身上。 */
276
+ readonly occupant: SessionFace;
277
+ readonly deviceId: string;
278
+ /** 验证过的复合身份;与设备行的 owner 在**同一事务**内比对,写进格的两属主列(绑定属主 = 设备属主)。 */
279
+ readonly owner: DeviceOwner;
291
280
  }
292
- /** 设备不存在**或**不是请求方的 —— 反枚举同形(见文件头注)。 */
293
- | {
294
- outcome: "device_not_found";
281
+ /**
282
+ * 首绑的判别式结果 —— 并发争抢**单赢家,无未定义态**。格的那几臂与判官同一张表(`DeviceBindOutcome`:`bound` / 同设备 `same`(幂等成功)/
283
+ * 异设备 `device_conflict` / 异车道 `tree_bound` / `mirror` / 住客不是判过的那一位或行不在 `gone`),外加设备行的两臂:
284
+ * 设备不存在**或**不是请求方的(反枚举同形,见文件头注)/ 已吊销。
285
+ */
286
+ export type PlaceDeviceExecutionResult = DeviceBindOutcome | {
287
+ readonly outcome: "device_not_found";
295
288
  } | {
296
- outcome: "device_revoked";
289
+ readonly outcome: "device_revoked";
297
290
  };
298
291
  /**
299
- * O4 显式换绑(2026-08-30 v2 稿 §2)。`owner` = **绑定行的**复合身份(HTTP 层已裁决:属主同域或
300
- * explicit operator;operator 代操作时传的也是行的 owner,不是操作员自己)——店内所有谓词恒带它,
301
- * 判别位在店里就消失(反枚举,文件头注)。`expectedRev` = 壳从 S-4 读面拿到的前态(强制显式,
302
- * 防两操作员并发换绑互踩)。
292
+ * O4 显式换绑(2026-08-30 v2 稿 §2;§3.3 转换表 rebind 行)。`occupant` = 换绑门那一读的住客;`owner` = 门判过的**格上**两属主列
293
+ * (HTTP 层已裁决:属主同域或 explicit operator;operator 代操作时传的也是格上的 owner,不是操作员自己)——店内所有谓词恒带它,
294
+ * 判别位在店里就消失(反枚举,文件头注)。`expectedRev` = 壳从执行放置读面拿到的前态(强制显式,防两操作员并发换绑互踩)。
303
295
  */
304
- export interface RebindSessionInput {
305
- rootSessionId: string;
306
- toDeviceId: string;
307
- expectedRev: number;
308
- owner: DeviceOwner;
296
+ export interface RebindDeviceExecutionInput {
297
+ readonly root: string;
298
+ readonly occupant: SessionFace;
299
+ readonly owner: DeviceOwner;
300
+ readonly expectedRev: number;
301
+ readonly targetDeviceId: string;
309
302
  }
310
303
  /**
311
- * O4 换绑的判别式结果(门序 = v2 稿 §2:目标设备在场 → 幂等短路 → 前态 CAS → 审计;活跃 run 门在
312
- * **HTTP 层**、CAS 之前判 —— run store 是另一只店,CAS 语句不兼职)。
304
+ * O4 换绑的判别式结果(门序 = v2 稿 §2 + 读写序律:目标设备在场 → 锁根行读认领 → 幂等短路 → 前态 CAS → 审计)。格的那几臂与判官同一张表
305
+ * (`DeviceRebindOutcome`:`rebound` 带被换下的那台 / 真幂等 `idempotent` / 前态输 `rev_conflict` 携当前格 / `gone` = 行不在、住客换了、不是 writer(device, D)、
306
+ * 或绑定属主不是判过的那一位 —— 反枚举同形),外加:目标设备不在 / 不是该 owner 的 / **已吊销**(三臂同折 404;吊销不单列 = 判别位在店里就消失);
307
+ * 写事务内锁根行后读到认领 `active_run`(SQL 后端;读写序律只在 SQL 后端立 —— 内存参照实现没有认领表,答不出这一臂,内存走廊的认领判据是路由的早拒)。
313
308
  */
314
- export type RebindSessionResult =
315
- /** CAS 赢家:`rev+1` + `bound_at` 刷新 + `session_rebound` 审计行**同一事务**提交。 */
316
- {
317
- outcome: "rebound";
318
- row: DeviceSessionRow;
319
- }
320
- /** 真幂等:`toDeviceId` ≡ 当前绑定 ∧ `expectedRev` ≡ 当前 rev ⇒ 回显不加 rev、不写审计。 */
321
- | {
322
- outcome: "idempotent";
323
- row: DeviceSessionRow;
324
- }
325
- /** 前态输(rev 不符,含「toDeviceId 同但 rev 错」——前态错就是错)⇒ 携当前行供壳一跳重定位。 */
326
- | {
327
- outcome: "rev_conflict";
328
- row: DeviceSessionRow;
329
- }
330
- /** 绑定行不在**或**不是该 owner 的 —— 反枚举同形(HTTP 层折 404 同串)。 */
331
- | {
332
- outcome: "binding_not_found";
333
- }
334
- /** 目标设备不在 / 不是该 owner 的 / **已吊销** —— v2 稿 §2 步 2:三臂同折 404(设备 id 也是
335
- * 不可枚举面);吊销不单列 = 判别位在店里就消失,消费层想泄也泄不出来。 */
336
- | {
337
- outcome: "device_not_found";
309
+ export type RebindDeviceExecutionResult = DeviceRebindOutcome | {
310
+ readonly outcome: "device_not_found";
311
+ } | {
312
+ readonly outcome: "active_run";
313
+ readonly activeTaskId: string;
338
314
  };
339
315
  export interface RevokeDeviceInput {
340
316
  deviceId: string;
@@ -415,14 +391,10 @@ export interface DeviceStore {
415
391
  /** 世代谓词命中 = true;0 行 = 已失权。 */
416
392
  heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
417
393
  releaseConnection(deviceId: string, generation: number): Promise<boolean>;
418
- bindSession(input: BindSessionInput): Promise<BindSessionResult>;
419
- /** O4 显式换绑(v2 稿 §2 步 2/3/5):目标设备门 + 前态 CAS + `session_rebound` 审计行同一事务。
420
- * 活跃 run 门**不在这里**(run store 是另一只店)—— 由 HTTP 层在调用前判(v2 稿 §2 步 4)。 */
421
- rebindSession(input: RebindSessionInput): Promise<RebindSessionResult>;
422
- getSessionBinding(rootSessionId: string): Promise<DeviceSessionRow | null>;
423
- /** purge coordinator 座(§6 注②:core 的会话删除是**逐 store 显式调用**,不是 DB 自动扫;
424
- * 只建表不接 purge = 删会话后绑定残留 ⇒ sessionId 重用致旧设备粘连/claim conflict/隐私残留)。 */
425
- deleteByRootSession(rootSessionId: string): Promise<number>;
394
+ /** 首绑(认领之后的初始放置写):设备行判(active ∧ 属主)+ 写格同一事务,按住客谓词定址。 */
395
+ placeDeviceExecution(input: PlaceDeviceExecutionInput): Promise<PlaceDeviceExecutionResult>;
396
+ /** O4 显式换绑(v2 稿 §2 步 2/3/5):目标设备门 + 写事务内锁根行读认领 + 前态 CAS + `session_rebound` 审计行同一事务。 */
397
+ rebindDeviceExecution(input: RebindDeviceExecutionInput): Promise<RebindDeviceExecutionResult>;
426
398
  appendAudit(input: AppendAuditInput): Promise<void>;
427
399
  listAudit(filter?: ListAuditFilter): Promise<DeviceAuditRow[]>;
428
400
  }
@@ -432,15 +404,19 @@ declare function cloneOwner(o: DeviceOwner): DeviceOwner;
432
404
  * 语义 oracle。**单线程 JS 的原子性**替代 SQL 事务:每个方法体内没有 `await`(除了返回值),所以
433
405
  * 「读-判-写」在事件循环上不可分割 —— 与 SQL 侧的单事务 CAS 语义等价,这也是为什么并发格
434
406
  * (两个并发消费同一 token)在这一形上必须给出与真库**同一个**赢家/输家分布。
407
+ *
408
+ * 绑定不在本店:它是会话行上的执行格。构造**必收**测试走廊的会话行 {@link InMemoryExecutionPlacementSeat}(同为参照实现,生产零引用)——
409
+ * 首绑 / 换绑在它上面做(判官与 SQL 语句模块同一组),读者经同一只席的 `read(root)` 读。
435
410
  */
436
411
  export declare class InMemoryDeviceStore implements DeviceStore {
437
412
  private readonly devices;
438
- private readonly sessions;
439
413
  private readonly tokens;
440
414
  private readonly audit;
441
415
  private auditSeq;
442
416
  private readonly now;
443
- constructor(opts?: {
417
+ private readonly seat;
418
+ constructor(opts: {
419
+ seat: InMemoryExecutionPlacementSeat;
444
420
  now?: () => number;
445
421
  });
446
422
  issueEnrollToken(input: IssueEnrollTokenInput): Promise<DeviceEnrollTokenRow>;
@@ -454,17 +430,14 @@ export declare class InMemoryDeviceStore implements DeviceStore {
454
430
  acquireConnection(input: AcquireConnectionInput): Promise<AcquireConnectionResult>;
455
431
  heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
456
432
  releaseConnection(deviceId: string, generation: number): Promise<boolean>;
457
- bindSession(input: BindSessionInput): Promise<BindSessionResult>;
458
- rebindSession(input: RebindSessionInput): Promise<RebindSessionResult>;
459
- getSessionBinding(rootSessionId: string): Promise<DeviceSessionRow | null>;
460
- deleteByRootSession(rootSessionId: string): Promise<number>;
433
+ placeDeviceExecution(input: PlaceDeviceExecutionInput): Promise<PlaceDeviceExecutionResult>;
434
+ rebindDeviceExecution(input: RebindDeviceExecutionInput): Promise<RebindDeviceExecutionResult>;
461
435
  appendAudit(input: AppendAuditInput): Promise<void>;
462
436
  listAudit(filter?: ListAuditFilter): Promise<DeviceAuditRow[]>;
463
437
  private isOnline;
464
438
  private pushAudit;
465
439
  /** 返回**拷贝**:调用方拿到的行改了也不能污染店内状态(SQL 侧天然如此,InMemory 必须显式做)。 */
466
440
  private snapshotDevice;
467
- private snapshotSession;
468
441
  }
469
442
  export { sameOwner as sameDeviceOwner, cloneOwner as cloneDeviceOwner };
470
443
  //# sourceMappingURL=device-store.d.ts.map