@sema-agent/server 7.50.0 → 7.52.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 (222) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +83 -20
  4. package/dist/adoption/plan.d.ts +4 -4
  5. package/dist/adoption/plan.js +11 -0
  6. package/dist/approval-card.d.ts +26 -2
  7. package/dist/approval-card.js +9 -0
  8. package/dist/approval-hmac.d.ts +1 -1
  9. package/dist/approval-reconciler.d.ts +2 -2
  10. package/dist/approval-reconciler.js +1 -1
  11. package/dist/bench/l8/artifact.d.ts +1 -1
  12. package/dist/bench/s1/arms.d.ts +1 -1
  13. package/dist/bench/s1/live-deps.d.ts +1 -1
  14. package/dist/bench/s1/oracle.d.ts +1 -1
  15. package/dist/bench/s1/row.d.ts +1 -1
  16. package/dist/boot/budget-tracing.d.ts +1 -1
  17. package/dist/boot/budget-tracing.js +2 -5
  18. package/dist/boot/coordinators.js +3 -1
  19. package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
  20. package/dist/boot/device-lane.d.ts +79 -0
  21. package/dist/boot/device-lane.js +63 -0
  22. package/dist/boot/engine-lease.d.ts +1 -1
  23. package/dist/boot/execution-env.d.ts +48 -2
  24. package/dist/boot/execution-env.js +57 -5
  25. package/dist/boot/governance-seams.d.ts +2 -2
  26. package/dist/boot/leader.d.ts +1 -1
  27. package/dist/boot/memory-boundary.d.ts +2 -2
  28. package/dist/boot/memory-consolidation.d.ts +6 -6
  29. package/dist/boot/org-memory.d.ts +1 -1
  30. package/dist/boot/parked-revive-gate.d.ts +3 -3
  31. package/dist/boot/reapers.d.ts +2 -2
  32. package/dist/boot/reapers.js +17 -4
  33. package/dist/boot/resolve-spec.d.ts +15 -0
  34. package/dist/boot/resolve-spec.js +40 -15
  35. package/dist/boot/retention-lane.d.ts +2 -2
  36. package/dist/boot/runner-deps.d.ts +17 -7
  37. package/dist/boot/runner-deps.js +4 -2
  38. package/dist/boot/runtime-caps.d.ts +23 -0
  39. package/dist/boot/runtime-caps.js +24 -2
  40. package/dist/boot/session-faces.d.ts +6 -1
  41. package/dist/boot/session-faces.js +17 -3
  42. package/dist/boot/shutdown.d.ts +8 -0
  43. package/dist/boot/shutdown.js +3 -1
  44. package/dist/boot/stores.js +40 -1
  45. package/dist/boot/task-list-lane.d.ts +2 -2
  46. package/dist/brain.js +51 -31
  47. package/dist/budget.d.ts +2 -2
  48. package/dist/capabilities/center-plugins.d.ts +1 -1
  49. package/dist/capabilities/collab-workflows.d.ts +1 -1
  50. package/dist/capabilities/hands-lane.d.ts +3 -3
  51. package/dist/capabilities/scenarios.d.ts +3 -3
  52. package/dist/capabilities/skills.d.ts +1 -1
  53. package/dist/config-center/hot-keys-registry.js +5 -0
  54. package/dist/config-center/http-client.d.ts +2 -2
  55. package/dist/config-center/read-face.d.ts +1 -1
  56. package/dist/config-center/restart-signal.d.ts +1 -1
  57. package/dist/config-center/types.d.ts +10 -3
  58. package/dist/config-invariants.d.ts +2 -2
  59. package/dist/config-invariants.js +18 -0
  60. package/dist/config-types.d.ts +90 -18
  61. package/dist/config.js +91 -19
  62. package/dist/deployment-governance.d.ts +1 -1
  63. package/dist/device-enrollment.d.ts +125 -0
  64. package/dist/device-enrollment.js +156 -0
  65. package/dist/device-store.d.ts +385 -0
  66. package/dist/device-store.js +407 -0
  67. package/dist/device-ws-hub.d.ts +182 -0
  68. package/dist/device-ws-hub.js +1012 -0
  69. package/dist/device-ws-protocol.d.ts +429 -0
  70. package/dist/device-ws-protocol.js +464 -0
  71. package/dist/env-facts.d.ts +5 -2
  72. package/dist/env-facts.js +1 -0
  73. package/dist/execution-lane-caps.d.ts +152 -0
  74. package/dist/execution-lane-caps.js +166 -0
  75. package/dist/fleet/fleet-bus.d.ts +1 -1
  76. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  77. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  78. package/dist/http/active-run-conflict.d.ts +2 -2
  79. package/dist/http/principal-gate.d.ts +1 -1
  80. package/dist/http/route-ctx.d.ts +24 -4
  81. package/dist/http/routes/admin-memory-optout.d.ts +48 -0
  82. package/dist/http/routes/admin-memory-optout.js +75 -0
  83. package/dist/http/routes/agents-roster.d.ts +1 -1
  84. package/dist/http/routes/approvals-assistant.js +21 -21
  85. package/dist/http/routes/capabilities.js +27 -11
  86. package/dist/http/routes/diagnostics.d.ts +1 -1
  87. package/dist/http/routes/leader.d.ts +1 -1
  88. package/dist/http/routes/memory-bundle.d.ts +1 -1
  89. package/dist/http/routes/memory-bundle.js +3 -1
  90. package/dist/http/routes/memory-compliance.d.ts +4 -4
  91. package/dist/http/routes/memory-origin.d.ts +4 -4
  92. package/dist/http/routes/memory-origin.js +3 -1
  93. package/dist/http/routes/memory-policy.js +6 -2
  94. package/dist/http/routes/retention-ops.d.ts +1 -1
  95. package/dist/http/routes/rules.d.ts +5 -2
  96. package/dist/http/routes/rules.js +84 -5
  97. package/dist/http/routes/run-memory-capture.d.ts +34 -0
  98. package/dist/http/routes/run-memory-capture.js +97 -0
  99. package/dist/http/routes/runs.d.ts +1 -0
  100. package/dist/http/routes/runs.js +212 -6
  101. package/dist/http/routes/session-sync.d.ts +4 -6
  102. package/dist/http/routes/session-sync.js +60 -47
  103. package/dist/http/routes/sessions.js +61 -188
  104. package/dist/http/routes/shared-memory.d.ts +1 -1
  105. package/dist/http/routes/tasks.js +1 -1
  106. package/dist/http/routes/workflows.js +19 -0
  107. package/dist/http/server.d.ts +49 -13
  108. package/dist/http/server.js +109 -13
  109. package/dist/http/verify-rounds.d.ts +1 -1
  110. package/dist/http/wire-types.d.ts +41 -8
  111. package/dist/leader/endpoint.d.ts +1 -1
  112. package/dist/leader/leader.d.ts +1 -1
  113. package/dist/leader/repair-oracle.d.ts +2 -2
  114. package/dist/leader/repair-oracle.js +1 -1
  115. package/dist/leader/wire.js +4 -2
  116. package/dist/main.js +23 -9
  117. package/dist/memory-bundle-engine.d.ts +1 -1
  118. package/dist/memory-operator-faces.d.ts +5 -5
  119. package/dist/memory-origin-fence.d.ts +50 -0
  120. package/dist/memory-origin-fence.js +15 -0
  121. package/dist/memory-posture.d.ts +13 -1
  122. package/dist/memory-posture.js +1 -0
  123. package/dist/memory-scope.d.ts +6 -1
  124. package/dist/memory-scope.js +3 -2
  125. package/dist/model-select.d.ts +2 -2
  126. package/dist/observability/fail-open.d.ts +17 -5
  127. package/dist/observability/fail-open.js +17 -5
  128. package/dist/orchestration/hardened-vm-runner.d.ts +9 -2
  129. package/dist/orchestration/hardened-vm-runner.js +11 -1
  130. package/dist/orchestration/hardened-vm-worker-runner.js +2 -2
  131. package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
  132. package/dist/plugins/background-shell-support.d.ts +2 -2
  133. package/dist/plugins/blob-backend.d.ts +7 -2
  134. package/dist/plugins/blob-backend.js +22 -18
  135. package/dist/plugins/caching-session-store.d.ts +7 -3
  136. package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
  137. package/dist/plugins/device-store-sql.d.ts +130 -0
  138. package/dist/plugins/device-store-sql.js +574 -0
  139. package/dist/plugins/file-history-store-sql.d.ts +170 -0
  140. package/dist/plugins/file-history-store-sql.js +827 -0
  141. package/dist/plugins/file-run-store.d.ts +44 -5
  142. package/dist/plugins/file-run-store.js +85 -13
  143. package/dist/plugins/fork-routing-session-store.d.ts +1 -1
  144. package/dist/plugins/fork-routing-session-store.js +2 -2
  145. package/dist/plugins/leader-run-store-sql.d.ts +1 -1
  146. package/dist/plugins/local-session-store.d.ts +9 -1
  147. package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
  148. package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
  149. package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
  150. package/dist/plugins/memory-origin-law.d.ts +2 -2
  151. package/dist/plugins/memory-run-store.d.ts +4 -3
  152. package/dist/plugins/memory-run-store.js +2 -1
  153. package/dist/plugins/permission-rule-store-file.d.ts +16 -6
  154. package/dist/plugins/permission-rule-store-file.js +77 -13
  155. package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
  156. package/dist/plugins/permission-rule-store-sql.js +123 -21
  157. package/dist/plugins/pg-pool.js +59 -0
  158. package/dist/plugins/pg-safe-json.d.ts +1 -1
  159. package/dist/plugins/remote-env-device.d.ts +271 -0
  160. package/dist/plugins/remote-env-device.js +727 -0
  161. package/dist/plugins/remote-env-k8s.d.ts +1 -1
  162. package/dist/plugins/remote-scratchpad.js +1 -1
  163. package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
  164. package/dist/plugins/retention-store-sql.d.ts +7 -7
  165. package/dist/plugins/retention-store-sql.js +6 -0
  166. package/dist/plugins/run-store-sql.d.ts +9 -3
  167. package/dist/plugins/run-store-sql.js +2 -2
  168. package/dist/plugins/send-user-file.d.ts +1 -1
  169. package/dist/plugins/session-placement.d.ts +1 -1
  170. package/dist/plugins/sql-errors.d.ts +1 -1
  171. package/dist/plugins/store-backend.d.ts +68 -29
  172. package/dist/plugins/store-backend.js +78 -15
  173. package/dist/plugins/store-contracts.d.ts +5 -3
  174. package/dist/plugins/task-list-store-sql.d.ts +1 -1
  175. package/dist/plugins/tidb-pool.js +59 -0
  176. package/dist/plugins/web-search.d.ts +1 -1
  177. package/dist/prompts-domain-validate.js +1 -1
  178. package/dist/resource-suspend.d.ts +1 -1
  179. package/dist/resource-window.d.ts +3 -3
  180. package/dist/rules-consent.d.ts +231 -9
  181. package/dist/rules-consent.js +411 -13
  182. package/dist/run-local.js +1 -0
  183. package/dist/runs.d.ts +15 -1
  184. package/dist/runs.js +19 -0
  185. package/dist/runtime-caps-resolver.d.ts +36 -1
  186. package/dist/runtime-caps-resolver.js +25 -0
  187. package/dist/runtime-governance.d.ts +1 -1
  188. package/dist/security.d.ts +5 -5
  189. package/dist/security.js +1 -1
  190. package/dist/session-sync.d.ts +52 -37
  191. package/dist/session-sync.js +19 -16
  192. package/dist/shared-memory-scope-authorizer.d.ts +1 -1
  193. package/dist/sighup-idle.d.ts +1 -1
  194. package/dist/spec-fields.d.ts +22 -1
  195. package/dist/spec-fields.js +11 -0
  196. package/dist/store-live-probe.d.ts +2 -2
  197. package/dist/task-a2a.d.ts +1 -1
  198. package/dist/task-cwd.d.ts +25 -0
  199. package/dist/task-cwd.js +3 -0
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/task-settings.d.ts +3 -3
  202. package/dist/task-settings.js +3 -1
  203. package/dist/task-workflow.d.ts +1 -1
  204. package/dist/text-bidi.d.ts +2 -2
  205. package/dist/tool-approval.d.ts +191 -9
  206. package/dist/tool-approval.js +248 -29
  207. package/dist/trace/core-keyset-guard.d.ts +1 -1
  208. package/dist/trace/engine-notice-wire.d.ts +6 -4
  209. package/dist/trace/engine-notice-wire.js +2 -0
  210. package/dist/trace/ledger-events.d.ts +1 -1
  211. package/dist/trace/ledger-sink.d.ts +2 -2
  212. package/dist/trace/project.d.ts +3 -2
  213. package/dist/trace/project.js +1 -0
  214. package/dist/turn-activity.d.ts +1 -1
  215. package/dist/wall-clock-jump-guard.d.ts +3 -3
  216. package/package.json +2 -2
  217. package/dist/http/tar.d.ts +0 -17
  218. package/dist/http/tar.js +0 -53
  219. package/dist/http/workspace-content.d.ts +0 -11
  220. package/dist/http/workspace-content.js +0 -20
  221. package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
  222. package/dist/plugins/file-snapshot-store-sql.js +0 -213
