@sema-agent/server 7.50.0 → 7.51.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 (47) hide show
  1. package/dist/boot/device-lane.d.ts +79 -0
  2. package/dist/boot/device-lane.js +63 -0
  3. package/dist/boot/execution-env.d.ts +48 -2
  4. package/dist/boot/execution-env.js +57 -5
  5. package/dist/boot/resolve-spec.d.ts +3 -0
  6. package/dist/boot/resolve-spec.js +12 -9
  7. package/dist/boot/session-faces.d.ts +5 -0
  8. package/dist/boot/session-faces.js +4 -1
  9. package/dist/boot/shutdown.d.ts +8 -0
  10. package/dist/boot/shutdown.js +3 -1
  11. package/dist/boot/stores.js +9 -0
  12. package/dist/config-center/types.d.ts +10 -3
  13. package/dist/config-invariants.d.ts +2 -2
  14. package/dist/config-invariants.js +18 -0
  15. package/dist/config-types.d.ts +12 -0
  16. package/dist/config.js +46 -9
  17. package/dist/device-enrollment.d.ts +125 -0
  18. package/dist/device-enrollment.js +156 -0
  19. package/dist/device-store.d.ts +385 -0
  20. package/dist/device-store.js +407 -0
  21. package/dist/device-ws-hub.d.ts +182 -0
  22. package/dist/device-ws-hub.js +1012 -0
  23. package/dist/device-ws-protocol.d.ts +429 -0
  24. package/dist/device-ws-protocol.js +464 -0
  25. package/dist/env-facts.d.ts +4 -1
  26. package/dist/env-facts.js +1 -0
  27. package/dist/execution-lane-caps.d.ts +152 -0
  28. package/dist/execution-lane-caps.js +166 -0
  29. package/dist/http/routes/capabilities.js +7 -2
  30. package/dist/http/server.d.ts +14 -0
  31. package/dist/http/server.js +4 -1
  32. package/dist/leader/wire.js +4 -2
  33. package/dist/main.js +10 -3
  34. package/dist/orchestration/hardened-vm-runner.d.ts +7 -0
  35. package/dist/orchestration/hardened-vm-runner.js +11 -1
  36. package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
  37. package/dist/plugins/device-store-sql.d.ts +130 -0
  38. package/dist/plugins/device-store-sql.js +574 -0
  39. package/dist/plugins/remote-env-device.d.ts +271 -0
  40. package/dist/plugins/remote-env-device.js +727 -0
  41. package/dist/plugins/remote-scratchpad.js +1 -1
  42. package/dist/plugins/store-backend.d.ts +9 -0
  43. package/dist/plugins/store-backend.js +3 -0
  44. package/dist/task-cwd.d.ts +25 -0
  45. package/dist/task-cwd.js +3 -0
  46. package/dist/task-settings.js +3 -1
  47. package/package.json +2 -2