package/dist/main.js CHANGED
@@ -30,6 +30,7 @@ import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
30
30
  import { RateLimiter } from "./observability/rate-limit.js";
31
31
  import { createHttpServer, explicitOperatorOk, hasServiceAuth } from "./http/server.js";
32
32
  import { exportMemoryScope } from "./memory-export.js";
33
+ import { workflowModelAllowlistFor } from "./task-workflow.js";
33
34
  import { createMemoryBundleFaces } from "./memory-bundle-engine.js";
34
35
  import { backendOwnsControlPlane, createMemoryComplianceFaces, createMemoryOriginFaces } from "./memory-operator-faces.js";
35
36
  import { performMemorySync } from "./memory-sync.js";
@@ -50,6 +51,7 @@ import { assertRetentionLaneWirable, startRetentionLane, RETENTION_LEASE_TTL_FAC
50
51
  import { assertConsolidationDriverWirable, buildConsolidationValveAudit, createMemoryConsolidationFaces, resolveConsolidationDriverSeat } from "./boot/memory-consolidation.js";
51
52
  import { startFleetReconciler } from "./fleet/fleet-reconciler.js";
52
53
  import { openStores } from "./boot/stores.js";
54
+ import { assertDeviceLaneWired, createDeviceLane } from "./boot/device-lane.js";
53
55
  import { runAdoptionBootScan } from "./boot/adoption.js";
54
56
  import { auditDormantPermissionRules } from "./boot/permission-rules-audit.js";
55
57
  import { createBudgetAndTracing } from "./boot/budget-tracing.js";
@@ -117,6 +119,7 @@ async function main() {
117
119
  const configCenter = await createConfigCenterRuntime({ config, logger, metrics, localRoot });
118
120
  await configCenter.applyLocalRemoteExec();
119
121
  const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, } = await openStores({ config, logger, metrics, localRoot });
122
+ const instanceId = uuidv7();
120
123
  const memoryBundleFaces = memoryEngine ? createMemoryBundleFaces(memoryEngine) : undefined;
121
124
  const memoryComplianceFaces = memoryEngine
122
125
  ? createMemoryComplianceFaces(memoryEngine, {
@@ -132,7 +135,7 @@ async function main() {
132
135
  if (adoptionBoot.scanned > 0 || adoptionBoot.error !== undefined) {
133
136
  logger.info("adoption_boot_scan", { ...adoptionBoot });
134
137
  }
135
- const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileSnapshotStore, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
138
+ const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileHistoryStore, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
136
139
  const permissionRuleStores = config.permissionRulesEnabled && backend ? backend.permissionRule() : undefined;
137
140
  const ruleConsent = permissionRuleStores ? createRuleConsentLane(permissionRuleStores) : undefined;
138
141
  if (config.permissionRulesEnabled && permissionRuleStores === undefined) {
@@ -144,7 +147,9 @@ async function main() {
144
147
  });
145
148
  }
146
149
  await auditDormantPermissionRules({ stores: permissionRuleStores, logger, explicit: config.permissionRulesEnabledExplicit });
147
- const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore });
150
+ const deviceLane = config.remoteExec?.provider === "device" ? createDeviceLane({ config, logger, metrics, backend, replicaId: instanceId, rateGate: () => rateLimiter }) : undefined;
151
+ const { perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, dropSessionScoped, executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, } = createExecutionEnv({ config, logger, metrics, taskAttachmentStore, ...(deviceLane ? { deviceLane } : {}) });
152
+ assertDeviceLaneWired(config.remoteExec?.provider === "device", deviceLane, executionEnvFactory !== undefined);
148
153
  const toolTracer = config.toolTrace ? createToolTracer(logger) : undefined;
149
154
  if (toolTracer)
150
155
  logger.info("tool_trace_enabled", {});
@@ -159,7 +164,8 @@ async function main() {
159
164
  getParkedRedeem: () => parkedAskRedeem,
160
165
  ruleScopeRootFor: (sessionId) => cardRuleScopeRoot(sessionId !== undefined ? perSessionCwd.get(sessionId) : undefined, config),
161
166
  });
162
- const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
167
+ const memoryOptOutGrantStore = backend?.memoryOptOutGrant();
168
+ const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger, memoryOptOutGrant: memoryOptOutGrantStore });
163
169
  const orgMemoryAdmission = createOrgMemoryAdmissionWiring({ config, logger, metrics });
164
170
  const sharedMemoryStore = orgMemoryAdmission.directory !== undefined
165
171
  ? backend?.sharedMemoryStore?.(createSharedMemoryScopeAuthorizer({
@@ -234,12 +240,14 @@ async function main() {
234
240
  retentionExecutorWired: config.retentionSweep.intervalSec > 0 && retentionExecutor !== undefined && retentionLaneStore !== undefined,
235
241
  });
236
242
  const mcpRevocations = configCenter.takeMcpRevocations();
243
+ const workflowModelAllowlistSnapshot = workflowModelAllowlistFor(config) ?? [];
237
244
  const runnerDeps = createRunnerDeps({
238
245
  hands: commitHands,
239
246
  mcpRevocations,
240
247
  config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, usageWindowStore, brain,
241
248
  pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
242
- memorySyncRunner, toolResultStore: runnerOffloadStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
249
+ memorySyncRunner, toolResultStore: runnerOffloadStore, sessionPolicyStore, runtimeCapsResolver, fileHistoryStore,
250
+ legacyRewindBoundaryProbe: backend?.legacyRewindBoundaryProbe ? backend.legacyRewindBoundaryProbe.bind(backend) : undefined,
243
251
  permissionRuleStore: permissionRuleStores?.provider,
244
252
  executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
245
253
  workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, orgMemoryAdmission,
@@ -328,7 +336,6 @@ async function main() {
328
336
  const runStore = backend ? backend.run() : undefined;
329
337
  const resumeAnchorStore = backend ? backend.resumeAnchor() : undefined;
330
338
  const approvalExemptionStore = backend ? backend.approvalExemption() : undefined;
331
- const instanceId = uuidv7();
332
339
  const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
333
340
  const capEnv = (name) => {
334
341
  const r = parseCapEnv(name, process.env[name]);
@@ -449,7 +456,7 @@ async function main() {
449
456
  let runDenySweep;
450
457
  const reaper = startReapers({
451
458
  config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
452
- rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap,
459
+ rateLimiter, costQuota, toolResultStore, fileHistoryStore, taskAttachmentStore, imageBakes, worktreeReap,
453
460
  workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
454
461
  backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores,
455
462
  getRunDenySweep: () => runDenySweep,
@@ -515,8 +522,10 @@ async function main() {
515
522
  configCenter.startRefreshLoop({ runnerTierFrozen, pricing, limitSync, swapRunnerModels });
516
523
  configCenter.initKeyResolver();
517
524
  const { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe } = createSessionFaces({
525
+ deviceStore: deviceLane?.store,
526
+ dropSessionScoped,
518
527
  config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore,
519
- resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileSnapshotStore,
528
+ resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryStore,
520
529
  workflowCompletionInbox, taskListLane,
521
530
  });
522
531
  const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend });
@@ -554,7 +563,7 @@ async function main() {
554
563
  checkpointStore,
555
564
  sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
556
565
  ruleConsent,
557
- fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
566
+ fileHistoryStore: fileHistoryStore ? fileHistoryStore : undefined,
558
567
  toolResultStore,
559
568
  taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined,
560
569
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
@@ -585,6 +594,7 @@ async function main() {
585
594
  sessionTitler: sessionTitler ? sessionTitler : undefined,
586
595
  };
587
596
  const seams = {
597
+ deviceHub: deviceLane?.hub,
588
598
  sessionAudit,
589
599
  routeJudgeBrain: brain,
590
600
  purgeSession: purgeSession ? purgeSession : undefined,
@@ -600,6 +610,7 @@ async function main() {
600
610
  memoryCompliance: memoryComplianceFaces,
601
611
  memoryOriginFace: memoryOriginFaces,
602
612
  memoryConsolidation: memoryConsolidationFaces,
613
+ memoryOptOutGrant: memoryOptOutGrantStore,
603
614
  orgMemoryDirectory: orgMemoryAdmission.directory,
604
615
  sharedMemoryStore: sharedMemoryStore ? sharedMemoryStore : undefined,
605
616
  sessionMirrorRuling: principalCaps
@@ -633,6 +644,7 @@ async function main() {
633
644
  modelReady: () => configCenter.modelReady(),
634
645
  scenarioDetails,
635
646
  workflowsCapable,
647
+ workflowModelAllowlist: workflowModelAllowlistSnapshot,
636
648
  staticWiring,
637
649
  memoryPosture,
638
650
  instanceId,
@@ -665,13 +677,14 @@ async function main() {
665
677
  config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver,
666
678
  getCenterPrompts: () => configCenter.getCenterPrompts(),
667
679
  getKeyResolver: () => configCenter.getKeyResolver(),
668
- taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv,
680
+ taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner,
669
681
  hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec,
670
682
  selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore,
671
683
  singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage,
672
684
  sessionEnvSelection,
673
685
  lockedKeys: governanceSeams.lockedKeys,
674
686
  liveQuestionFace: question,
687
+ approverSeat: createRunnerDepsOnAsk(toolApproval),
675
688
  });
676
689
  const runnerFor = (spec) => pickHandsRunner(handsLanes.laneOf(spec), { full: runner, handsless: handslessRunner });
677
690
  const sideQuery = createSideQueryLane({ brain, config, getKeyResolver: () => configCenter.getKeyResolver() });
@@ -785,6 +798,7 @@ async function main() {
785
798
  version: serviceVersion(),
786
799
  runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState,
787
800
  storeLiveProbe, configCenter,
801
+ deviceHub: deviceLane?.hub,
788
802
  retentionLane: retentionLaneStarted,
789
803
  releaseRetentionLease: retentionLaneStore !== undefined ? () => retentionLaneStore.release(instanceId) : undefined,
790
804
  });
@@ -29,7 +29,7 @@
29
29
  * HTTP 面诚实 501。
30
30
  *
31
31
  * 🔴 首版把这件事当成**运行期**事实(照常挂载,让 core 每次抛 `memory.export_incomplete` / 422),
32
- * 被 codex 对抗复审驳倒(#277 提货批 [medium],验真后修):`typeof backend.exportSnapshotOf === "function"`
32
+ * 被 codex 交叉复审驳倒(#277 提货批 [medium],验真后修):`typeof backend.exportSnapshotOf === "function"`
33
33
  * 是一次**属性检查**,boot 期就判得出;而且它是**永久**缺席(换后端才会变),不是瞬时状态。照旧挂着
34
34
  * 就成了「能力位说 yes、每一次调用都确定性失败」——本仓「says yes ⟺ route works」是结构性承诺,
35
35
  * 一个恒假的 yes 会让壳给用户开出一条走不通的迁移流程。
@@ -23,7 +23,7 @@
23
23
  * Runner 每任务现构的引擎会看到**两个不同的控制面**。
24
24
  *
25
25
  * ⇒ 裁:`controlPlaneRoot` 缺席 ⇒ 本工厂返回 **undefined**,两口整个不挂载,HTTP 面诚实 501。今天满足这一
26
- * 条的只有 core 自带的 `FileMemoryEngineBackend`(它把控制面钉死在 config-root 一侧);本仓两只 SQL 记忆
26
+ * 条的只有 core 自带的 `FileMemoryEngineBackend`(它把控制面写死在 config-root 一侧);本仓两只 SQL 记忆
27
27
  * 孪生(`plugins/memory-engine-{pg,tidb}.ts`)没有这个面 ⇒ 在那些部署上两口不挂。SQL 控制面是 core 的候升件
28
28
  * (design/316 §0b-4 咨询单),**绝不**在本层为它们现造一个「够用的」控制面 —— 那是把一个 core 拒绝生产的
29
29
  * 半吊子治理面自己造出来,与 bundle 那条「a governance-less export is the laundering shape」同族。
@@ -36,7 +36,7 @@
36
36
  *
37
37
  * 判据是一次**属性检查**、boot 期就判得出、而且是**永久**缺席(换后端才会变)——不是瞬时状态。照旧挂着
38
38
  * 就成了「能力位说 yes、每一次调用都确定性失败」,而本仓「says yes ⟺ route works」是结构性承诺。
39
- * (这条纪律是 #264 提货批被 codex 对抗复审驳倒后立的,见 memory-bundle-engine.ts 头注。)
39
+ * (这条纪律是 #264 提货批被 codex 交叉复审驳倒后立的,见 memory-bundle-engine.ts 头注。)
40
40
  *
41
41
  * ⚠️ **刻意不把审计/托管/证据三个可选面编进构造判定**:`committedSnapshotOf` / `custodyOf` 缺席时 core
42
42
  * 明写要诚实答 `binding:"unknown"` / `contentState:"capability-absent"` / `custody:"capability-absent"`
@@ -70,7 +70,7 @@ export interface MemoryComplianceFaces {
70
70
  }) => Promise<MemoryErasureAttestation>;
71
71
  }
72
72
  /**
73
- * **选择子替身**的拦截谓词(codex 对抗复审 [critical],亲验复现后修 —— 复现器与结论逐条在
73
+ * **选择子替身**的拦截谓词(codex 交叉复审 [critical],亲验复现后修 —— 复现器与结论逐条在
74
74
  * `test/memory-compliance-http.test.ts` 的同名 describe 与 `test/memory-operator-faces.test.ts` 里)。
75
75
  *
76
76
  * ## 病(在 core 5.57.0 上实测,不是纸面推演)
@@ -82,7 +82,7 @@ export interface MemoryComplianceFaces {
82
82
  * `__proto__` 那一下改的是克隆体的**原型**,于是这个键从 `Object.keys` 里消失;
83
83
  * · core 的 `erasureRequestInvalid` 数的是 `Object.keys` ⇒ 只看见一个 `scope` ⇒ **判合法**;
84
84
  * · 而两条执行腿的分派都写作 `"ids" in select`(engine.js:477 降级腿、file-backend.js:2614 证据腿),
85
- * `in` **查原型链** ⇒ 继承来的 `ids` 赢,删的是攻击者点名的那批 id;
85
+ * `in` **查原型链** ⇒ 继承来的 `ids` 赢,删的是不当调用方点名的那批 id;
86
86
  * · 收执里的 `select` / `selectHash` 记的却是那个自有的 `scope`。
87
87
  * ⇒ **删掉的东西与凭据上写的不是同一批**,而这一口的全部意义就是那份可归档的凭据;删除不可回滚。
88
88
  *
@@ -94,7 +94,7 @@ export interface MemoryComplianceFaces {
94
94
  * 本仓禁止的形,而且悄悄改写调用方的请求会让它以为自己删的是别的东西)。
95
95
  *
96
96
  * 两个执法点共用**这一只**谓词:路由拿它出 400(wire 可见、有钉),seam 拿它 fail-closed 兜底(seam 是
97
- * 可导出模块,不能靠「今天只有一个调用方」这句话来安全;走到那一臂 = 路由的门被绕过了 = 装配 bug,
97
+ * 可导出模块,不能靠「今天只有一个调用方」这句话来安全;走到那一臂 = 路由的门被跳过了 = 装配 bug,
98
98
  * 500 fail-loud 是对的)。刻意不做「只在 seam 拦」:那样 wire 上就没有一句能测的诚实拒。
99
99
  *
100
100
  * 走法是**迭代**而非递归:体的深度只受体积上限约束,一个深嵌套的 JSON 能把递归版打爆栈(把一个
@@ -0,0 +1,50 @@
1
+ /**
2
+ * design/383 S-6 / S-50 §三(S-49 F3)—— 带 `origin` 标的记忆条目在 wire 读口上的 **fence-or-refuse**。
3
+ *
4
+ * ## 病(S-6 禁形)
5
+ * 记忆条目的 `origin` 标(design/336 provenance 标记族)是 additive 键。旧 SDK / 旧壳的 strict/strip parse 会
6
+ * **静默剥掉**它 ⇒ 一条「外源已标」的条目摊平成无标文本,消费端再把它当洁净内容引用/回写 = 出处被洗掉。
7
+ * core 无法在 wire 上强制(它不认识客户端),383 §3.4 显式授权 server 裁三点,S-50 §三裁定如下。
8
+ *
9
+ * ## 三点裁定(逐字)
10
+ * 1. **判定点 = 端点级能力位,不猜客户端版本**:server 无 per-client 版本可见性。凡投影带标条目的读口,
11
+ * **缺省不投带标条目**;消费方以查询参数 `originAware=1` 声明「我认识 origin 键族」后才放行。参数缺席 =
12
+ * 老客户端假定成立 ⇒ fence。
13
+ * 2. **粒度 = 整条目扣留,不做字段降级**:字段级(剥 origin 只给文本)恰是被禁的摊平。整条目扣留 + 计数披露
14
+ * (`refusedUntrusted` 计数拒的既有先例形):响应带 `withheldOriginTagged: <n>`(additive,**只在 n>0 时铸**——
15
+ * 只记真),消费端机读得出「有 n 条因我没声明能力而被围栏」。
16
+ * 3. **能力位形 = 每请求查询参数,不是每客户端注册**:注册制引入一张会陈旧的表;查询参数让「谁认识键族」由
17
+ * 当次调用自证,零状态。SDK 新版缺省带 `originAware=1`;旧 SDK 不带 ⇒ 结构性 fence。
18
+ *
19
+ * ## 判「带标」的谓词
20
+ * 条目对象上 **own** `origin` 键且值非 `undefined`。不读 `origin` 的内部形(那是 core 的开放判别式),只判在场。
21
+ *
22
+ * ## 射程(S-50 §三.1 点名的读口,本仓现状三处)
23
+ * · `GET /v1/memory/export?scope=`(memory-policy.ts,属主自助条目读,分页/全量两腿)
24
+ * · `GET /v1/memory/origin/external?scopes=`(memory-origin.ts,外源标记人面 —— 它的每条都带标,缺省下
25
+ * 全部扣留 + 计数:operator 的工具必须声明能力位才看得见,这正是「围栏」的意思,不是 bug)
26
+ * · `POST /v1/memory/export`(memory-bundle.ts,治理携出包 —— 包经 SDK 落盘再 import,旧 SDK 剥标后 import
27
+ * 会把无标条目当洁净内容收进另一台 = 跨库洗出处;所以包也围栏,计数键落在包顶层)
28
+ * 不围栏的:`GET /v1/memory/entries/:id/provenance`(单条出处账,消费它的人就是来读 origin 的,且响应根不是
29
+ * 条目)、`GET /v1/memory/origin/clearances`(`buildClearanceRowWire` 深白名单投影,已是显式形)。
30
+ *
31
+ * ## 读面行为收窄 = L3(S-50 §三.3 成文)
32
+ * 存量消费者若依赖读到带标条目(现状 = origin 键族尚未在这些读口上稳定出现;真实射程 = 蒸馏产物到货后的新条目),
33
+ * 从本版起须带 `originAware=1`。判据入 383 片3 判据面。
34
+ */
35
+ /** 查询参数名与合法值(闭集:只认字面 `1`;`true`/`yes` 不认 —— 能力位不做宽松解析)。 */
36
+ export declare const ORIGIN_AWARE_QUERY = "originAware";
37
+ /** 从 `URLSearchParams` 读能力位:`originAware=1` ⇒ true;缺席/其它值 ⇒ false(老客户端假定)。 */
38
+ export declare function readOriginAware(params: URLSearchParams): boolean;
39
+ /** 「这条条目带 origin 标吗」—— own 键在场且非 undefined。纯谓词,不读 origin 的形。 */
40
+ export declare function isOriginTagged(entry: unknown): boolean;
41
+ /**
42
+ * 围栏(`build*`:纯数据)。`originAware=true` ⇒ 原数组同一引用、无计数键(零行为差);否则扣留带标条目,
43
+ * 且**只在扣留数 >0 时**铸 `withheldOriginTagged`(只记真:一个恒在场的 `0` 会让消费端把「没围栏」误读成
44
+ * 「围栏了但没扣」)。
45
+ */
46
+ export declare function buildOriginFence<T>(entries: readonly T[], originAware: boolean): {
47
+ entries: readonly T[];
48
+ withheldOriginTagged?: number;
49
+ };
50
+ //# sourceMappingURL=memory-origin-fence.d.ts.map
@@ -0,0 +1,15 @@
1
+ export const ORIGIN_AWARE_QUERY = "originAware";
2
+ export function readOriginAware(params) {
3
+ return params.get(ORIGIN_AWARE_QUERY) === "1";
4
+ }
5
+ export function isOriginTagged(entry) {
6
+ return typeof entry === "object" && entry !== null && Object.prototype.hasOwnProperty.call(entry, "origin") && entry.origin !== undefined;
7
+ }
8
+ export function buildOriginFence(entries, originAware) {
9
+ if (originAware)
10
+ return { entries };
11
+ const kept = entries.filter((e) => !isOriginTagged(e));
12
+ const withheld = entries.length - kept.length;
13
+ return withheld > 0 ? { entries: kept, withheldOriginTagged: withheld } : { entries: kept };
14
+ }
15
+ //# sourceMappingURL=memory-origin-fence.js.map
@@ -74,10 +74,22 @@ export interface MemoryPosture {
74
74
  * 怎么处置」,是运维诊断料,刻意不进 `GET /v1/capabilities` 的窄投影。
75
75
  */
76
76
  provenance: "off" | "carry" | null;
77
+ /**
78
+ * design/383 §3.1(core 7.0.0 片3,S-49 S-1b):本部署的 **capture 姿态**(`MEMORY_CAPTURE_POLICY`)——
79
+ * per-principal `allowMemoryOptOut` verdict 被读进的那个框(`open` fail-safe / `governed` fail-closed /
80
+ * `capture-required` 全拒)。
81
+ *
82
+ * 🔴 `null` = **本部署没设这个键** ⇒ 引擎按它自己的缺省跑。**不是**「等于 open」——
83
+ * 与 `provenance` / `delegationEvidence` 同一条诚实纪律:本读面不替 core 宣布它的缺省。
84
+ *
85
+ * 受众 = **operator 面独占**(同上两位):它回答的是「这台机器怎么裁 opt-out 声明」,是运维诊断料,
86
+ * 刻意不进 `GET /v1/capabilities` 的窄投影。
87
+ */
88
+ capturePolicy: "open" | "governed" | "capture-required" | null;
77
89
  }
78
90
  /** 推导入参 = 装配现场的真事实(不在本函数里重算任何一件)。 */
79
91
  export interface MemoryPostureFacts {
80
- config: Pick<ServiceConfig, "memoryEngineEnabled" | "memoryEngineBackend" | "requirePrincipal" | "memoryEmbedder" | "memoryDelegationEvidence" | "memoryProvenance">;
92
+ config: Pick<ServiceConfig, "memoryEngineEnabled" | "memoryEngineBackend" | "requirePrincipal" | "memoryEmbedder" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy">;
81
93
  /** 引擎这一腿真的接上了吗(`memoryEngine !== undefined`)—— 装配结果。 */
82
94
  engineWired: boolean;
83
95
  /** 两支 SQL 后端从**实例 getter** 读到的档位;file 腿传 `undefined`(无此面)。 */
@@ -20,6 +20,7 @@ export function buildMemoryPosture(facts) {
20
20
  embedder: embedder !== undefined ? { model: embedder.model, dimensions: embedder.dimensions } : null,
21
21
  delegationEvidence: config.memoryDelegationEvidence ?? null,
22
22
  provenance: config.memoryProvenance ?? null,
23
+ capturePolicy: config.memoryCapturePolicy ?? null,
23
24
  };
24
25
  }
25
26
  export function projectMemoryEngineCapability(posture) {
@@ -104,10 +104,15 @@ export declare function memorySpecForRequest(scope: string | undefined, memoryWr
104
104
  * core legacy 语义,零迁移)。 */
105
105
  originPolicy?: {
106
106
  multiTenant: boolean;
107
- }): {
107
+ },
108
+ /** design/383 §2.1(S-49):请求的 `memoryCapture:"off"` 声明 ⇒ `TaskSpec.memory.capture:"off"`(单成员闭集,
109
+ * 拼写门在 prepareSpec 的 400 门 + core 的 `config.memory_capture_spelling`)。缺席 ⇒ 键不铸(缺席就是照常采集
110
+ * 的唯一形)。与 `memoryWrite` 正交:那是「本 run 只读平面」,这是「本会话永远不进长期记忆」。 */
111
+ memoryCapture?: "off"): {
108
112
  scopes: string[];
109
113
  writeScope?: string | null;
110
114
  scopeContract?: "v2";
111
115
  scopeOrigins?: Record<string, "deployment" | "request">;
116
+ capture?: "off";
112
117
  } | undefined;
113
118
  //# sourceMappingURL=memory-scope.d.ts.map
@@ -69,7 +69,7 @@ export function memoryEngineBackendFor(config, fallbackRoot) {
69
69
  const dir = join(root, "memory");
70
70
  return { backend: new FileMemoryEngineBackend(dir, { controlDir: deriveControlPlaneDir(root, dir) }), root };
71
71
  }
72
- export function memorySpecForRequest(scope, memoryWrite, defaultScopes, originPolicy) {
72
+ export function memorySpecForRequest(scope, memoryWrite, defaultScopes, originPolicy, memoryCapture) {
73
73
  if (!scope)
74
74
  return undefined;
75
75
  const isV2 = isV2ScopeKey;
@@ -86,6 +86,7 @@ export function memorySpecForRequest(scope, memoryWrite, defaultScopes, originPo
86
86
  }
87
87
  }
88
88
  const origins = Object.keys(stamped).length > 0 ? { scopeOrigins: stamped } : {};
89
- return memoryWrite === false ? { ...layered, writeScope: null, ...v2, ...origins } : { ...layered, ...v2, ...origins };
89
+ const capture = memoryCapture === "off" ? { capture: "off" } : {};
90
+ return memoryWrite === false ? { ...layered, writeScope: null, ...v2, ...origins, ...capture } : { ...layered, ...v2, ...origins, ...capture };
90
91
  }
91
92
  //# sourceMappingURL=memory-scope.js.map
@@ -42,7 +42,7 @@ export interface ModelNotAllowed {
42
42
  * #233 / A-002.8 —— **归一条款**:membership 判定前把 ref 归一到**终端 catalog 条目**再比。
43
43
  *
44
44
  * 为什么不能拿字符串直接跟名单比:`expandTiers` 的增广目录里,档位词(`pro`)与 CC 别名(`sonnet`)
45
- * 的值与 `config.models[<name>]` 是**同一个对象引用**——裸串比较会让 `@pro` 绕过一张按 name 写的名单
45
+ * 的值与 `config.models[<name>]` 是**同一个对象引用**——裸串比较会让 `@pro` 跳过一张按 name 写的名单
46
46
  * (同一只模型,两个键)。所以两边(ref 与每个名单条目)都先经 {@link matchCatalogModel} 解到目录条目,
47
47
  * 再按 **引用相等** 判同。id 形的 ref 不需要特别处理:`matchCatalogModel` 本身就是 name+id 双键索引,
48
48
  * 它会把 id 解成目录键,引用相等接着就对上了。
@@ -73,7 +73,7 @@ export declare function terminalCatalogName(ref: string, catalog: Record<string,
73
73
  /**
74
74
  * [865]② name+id 双键目录索引:返回 ref 命中的目录 **name 键**(spec.model 的规范形),未知 → undefined。
75
75
  * - name 命中用 Object.hasOwn(🔴 不用真值索引:Object.prototype 继承键 "constructor"/"__proto__" 等在裸
76
- * 索引下是真值,会绕过 allow-list——沿袭原实现的纪律)。
76
+ * 索引下是真值,会跳过 allow-list——沿袭原实现的纪律)。
77
77
  * - id 命中扫目录值(`Model.id`),跳过 "default" 别名键(它与其指向的真身共享 id,解析到别名会让 /model
78
78
  * 面板与实际路由再度撕裂);同 id 多 name 时取目录序第一个(与 /v1/models 列表序一致,确定性)。
79
79
  */
@@ -9,9 +9,13 @@ export interface FailOpenTagEntry {
9
9
  }
10
10
  /** 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。 */
11
11
  export declare const FAIL_OPEN_TAGS: {
12
+ readonly "server.memory-optout-grant.onfault-hook-threw": {
13
+ readonly cls: "F";
14
+ readonly note: "design/383 S-1(applyMemoryOptOutGrant):本地 grant 腿故障时的**披露钩子**(onFault → warn 日志)自己抛了。钩子只是观测面:吞掉它保住的是「resolver 不因一行日志失败而 reject」(那会把一条腿的故障扩成整车故障 —— core 的 resolver-throw 降级臂作废同一次解析里 center 已答好的 allowWorkflows/allowFork);代价是**那一行披露丢了**,而 `allowMemoryOptOut` 键缺席这件事本身不受影响(fail-closed/fail-safe 极性由 core 按姿态判)。本计数让「披露钩子一直在炸」显形。";
15
+ };
12
16
  readonly "server.run-store.file-hydrate-unreadable": {
13
17
  readonly cls: "P-DEBT";
14
- readonly note: "件6(#43 案 [4281]):`FileRunStore.hydrate()` 的 `readdirSync(runs/)` 失败 ⇒ 引擎以**空账**起动。旧形是空 catch + return:data root 不可读(权限/挂载丢失/路径被换走)时零告警,此后每一次 getRun 恒 miss —— 而「恒 miss」正是 #43 现场看到的形,一次环境故障被伪装成「本来就没有这条 run」。方向问题在于吞掉之后被读成的是「证不出有行 ⇒ 没有行」,两句话不等价。**不拒启是刻意保留的行为**(拒启是行为变更,另立),所以这条留作债:响亮化(error 级启动日志带 runs 绝对路径+异常文案)+ 本计数,让它在遥测里显形而不是只活在一行日志里。终局=给 file 后端一条可选的 fail-closed 启动门。";
18
+ readonly note: "件6(#43 案 [4281]):`FileRunStore.hydrate()` 的 `readdirSync(runs/)` 失败 ⇒ 引擎以**空账**起动。旧形是空 catch + return:data root 不可读(权限/挂载丢失/路径被换走)时零告警,此后每一次 getRun 恒 miss —— 而「恒 miss」正是 #43 现场看到的形,一次环境故障被伪装成「本来就没有这条 run」。方向问题在于吞掉之后被读成的是「证不出有行 ⇒ 没有行」,两句话不等价。**缺省不拒启是刻意保留的行为**,所以这条留作债:响亮化(error 级启动日志带 runs 绝对路径+异常文案)+ 本计数,让它在遥测里显形而不是只活在一行日志里。⚠️ 2026-08-29(S-46 件B)起终局格已兑现:部署可用 `RUN_STORE_STRICT_HYDRATE=on` 把这一格换成 **fail-closed 启动门**(拒启,遗言同样带绝对路径+异常文案);本计数因此只在**缺省 off** 档上产生 —— 读遥测时别把「计数为零」直接读成「没有部署踩到这条路」,也可能是它们开了拒启门、根本起不来。";
15
19
  };
16
20
  readonly "server.fork-routing.placed-probe-host-error": {
17
21
  readonly cls: "F";
@@ -129,10 +133,6 @@ export declare const FAIL_OPEN_TAGS: {
129
133
  readonly cls: "F";
130
134
  readonly note: "[4913]:durable park 投影腿在 strip 之前拿结果里的 checkpointToken 换 `pendingAction.toolCallId`(cs.get 一次读),那次读**抛了**(store 抖动 / 滚动升级期 checkpoint 格式 version guard / 行已被并发消费)⇒ 本次 park 的 wire 投影**缺 `toolCallId` 键**,其余键(gate/checkpointId)照旧。放行的最坏后果 = 消费方(cli 判别子 v3 写者②)退回该键到货前的行为——同族多兄弟 durable park 分不出主角、诚实全不标(cli 侧 S3 局限,成文的旧现状),纯展示/关联面,不参与任何门/CAS/resume 判定,故 F 类。绝不挡终局路径是承重的:park 投影点全在 done/suspended 事件写链上,让它抛会把一次 store 抖动升级成整条 run 终局写失败。必须留痕:键缺席与「tool-less park 本就无键」([1995]② OMITTED 契约)在 wire 上同形,不计数则「投影为什么总缺」在遥测里永不显形。";
131
135
  };
132
- readonly "server.model-key.degrade-hop-borrows-gateway-key": {
133
- readonly cls: "P-DEBT";
134
- readonly note: "[4998] BROKEN 实锤的**留痕半场**(#345 codex 对抗复审 R1-[high] 的可采纳部分,亲验落实)。`brain.ts` 的反应式降级 hop 在降级目标**没有自己的凭据**时回落主网关 key(`own ?? config.gatewayApiKey`),而那只 fallback brain 是用 `fallbackModel.baseUrl` 当 `config.baseUrl` 造的 —— 于是 core 5.57.0 的配对裁决器看到的是「部署凭据 + 它自己的声明根」⇒ 判 `paired` ⇒ 照发。🔴 **上游的法在这条路上结构性看不见问题**:不是 core 漏判,是**本仓把一个撒谎的 config 递给了它**(把主网关的 key 与降级目标的根说成一对)。真读数:`test/brain.test.ts` 的「既有腿不回归」格实测 `Bearer primary-gateway-key` 打到 `http://degrade-gw.test/v1/chat/completions`。为什么记债而不是本批修:正解要么是「off-root 且无自 key ⇒ 降级链按不可用后退」(改的是**可用性**轴,会让一类既有部署的降级当场失效),要么是「把目标自己的凭据改走 per-model 位(`Model.headers`)让 core 判得见」(改核心装配 + 既有钉字节),两者都是独立设计件、不是提货批的尾巴。判据 = 解不出目标自己的 key ∧ 目标 baseUrl 既不是 `gatewayBaseUrl` 也不在 `gatewayFallbackUrls`(后者按同文件成文契约 = 同一逻辑网关/同一 key 有效域,不算换家),归一用 core 的 `normalizeBaseUrl`(与本仓其它配对判定同一只)。⚠️ 与已撤线的 `off-route-model-uses-gateway-key` **不是同一条路**:那条的铸点是 side-query/WebFetch 的 per-model auth 座,而座位恒以**被请求的那只**模型被调用、位置在装饰器栈**之外** —— 它从来没有、也不可能观察到降级 hop 换上的 `fallbackModel`。所以这不是「撤早了」,是一条**一直没人看**的姊妹路。";
135
- };
136
136
  readonly "server.exec-lane.ssh-host-key-unverified": {
137
137
  readonly cls: "P-DEBT";
138
138
  readonly note: "#296(A-052.2,clay 裁定 C-R21):`REMOTE_EXEC=ssh` 且**两只主机密钥旋钮都缺席**(`SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS`)⇒ 本次连接接受**任何**主机密钥,等价 `ssh -o StrictHostKeyChecking=no`。放行的最坏后果:一次中间人拿到这条会话的**全部命令、全部产物、以及 agent 在那台机器上的全部动作**(私钥仍在控制面手里、拿不走,design/61 §5 #2)。⇒ 保护型欠账,不是体验回退。**方向是裁定的、不是遗漏**:本 lane 的既有部署形是「AI 编排批量部署到一批 SSH 可达的机器」,那些机器的指纹通常不在任何 known_hosts 里,默认拒连 = 一次静默的可用性断裂;C-R21 裁「在场即严格,缺席即响亮」。补偿三件:①**每连接**一条 `ssh_host_key_unverified` warn(不是每进程一次 —— 一条长期裸连的 lane 必须在每条连接上都看得见);②本计数(遥测里「有多少条连接是裸连」是一个可观测的数,曲线应当只降不升);③两旋钮任一在场即**严格校验、不符拒连**(拒因带两半指纹 + `ssh-keyscan` 指路)。终局 = 部署把旋钮配上(那时本计数归零);旋钮不配而想收紧的部署级开关(「无旋钮即拒启」)是下一件,不在 C-R21 射程。";
@@ -141,6 +141,18 @@ export declare const FAIL_OPEN_TAGS: {
141
141
  readonly cls: "F";
142
142
  readonly note: "#343(黑板 [5100] Q3):park 墓碑表(#329 迟到回决的 wire approvalId → 店内坐标指路条)**全局帽已满、而本 principal 还没到自己的 per-principal 子帽** ⇒ 这条新墓碑**不落**(拒新,一格都不挤别人)。放行的最坏后果 = 这一只 ask 的 **live 腿指路便利**缺席,壳按 Yes 拿到的是 404 `unknown_or_other_replica` —— 也就是 #329 之前的那一格逐字行为,既不会错兑也不会双兑;**完整兑付面一个字节没动**(park 行本体在 durable 店未失,`GET /v1/approvals` 列表 + `/v1/approvals/:sessionId/decide` 仍可回决),故 F 类而不是保护型。为什么方向是「拒新」而不是修前的「FIFO 挤最旧」:那个形让**甲的用量决定乙的可用性**(已鉴权租户量产 park 终局 ask 即可把别家墓碑全部挤出,merge-rescan 744 confirmed),而 park 是常态路由、不需要任何异常手法。必须留痕:「表被别人打满」与「这张卡本来就没有墓碑」在 wire 上完全同形,不计数则运维只能靠「用户说按 Yes 老是 404」这种间接症状发现,而那正是 [4845] 排障代价实证过的那条路。detail 带 owner(判「压力来自谁」的第一手线索)。本 tag 有**两条臂、两个根因**,detail 判别:(a) 全局帽满且本 principal 未超子帽 ⇒ 拒新(detail 带 owner);(b) 本 principal 超子帽但名下条目**全在飞**、无一可逐 ⇒ 拒新(detail 带 `reason=all-inflight`)。超子帽且**有**可逐条目的常态臂逐自己最旧的一条(量产者自伤,设计内有界表纪律)才不走本 tag。";
143
143
  };
144
+ readonly "server.approvals.stale-echoed-token-unreadable": {
145
+ readonly cls: "F";
146
+ readonly note: "S-02 codex R1-[medium](验真后修):decide 的 409 `approval_stale` 臂对**调用方回显的旧 token** 做归属判定(`cs.get(binding.checkpointToken)`),那次读**抛了**(store 抖动 / 版本守卫 / 行恰被清理)。修前这里是静默 `catch(()=>null)`,「读不出」被折成「确认不在库」——叠上 S-02 的 404 收口后,一次 store 抖动就会把合法旧 token 的 409+terminal 谎报成 404「没有这条审批」。修后:unreadable ⇒ **保持 409**(`terminal` 不披露——证不出归属就不说,与 2026-07-26 纪律同向;404 折叠只在读成功且未命中时走,探测方无法自控 store 故障 ⇒ 谕示封闭不破)。放行的最坏后果 = 该次拒体 `terminal` 诚实缺席(`currentPending` 由它自己的读口与属主复核另判,不受本臂牵连),壳退回重拉列表,judgment 面 fail-safe,故 F 类。留痕理由:这次读是 terminal 披露与 404 折叠两个判定的输入,长期抖动必须在遥测显形。";
147
+ };
148
+ readonly "server.approvals.stale-current-pending-unreadable": {
149
+ readonly cls: "F";
150
+ readonly note: "S-02 件2([5522]④→[5525]):409 `approval_stale` 拒体铸 additive 指路键 `currentPending`(本会话当前 pending 的 D-1 坐标,壳一跳重定位)时,当前 pending 行的 `cs.get(token)` 读**抛了**(store 抖动 / 滚动升级期 checkpoint 格式 version guard / 行恰被并发消费)⇒ 本次拒体**缺 `currentPending` 键**,409 本体与 `terminal` 判定照旧。放行的最坏后果 = 壳退回该键到货前的行为(重拉 GET /v1/approvals 自行重定位),纯指路/便利面,不参与任何门/CAS/resume 判定,故 F 类(同族先例:`server.park.toolcallid-read-failed`,同为 cs.get 投影读失败臂)。必须留痕:键缺席与「此刻确实没有 pending」在 wire 上同形,不计数则「指路为什么总缺」在遥测里永不显形。";
151
+ };
152
+ readonly "server.file-history.retention-trim-failed": {
153
+ readonly cls: "F";
154
+ readonly note: "S-15 twin 保留条款(core 7.0.0 design/381 §片2 的 SQL 孪生):boundary 提交后的自剪 pass(core 共享选择函数 `fileHistoryBoundariesToKeep` + 既有 `reap` 级联)失败 ⇒ 本轮 boundary **已提交**、rewind 座照常,只是这一轮没剪。放行的是「GC 失败不失败用户这一轮」(core InMemory/File 参照同形 `reap(...).catch(() => {})`,但参照是无声吞,本仓走留痕三件套);最坏后果 = 该 scope 暂时超上限一个 boundary,下一次提交的 pass 按**当前全集**重算、连它一起剪(不累积欠账)。detail 带 scope / entry / 异常文案。";
155
+ };
144
156
  };
145
157
  /** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
146
158
  export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
@@ -1,8 +1,12 @@
1
1
  import { appendFileSync } from "node:fs";
2
2
  export const FAIL_OPEN_TAGS = {
3
+ "server.memory-optout-grant.onfault-hook-threw": {
4
+ cls: "F",
5
+ note: "design/383 S-1(applyMemoryOptOutGrant):本地 grant 腿故障时的**披露钩子**(onFault → warn 日志)自己抛了。钩子只是观测面:吞掉它保住的是「resolver 不因一行日志失败而 reject」(那会把一条腿的故障扩成整车故障 —— core 的 resolver-throw 降级臂作废同一次解析里 center 已答好的 allowWorkflows/allowFork);代价是**那一行披露丢了**,而 `allowMemoryOptOut` 键缺席这件事本身不受影响(fail-closed/fail-safe 极性由 core 按姿态判)。本计数让「披露钩子一直在炸」显形。",
6
+ },
3
7
  "server.run-store.file-hydrate-unreadable": {
4
8
  cls: "P-DEBT",
5
- note: "件6(#43 案 [4281]):`FileRunStore.hydrate()` 的 `readdirSync(runs/)` 失败 ⇒ 引擎以**空账**起动。旧形是空 catch + return:data root 不可读(权限/挂载丢失/路径被换走)时零告警,此后每一次 getRun 恒 miss —— 而「恒 miss」正是 #43 现场看到的形,一次环境故障被伪装成「本来就没有这条 run」。方向问题在于吞掉之后被读成的是「证不出有行 ⇒ 没有行」,两句话不等价。**不拒启是刻意保留的行为**(拒启是行为变更,另立),所以这条留作债:响亮化(error 级启动日志带 runs 绝对路径+异常文案)+ 本计数,让它在遥测里显形而不是只活在一行日志里。终局=给 file 后端一条可选的 fail-closed 启动门。",
9
+ note: "件6(#43 案 [4281]):`FileRunStore.hydrate()` 的 `readdirSync(runs/)` 失败 ⇒ 引擎以**空账**起动。旧形是空 catch + return:data root 不可读(权限/挂载丢失/路径被换走)时零告警,此后每一次 getRun 恒 miss —— 而「恒 miss」正是 #43 现场看到的形,一次环境故障被伪装成「本来就没有这条 run」。方向问题在于吞掉之后被读成的是「证不出有行 ⇒ 没有行」,两句话不等价。**缺省不拒启是刻意保留的行为**,所以这条留作债:响亮化(error 级启动日志带 runs 绝对路径+异常文案)+ 本计数,让它在遥测里显形而不是只活在一行日志里。⚠️ 2026-08-29(S-46 件B)起终局格已兑现:部署可用 `RUN_STORE_STRICT_HYDRATE=on` 把这一格换成 **fail-closed 启动门**(拒启,遗言同样带绝对路径+异常文案);本计数因此只在**缺省 off** 档上产生 —— 读遥测时别把「计数为零」直接读成「没有部署踩到这条路」,也可能是它们开了拒启门、根本起不来。",
6
10
  },
7
11
  "server.fork-routing.placed-probe-host-error": {
8
12
  cls: "F",
@@ -120,10 +124,6 @@ export const FAIL_OPEN_TAGS = {
120
124
  cls: "F",
121
125
  note: "[4913]:durable park 投影腿在 strip 之前拿结果里的 checkpointToken 换 `pendingAction.toolCallId`(cs.get 一次读),那次读**抛了**(store 抖动 / 滚动升级期 checkpoint 格式 version guard / 行已被并发消费)⇒ 本次 park 的 wire 投影**缺 `toolCallId` 键**,其余键(gate/checkpointId)照旧。放行的最坏后果 = 消费方(cli 判别子 v3 写者②)退回该键到货前的行为——同族多兄弟 durable park 分不出主角、诚实全不标(cli 侧 S3 局限,成文的旧现状),纯展示/关联面,不参与任何门/CAS/resume 判定,故 F 类。绝不挡终局路径是承重的:park 投影点全在 done/suspended 事件写链上,让它抛会把一次 store 抖动升级成整条 run 终局写失败。必须留痕:键缺席与「tool-less park 本就无键」([1995]② OMITTED 契约)在 wire 上同形,不计数则「投影为什么总缺」在遥测里永不显形。",
122
126
  },
123
- "server.model-key.degrade-hop-borrows-gateway-key": {
124
- cls: "P-DEBT",
125
- note: "[4998] BROKEN 实锤的**留痕半场**(#345 codex 对抗复审 R1-[high] 的可采纳部分,亲验落实)。`brain.ts` 的反应式降级 hop 在降级目标**没有自己的凭据**时回落主网关 key(`own ?? config.gatewayApiKey`),而那只 fallback brain 是用 `fallbackModel.baseUrl` 当 `config.baseUrl` 造的 —— 于是 core 5.57.0 的配对裁决器看到的是「部署凭据 + 它自己的声明根」⇒ 判 `paired` ⇒ 照发。🔴 **上游的法在这条路上结构性看不见问题**:不是 core 漏判,是**本仓把一个撒谎的 config 递给了它**(把主网关的 key 与降级目标的根说成一对)。真读数:`test/brain.test.ts` 的「既有腿不回归」格实测 `Bearer primary-gateway-key` 打到 `http://degrade-gw.test/v1/chat/completions`。为什么记债而不是本批修:正解要么是「off-root 且无自 key ⇒ 降级链按不可用后退」(改的是**可用性**轴,会让一类既有部署的降级当场失效),要么是「把目标自己的凭据改走 per-model 位(`Model.headers`)让 core 判得见」(改核心装配 + 既有钉字节),两者都是独立设计件、不是提货批的尾巴。判据 = 解不出目标自己的 key ∧ 目标 baseUrl 既不是 `gatewayBaseUrl` 也不在 `gatewayFallbackUrls`(后者按同文件成文契约 = 同一逻辑网关/同一 key 有效域,不算换家),归一用 core 的 `normalizeBaseUrl`(与本仓其它配对判定同一只)。⚠️ 与已撤线的 `off-route-model-uses-gateway-key` **不是同一条路**:那条的铸点是 side-query/WebFetch 的 per-model auth 座,而座位恒以**被请求的那只**模型被调用、位置在装饰器栈**之外** —— 它从来没有、也不可能观察到降级 hop 换上的 `fallbackModel`。所以这不是「撤早了」,是一条**一直没人看**的姊妹路。",
126
- },
127
127
  "server.exec-lane.ssh-host-key-unverified": {
128
128
  cls: "P-DEBT",
129
129
  note: "#296(A-052.2,clay 裁定 C-R21):`REMOTE_EXEC=ssh` 且**两只主机密钥旋钮都缺席**(`SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS`)⇒ 本次连接接受**任何**主机密钥,等价 `ssh -o StrictHostKeyChecking=no`。放行的最坏后果:一次中间人拿到这条会话的**全部命令、全部产物、以及 agent 在那台机器上的全部动作**(私钥仍在控制面手里、拿不走,design/61 §5 #2)。⇒ 保护型欠账,不是体验回退。**方向是裁定的、不是遗漏**:本 lane 的既有部署形是「AI 编排批量部署到一批 SSH 可达的机器」,那些机器的指纹通常不在任何 known_hosts 里,默认拒连 = 一次静默的可用性断裂;C-R21 裁「在场即严格,缺席即响亮」。补偿三件:①**每连接**一条 `ssh_host_key_unverified` warn(不是每进程一次 —— 一条长期裸连的 lane 必须在每条连接上都看得见);②本计数(遥测里「有多少条连接是裸连」是一个可观测的数,曲线应当只降不升);③两旋钮任一在场即**严格校验、不符拒连**(拒因带两半指纹 + `ssh-keyscan` 指路)。终局 = 部署把旋钮配上(那时本计数归零);旋钮不配而想收紧的部署级开关(「无旋钮即拒启」)是下一件,不在 C-R21 射程。",
@@ -132,6 +132,18 @@ export const FAIL_OPEN_TAGS = {
132
132
  cls: "F",
133
133
  note: "#343(黑板 [5100] Q3):park 墓碑表(#329 迟到回决的 wire approvalId → 店内坐标指路条)**全局帽已满、而本 principal 还没到自己的 per-principal 子帽** ⇒ 这条新墓碑**不落**(拒新,一格都不挤别人)。放行的最坏后果 = 这一只 ask 的 **live 腿指路便利**缺席,壳按 Yes 拿到的是 404 `unknown_or_other_replica` —— 也就是 #329 之前的那一格逐字行为,既不会错兑也不会双兑;**完整兑付面一个字节没动**(park 行本体在 durable 店未失,`GET /v1/approvals` 列表 + `/v1/approvals/:sessionId/decide` 仍可回决),故 F 类而不是保护型。为什么方向是「拒新」而不是修前的「FIFO 挤最旧」:那个形让**甲的用量决定乙的可用性**(已鉴权租户量产 park 终局 ask 即可把别家墓碑全部挤出,merge-rescan 744 confirmed),而 park 是常态路由、不需要任何异常手法。必须留痕:「表被别人打满」与「这张卡本来就没有墓碑」在 wire 上完全同形,不计数则运维只能靠「用户说按 Yes 老是 404」这种间接症状发现,而那正是 [4845] 排障代价实证过的那条路。detail 带 owner(判「压力来自谁」的第一手线索)。本 tag 有**两条臂、两个根因**,detail 判别:(a) 全局帽满且本 principal 未超子帽 ⇒ 拒新(detail 带 owner);(b) 本 principal 超子帽但名下条目**全在飞**、无一可逐 ⇒ 拒新(detail 带 `reason=all-inflight`)。超子帽且**有**可逐条目的常态臂逐自己最旧的一条(量产者自伤,设计内有界表纪律)才不走本 tag。",
134
134
  },
135
+ "server.approvals.stale-echoed-token-unreadable": {
136
+ cls: "F",
137
+ note: "S-02 codex R1-[medium](验真后修):decide 的 409 `approval_stale` 臂对**调用方回显的旧 token** 做归属判定(`cs.get(binding.checkpointToken)`),那次读**抛了**(store 抖动 / 版本守卫 / 行恰被清理)。修前这里是静默 `catch(()=>null)`,「读不出」被折成「确认不在库」——叠上 S-02 的 404 收口后,一次 store 抖动就会把合法旧 token 的 409+terminal 谎报成 404「没有这条审批」。修后:unreadable ⇒ **保持 409**(`terminal` 不披露——证不出归属就不说,与 2026-07-26 纪律同向;404 折叠只在读成功且未命中时走,探测方无法自控 store 故障 ⇒ 谕示封闭不破)。放行的最坏后果 = 该次拒体 `terminal` 诚实缺席(`currentPending` 由它自己的读口与属主复核另判,不受本臂牵连),壳退回重拉列表,judgment 面 fail-safe,故 F 类。留痕理由:这次读是 terminal 披露与 404 折叠两个判定的输入,长期抖动必须在遥测显形。",
138
+ },
139
+ "server.approvals.stale-current-pending-unreadable": {
140
+ cls: "F",
141
+ note: "S-02 件2([5522]④→[5525]):409 `approval_stale` 拒体铸 additive 指路键 `currentPending`(本会话当前 pending 的 D-1 坐标,壳一跳重定位)时,当前 pending 行的 `cs.get(token)` 读**抛了**(store 抖动 / 滚动升级期 checkpoint 格式 version guard / 行恰被并发消费)⇒ 本次拒体**缺 `currentPending` 键**,409 本体与 `terminal` 判定照旧。放行的最坏后果 = 壳退回该键到货前的行为(重拉 GET /v1/approvals 自行重定位),纯指路/便利面,不参与任何门/CAS/resume 判定,故 F 类(同族先例:`server.park.toolcallid-read-failed`,同为 cs.get 投影读失败臂)。必须留痕:键缺席与「此刻确实没有 pending」在 wire 上同形,不计数则「指路为什么总缺」在遥测里永不显形。",
142
+ },
143
+ "server.file-history.retention-trim-failed": {
144
+ cls: "F",
145
+ note: "S-15 twin 保留条款(core 7.0.0 design/381 §片2 的 SQL 孪生):boundary 提交后的自剪 pass(core 共享选择函数 `fileHistoryBoundariesToKeep` + 既有 `reap` 级联)失败 ⇒ 本轮 boundary **已提交**、rewind 座照常,只是这一轮没剪。放行的是「GC 失败不失败用户这一轮」(core InMemory/File 参照同形 `reap(...).catch(() => {})`,但参照是无声吞,本仓走留痕三件套);最坏后果 = 该 scope 暂时超上限一个 boundary,下一次提交的 pass 按**当前全集**重算、连它一起剪(不累积欠账)。detail 带 scope / entry / 异常文案。",
146
+ },
135
147
  };
136
148
  export function failOpenTagForDroppedFrame(frameType) {
137
149
  switch (frameType) {
@@ -22,12 +22,12 @@ import { type WorkflowScriptRunner, type WorkflowPrimitives } from "@sema-agent/
22
22
  * 4. ERRORS from host fns are marshalled to a CONTEXT-LOCAL `Error` (the beyond-battery leak core's
23
23
  * battery misses).
24
24
  * 5. Data crossing INTO a host fn (bridged args) and OUT to the host caller (the result + meta) is
25
- * sterilized of the `__proto__`/`constructor`/`prototype` tampering gadget ({@link scrubProtoDeep}) —
25
+ * sterilized of the `__proto__`/`constructor`/`prototype` rewriting gadget ({@link scrubProtoDeep}) —
26
26
  * the marshal-bridge boundary-crossing case — and marshalled to plain data (never a live cross-realm object
27
27
  * carrying getter/toJSON/then traps).
28
28
  *
29
29
  * 🔴 SCOPE (v1, clay 2026-06-23): this is BOUNDARY isolation (no host-code readout / unintended host execution / cross-tenant credential
30
- * reach / host prototype tampering) — VALIDATED. It is NOT full RESOURCE isolation: an in-process `vm`
30
+ * reach / host prototype rewriting) — VALIDATED. It is NOT full RESOURCE isolation: an in-process `vm`
31
31
  * cannot hard-cap heap, so a runaway-allocation script can OOM the shared process (a resource-exhaustion issue, NOT a data-boundary issue). v1
32
32
  * mitigates with caps (maxAgents + a wall-clock timeout + a per-sync-chunk vm timeout) + monitoring. For HARD
33
33
  * heap/CPU isolation use {@link import("./hardened-vm-worker-runner.js").createWorkerHardenedVmRunner} — the
@@ -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;
@@ -77,7 +77,7 @@ export declare const SERVED_FENCE_MS: number;
77
77
  * 只吐一条(entry 的存储 status 列仍归一,那是账本的双终态列,不动;RAW status 本就随 payload extras
78
78
  * 透传)。这与 {@link taskNotificationStreamKey} 的 RAW 语义对齐(stream 侧早已保 RAW)。
79
79
  *
80
- * 编码=防碰撞元组:域/taskId/status 走 JSON 数组(调用方自由串伪造不了分隔符);`#seq` 尾折在 JSON 之外
80
+ * 编码=防碰撞元组:域/taskId/status 走 JSON 数组(调用方自由串自造不了分隔符);`#seq` 尾折在 JSON 之外
81
81
  * ——JSON 段恒以 `"]` 结尾,`#digits` 尾无歧义,于是 {@link foldKeyFamily} 的剥尾语义 AND SQL 双生子
82
82
  * (workflow-run-store-sql 的 `regexp_replace(run_id, '#[0-9]+$', '')` 家族比对)原样成立——把 seq 折进
83
83
  * JSON 元组会让 SQL 侧家族 ack/fence 永 miss(那是另一批的真库改动面,本修有意不碰)。
@@ -284,7 +284,7 @@ priority?: SystemInjectionPriority): WorkflowCompletionInboxEntry;
284
284
  * review, 1.78) — which under the old rule silently killed the multi-tenant push for every sync-leg workflow;
285
285
  * 2. the payload's `principal` — core threads `spec.principal`, which in THIS service is ALWAYS the VERIFIED
286
286
  * identity (the 1.55 F-fix invariant, main.ts: gated door = BFF-verified header, direct door = JWT `sub`,
287
- * never the spoofable header; core 1.187 hardened the `TaskSpec.principal` contract to exactly this).
287
+ * never the self-asserted header; core 1.187 hardened the `TaskSpec.principal` contract to exactly this).
288
288
  * Captured at SPAWN time → immutable, no delete-then-reclaim window. This is also the same value core's own
289
289
  * workflow scope guard (`canAccessWorkflowRun`) keys off;
290
290
  * 3. neither → single-user stamps `owner: null` (no tenancy boundary); multi-tenant (`requirePrincipal`)
@@ -6,10 +6,10 @@
6
6
  * adapter (E2B, kata/k8s, …) must enforce identically, so a transport author only writes the 4 transport
7
7
  * primitives and cannot accidentally re-implement (or skip) an invariant:
8
8
  *
9
- * - **opaque shellId** — a fresh `bg_<n>_<uuid>` brand, NEVER derived from the provider's job/pid (越权红线,
9
+ * - **opaque shellId** — a fresh `bg_<n>_<uuid>` brand, NEVER derived from the provider's job/pid (超范围红线,
10
10
  * design/103 §3.8). The provider id lives only inside the driver state.
11
11
  * - **owned-id isolation** — `poll`/`kill` look the shellId up in THIS manager's registry; an id from another env
12
- * (or a forged one) is `not_found`, never resolved against the provider (design/103 §3.8 / interface 越权契约).
12
+ * (or a forged one) is `not_found`, never resolved against the provider (design/103 §3.8 / interface 超范围契约).
13
13
  * - **maxConcurrent** — counts only `running` shells (terminal entries are kept for residual polling but free their
14
14
  * slot, so "TaskStop one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
15
15
  * - **timeout hard wall** — a control-plane timer kills the shell and flips it to `killed` at the bounded BG