@@ -0,0 +1,166 @@
1
+ const NO = "unsupported";
2
+ const YES = "supported";
3
+ const SU = "single-user-only";
4
+ export function executionLaneCaps(lane) {
5
+ switch (lane) {
6
+ case "in-process":
7
+ return {
8
+ callerCwd: NO,
9
+ backgroundShell: NO,
10
+ scheduler: NO,
11
+ lsp: NO,
12
+ sandboxSendUserFile: NO,
13
+ isolation: NO,
14
+ suspendable: NO,
15
+ remoteScratchpad: NO,
16
+ sandboxPathAdjudication: NO,
17
+ worktreeIsolation: NO,
18
+ memoryPersistenceCapable: NO,
19
+ hostFilePlane: YES,
20
+ coreRemoteExecutionEnv: NO,
21
+ };
22
+ case "host":
23
+ return {
24
+ callerCwd: SU,
25
+ backgroundShell: SU,
26
+ scheduler: SU,
27
+ lsp: SU,
28
+ sandboxSendUserFile: NO,
29
+ isolation: NO,
30
+ suspendable: NO,
31
+ remoteScratchpad: NO,
32
+ sandboxPathAdjudication: NO,
33
+ worktreeIsolation: YES,
34
+ memoryPersistenceCapable: YES,
35
+ hostFilePlane: YES,
36
+ coreRemoteExecutionEnv: YES,
37
+ };
38
+ case "e2b":
39
+ return {
40
+ callerCwd: NO,
41
+ backgroundShell: YES,
42
+ scheduler: NO,
43
+ lsp: YES,
44
+ sandboxSendUserFile: YES,
45
+ isolation: YES,
46
+ suspendable: YES,
47
+ remoteScratchpad: YES,
48
+ sandboxPathAdjudication: YES,
49
+ worktreeIsolation: NO,
50
+ memoryPersistenceCapable: NO,
51
+ hostFilePlane: NO,
52
+ coreRemoteExecutionEnv: YES,
53
+ };
54
+ case "k8s":
55
+ return {
56
+ callerCwd: NO,
57
+ backgroundShell: YES,
58
+ scheduler: NO,
59
+ lsp: YES,
60
+ sandboxSendUserFile: YES,
61
+ isolation: YES,
62
+ suspendable: YES,
63
+ remoteScratchpad: YES,
64
+ sandboxPathAdjudication: YES,
65
+ worktreeIsolation: NO,
66
+ memoryPersistenceCapable: NO,
67
+ hostFilePlane: NO,
68
+ coreRemoteExecutionEnv: YES,
69
+ };
70
+ case "ssh":
71
+ return {
72
+ callerCwd: NO,
73
+ backgroundShell: NO,
74
+ scheduler: NO,
75
+ lsp: NO,
76
+ sandboxSendUserFile: SU,
77
+ isolation: NO,
78
+ suspendable: NO,
79
+ remoteScratchpad: YES,
80
+ sandboxPathAdjudication: YES,
81
+ worktreeIsolation: NO,
82
+ memoryPersistenceCapable: NO,
83
+ hostFilePlane: NO,
84
+ coreRemoteExecutionEnv: YES,
85
+ };
86
+ case "adb":
87
+ return {
88
+ callerCwd: NO,
89
+ backgroundShell: NO,
90
+ scheduler: NO,
91
+ lsp: NO,
92
+ sandboxSendUserFile: NO,
93
+ isolation: NO,
94
+ suspendable: NO,
95
+ remoteScratchpad: NO,
96
+ sandboxPathAdjudication: YES,
97
+ worktreeIsolation: NO,
98
+ memoryPersistenceCapable: NO,
99
+ hostFilePlane: NO,
100
+ coreRemoteExecutionEnv: YES,
101
+ };
102
+ case "local-docker":
103
+ return {
104
+ callerCwd: NO,
105
+ backgroundShell: NO,
106
+ scheduler: NO,
107
+ lsp: NO,
108
+ sandboxSendUserFile: NO,
109
+ isolation: YES,
110
+ suspendable: NO,
111
+ remoteScratchpad: YES,
112
+ sandboxPathAdjudication: YES,
113
+ worktreeIsolation: NO,
114
+ memoryPersistenceCapable: NO,
115
+ hostFilePlane: NO,
116
+ coreRemoteExecutionEnv: YES,
117
+ };
118
+ case "device":
119
+ return {
120
+ callerCwd: YES,
121
+ backgroundShell: NO,
122
+ scheduler: NO,
123
+ lsp: NO,
124
+ sandboxSendUserFile: NO,
125
+ isolation: NO,
126
+ suspendable: NO,
127
+ remoteScratchpad: YES,
128
+ sandboxPathAdjudication: YES,
129
+ worktreeIsolation: NO,
130
+ memoryPersistenceCapable: NO,
131
+ hostFilePlane: NO,
132
+ coreRemoteExecutionEnv: YES,
133
+ };
134
+ }
135
+ }
136
+ const EXECUTION_LANE_WORDS = {
137
+ "in-process": true,
138
+ host: true,
139
+ e2b: true,
140
+ k8s: true,
141
+ ssh: true,
142
+ adb: true,
143
+ "local-docker": true,
144
+ device: true,
145
+ };
146
+ export function isExecutionLane(word) {
147
+ return Object.hasOwn(EXECUTION_LANE_WORDS, word);
148
+ }
149
+ export const ALL_EXECUTION_LANES = Object.freeze(Object.keys(EXECUTION_LANE_WORDS).filter(isExecutionLane));
150
+ export function executionLaneOf(provider) {
151
+ if (provider === undefined)
152
+ return "in-process";
153
+ return isExecutionLane(provider) ? provider : undefined;
154
+ }
155
+ export function laneCapabilityHolds(cap, deployment) {
156
+ switch (cap) {
157
+ case "supported":
158
+ return true;
159
+ case "unsupported":
160
+ return false;
161
+ case "single-user-only":
162
+ return deployment.requirePrincipal !== true;
163
+ }
164
+ }
165
+ export const PLANNED_LANE_DIVERGENCES = Object.freeze([]);
166
+ //# sourceMappingURL=execution-lane-caps.js.map
@@ -2,7 +2,8 @@ import { DEFAULT_EFFORT_LEVELS, expandTiers, routePairingStatus } from "@sema-ag
2
2
  import { isModelAllowlisted } from "../../model-select.js";
3
3
  import { resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { isSealedKeyPoison } from "../../sealed-key.js";
5
- import { cwdHonored } from "../../task-cwd.js";
5
+ import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
6
+ import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
6
7
  import { a2aInjectionHonored } from "../../task-a2a.js";
7
8
  import { mcpInjectionHonored } from "../../task-mcp.js";
8
9
  import { sendJson, sendError } from "../send.js";
@@ -47,6 +48,9 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
47
48
  artifacts: Boolean(deps.runStore),
48
49
  approvals: Boolean(deps.checkpointStore),
49
50
  sessions: Boolean(deps.sessionAudit),
51
+ deviceExecutor: deps.deviceHub
52
+ ? { enabled: true, protocolVersion: DEVICE_PROTOCOL_VERSION, maxInflightPerDevice: deps.config.remoteExec?.provider === "device" ? (deps.config.remoteExec.maxInflightPerDevice ?? 4) : 4, wsPath: DEVICE_WS_PATH }
53
+ : false,
50
54
  sessionEvents: Boolean(deps.sessionWatch && deps.sessionStorage?.getLeafId && deps.sessionStorage?.ownerOf),
51
55
  fleet: deps.fleetBus
52
56
  ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "advisory", hookFailureNotice: true }
@@ -86,7 +90,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
86
90
  scratchpadDir: true,
87
91
  subagentSteer: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
88
92
  subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
89
- taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config), hooks: deps.config.requirePrincipal !== true },
93
+ taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: deps.config.requirePrincipal !== true },
90
94
  modeShellGateTranslation: true,
91
95
  permissionModeAuto: {
92
96
  accepted: true,
@@ -103,6 +107,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
103
107
  retainBackgroundProcesses: deps.config.requirePrincipal !== true,
104
108
  interactiveTools: true,
105
109
  projectContext: cwdHonored(deps.config),
110
+ callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
106
111
  mcpInjection: mcpInjectionHonored(deps.config),
107
112
  a2a: true,
108
113
  a2aInjection: a2aInjectionHonored(deps.config),
@@ -242,6 +242,20 @@ export interface ServiceCoordinatorDeps {
242
242
  * 重组零件——见 backend 的同款论证)。parked-revive 三件是同族的构件缝(boot 期从生产装配取的裸
243
243
  * ToolSpec + 成员集 + 父约束重建器),与 backgroundAgentStore 齐备才构成赎回腿。 */
244
244
  export interface ServiceSeamDeps {
245
+ /**
246
+ * device lane 的 WS 汇聚端(design/device-executor-lane-v2 §5,车A-3)—— 本仓**首个** WS 服务端。
247
+ * 给了就把 `GET /v1/device/ws` 的 upgrade 处理器挂到这台 `http.Server` 上;缺席 = 本部署没有 device
248
+ * 车道 ⇒ 该路径连 upgrade 都不认(与 `REMOTE_EXEC=device` 的拒启门同一条诚实线)。
249
+ *
250
+ * ⚠️ 排空联动由**装配层**驱动:`drainState.draining` 翻真时,main.ts 须同时调 `deviceHub.beginDrain()`
251
+ * ——两者刻意不在这里自动耦合,因为 §8-R7 的顺序(新提交 503 → 新 upgrade 拒 → 停发新指令 →
252
+ * 在途结果照收 → 才断连)是**部署编排**的语义,不是 HTTP 层能自作主张的。
253
+ *
254
+ * 🔴 **当前生产装配恒缺席,这是有意的**(`parkedKnownAgentTypes` 同族形):车A-3 只交汇聚端与这条
255
+ * 挂载缝;`main.ts` 侧的真装配(建店 → 建 hub → 传这个键 → 与 `drainState` 联动 → 与
256
+ * `REMOTE_EXEC=device` 拒启门对齐)属**车A-4/端点车**。在那之前本键只有测试口消费,不是死枝。
257
+ */
258
+ deviceHub?: import("../device-ws-hub.js").DeviceWsHub;
245
259
  /** Audit回溯: current context (+ summary) for a session, plus its owner. Enables GET /v1/sessions/:id. */
246
260
  sessionAudit?: (sessionId: string) => Promise<({
247
261
  owner: string | null;
@@ -244,6 +244,7 @@ export function createHttpServer(rawDeps) {
244
244
  res.end();
245
245
  });
246
246
  });
247
+ deps.deviceHub?.attach(server);
247
248
  const routeCtxBase = {
248
249
  deps,
249
250
  registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
@@ -1604,6 +1605,8 @@ export function createHttpServer(rawDeps) {
1604
1605
  e.code === "resume.constraint_unprojectable" ||
1605
1606
  e.code === "resume.usage_window_exhausted" ||
1606
1607
  e.code === "resume.principal_mismatch" ||
1608
+ e.code === "resume.placement_mismatch" ||
1609
+ e.code === "resume.preflight_rejected" ||
1607
1610
  e.code.startsWith("wake.") ||
1608
1611
  (e.code === "checkpoint.unsupported_version" && checkpointRowRedeemableElsewhere(e.detail?.reason));
1609
1612
  if (claimedRow && taskId && deps.runStore) {
@@ -1633,7 +1636,7 @@ export function createHttpServer(rawDeps) {
1633
1636
  ? (outcome.gate === "policy_ask" ? "approval_binding_mismatch" : "resume_outcome_invalid")
1634
1637
  : e.code,
1635
1638
  ...(e.detail?.field ? { field: e.detail.field } : {}),
1636
- ...(e.code === "resume.usage_window_exhausted" && typeof e.detail?.retryAfterMs === "number" && Number.isFinite(e.detail.retryAfterMs) && e.detail.retryAfterMs > 0
1639
+ ...((e.code === "resume.usage_window_exhausted" || e.code === "resume.preflight_rejected") && typeof e.detail?.retryAfterMs === "number" && Number.isFinite(e.detail.retryAfterMs) && e.detail.retryAfterMs > 0
1637
1640
  ? { retryAfterSec: Math.max(1, Math.ceil(e.detail.retryAfterMs / 1000)) }
1638
1641
  : {}),
1639
1642
  ...(retriable ? { retriable: true } : {}),
@@ -366,7 +366,8 @@ export function createLeaderRunner(cfg) {
366
366
  };
367
367
  const provisionIntegrationSandbox = async () => {
368
368
  if (cfg.envFactory) {
369
- const env = cfg.envFactory({ sessionId: `leader-integ-${Date.now()}` });
369
+ const integSessionId = `leader-integ-${Date.now()}`;
370
+ const env = cfg.envFactory({ sessionId: integSessionId, placementRootSessionId: integSessionId });
370
371
  try {
371
372
  await sh(env)(body.seedCmd);
372
373
  await injectOracles(env);
@@ -412,7 +413,8 @@ export function createLeaderRunner(cfg) {
412
413
  const resolveRepair = async (solo, _sub) => {
413
414
  let env;
414
415
  if (cfg.envFactory) {
415
- env = cfg.envFactory({ sessionId: `leader-grader-${Date.now()}` });
416
+ const graderSessionId = `leader-grader-${Date.now()}`;
417
+ env = cfg.envFactory({ sessionId: graderSessionId, placementRootSessionId: graderSessionId });
416
418
  }
417
419
  else {
418
420
  if (!cfg.e2bApiKey)
package/dist/main.js CHANGED
@@ -50,6 +50,7 @@ import { assertRetentionLaneWirable, startRetentionLane, RETENTION_LEASE_TTL_FAC
50
50
  import { assertConsolidationDriverWirable, buildConsolidationValveAudit, createMemoryConsolidationFaces, resolveConsolidationDriverSeat } from "./boot/memory-consolidation.js";
51
51
  import { startFleetReconciler } from "./fleet/fleet-reconciler.js";
52
52
  import { openStores } from "./boot/stores.js";
53
+ import { assertDeviceLaneWired, createDeviceLane } from "./boot/device-lane.js";
53
54
  import { runAdoptionBootScan } from "./boot/adoption.js";
54
55
  import { auditDormantPermissionRules } from "./boot/permission-rules-audit.js";
55
56
  import { createBudgetAndTracing } from "./boot/budget-tracing.js";
@@ -117,6 +118,7 @@ async function main() {
117
118
  const configCenter = await createConfigCenterRuntime({ config, logger, metrics, localRoot });
118
119
  await configCenter.applyLocalRemoteExec();
119
120
  const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, } = await openStores({ config, logger, metrics, localRoot });
121
+ const instanceId = uuidv7();
120
122
  const memoryBundleFaces = memoryEngine ? createMemoryBundleFaces(memoryEngine) : undefined;
121
123
  const memoryComplianceFaces = memoryEngine
122
124
  ? createMemoryComplianceFaces(memoryEngine, {
@@ -144,7 +146,9 @@ async function main() {
144
146
  });
145
147
  }
146
148
  await auditDormantPermissionRules({ stores: permissionRuleStores, logger, explicit: config.permissionRulesEnabledExplicit });
147
- const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore });
149
+ const deviceLane = config.remoteExec?.provider === "device" ? createDeviceLane({ config, logger, metrics, backend, replicaId: instanceId, rateGate: () => rateLimiter }) : undefined;
150
+ const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, dropSessionScoped, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore, ...(deviceLane ? { deviceLane } : {}) });
151
+ assertDeviceLaneWired(config.remoteExec?.provider === "device", deviceLane, executionEnvFactory !== undefined);
148
152
  const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
149
153
  if (toolTracer)
150
154
  logger.info("tool_trace_enabled", {});
@@ -328,7 +332,6 @@ async function main() {
328
332
  const runStore = backend ? backend.run() : undefined;
329
333
  const resumeAnchorStore = backend ? backend.resumeAnchor() : undefined;
330
334
  const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
331
- const instanceId = uuidv7();
332
335
  const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
333
336
  const capEnv = (name) => {
334
337
  const r = parseCapEnv(name, process.env[name]);
@@ -515,6 +518,8 @@ async function main() {
515
518
  configCenter.startRefreshLoop({ runnerTierFrozen, pricing, limitSync, swapRunnerModels });
516
519
  configCenter.initKeyResolver();
517
520
  const { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe } = createSessionFaces({
521
+ deviceStore: deviceLane?.store,
522
+ dropSessionScoped,
518
523
  config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore,
519
524
  resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileSnapshotStore,
520
525
  workflowCompletionInbox, taskListLane,
@@ -585,6 +590,7 @@ async function main() {
585
590
  sessionTitler: sessionTitler ? sessionTitler : undefined,
586
591
  };
587
592
  const seams = {
593
+ deviceHub: deviceLane?.hub,
588
594
  sessionAudit,
589
595
  routeJudgeBrain: brain,
590
596
  purgeSession: purgeSession ? purgeSession : undefined,
@@ -665,7 +671,7 @@ async function main() {
665
671
  config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver,
666
672
  getCenterPrompts: () => configCenter.getCenterPrompts(),
667
673
  getKeyResolver: () => configCenter.getKeyResolver(),
668
- taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv,
674
+ taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner,
669
675
  hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec,
670
676
  selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore,
671
677
  singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage,
@@ -785,6 +791,7 @@ async function main() {
785
791
  version: serviceVersion(),
786
792
  runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState,
787
793
  storeLiveProbe, configCenter,
794
+ deviceHub: deviceLane?.hub,
788
795
  retentionLane: retentionLaneStarted,
789
796
  releaseRetentionLease: retentionLaneStore !== undefined ? () => retentionLaneStore.release(instanceId) : undefined,
790
797
  });
@@ -62,6 +62,13 @@ export interface HardenedVmLimits {
62
62
  * A script exceeding it OOMs the WORKER (killed), never the shared main process. Default 128. */
63
63
  maxHeapMb?: number;
64
64
  }
65
+ /** [5434]①(clay 亲机四 run 三次恰在 600s 整点全灭):workflow 总超时默认与旋钮的**单点**。
66
+ * 旧默认 600_000(10 分钟)对真实 workflow(验证 agent 逐条重跑清单/任何带审批等待的车)结构性不够——
67
+ * 默认抬到 **1 小时**;部署经 `WORKFLOW_TOTAL_TIMEOUT_MS` 显式配,域 [60_000, 86_400_000](1 分钟..24h),
68
+ * 坏值**响亮拒**(#210 立律:旋钮坏值禁静默回默认)。0/负数不是「禁用」:无界 runaway 脚本会吞掉共享
69
+ * 副本的事件循环/堆,禁用臂刻意不提供——要更久就把值配大。模块加载时读一次(坏 env = boot fail-loud)。
70
+ * 超时终态分型(partial vs failed,[5434]① 后半)涉 wire status 语义,与 core workflow 终态形另批对表。 */
71
+ export declare function workflowTotalTimeoutMsDefault(): number;
65
72
  /**
66
73
  * The realm test itself, exported so there is exactly ONE ruler for "did this rejection come from an untrusted
67
74
  * script?" in the process. A promise minted INSIDE a vm context fails `instanceof` against the HOST `Promise`
@@ -1,7 +1,17 @@
1
1
  import vm from "node:vm";
2
2
  import { splitWorkflowMeta, WorkflowScriptError, } from "@sema-agent/core";
3
+ export function workflowTotalTimeoutMsDefault() {
4
+ const raw = process.env.WORKFLOW_TOTAL_TIMEOUT_MS;
5
+ if (raw === undefined || raw === "")
6
+ return 3_600_000;
7
+ const n = Number(raw);
8
+ if (!Number.isSafeInteger(n) || n < 60_000 || n > 86_400_000) {
9
+ throw new Error(`env WORKFLOW_TOTAL_TIMEOUT_MS must be an integer in [60000, 86400000] ms (got ${JSON.stringify(raw)}) — unset it for the 3600000 default`);
10
+ }
11
+ return n;
12
+ }
3
13
  const DEFAULTS = {
4
- totalTimeoutMs: 600_000,
14
+ totalTimeoutMs: workflowTotalTimeoutMsDefault(),
5
15
  syncTimeoutMs: 5_000,
6
16
  maxAgents: 1000,
7
17
  concurrency: 12,
@@ -1,7 +1,7 @@
1
1
  import { Worker } from "node:worker_threads";
2
2
  import { WorkflowScriptError } from "@sema-agent/core";
3
- import { scopedPhaseGate } from "./hardened-vm-runner.js";
4
- const DEFAULTS = { totalTimeoutMs: 600_000, syncTimeoutMs: 5_000, maxAgents: 1000, concurrency: 12, maxHeapMb: 128 };
3
+ import { scopedPhaseGate, workflowTotalTimeoutMsDefault } from "./hardened-vm-runner.js";
4
+ const DEFAULTS = { totalTimeoutMs: workflowTotalTimeoutMsDefault(), syncTimeoutMs: 5_000, maxAgents: 1000, concurrency: 12, maxHeapMb: 128 };
5
5
  const IS_TS = import.meta.url.endsWith(".ts");
6
6
  const WORKER_URL = new URL(IS_TS ? "./hardened-vm-worker.ts" : "./hardened-vm-worker.js", import.meta.url);
7
7
  const WORKER_EXEC_ARGV = IS_TS ? ["--import", "tsx/esm"] : undefined;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * device lane 四表持久层(design/device-executor-lane-v2 §6)—— `DeviceStore` 双方言实现
3
+ * (`TiDBDeviceStore` / `PgDeviceStore`,SINGLE-FILE DUAL-DIALECT,design/158 A12 形:一个
4
+ * `SqlDeviceStore` 接 `SqlDriver`,两个薄 ctor 子类落方言绑定)。语义真源 = `../device-store.js`
5
+ * (类型 + 闭集 + InMemory 参照实现),准入链 = `../device-enrollment.js`。
6
+ *
7
+ * 四张表:
8
+ * · `devices` —— 设备注册簿 + presence 投影(单设备单活连 ⇒ 1:1,省一次 join,§6 注①)
9
+ * · `device_enroll_tokens` —— 一次性 enrollment 凭据(明文一现,落库只有 sha256)
10
+ * · `device_sessions` —— **根会话**↔设备绑定(resume 路由真源,§4.5)
11
+ * · `device_audit` —— 安全轴事件审计(§4.8 标「审计行」的 8 个 event)
12
+ * 刻意**没有** `device_instructions` 表:指令 pending 表是内存态(§6 注③——指令比 run 短命,落库只会
13
+ * 造「表里 pending、run 已死」的僵尸行;at-most-once 靠 §5.3 投递边界,不靠指令落库)。
14
+ *
15
+ * ── CAS 铁则(与 approval-ask-store-sql.ts 头注同源,写在这里防遗忘)─────────────────────────────
16
+ * 1. 一切转移带 `WHERE <前态>`,赢 = affected 恰为 1。禁读-改-写两步(TOCTOU 窗口)。
17
+ * 2. 🔴 MySQL/TiDB `affectedRows` 坑:同值 UPDATE(SET 的新值与当前列逐字节相同)affectedRows=0
18
+ * ——**所有** CAS UPDATE 因此必须带一个恒变列(`rev = rev + 1`)。本文件每一条 CAS UPDATE 都带。
19
+ * 在本店这条尤其致命:心跳续租在「同一毫秒内重复心跳」时 SET 的值可能与当前值相同,没有 rev
20
+ * 就会被判成「本连接已失权」⇒ 设备被自己的心跳踢下线。
21
+ * 3. 事务:`SqlDriver.connect()` → `begin()`/`beginPessimistic()` → `query()` → `commit()`,失败
22
+ * `rollback()`。**失败臂的回读必须走当前这条 `conn`**(approval 店复审 F1 的真缺陷:走池级 query
23
+ * 会与自己持有的连接死锁),且**先 rollback 再读**(读到最新已提交视图)。
24
+ *
25
+ * ── 方言差异(A12 doctrine:每处显式写在调用点)────────────────────────────────────────────────────
26
+ * - `?`(位置序) vs `$n`(显式编号);动态 WHERE(`listAudit`)靠 `ph(dialect, n)` 生成。
27
+ * - `INSERT IGNORE` vs `ON CONFLICT (pk) DO NOTHING`(`bindSession` 的首绑 CAS)。
28
+ * - `AUTO_INCREMENT` vs `BIGSERIAL`;`JSON` vs `JSONB`(+ `$n::jsonb` 显式 cast)。
29
+ * - `KEY …` 内联 vs 独立 `CREATE INDEX IF NOT EXISTS`。
30
+ * - 回读名单:PG 用 `UPDATE … RETURNING`,TiDB 无 RETURNING ⇒ 同事务 `UPDATE` 后 `SELECT`。
31
+ * - 时长运算:`NOW(3) + INTERVAL ? SECOND` vs `now() + ($n::double precision * INTERVAL '1 second')`。
32
+ * - 键列字节等价:MySQL `VARBINARY` vs PG `VARCHAR … COLLATE "C"`(理由见 ../device-store.ts 头注;
33
+ * `utf8mb4_bin` **不够**,它仍是 PAD SPACE collation)。
34
+ * - VARBINARY 的读回:mysql2 给 **Buffer**,PG 给 string ⇒ 一律过 `bufToStr`(mailbox 真库坑同解)。
35
+ *
36
+ * ── 时间列的存取形(§4.6 task-R3-F18「时间权威 = DB」的落地)──────────────────────────────────────
37
+ * 列是原生 `DATETIME(3)` / `TIMESTAMPTZ(3)`,但**跨边界一律是 epoch 毫秒**:写侧送**时长秒**由 SQL 从
38
+ * DB 的 now 起算,读侧在 SELECT 里就转成 `*_ms`。
39
+ * 🔴 为什么不让驱动把时间戳解成 JS `Date` 再比:①mysql2 按**进程**时区解析 DATETIME,DB 会话时区与
40
+ * Node 时区不同即整体偏移几小时(而这条偏移落在「租约还有效吗」上就是双主);②`Date.now()` 一旦进入
41
+ * 判据,副本时钟落后即越租。两条都在 SELECT 表达式里就地关掉。
42
+ */
43
+ import type { Pool as MySqlPool } from "mysql2/promise";
44
+ import type { Pool as PgPool } from "pg";
45
+ import { type SqlDriver } from "./sql-driver.js";
46
+ import { type AcquireConnectionInput, type AcquireConnectionResult, type AppendAuditInput, type BindSessionInput, type BindSessionResult, type ConsumeEnrollTokenInput, type ConsumeEnrollTokenResult, type DeviceAdmissionView, type DeviceAuditRow, type DeviceEnrollTokenRow, type DeviceOwner, type DeviceRow, type DeviceSessionRow, type DeviceStore, type HeartbeatConnectionInput, type IssueEnrollTokenInput, type ListAuditFilter, type RevokeDeviceInput, type RevokeDeviceResult } from "../device-store.js";
47
+ /**
48
+ * ── 命名(**落库一律单数**,与设计稿的复数形不同)──────────────────────────────────────────────────
49
+ * design v2 §6 的 DDL 写的是 `devices` / `device_sessions` / `device_enrollment_tokens`(复数),但本仓有一条
50
+ * 常驻机器门:`test/schema-naming-invariants.test.ts` 判据① —— 建表的表名必须在 `TABLE_VOCABULARY` 闭集里
51
+ * **且词表全是单数形**。#270 车1 的 `retention_hold` / `retention_tombstone` 是同一处先例(那份设计稿同样
52
+ * 写的复数,落库单数)。所以四张表落库名为 `device` / `device_session` / `device_enroll_token` /
53
+ * `device_audit`,**表结构与设计稿逐列一致**,只有名字随仓规。新表 = 先进词表再建。
54
+ */
55
+ export declare const DEVICES_TABLE = "device";
56
+ export declare const DEVICE_ENROLL_TOKENS_TABLE = "device_enroll_token";
57
+ export declare const DEVICE_SESSIONS_TABLE = "device_session";
58
+ export declare const DEVICE_AUDIT_TABLE = "device_audit";
59
+ export declare const TIDB_DEVICE_STATEMENTS: readonly string[];
60
+ /** {@link TIDB_DEVICE_STATEMENTS} 的遍历壳(集成测试用;生产装配由中央 ensureSchema 展开同一份数组)。 */
61
+ export declare function ensureTiDBDeviceSchema(pool: MySqlPool): Promise<void>;
62
+ type PgQuery = (sql: string, params?: unknown[]) => Promise<unknown>;
63
+ export declare function ensurePgDeviceSchema(q: PgQuery): Promise<void>;
64
+ export declare class SqlDeviceStore implements DeviceStore {
65
+ private readonly db;
66
+ constructor(db: SqlDriver);
67
+ private q;
68
+ private json;
69
+ issueEnrollToken(input: IssueEnrollTokenInput): Promise<DeviceEnrollTokenRow>;
70
+ getEnrollToken(tokenHash: string): Promise<DeviceEnrollTokenRow | null>;
71
+ /**
72
+ * §6 注册协议 —— **单事务**:消费 CAS + INSERT devices + 审计行一起提交。
73
+ *
74
+ * 谓词四件(缺一即 affected=0):`consumed_at IS NULL`(一次性)、`expires_at > NOW(3)`(TTL,DB 钟)、
75
+ * `target_tenant/target_subject` 等值(绑定 principal)。affected=0 的**理由**靠回滚后的回读判别 ——
76
+ * 顺序刻意是「先 principal 后消费态」:token 不是你的就连「它被没被用过」都不告诉你。
77
+ */
78
+ consumeEnrollToken(input: ConsumeEnrollTokenInput): Promise<ConsumeEnrollTokenResult>;
79
+ /** 事务内/持连接期间的设备回读(见 consumeEnrollToken 的 F2 注:此期间**禁**走池级读)。 */
80
+ private readDeviceOn;
81
+ readDeviceForAdmission(deviceId: string, owner: DeviceOwner): Promise<DeviceAdmissionView | null>;
82
+ getDeviceUnscoped(deviceId: string): Promise<DeviceRow | null>;
83
+ listDevicesByOwner(owner: DeviceOwner): Promise<DeviceRow[]>;
84
+ /**
85
+ * 吊销 = 终态 CAS(`status='active'` 前态)+ **租约作废 + 世代推进**(§8-R4 在途语义):
86
+ * 世代一推,在途连接的每一次带世代谓词的写(心跳/结果帧入账)当场影响 0 行 ⇒ 设备侧立即失权。
87
+ */
88
+ revokeDevice(input: RevokeDeviceInput): Promise<RevokeDeviceResult>;
89
+ /**
90
+ * 连接建立 = 带 expiry 的租约 CAS。谓词 `lease_expires_at IS NULL OR <= NOW(3)`:**活租约恒不被撕**
91
+ * (§5.2 单活连不变量;`DEVICE_SUPERSEDE=stale_only` 的语义就在这一句),过期租约才可被新连夺取。
92
+ * 赢家 `conn_generation` 单调 +1,后续所有写都带这个世代谓词。
93
+ */
94
+ acquireConnection(input: AcquireConnectionInput): Promise<AcquireConnectionResult>;
95
+ /**
96
+ * 心跳续租 —— 谓词 = **世代等值 + active**。影响 0 行 = 本连接已失权(被顶替 / 被吊销)⇒ 调用方
97
+ * 主动断开(§4.6 步 2)。
98
+ *
99
+ * 🔴 允许续一条**已过期但没被别人夺走**的租约(generation 未变 ⇒ 排他性仍在本连接手上);夺租者
100
+ * 一旦成功,generation 就变了,这条心跳当场 0 行。`rev = rev + 1` 是 CAS 铁则②:同毫秒重复心跳的
101
+ * SET 值可能与当前逐字节相同,没有它 MySQL 会把「成功」报成 affectedRows=0。
102
+ */
103
+ heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
104
+ releaseConnection(deviceId: string, generation: number): Promise<boolean>;
105
+ /**
106
+ * §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)与 `INSERT IGNORE` 在一个事务里。
107
+ *
108
+ * 🔴 `beginPessimistic` + `FOR UPDATE`:设备行必须是**当前读**。快照读下,一次与吊销并发的绑定会
109
+ * 读到「还 active」的旧版本,于是把 session 绑到一台**刚被吊销**的设备上(sql-driver.ts 头注点名的
110
+ * double-admit 形)。
111
+ * 并发首绑:`INSERT IGNORE` / `ON CONFLICT DO NOTHING` 的 affected 是引擎对「谁赢」的权威回答;
112
+ * 输家回读既有行 —— 同 deviceId = 幂等成功,异 deviceId = `conflict`(无未定义态)。
113
+ */
114
+ bindSession(input: BindSessionInput): Promise<BindSessionResult>;
115
+ getSessionBinding(rootSessionId: string): Promise<DeviceSessionRow | null>;
116
+ deleteByRootSession(rootSessionId: string): Promise<number>;
117
+ appendAudit(input: AppendAuditInput): Promise<void>;
118
+ /** 审计写的**唯一**语句(池级与事务内共用;`SqlExec` 是两者的公共面)。 */
119
+ private insertAuditOn;
120
+ listAudit(filter?: ListAuditFilter): Promise<DeviceAuditRow[]>;
121
+ }
122
+ /** 方言绑定薄壳(A12 形:ctor 收池,行为全在基类)。 */
123
+ export declare class TiDBDeviceStore extends SqlDeviceStore {
124
+ constructor(pool: MySqlPool);
125
+ }
126
+ export declare class PgDeviceStore extends SqlDeviceStore {
127
+ constructor(pool: PgPool);
128
+ }
129
+ export {};
130
+ //# sourceMappingURL=device-store-sql.d.ts.map