humanish 0.104.3 → 0.106.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 (1407) hide show
  1. package/AGENTS.md +89 -94
  2. package/ARCHITECTURE.md +146 -0
  3. package/CHANGELOG.md +502 -0
  4. package/CONTEXT.md +51 -0
  5. package/CONTRIBUTING.md +111 -9
  6. package/README.md +213 -124
  7. package/SECURITY.md +1 -1
  8. package/TELEMETRY.md +112 -0
  9. package/dist/actors/affordance.d.ts +56 -0
  10. package/dist/actors/affordance.js +155 -0
  11. package/dist/actors/agent-session.js +42 -0
  12. package/dist/actors/codex/app-server-actor-trace.d.ts +11 -0
  13. package/dist/actors/codex/app-server-actor-trace.js +211 -0
  14. package/dist/actors/codex/app-server-client.d.ts +39 -0
  15. package/dist/actors/codex/app-server-client.js +116 -0
  16. package/dist/actors/codex/app-server-trace.d.ts +154 -0
  17. package/dist/actors/codex/app-server-trace.js +424 -0
  18. package/dist/actors/codex/app-server-ui.d.ts +40 -0
  19. package/dist/actors/codex/app-server-ui.js +328 -0
  20. package/dist/actors/codex/app-server.d.ts +44 -0
  21. package/dist/actors/codex/app-server.js +268 -0
  22. package/dist/actors/codex/qualified-versions.d.ts +45 -0
  23. package/dist/actors/codex/qualified-versions.js +87 -0
  24. package/dist/actors/codex/restricted-executable.d.ts +17 -0
  25. package/dist/actors/codex/restricted-executable.js +147 -0
  26. package/dist/actors/codex/restricted-launch.d.ts +116 -0
  27. package/dist/actors/codex/restricted-launch.js +222 -0
  28. package/dist/actors/codex/restricted-participant-policy.d.ts +46 -0
  29. package/dist/actors/codex/restricted-participant-policy.js +82 -0
  30. package/dist/actors/codex/restricted-participant.d.ts +20 -0
  31. package/dist/actors/codex/restricted-participant.js +432 -0
  32. package/dist/actors/codex/restricted-policy.d.ts +70 -0
  33. package/dist/actors/codex/restricted-policy.js +326 -0
  34. package/dist/actors/codex/restricted-session.d.ts +65 -0
  35. package/dist/actors/codex/restricted-session.js +432 -0
  36. package/dist/actors/codex/restricted-transport.d.ts +73 -0
  37. package/dist/actors/codex/restricted-transport.js +335 -0
  38. package/dist/actors/codex/restricted-turn.d.ts +62 -0
  39. package/dist/actors/codex/restricted-turn.js +284 -0
  40. package/dist/actors/computer-use/actor.d.ts +77 -0
  41. package/dist/actors/computer-use/actor.js +78 -0
  42. package/dist/actors/computer-use/admission-limit.d.ts +12 -0
  43. package/dist/actors/computer-use/admission-limit.js +19 -0
  44. package/dist/actors/computer-use/executor-error.js +48 -0
  45. package/dist/actors/computer-use/loop/actions.d.ts +26 -0
  46. package/dist/actors/computer-use/loop/actions.js +245 -0
  47. package/dist/actors/computer-use/loop/backstop.d.ts +42 -0
  48. package/dist/actors/computer-use/loop/backstop.js +202 -0
  49. package/dist/actors/computer-use/loop/debrief.d.ts +19 -0
  50. package/dist/actors/computer-use/loop/debrief.js +164 -0
  51. package/dist/actors/computer-use/loop/ending.d.ts +86 -0
  52. package/dist/actors/computer-use/loop/ending.js +316 -0
  53. package/dist/actors/computer-use/loop/observation.d.ts +55 -0
  54. package/dist/actors/computer-use/loop/observation.js +246 -0
  55. package/dist/actors/computer-use/loop/output-limit.d.ts +11 -0
  56. package/dist/actors/computer-use/loop/output-limit.js +21 -0
  57. package/dist/actors/computer-use/loop/provider-call.d.ts +19 -0
  58. package/dist/actors/computer-use/loop/provider-call.js +208 -0
  59. package/dist/actors/computer-use/loop/race.d.ts +34 -0
  60. package/dist/actors/computer-use/loop/race.js +79 -0
  61. package/dist/actors/computer-use/loop/session.d.ts +77 -0
  62. package/dist/actors/computer-use/loop/session.js +135 -0
  63. package/dist/actors/computer-use/loop/spend.d.ts +30 -0
  64. package/dist/actors/computer-use/loop/spend.js +85 -0
  65. package/dist/actors/computer-use/loop/trace.d.ts +35 -0
  66. package/dist/actors/computer-use/loop/trace.js +140 -0
  67. package/dist/actors/computer-use/loop/types.d.ts +404 -0
  68. package/dist/actors/computer-use/loop/types.js +1 -0
  69. package/dist/actors/computer-use/loop/usage.d.ts +102 -0
  70. package/dist/actors/computer-use/loop/usage.js +355 -0
  71. package/dist/actors/computer-use/loop.d.ts +11 -0
  72. package/dist/actors/computer-use/loop.js +248 -0
  73. package/dist/actors/computer-use/openai-provider.d.ts +107 -0
  74. package/dist/actors/computer-use/openai-provider.js +472 -0
  75. package/dist/actors/computer-use/openai-requests.d.ts +35 -0
  76. package/dist/actors/computer-use/openai-requests.js +74 -0
  77. package/dist/actors/computer-use/openai-wire.d.ts +67 -0
  78. package/dist/actors/computer-use/openai-wire.js +365 -0
  79. package/dist/actors/computer-use/provider-error.d.ts +27 -0
  80. package/dist/actors/computer-use/provider-error.js +86 -0
  81. package/dist/actors/computer-use/speech.js +6 -0
  82. package/dist/actors/contract.d.ts +315 -0
  83. package/dist/actors/contract.js +201 -0
  84. package/dist/actors/goal-source.js +38 -0
  85. package/dist/actors/local-agent/claude-session.d.ts +43 -0
  86. package/dist/actors/local-agent/claude-session.js +370 -0
  87. package/dist/actors/local-agent/cli.d.ts +115 -0
  88. package/dist/actors/local-agent/cli.js +413 -0
  89. package/dist/actors/local-agent/readiness.d.ts +31 -0
  90. package/dist/actors/local-agent/readiness.js +45 -0
  91. package/dist/actors/openai-endpoint.d.ts +2 -0
  92. package/dist/actors/openai-endpoint.js +2 -0
  93. package/dist/actors/output-token-limit.js +4 -0
  94. package/dist/actors/reasoning-effort.js +33 -0
  95. package/dist/actors/registry.d.ts +59 -0
  96. package/dist/actors/registry.js +89 -0
  97. package/dist/actors/scripted-browser/actor.d.ts +49 -0
  98. package/dist/actors/scripted-browser/actor.js +428 -0
  99. package/dist/actors/scripted-browser/browser-command.d.ts +1 -0
  100. package/dist/actors/scripted-browser/browser-command.js +48 -0
  101. package/dist/actors/scripted-browser/journey.d.ts +9 -0
  102. package/dist/actors/scripted-browser/journey.js +171 -0
  103. package/dist/actors/scripted-browser/steps.d.ts +60 -0
  104. package/dist/actors/scripted-browser/steps.js +384 -0
  105. package/dist/actors/scripted-browser/types.d.ts +135 -0
  106. package/dist/actors/scripted-browser/types.js +26 -0
  107. package/dist/actors/stop-cause.d.ts +9 -0
  108. package/dist/actors/stop-cause.js +42 -0
  109. package/dist/actors/stop-conditions.d.ts +40 -0
  110. package/dist/actors/stop-conditions.js +65 -0
  111. package/dist/analysis/automatic-completion.d.ts +38 -0
  112. package/dist/analysis/automatic-completion.js +90 -0
  113. package/dist/analysis/automatic-config.d.ts +41 -0
  114. package/dist/analysis/automatic-config.js +135 -0
  115. package/dist/analysis/automatic.d.ts +17 -0
  116. package/dist/analysis/automatic.js +279 -0
  117. package/dist/analysis/codex-config.d.ts +8 -0
  118. package/dist/analysis/codex-config.js +69 -0
  119. package/dist/analysis/evidence-sources.d.ts +23 -0
  120. package/dist/analysis/evidence-sources.js +251 -0
  121. package/dist/analysis/evidence.d.ts +18 -0
  122. package/dist/analysis/evidence.js +482 -0
  123. package/dist/analysis/execute.d.ts +52 -0
  124. package/dist/analysis/execute.js +504 -0
  125. package/dist/analysis/image-tokens.d.ts +1 -0
  126. package/dist/analysis/image-tokens.js +41 -0
  127. package/dist/analysis/job.d.ts +97 -0
  128. package/dist/analysis/job.js +311 -0
  129. package/dist/analysis/load.d.ts +3 -0
  130. package/dist/analysis/load.js +11 -0
  131. package/dist/analysis/provider.d.ts +57 -0
  132. package/dist/analysis/provider.js +227 -0
  133. package/dist/analysis/restricted-codex.d.ts +19 -0
  134. package/dist/analysis/restricted-codex.js +23 -0
  135. package/dist/analysis/service.d.ts +72 -0
  136. package/dist/analysis/service.js +399 -0
  137. package/dist/analysis/sharing.d.ts +11 -0
  138. package/dist/analysis/sharing.js +44 -0
  139. package/dist/analysis/store-executions.d.ts +27 -0
  140. package/dist/analysis/store-executions.js +224 -0
  141. package/dist/analysis/store.d.ts +31 -0
  142. package/dist/analysis/store.js +315 -0
  143. package/dist/analysis/types.d.ts +217 -0
  144. package/dist/analysis/types.js +12 -0
  145. package/dist/analysis/validation.d.ts +225 -0
  146. package/dist/analysis/validation.js +562 -0
  147. package/dist/browser-control/client.d.ts +18 -0
  148. package/dist/browser-control/client.js +239 -0
  149. package/dist/browser-control/dispatcher.d.ts +19 -0
  150. package/dist/browser-control/dispatcher.js +221 -0
  151. package/dist/browser-control/protocol.d.ts +492 -0
  152. package/dist/browser-control/protocol.js +360 -0
  153. package/dist/browser-control/recording-transfer.d.ts +3 -0
  154. package/dist/browser-control/recording-transfer.js +48 -0
  155. package/dist/browser-control/transport.d.ts +27 -0
  156. package/dist/browser-control/transport.js +189 -0
  157. package/dist/cli/commands/analysis-signals.d.ts +7 -0
  158. package/dist/cli/commands/analysis-signals.js +25 -0
  159. package/dist/cli/commands/analyze.d.ts +3 -0
  160. package/dist/cli/commands/analyze.js +212 -0
  161. package/dist/cli/commands/codex.d.ts +3 -0
  162. package/dist/cli/commands/codex.js +145 -0
  163. package/dist/cli/commands/comms.d.ts +3 -0
  164. package/dist/cli/commands/comms.js +225 -0
  165. package/dist/cli/commands/feedback.d.ts +3 -0
  166. package/dist/cli/commands/feedback.js +138 -0
  167. package/dist/cli/commands/lab-format.d.ts +8 -0
  168. package/dist/cli/commands/lab-format.js +105 -0
  169. package/dist/cli/commands/lab-route-computer-use.d.ts +20 -0
  170. package/dist/cli/commands/lab-route-computer-use.js +251 -0
  171. package/dist/cli/commands/lab-route-open.d.ts +27 -0
  172. package/dist/cli/commands/lab-route-open.js +37 -0
  173. package/dist/cli/commands/lab-route-preview.d.ts +20 -0
  174. package/dist/cli/commands/lab-route-preview.js +74 -0
  175. package/dist/cli/commands/lab-route-run.d.ts +17 -0
  176. package/dist/cli/commands/lab-route-run.js +31 -0
  177. package/dist/cli/commands/lab-route-scripted.d.ts +19 -0
  178. package/dist/cli/commands/lab-route-scripted.js +50 -0
  179. package/dist/cli/commands/lab-route-shared-world.d.ts +20 -0
  180. package/dist/cli/commands/lab-route-shared-world.js +135 -0
  181. package/dist/cli/commands/lab-route-terminal.d.ts +19 -0
  182. package/dist/cli/commands/lab-route-terminal.js +49 -0
  183. package/dist/cli/commands/lab-run.d.ts +9 -0
  184. package/dist/cli/commands/lab-run.js +166 -0
  185. package/dist/cli/commands/lab-scorer.d.ts +30 -0
  186. package/dist/cli/commands/lab-scorer.js +17 -0
  187. package/dist/cli/commands/lab.d.ts +3 -0
  188. package/dist/cli/commands/lab.js +193 -0
  189. package/dist/cli/commands/observe.d.ts +4 -0
  190. package/dist/cli/commands/observe.js +345 -0
  191. package/dist/cli/commands/runs.d.ts +11 -0
  192. package/dist/cli/commands/runs.js +314 -0
  193. package/dist/cli/commands/setup.d.ts +11 -0
  194. package/dist/cli/commands/setup.js +325 -0
  195. package/dist/cli/commands/tui.d.ts +32 -0
  196. package/dist/cli/commands/tui.js +305 -0
  197. package/dist/cli/commands/watch.d.ts +3 -0
  198. package/dist/cli/commands/watch.js +245 -0
  199. package/dist/cli/doctor.d.ts +39 -0
  200. package/dist/cli/doctor.js +263 -0
  201. package/dist/cli/first-run-path.d.ts +67 -0
  202. package/dist/cli/first-run-path.js +150 -0
  203. package/dist/cli/io.d.ts +85 -0
  204. package/dist/cli/io.js +232 -0
  205. package/dist/cli/observer-follow.d.ts +56 -0
  206. package/dist/cli/observer-follow.js +160 -0
  207. package/dist/cli/orientation.d.ts +30 -0
  208. package/dist/cli/orientation.js +114 -0
  209. package/dist/cli/program.d.ts +15 -0
  210. package/dist/cli/program.js +338 -0
  211. package/dist/cli/secret-prompt.js +39 -0
  212. package/dist/cli/telemetry.js +351 -0
  213. package/dist/cli.js +2 -4
  214. package/dist/comms/agentmail.d.ts +29 -0
  215. package/dist/comms/agentmail.js +568 -0
  216. package/dist/comms/capture-surface.d.ts +54 -0
  217. package/dist/comms/capture-surface.js +468 -0
  218. package/dist/comms/catch-host.d.ts +52 -0
  219. package/dist/comms/catch-host.js +192 -0
  220. package/dist/comms/connections.d.ts +20 -0
  221. package/dist/comms/connections.js +116 -0
  222. package/dist/comms/email-catch.d.ts +49 -0
  223. package/dist/comms/email-catch.js +236 -0
  224. package/dist/comms/extract.d.ts +7 -0
  225. package/dist/comms/extract.js +65 -0
  226. package/dist/comms/fake-inbox.d.ts +39 -0
  227. package/dist/comms/fake-inbox.js +136 -0
  228. package/dist/comms/images.d.ts +7 -0
  229. package/dist/comms/images.js +53 -0
  230. package/dist/comms/lease-store.d.ts +91 -0
  231. package/dist/comms/lease-store.js +600 -0
  232. package/dist/comms/providers.d.ts +30 -0
  233. package/dist/comms/providers.js +24 -0
  234. package/dist/comms/receiving-common.d.ts +17 -0
  235. package/dist/comms/receiving-common.js +82 -0
  236. package/dist/comms/receiving-evidence.d.ts +4 -0
  237. package/dist/comms/receiving-evidence.js +59 -0
  238. package/dist/comms/receiving-recovery.d.ts +22 -0
  239. package/dist/comms/receiving-recovery.js +100 -0
  240. package/dist/comms/receiving-render.d.ts +16 -0
  241. package/dist/comms/receiving-render.js +409 -0
  242. package/dist/comms/receiving-runtime.d.ts +41 -0
  243. package/dist/comms/receiving-runtime.js +66 -0
  244. package/dist/comms/receiving-surface.d.ts +8 -0
  245. package/dist/comms/receiving-surface.js +156 -0
  246. package/dist/comms/receiving-types.d.ts +122 -0
  247. package/dist/comms/receiving-types.js +3 -0
  248. package/dist/comms/receiving.d.ts +33 -0
  249. package/dist/comms/receiving.js +508 -0
  250. package/dist/comms/sandbox-catch-script.d.ts +16 -0
  251. package/dist/comms/sandbox-catch-script.js +331 -0
  252. package/dist/comms/sandbox-catch.d.ts +214 -0
  253. package/dist/comms/sandbox-catch.js +451 -0
  254. package/dist/comms/setup.d.ts +65 -0
  255. package/dist/comms/setup.js +259 -0
  256. package/dist/comms/thread-evidence.d.ts +28 -0
  257. package/dist/comms/thread-evidence.js +30 -0
  258. package/dist/comms/types.d.ts +76 -0
  259. package/dist/comms/types.js +8 -0
  260. package/dist/deprecated.d.ts +90 -0
  261. package/dist/deprecated.js +106 -0
  262. package/dist/evidence/browser-hygiene.js +42 -0
  263. package/dist/evidence/desktop-recorder.d.ts +39 -0
  264. package/dist/evidence/desktop-recorder.js +340 -0
  265. package/dist/evidence/desktop-recording-artifact.d.ts +5 -0
  266. package/dist/evidence/desktop-recording-artifact.js +53 -0
  267. package/dist/evidence/desktop-recording-types.d.ts +27 -0
  268. package/dist/evidence/desktop-recording-types.js +16 -0
  269. package/dist/evidence/encoded-text.d.ts +27 -0
  270. package/dist/evidence/encoded-text.js +243 -0
  271. package/dist/evidence/frame-signature.d.ts +17 -0
  272. package/dist/evidence/frame-signature.js +111 -0
  273. package/dist/evidence/image.d.ts +24 -0
  274. package/dist/evidence/image.js +170 -0
  275. package/dist/evidence/plain-text.d.ts +8 -0
  276. package/dist/evidence/plain-text.js +16 -0
  277. package/dist/evidence/redaction.d.ts +110 -0
  278. package/dist/evidence/redaction.js +329 -0
  279. package/dist/feedback/draft.d.ts +58 -0
  280. package/dist/feedback/draft.js +309 -0
  281. package/dist/feedback/export-bundle.d.ts +6 -0
  282. package/dist/feedback/export-bundle.js +585 -0
  283. package/dist/feedback/export.d.ts +50 -0
  284. package/dist/feedback/export.js +392 -0
  285. package/dist/feedback/feedback.d.ts +37 -0
  286. package/dist/feedback/feedback.js +282 -0
  287. package/dist/feedback/proof.d.ts +8 -0
  288. package/dist/feedback/proof.js +56 -0
  289. package/dist/guest/bootstrap.d.ts +55 -0
  290. package/dist/guest/bootstrap.js +348 -0
  291. package/dist/guest/browser-tools.d.ts +8 -0
  292. package/dist/guest/browser-tools.js +72 -0
  293. package/dist/guest/chromium-text.d.ts +62 -0
  294. package/dist/guest/chromium-text.js +386 -0
  295. package/dist/guest/desktop-executor.d.ts +57 -0
  296. package/dist/guest/desktop-executor.js +256 -0
  297. package/dist/guest/desktop-keys.d.ts +8 -0
  298. package/dist/guest/desktop-keys.js +89 -0
  299. package/dist/guest/desktop-media.d.ts +70 -0
  300. package/dist/guest/desktop-media.js +385 -0
  301. package/dist/guest/desktop-native.d.ts +14 -0
  302. package/dist/guest/desktop-native.js +156 -0
  303. package/dist/guest/media-config.d.ts +15 -0
  304. package/dist/guest/media-config.js +16 -0
  305. package/dist/guest/runtime-desktop.d.ts +45 -0
  306. package/dist/guest/runtime-desktop.js +420 -0
  307. package/dist/guest/runtime-processes.d.ts +28 -0
  308. package/dist/guest/runtime-processes.js +94 -0
  309. package/dist/guest/runtime.d.ts +31 -0
  310. package/dist/guest/runtime.js +124 -0
  311. package/dist/guest-media-worker.js +184 -48
  312. package/dist/guest-runtime-main.js +23 -10
  313. package/dist/guest-runtime-revision.js +1 -1
  314. package/dist/index.d.ts +32 -83
  315. package/dist/index.js +21 -42
  316. package/dist/keys/env-file.d.ts +15 -0
  317. package/dist/keys/env-file.js +109 -0
  318. package/dist/keys/key-resolution.d.ts +58 -0
  319. package/dist/keys/key-resolution.js +389 -0
  320. package/dist/lab/adapter-extension.d.ts +75 -0
  321. package/dist/lab/adapter-extension.js +253 -0
  322. package/dist/lab/adapter-scorer-loader.d.ts +43 -0
  323. package/dist/lab/adapter-scorer-loader.js +130 -0
  324. package/dist/lab/bag-overrides.d.ts +18 -0
  325. package/dist/lab/bag-overrides.js +121 -0
  326. package/dist/lab/composition-rules.d.ts +5 -0
  327. package/dist/lab/composition-rules.js +283 -0
  328. package/dist/lab/config.d.ts +7 -0
  329. package/dist/lab/config.js +208 -0
  330. package/dist/lab/device-presets.d.ts +97 -0
  331. package/dist/lab/device-presets.js +87 -0
  332. package/dist/lab/discover.d.ts +65 -0
  333. package/dist/lab/discover.js +422 -0
  334. package/dist/lab/doctor.d.ts +39 -0
  335. package/dist/lab/doctor.js +233 -0
  336. package/dist/lab/init-paths.d.ts +5 -0
  337. package/dist/lab/init-paths.js +89 -0
  338. package/dist/lab/init-plan.d.ts +20 -0
  339. package/dist/lab/init-plan.js +163 -0
  340. package/dist/lab/init-templates.d.ts +28 -0
  341. package/dist/lab/init-templates.js +416 -0
  342. package/dist/lab/init.d.ts +39 -0
  343. package/dist/lab/init.js +287 -0
  344. package/dist/lab/keys.d.ts +2 -0
  345. package/dist/lab/keys.js +264 -0
  346. package/dist/lab/parse/actors.d.ts +14 -0
  347. package/dist/lab/parse/actors.js +453 -0
  348. package/dist/lab/parse/comms.d.ts +12 -0
  349. package/dist/lab/parse/comms.js +246 -0
  350. package/dist/lab/parse/execution.d.ts +19 -0
  351. package/dist/lab/parse/execution.js +356 -0
  352. package/dist/lab/parse/subject-state.d.ts +30 -0
  353. package/dist/lab/parse/subject-state.js +179 -0
  354. package/dist/lab/parse/subject.d.ts +9 -0
  355. package/dist/lab/parse/subject.js +420 -0
  356. package/dist/lab/parse/values.d.ts +17 -0
  357. package/dist/lab/parse/values.js +65 -0
  358. package/dist/lab/participant-assignment.d.ts +12 -0
  359. package/dist/lab/participant-assignment.js +19 -0
  360. package/dist/lab/persona-resolve.d.ts +39 -0
  361. package/dist/lab/persona-resolve.js +118 -0
  362. package/dist/lab/persona.d.ts +39 -0
  363. package/dist/lab/persona.js +192 -0
  364. package/dist/lab/plan-base.d.ts +24 -0
  365. package/dist/lab/plan-base.js +113 -0
  366. package/dist/lab/plan-participants.d.ts +81 -0
  367. package/dist/lab/plan-participants.js +92 -0
  368. package/dist/lab/plan-types.d.ts +269 -0
  369. package/dist/lab/plan-types.js +5 -0
  370. package/dist/lab/plan.d.ts +27 -0
  371. package/dist/lab/plan.js +156 -0
  372. package/dist/lab/preflight-probes.d.ts +3 -0
  373. package/dist/lab/preflight-probes.js +312 -0
  374. package/dist/lab/preflight-result.d.ts +7 -0
  375. package/dist/lab/preflight-result.js +43 -0
  376. package/dist/lab/preflight.d.ts +95 -0
  377. package/dist/lab/preflight.js +117 -0
  378. package/dist/lab/route-inputs.d.ts +17 -0
  379. package/dist/lab/route-inputs.js +95 -0
  380. package/dist/lab/routing.d.ts +81 -0
  381. package/dist/lab/routing.js +214 -0
  382. package/dist/lab/run-lab-events.d.ts +69 -0
  383. package/dist/lab/run-lab-events.js +95 -0
  384. package/dist/lab/run-lab-options.d.ts +82 -0
  385. package/dist/lab/run-lab-options.js +399 -0
  386. package/dist/lab/summary.d.ts +60 -0
  387. package/dist/lab/summary.js +168 -0
  388. package/dist/lab/tasks.d.ts +91 -0
  389. package/dist/lab/tasks.js +139 -0
  390. package/dist/lab/types.d.ts +813 -0
  391. package/dist/lab/types.js +6 -0
  392. package/dist/lab/validation.d.ts +67 -0
  393. package/dist/lab/validation.js +387 -0
  394. package/dist/lab/warnings.d.ts +2 -0
  395. package/dist/lab/warnings.js +338 -0
  396. package/dist/observer/artifact.d.ts +23 -0
  397. package/dist/observer/artifact.js +235 -0
  398. package/dist/observer/data.d.ts +115 -0
  399. package/dist/observer/data.js +258 -0
  400. package/dist/observer/exposure.d.ts +64 -0
  401. package/dist/observer/exposure.js +143 -0
  402. package/dist/observer/http.js +50 -0
  403. package/dist/observer/library.js +190 -0
  404. package/dist/observer/listen.js +85 -0
  405. package/dist/observer/live.d.ts +9 -0
  406. package/dist/observer/live.js +25 -0
  407. package/dist/observer/pinned-files.d.ts +30 -0
  408. package/dist/observer/pinned-files.js +160 -0
  409. package/dist/observer/render.d.ts +69 -0
  410. package/dist/observer/render.js +350 -0
  411. package/dist/observer/run-inventory.d.ts +35 -0
  412. package/dist/observer/run-inventory.js +95 -0
  413. package/dist/observer/run-routes.d.ts +37 -0
  414. package/dist/observer/run-routes.js +373 -0
  415. package/dist/observer/serve.d.ts +97 -0
  416. package/dist/observer/serve.js +305 -0
  417. package/dist/observer/static.js +369 -0
  418. package/dist/observer/tunnel.js +112 -0
  419. package/dist/observer-app.html +9 -20
  420. package/dist/routes/computer-use/bundle-parts.d.ts +50 -0
  421. package/dist/routes/computer-use/bundle-parts.js +177 -0
  422. package/dist/routes/computer-use/bundle.d.ts +56 -0
  423. package/dist/routes/computer-use/bundle.js +184 -0
  424. package/dist/routes/computer-use/diagnostics.d.ts +38 -0
  425. package/dist/routes/computer-use/diagnostics.js +94 -0
  426. package/dist/routes/computer-use/e2b-desktop/comms.d.ts +65 -0
  427. package/dist/routes/computer-use/e2b-desktop/comms.js +225 -0
  428. package/dist/routes/computer-use/e2b-desktop/desktop.d.ts +3 -0
  429. package/dist/routes/computer-use/e2b-desktop/desktop.js +91 -0
  430. package/dist/routes/computer-use/e2b-desktop/fidelity.d.ts +65 -0
  431. package/dist/routes/computer-use/e2b-desktop/fidelity.js +156 -0
  432. package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +18 -0
  433. package/dist/routes/computer-use/e2b-desktop/prepare.js +198 -0
  434. package/dist/routes/computer-use/e2b-desktop/start.d.ts +14 -0
  435. package/dist/routes/computer-use/e2b-desktop/start.js +193 -0
  436. package/dist/routes/computer-use/e2b-desktop/state.d.ts +65 -0
  437. package/dist/routes/computer-use/e2b-desktop/state.js +69 -0
  438. package/dist/routes/computer-use/e2b-desktop/teardown.d.ts +6 -0
  439. package/dist/routes/computer-use/e2b-desktop/teardown.js +152 -0
  440. package/dist/routes/computer-use/external-comms.d.ts +20 -0
  441. package/dist/routes/computer-use/external-comms.js +55 -0
  442. package/dist/routes/computer-use/fanout-bundle.d.ts +11 -0
  443. package/dist/routes/computer-use/fanout-bundle.js +274 -0
  444. package/dist/routes/computer-use/fanout-lanes.d.ts +15 -0
  445. package/dist/routes/computer-use/fanout-lanes.js +302 -0
  446. package/dist/routes/computer-use/in-process-desktop.d.ts +17 -0
  447. package/dist/routes/computer-use/in-process-desktop.js +40 -0
  448. package/dist/routes/computer-use/lanes.d.ts +35 -0
  449. package/dist/routes/computer-use/lanes.js +321 -0
  450. package/dist/routes/computer-use/legacy-lane-spec.d.ts +62 -0
  451. package/dist/routes/computer-use/legacy-lane-spec.js +39 -0
  452. package/dist/routes/computer-use/live-flush.d.ts +42 -0
  453. package/dist/routes/computer-use/live-flush.js +154 -0
  454. package/dist/routes/computer-use/local-tree-pack.d.ts +27 -0
  455. package/dist/routes/computer-use/local-tree-pack.js +47 -0
  456. package/dist/routes/computer-use/local-vm.d.ts +20 -0
  457. package/dist/routes/computer-use/local-vm.js +218 -0
  458. package/dist/routes/computer-use/participant-desktop.d.ts +63 -0
  459. package/dist/routes/computer-use/participant-desktop.js +33 -0
  460. package/dist/routes/computer-use/participant-facts.d.ts +6 -0
  461. package/dist/routes/computer-use/participant-facts.js +22 -0
  462. package/dist/routes/computer-use/participant-model.d.ts +30 -0
  463. package/dist/routes/computer-use/participant-model.js +222 -0
  464. package/dist/routes/computer-use/participant-prompt.d.ts +53 -0
  465. package/dist/routes/computer-use/participant-prompt.js +87 -0
  466. package/dist/routes/computer-use/participant-runs.d.ts +67 -0
  467. package/dist/routes/computer-use/participant-runs.js +255 -0
  468. package/dist/routes/computer-use/plan.d.ts +56 -0
  469. package/dist/routes/computer-use/plan.js +308 -0
  470. package/dist/routes/computer-use/preflight.d.ts +26 -0
  471. package/dist/routes/computer-use/preflight.js +85 -0
  472. package/dist/routes/computer-use/rerun-selection.d.ts +19 -0
  473. package/dist/routes/computer-use/rerun-selection.js +121 -0
  474. package/dist/routes/computer-use/result.d.ts +7 -0
  475. package/dist/routes/computer-use/result.js +292 -0
  476. package/dist/routes/computer-use/route.d.ts +33 -0
  477. package/dist/routes/computer-use/route.js +145 -0
  478. package/dist/routes/computer-use/run-lanes.d.ts +59 -0
  479. package/dist/routes/computer-use/run-lanes.js +135 -0
  480. package/dist/routes/computer-use/self-report.d.ts +21 -0
  481. package/dist/routes/computer-use/self-report.js +231 -0
  482. package/dist/routes/computer-use/setup.d.ts +136 -0
  483. package/dist/routes/computer-use/setup.js +371 -0
  484. package/dist/routes/computer-use/single-bundle.d.ts +97 -0
  485. package/dist/routes/computer-use/single-bundle.js +393 -0
  486. package/dist/routes/computer-use/subject-projection.d.ts +51 -0
  487. package/dist/routes/computer-use/subject-projection.js +159 -0
  488. package/dist/routes/computer-use/types.d.ts +539 -0
  489. package/dist/routes/computer-use/types.js +37 -0
  490. package/dist/routes/preview.d.ts +13 -0
  491. package/dist/routes/preview.js +36 -0
  492. package/dist/routes/scripted/bundle.d.ts +47 -0
  493. package/dist/routes/scripted/bundle.js +195 -0
  494. package/dist/routes/scripted/plan.d.ts +30 -0
  495. package/dist/routes/scripted/plan.js +95 -0
  496. package/dist/routes/scripted/result.d.ts +48 -0
  497. package/dist/routes/scripted/result.js +176 -0
  498. package/dist/routes/scripted/route.d.ts +28 -0
  499. package/dist/routes/scripted/route.js +218 -0
  500. package/dist/routes/scripted/scenario.d.ts +21 -0
  501. package/dist/routes/scripted/scenario.js +102 -0
  502. package/dist/routes/scripted/session-result.d.ts +8 -0
  503. package/dist/routes/scripted/session-result.js +60 -0
  504. package/dist/routes/scripted/setup.d.ts +45 -0
  505. package/dist/routes/scripted/setup.js +126 -0
  506. package/dist/routes/scripted/subject.d.ts +49 -0
  507. package/dist/routes/scripted/subject.js +156 -0
  508. package/dist/routes/scripted/surface-records.d.ts +26 -0
  509. package/dist/routes/scripted/surface-records.js +88 -0
  510. package/dist/routes/scripted/surface-sessions.d.ts +13 -0
  511. package/dist/routes/scripted/surface-sessions.js +40 -0
  512. package/dist/routes/scripted/types.d.ts +88 -0
  513. package/dist/routes/scripted/types.js +3 -0
  514. package/dist/routes/shared-world/bundle.d.ts +20 -0
  515. package/dist/routes/shared-world/bundle.js +446 -0
  516. package/dist/routes/shared-world/checkpoints.d.ts +25 -0
  517. package/dist/routes/shared-world/checkpoints.js +46 -0
  518. package/dist/routes/shared-world/comms.d.ts +68 -0
  519. package/dist/routes/shared-world/comms.js +176 -0
  520. package/dist/routes/shared-world/external-public.d.ts +21 -0
  521. package/dist/routes/shared-world/external-public.js +130 -0
  522. package/dist/routes/shared-world/handoff.d.ts +62 -0
  523. package/dist/routes/shared-world/handoff.js +348 -0
  524. package/dist/routes/shared-world/lobby-code.d.ts +25 -0
  525. package/dist/routes/shared-world/lobby-code.js +152 -0
  526. package/dist/routes/shared-world/plan.d.ts +27 -0
  527. package/dist/routes/shared-world/plan.js +174 -0
  528. package/dist/routes/shared-world/provenance.d.ts +27 -0
  529. package/dist/routes/shared-world/provenance.js +65 -0
  530. package/dist/routes/shared-world/provisioned.d.ts +57 -0
  531. package/dist/routes/shared-world/provisioned.js +505 -0
  532. package/dist/routes/shared-world/result.d.ts +18 -0
  533. package/dist/routes/shared-world/result.js +327 -0
  534. package/dist/routes/shared-world/route.d.ts +21 -0
  535. package/dist/routes/shared-world/route.js +277 -0
  536. package/dist/routes/shared-world/seat-records.d.ts +20 -0
  537. package/dist/routes/shared-world/seat-records.js +213 -0
  538. package/dist/routes/shared-world/seats.d.ts +38 -0
  539. package/dist/routes/shared-world/seats.js +208 -0
  540. package/dist/routes/shared-world/setup.d.ts +48 -0
  541. package/dist/routes/shared-world/setup.js +200 -0
  542. package/dist/routes/shared-world/types.d.ts +354 -0
  543. package/dist/routes/shared-world/types.js +29 -0
  544. package/dist/routes/terminal/adapter.d.ts +37 -0
  545. package/dist/routes/terminal/adapter.js +201 -0
  546. package/dist/routes/terminal/bundle.d.ts +79 -0
  547. package/dist/routes/terminal/bundle.js +305 -0
  548. package/dist/routes/terminal/credentials.d.ts +34 -0
  549. package/dist/routes/terminal/credentials.js +102 -0
  550. package/dist/routes/terminal/dry-run.d.ts +24 -0
  551. package/dist/routes/terminal/dry-run.js +157 -0
  552. package/dist/routes/terminal/encoding.js +79 -0
  553. package/dist/routes/terminal/ledger.d.ts +49 -0
  554. package/dist/routes/terminal/ledger.js +170 -0
  555. package/dist/routes/terminal/lifetime.d.ts +10 -0
  556. package/dist/routes/terminal/lifetime.js +22 -0
  557. package/dist/routes/terminal/live-finish.d.ts +31 -0
  558. package/dist/routes/terminal/live-finish.js +215 -0
  559. package/dist/routes/terminal/live-sandbox.d.ts +69 -0
  560. package/dist/routes/terminal/live-sandbox.js +452 -0
  561. package/dist/routes/terminal/persona.d.ts +20 -0
  562. package/dist/routes/terminal/persona.js +34 -0
  563. package/dist/routes/terminal/plan.d.ts +21 -0
  564. package/dist/routes/terminal/plan.js +115 -0
  565. package/dist/routes/terminal/recorder.d.ts +23 -0
  566. package/dist/routes/terminal/recorder.js +77 -0
  567. package/dist/routes/terminal/result.d.ts +47 -0
  568. package/dist/routes/terminal/result.js +114 -0
  569. package/dist/routes/terminal/route.d.ts +23 -0
  570. package/dist/routes/terminal/route.js +170 -0
  571. package/dist/routes/terminal/runtime-auth.d.ts +12 -0
  572. package/dist/routes/terminal/runtime-auth.js +23 -0
  573. package/dist/routes/terminal/runtime.d.ts +14 -0
  574. package/dist/routes/terminal/runtime.js +47 -0
  575. package/dist/routes/terminal/sandbox.d.ts +35 -0
  576. package/dist/routes/terminal/sandbox.js +126 -0
  577. package/dist/routes/terminal/session.d.ts +25 -0
  578. package/dist/routes/terminal/session.js +199 -0
  579. package/dist/routes/terminal/token-usage.d.ts +15 -0
  580. package/dist/routes/terminal/token-usage.js +93 -0
  581. package/dist/routes/terminal/trace.d.ts +40 -0
  582. package/dist/routes/terminal/trace.js +172 -0
  583. package/dist/routes/terminal/types.d.ts +342 -0
  584. package/dist/routes/terminal/types.js +31 -0
  585. package/dist/run/bundle-shape.d.ts +7 -0
  586. package/dist/run/bundle-shape.js +369 -0
  587. package/dist/run/bundle.d.ts +547 -0
  588. package/dist/run/bundle.js +44 -0
  589. package/dist/run/concurrency.js +24 -0
  590. package/dist/run/contained-output.d.ts +50 -0
  591. package/dist/run/contained-output.js +351 -0
  592. package/dist/run/cost-summary.d.ts +28 -0
  593. package/dist/run/cost-summary.js +213 -0
  594. package/dist/run/costs.js +140 -0
  595. package/dist/run/detail.d.ts +62 -0
  596. package/dist/run/detail.js +139 -0
  597. package/dist/run/dry-run-inputs.d.ts +9 -0
  598. package/dist/run/dry-run-inputs.js +90 -0
  599. package/dist/run/dry-run.d.ts +6 -0
  600. package/dist/run/dry-run.js +362 -0
  601. package/dist/run/feedback-shape.d.ts +7 -0
  602. package/dist/run/feedback-shape.js +89 -0
  603. package/dist/run/git-state.d.ts +35 -0
  604. package/dist/run/git-state.js +316 -0
  605. package/dist/run/git-workspace.d.ts +32 -0
  606. package/dist/run/git-workspace.js +450 -0
  607. package/dist/run/judge.d.ts +210 -0
  608. package/dist/run/judge.js +240 -0
  609. package/dist/run/locate.d.ts +22 -0
  610. package/dist/run/locate.js +122 -0
  611. package/dist/run/outcomes.d.ts +31 -0
  612. package/dist/run/outcomes.js +210 -0
  613. package/dist/run/participant-records.d.ts +28 -0
  614. package/dist/run/participant-records.js +63 -0
  615. package/dist/run/participant.d.ts +29 -0
  616. package/dist/run/participant.js +12 -0
  617. package/dist/run/paths.d.ts +79 -0
  618. package/dist/run/paths.js +356 -0
  619. package/dist/run/preflight-receipts.d.ts +46 -0
  620. package/dist/run/preflight-receipts.js +173 -0
  621. package/dist/run/pricing.d.ts +124 -0
  622. package/dist/run/pricing.js +298 -0
  623. package/dist/run/project.d.ts +8 -0
  624. package/dist/run/project.js +103 -0
  625. package/dist/run/projection.d.ts +184 -0
  626. package/dist/run/projection.js +356 -0
  627. package/dist/run/reclaim.d.ts +41 -0
  628. package/dist/run/reclaim.js +183 -0
  629. package/dist/run/results.d.ts +88 -0
  630. package/dist/run/results.js +3 -0
  631. package/dist/run/run-index.d.ts +107 -0
  632. package/dist/run/run-index.js +243 -0
  633. package/dist/run/run.d.ts +103 -0
  634. package/dist/run/run.js +199 -0
  635. package/dist/run/sandbox-receipts.d.ts +31 -0
  636. package/dist/run/sandbox-receipts.js +49 -0
  637. package/dist/run/shared-world-evidence.d.ts +220 -0
  638. package/dist/run/shared-world-evidence.js +49 -0
  639. package/dist/run/shared-world-shape.d.ts +26 -0
  640. package/dist/run/shared-world-shape.js +181 -0
  641. package/dist/run/stats.d.ts +82 -0
  642. package/dist/run/stats.js +258 -0
  643. package/dist/run/status.d.ts +111 -0
  644. package/dist/run/status.js +176 -0
  645. package/dist/run/stored-runs.d.ts +38 -0
  646. package/dist/run/stored-runs.js +273 -0
  647. package/dist/run/stream-shape.d.ts +4 -0
  648. package/dist/run/stream-shape.js +160 -0
  649. package/dist/run/streams.d.ts +257 -0
  650. package/dist/run/streams.js +12 -0
  651. package/dist/run/study-files.d.ts +35 -0
  652. package/dist/run/study-files.js +164 -0
  653. package/dist/run/terminal-contract.d.ts +25 -0
  654. package/dist/run/terminal-contract.js +47 -0
  655. package/dist/run/text.d.ts +2 -0
  656. package/dist/run/text.js +4 -0
  657. package/dist/run/transient-comms-secrets.js +72 -0
  658. package/dist/run/type-guards.d.ts +6 -0
  659. package/dist/run/type-guards.js +18 -0
  660. package/dist/run-lab.d.ts +110 -0
  661. package/dist/run-lab.js +148 -0
  662. package/dist/subject/clone.d.ts +37 -0
  663. package/dist/subject/clone.js +62 -0
  664. package/dist/subject/desktop-cli.d.ts +17 -0
  665. package/dist/subject/desktop-cli.js +51 -0
  666. package/dist/subject/local-tree-archive.d.ts +82 -0
  667. package/dist/subject/local-tree-archive.js +431 -0
  668. package/dist/subject/local-tree.d.ts +28 -0
  669. package/dist/subject/local-tree.js +58 -0
  670. package/dist/subject/node-bootstrap.d.ts +12 -0
  671. package/dist/subject/node-bootstrap.js +66 -0
  672. package/dist/subject/runtime.d.ts +13 -0
  673. package/dist/subject/runtime.js +50 -0
  674. package/dist/subject/serve.d.ts +39 -0
  675. package/dist/subject/serve.js +249 -0
  676. package/dist/subject/state.d.ts +23 -0
  677. package/dist/subject/state.js +58 -0
  678. package/dist/subject/steps.d.ts +37 -0
  679. package/dist/subject/steps.js +39 -0
  680. package/dist/substrates/command-failure.d.ts +23 -0
  681. package/dist/substrates/command-failure.js +45 -0
  682. package/dist/substrates/desktop-session.d.ts +41 -0
  683. package/dist/substrates/desktop-session.js +59 -0
  684. package/dist/substrates/detached.d.ts +55 -0
  685. package/dist/substrates/detached.js +139 -0
  686. package/dist/substrates/e2b/cdp-probe.d.ts +99 -0
  687. package/dist/substrates/e2b/cdp-probe.js +556 -0
  688. package/dist/substrates/e2b/desktop-browser.d.ts +81 -0
  689. package/dist/substrates/e2b/desktop-browser.js +410 -0
  690. package/dist/substrates/e2b/desktop-cdp.d.ts +77 -0
  691. package/dist/substrates/e2b/desktop-cdp.js +211 -0
  692. package/dist/substrates/e2b/desktop-executor.d.ts +92 -0
  693. package/dist/substrates/e2b/desktop-executor.js +302 -0
  694. package/dist/substrates/e2b/desktop-geometry.d.ts +83 -0
  695. package/dist/substrates/e2b/desktop-geometry.js +331 -0
  696. package/dist/substrates/e2b/desktop-media.d.ts +29 -0
  697. package/dist/substrates/e2b/desktop-media.js +151 -0
  698. package/dist/substrates/e2b/desktop-recording.d.ts +18 -0
  699. package/dist/substrates/e2b/desktop-recording.js +238 -0
  700. package/dist/substrates/e2b/desktop-resources.d.ts +11 -0
  701. package/dist/substrates/e2b/desktop-resources.js +35 -0
  702. package/dist/substrates/e2b/desktop-screenshot-cleanup.d.ts +15 -0
  703. package/dist/substrates/e2b/desktop-screenshot-cleanup.js +66 -0
  704. package/dist/substrates/e2b/lifetime.d.ts +12 -0
  705. package/dist/substrates/e2b/lifetime.js +18 -0
  706. package/dist/substrates/e2b/sandbox.d.ts +102 -0
  707. package/dist/substrates/e2b/sandbox.js +202 -0
  708. package/dist/substrates/e2b/sdk.d.ts +194 -0
  709. package/dist/substrates/e2b/sdk.js +168 -0
  710. package/dist/substrates/e2b/shell.d.ts +10 -0
  711. package/dist/substrates/e2b/shell.js +61 -0
  712. package/dist/substrates/local/captured-inbox.d.ts +6 -0
  713. package/dist/substrates/local/captured-inbox.js +87 -0
  714. package/dist/substrates/local/firecracker-desktop.d.ts +25 -0
  715. package/dist/substrates/local/firecracker-desktop.js +315 -0
  716. package/dist/substrates/local/runtime-config.d.ts +6 -0
  717. package/dist/substrates/local/runtime-config.js +91 -0
  718. package/dist/substrates/local/runtime-host.d.ts +30 -0
  719. package/dist/substrates/local/runtime-host.js +143 -0
  720. package/dist/substrates/local/runtime-release.d.ts +5 -0
  721. package/dist/substrates/local/runtime-release.js +32 -0
  722. package/dist/substrates/local/runtime-ssh.d.ts +9 -0
  723. package/dist/substrates/local/runtime-ssh.js +129 -0
  724. package/dist/substrates/local/runtime.d.ts +25 -0
  725. package/dist/substrates/local/runtime.js +190 -0
  726. package/dist/substrates/shell.d.ts +44 -0
  727. package/dist/substrates/shell.js +22 -0
  728. package/dist/tui/actions.d.ts +39 -0
  729. package/dist/tui/actions.js +203 -0
  730. package/dist/tui/contract.d.ts +144 -0
  731. package/dist/tui/contract.js +52 -0
  732. package/dist/tui/launch.js +172 -0
  733. package/dist/tui/project.d.ts +10 -0
  734. package/dist/tui/project.js +17 -0
  735. package/dist/tui-app.js +125 -125
  736. package/dist/verify/actor.d.ts +27 -0
  737. package/dist/verify/actor.js +292 -0
  738. package/dist/verify/artifacts.d.ts +47 -0
  739. package/dist/verify/artifacts.js +373 -0
  740. package/dist/verify/costs.d.ts +30 -0
  741. package/dist/verify/costs.js +129 -0
  742. package/dist/verify/liveness.d.ts +7 -0
  743. package/dist/verify/liveness.js +72 -0
  744. package/dist/verify/rerun.d.ts +3 -0
  745. package/dist/verify/rerun.js +46 -0
  746. package/dist/verify/shared-world-concurrent.d.ts +11 -0
  747. package/dist/verify/shared-world-concurrent.js +346 -0
  748. package/dist/verify/shared-world.d.ts +8 -0
  749. package/dist/verify/shared-world.js +295 -0
  750. package/dist/verify/subject.d.ts +17 -0
  751. package/dist/verify/subject.js +123 -0
  752. package/dist/verify/verify.d.ts +34 -0
  753. package/dist/verify/verify.js +333 -0
  754. package/docs/README.md +67 -0
  755. package/docs/architecture/actor-contract.md +215 -174
  756. package/docs/architecture/browser-control.md +51 -37
  757. package/docs/architecture/comms-inbox.md +8 -8
  758. package/docs/architecture/desktop-recording.md +2 -2
  759. package/docs/architecture/desktop-sessions.md +33 -21
  760. package/docs/architecture/external-public-shared-world.md +80 -64
  761. package/docs/architecture/github-feedback-loop.md +40 -27
  762. package/docs/architecture/guest-desktop.md +22 -10
  763. package/docs/architecture/local-browser-runtime.md +30 -21
  764. package/docs/architecture/observer-review.md +9 -9
  765. package/docs/architecture/observer.md +53 -57
  766. package/docs/architecture/participant-media.md +3 -3
  767. package/docs/architecture/project-layout.md +33 -137
  768. package/docs/architecture/real-email-receiving.md +7 -6
  769. package/docs/architecture/restricted-codex-analysis.md +211 -25
  770. package/docs/architecture/serve.md +89 -87
  771. package/docs/architecture/state-driven-executor.md +79 -64
  772. package/docs/architecture/task-protocol-support.md +18 -18
  773. package/docs/architecture/terminal-product-lane.md +92 -86
  774. package/docs/contracts/adapter-admission.md +8 -4
  775. package/docs/contracts/adapter-fixtures.md +6 -6
  776. package/docs/contracts/core.md +22 -24
  777. package/docs/contracts/feedback.md +33 -23
  778. package/docs/contracts/policy.md +28 -31
  779. package/docs/contracts/run-bundle.md +168 -82
  780. package/docs/contracts/schemas.md +594 -379
  781. package/docs/contracts/study-analysis.md +2 -2
  782. package/docs/contracts/study-costs.md +12 -12
  783. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +36 -0
  784. package/docs/decisions/0002-observer-is-one-self-contained-file.md +30 -0
  785. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +35 -0
  786. package/docs/decisions/0004-participant-desktops-stay-in-the-route.md +31 -0
  787. package/docs/decisions/README.md +16 -0
  788. package/docs/goals/current.md +22 -46
  789. package/docs/principles/actor-fidelity.md +51 -63
  790. package/docs/principles/engineering.md +81 -0
  791. package/docs/principles/invariants-and-defaults.md +48 -45
  792. package/docs/principles/self-driving-harness.md +11 -10
  793. package/docs/principles/three-roles.md +12 -13
  794. package/docs/product/automatic-analysis.md +39 -9
  795. package/docs/product/lobby-trivia-3player-external-public.md +14 -13
  796. package/docs/product/open-source-install-experience.md +34 -38
  797. package/docs/ramp/README.md +91 -213
  798. package/docs/release/open-source-readiness.md +5 -2
  799. package/docs/release/public-readiness-standard.md +1 -1
  800. package/examples/README.md +11 -0
  801. package/examples/participant/README.md +80 -0
  802. package/examples/participant/app.mjs +55 -0
  803. package/examples/participant/run.mjs +116 -0
  804. package/examples/scorer/README.md +29 -0
  805. package/examples/scorer/run.mjs +63 -0
  806. package/examples/scorer/scorer.mjs +20 -0
  807. package/package.json +61 -57
  808. package/skills/humanish/SKILL.md +112 -83
  809. package/skills/humanish/agents/openai.yaml +2 -2
  810. package/dist/actor-contract.d.ts +0 -288
  811. package/dist/actor-contract.js +0 -340
  812. package/dist/actor-contract.js.map +0 -1
  813. package/dist/actor-goal-source.js +0 -32
  814. package/dist/actor-goal-source.js.map +0 -1
  815. package/dist/actor-registry.d.ts +0 -77
  816. package/dist/actor-registry.js +0 -106
  817. package/dist/actor-registry.js.map +0 -1
  818. package/dist/actor-stop-cause.d.ts +0 -9
  819. package/dist/actor-stop-cause.js +0 -42
  820. package/dist/actor-stop-cause.js.map +0 -1
  821. package/dist/adapter-extension.d.ts +0 -76
  822. package/dist/adapter-extension.js +0 -273
  823. package/dist/adapter-extension.js.map +0 -1
  824. package/dist/adapter-scorer-loader.d.ts +0 -42
  825. package/dist/adapter-scorer-loader.js +0 -129
  826. package/dist/adapter-scorer-loader.js.map +0 -1
  827. package/dist/affordance.d.ts +0 -53
  828. package/dist/affordance.js +0 -145
  829. package/dist/affordance.js.map +0 -1
  830. package/dist/agent-session.js +0 -43
  831. package/dist/agent-session.js.map +0 -1
  832. package/dist/argv.d.ts +0 -1
  833. package/dist/argv.js +0 -8
  834. package/dist/argv.js.map +0 -1
  835. package/dist/artifact-reference.d.ts +0 -22
  836. package/dist/artifact-reference.js +0 -47
  837. package/dist/artifact-reference.js.map +0 -1
  838. package/dist/automatic-analysis-completion.d.ts +0 -25
  839. package/dist/automatic-analysis-completion.js +0 -56
  840. package/dist/automatic-analysis-completion.js.map +0 -1
  841. package/dist/automatic-analysis-config.d.ts +0 -38
  842. package/dist/automatic-analysis-config.js +0 -68
  843. package/dist/automatic-analysis-config.js.map +0 -1
  844. package/dist/automatic-study-analysis.d.ts +0 -17
  845. package/dist/automatic-study-analysis.js +0 -195
  846. package/dist/automatic-study-analysis.js.map +0 -1
  847. package/dist/browser-control-client.d.ts +0 -18
  848. package/dist/browser-control-client.js +0 -176
  849. package/dist/browser-control-client.js.map +0 -1
  850. package/dist/browser-control-dispatcher.d.ts +0 -19
  851. package/dist/browser-control-dispatcher.js +0 -147
  852. package/dist/browser-control-dispatcher.js.map +0 -1
  853. package/dist/browser-control-protocol.d.ts +0 -472
  854. package/dist/browser-control-protocol.js +0 -185
  855. package/dist/browser-control-protocol.js.map +0 -1
  856. package/dist/browser-control-recording-transfer.d.ts +0 -4
  857. package/dist/browser-control-recording-transfer.js +0 -42
  858. package/dist/browser-control-recording-transfer.js.map +0 -1
  859. package/dist/browser-control-transport.d.ts +0 -27
  860. package/dist/browser-control-transport.js +0 -177
  861. package/dist/browser-control-transport.js.map +0 -1
  862. package/dist/browser-evidence-hygiene.js +0 -43
  863. package/dist/browser-evidence-hygiene.js.map +0 -1
  864. package/dist/chrome-cdp-probe.d.ts +0 -97
  865. package/dist/chrome-cdp-probe.js +0 -536
  866. package/dist/chrome-cdp-probe.js.map +0 -1
  867. package/dist/claude-agent-sdk.d.ts +0 -94
  868. package/dist/claude-agent-sdk.js +0 -367
  869. package/dist/claude-agent-sdk.js.map +0 -1
  870. package/dist/cli.js.map +0 -1
  871. package/dist/codex-app-server-ui.d.ts +0 -39
  872. package/dist/codex-app-server-ui.js +0 -357
  873. package/dist/codex-app-server-ui.js.map +0 -1
  874. package/dist/codex-app-server.d.ts +0 -144
  875. package/dist/codex-app-server.js +0 -754
  876. package/dist/codex-app-server.js.map +0 -1
  877. package/dist/command-failure.d.ts +0 -43
  878. package/dist/command-failure.js +0 -73
  879. package/dist/command-failure.js.map +0 -1
  880. package/dist/comms-agentmail.d.ts +0 -25
  881. package/dist/comms-agentmail.js +0 -418
  882. package/dist/comms-agentmail.js.map +0 -1
  883. package/dist/comms-catch-host.d.ts +0 -52
  884. package/dist/comms-catch-host.js +0 -178
  885. package/dist/comms-catch-host.js.map +0 -1
  886. package/dist/comms-connections.d.ts +0 -52
  887. package/dist/comms-connections.js +0 -123
  888. package/dist/comms-connections.js.map +0 -1
  889. package/dist/comms-email-catch.d.ts +0 -54
  890. package/dist/comms-email-catch.js +0 -212
  891. package/dist/comms-email-catch.js.map +0 -1
  892. package/dist/comms-evidence.d.ts +0 -27
  893. package/dist/comms-evidence.js +0 -31
  894. package/dist/comms-evidence.js.map +0 -1
  895. package/dist/comms-fake-inbox.d.ts +0 -45
  896. package/dist/comms-fake-inbox.js +0 -184
  897. package/dist/comms-fake-inbox.js.map +0 -1
  898. package/dist/comms-images.d.ts +0 -7
  899. package/dist/comms-images.js +0 -39
  900. package/dist/comms-images.js.map +0 -1
  901. package/dist/comms-inbox.d.ts +0 -66
  902. package/dist/comms-inbox.js +0 -389
  903. package/dist/comms-inbox.js.map +0 -1
  904. package/dist/comms-lease-store.d.ts +0 -91
  905. package/dist/comms-lease-store.js +0 -495
  906. package/dist/comms-lease-store.js.map +0 -1
  907. package/dist/comms-receiving-evidence.d.ts +0 -46
  908. package/dist/comms-receiving-evidence.js +0 -35
  909. package/dist/comms-receiving-evidence.js.map +0 -1
  910. package/dist/comms-receiving-inbox.d.ts +0 -16
  911. package/dist/comms-receiving-inbox.js +0 -410
  912. package/dist/comms-receiving-inbox.js.map +0 -1
  913. package/dist/comms-receiving-runtime.d.ts +0 -25
  914. package/dist/comms-receiving-runtime.js +0 -45
  915. package/dist/comms-receiving-runtime.js.map +0 -1
  916. package/dist/comms-receiving-types.d.ts +0 -98
  917. package/dist/comms-receiving-types.js +0 -2
  918. package/dist/comms-receiving-types.js.map +0 -1
  919. package/dist/comms-receiving.d.ts +0 -58
  920. package/dist/comms-receiving.js +0 -554
  921. package/dist/comms-receiving.js.map +0 -1
  922. package/dist/comms-sandbox-catch.d.ts +0 -229
  923. package/dist/comms-sandbox-catch.js +0 -737
  924. package/dist/comms-sandbox-catch.js.map +0 -1
  925. package/dist/comms-setup.d.ts +0 -40
  926. package/dist/comms-setup.js +0 -121
  927. package/dist/comms-setup.js.map +0 -1
  928. package/dist/comms-types.d.ts +0 -72
  929. package/dist/comms-types.js +0 -10
  930. package/dist/comms-types.js.map +0 -1
  931. package/dist/computer-use-actor.d.ts +0 -76
  932. package/dist/computer-use-actor.js +0 -68
  933. package/dist/computer-use-actor.js.map +0 -1
  934. package/dist/computer-use.d.ts +0 -429
  935. package/dist/computer-use.js +0 -1749
  936. package/dist/computer-use.js.map +0 -1
  937. package/dist/concurrency.js +0 -27
  938. package/dist/concurrency.js.map +0 -1
  939. package/dist/concurrent-shared-world-lab.d.ts +0 -186
  940. package/dist/concurrent-shared-world-lab.js +0 -2179
  941. package/dist/concurrent-shared-world-lab.js.map +0 -1
  942. package/dist/core/git-state.d.ts +0 -33
  943. package/dist/core/git-state.js +0 -277
  944. package/dist/core/git-state.js.map +0 -1
  945. package/dist/core/git-workspace.d.ts +0 -31
  946. package/dist/core/git-workspace.js +0 -447
  947. package/dist/core/git-workspace.js.map +0 -1
  948. package/dist/core/index.d.ts +0 -4
  949. package/dist/core/index.js +0 -3
  950. package/dist/core/index.js.map +0 -1
  951. package/dist/core/run-primitives.d.ts +0 -66
  952. package/dist/core/run-primitives.js +0 -120
  953. package/dist/core/run-primitives.js.map +0 -1
  954. package/dist/cua-actor-lab.d.ts +0 -855
  955. package/dist/cua-actor-lab.js +0 -3545
  956. package/dist/cua-actor-lab.js.map +0 -1
  957. package/dist/cua-admission-limit.d.ts +0 -12
  958. package/dist/cua-admission-limit.js +0 -20
  959. package/dist/cua-admission-limit.js.map +0 -1
  960. package/dist/cua-desktop-lane.d.ts +0 -35
  961. package/dist/cua-desktop-lane.js +0 -13
  962. package/dist/cua-desktop-lane.js.map +0 -1
  963. package/dist/cua-diagnostics.d.ts +0 -38
  964. package/dist/cua-diagnostics.js +0 -68
  965. package/dist/cua-diagnostics.js.map +0 -1
  966. package/dist/cua-executor-error.js +0 -48
  967. package/dist/cua-executor-error.js.map +0 -1
  968. package/dist/cua-provider-error.d.ts +0 -16
  969. package/dist/cua-provider-error.js +0 -35
  970. package/dist/cua-provider-error.js.map +0 -1
  971. package/dist/cua-speech.js +0 -2
  972. package/dist/cua-speech.js.map +0 -1
  973. package/dist/desktop-recorder.d.ts +0 -39
  974. package/dist/desktop-recorder.js +0 -226
  975. package/dist/desktop-recorder.js.map +0 -1
  976. package/dist/desktop-recording-artifact.d.ts +0 -5
  977. package/dist/desktop-recording-artifact.js +0 -54
  978. package/dist/desktop-recording-artifact.js.map +0 -1
  979. package/dist/desktop-recording-types.d.ts +0 -24
  980. package/dist/desktop-recording-types.js +0 -14
  981. package/dist/desktop-recording-types.js.map +0 -1
  982. package/dist/desktop-session.d.ts +0 -41
  983. package/dist/desktop-session.js +0 -47
  984. package/dist/desktop-session.js.map +0 -1
  985. package/dist/device-presets.d.ts +0 -67
  986. package/dist/device-presets.js +0 -55
  987. package/dist/device-presets.js.map +0 -1
  988. package/dist/doctor-lab.d.ts +0 -38
  989. package/dist/doctor-lab.js +0 -126
  990. package/dist/doctor-lab.js.map +0 -1
  991. package/dist/e2b-cua-desktop.d.ts +0 -3
  992. package/dist/e2b-cua-desktop.js +0 -708
  993. package/dist/e2b-cua-desktop.js.map +0 -1
  994. package/dist/e2b-cua-provisioning.d.ts +0 -314
  995. package/dist/e2b-cua-provisioning.js +0 -1219
  996. package/dist/e2b-cua-provisioning.js.map +0 -1
  997. package/dist/e2b-desktop-executor.d.ts +0 -101
  998. package/dist/e2b-desktop-executor.js +0 -297
  999. package/dist/e2b-desktop-executor.js.map +0 -1
  1000. package/dist/e2b-desktop-launch.d.ts +0 -243
  1001. package/dist/e2b-desktop-launch.js +0 -234
  1002. package/dist/e2b-desktop-launch.js.map +0 -1
  1003. package/dist/e2b-desktop-media.d.ts +0 -14
  1004. package/dist/e2b-desktop-media.js +0 -58
  1005. package/dist/e2b-desktop-media.js.map +0 -1
  1006. package/dist/e2b-desktop-recording.d.ts +0 -16
  1007. package/dist/e2b-desktop-recording.js +0 -162
  1008. package/dist/e2b-desktop-recording.js.map +0 -1
  1009. package/dist/e2b-desktop-resources.d.ts +0 -11
  1010. package/dist/e2b-desktop-resources.js +0 -33
  1011. package/dist/e2b-desktop-resources.js.map +0 -1
  1012. package/dist/e2b-desktop-screenshot-cleanup.d.ts +0 -15
  1013. package/dist/e2b-desktop-screenshot-cleanup.js +0 -67
  1014. package/dist/e2b-desktop-screenshot-cleanup.js.map +0 -1
  1015. package/dist/e2b-desktop-session.d.ts +0 -7
  1016. package/dist/e2b-desktop-session.js +0 -29
  1017. package/dist/e2b-desktop-session.js.map +0 -1
  1018. package/dist/e2b-detached.d.ts +0 -53
  1019. package/dist/e2b-detached.js +0 -130
  1020. package/dist/e2b-detached.js.map +0 -1
  1021. package/dist/e2b-terminal-lab.d.ts +0 -395
  1022. package/dist/e2b-terminal-lab.js +0 -2041
  1023. package/dist/e2b-terminal-lab.js.map +0 -1
  1024. package/dist/env-file.d.ts +0 -14
  1025. package/dist/env-file.js +0 -108
  1026. package/dist/env-file.js.map +0 -1
  1027. package/dist/export-bundle.d.ts +0 -7
  1028. package/dist/export-bundle.js +0 -441
  1029. package/dist/export-bundle.js.map +0 -1
  1030. package/dist/export.d.ts +0 -49
  1031. package/dist/export.js +0 -308
  1032. package/dist/export.js.map +0 -1
  1033. package/dist/feedback-proof.d.ts +0 -8
  1034. package/dist/feedback-proof.js +0 -44
  1035. package/dist/feedback-proof.js.map +0 -1
  1036. package/dist/feedback.d.ts +0 -76
  1037. package/dist/feedback.js +0 -496
  1038. package/dist/feedback.js.map +0 -1
  1039. package/dist/first-run-path.d.ts +0 -55
  1040. package/dist/first-run-path.js +0 -136
  1041. package/dist/first-run-path.js.map +0 -1
  1042. package/dist/frame-signature.d.ts +0 -24
  1043. package/dist/frame-signature.js +0 -128
  1044. package/dist/frame-signature.js.map +0 -1
  1045. package/dist/guest-bootstrap.d.ts +0 -55
  1046. package/dist/guest-bootstrap.js +0 -285
  1047. package/dist/guest-bootstrap.js.map +0 -1
  1048. package/dist/guest-browser-tools.d.ts +0 -8
  1049. package/dist/guest-browser-tools.js +0 -66
  1050. package/dist/guest-browser-tools.js.map +0 -1
  1051. package/dist/guest-chromium-text.d.ts +0 -27
  1052. package/dist/guest-chromium-text.js +0 -281
  1053. package/dist/guest-chromium-text.js.map +0 -1
  1054. package/dist/guest-desktop-executor.d.ts +0 -22
  1055. package/dist/guest-desktop-executor.js +0 -178
  1056. package/dist/guest-desktop-executor.js.map +0 -1
  1057. package/dist/guest-desktop-media.d.ts +0 -33
  1058. package/dist/guest-desktop-media.js +0 -281
  1059. package/dist/guest-desktop-media.js.map +0 -1
  1060. package/dist/guest-desktop-native.d.ts +0 -14
  1061. package/dist/guest-desktop-native.js +0 -131
  1062. package/dist/guest-desktop-native.js.map +0 -1
  1063. package/dist/guest-media-config.d.ts +0 -15
  1064. package/dist/guest-media-config.js +0 -8
  1065. package/dist/guest-media-config.js.map +0 -1
  1066. package/dist/guest-media-worker.js.map +0 -1
  1067. package/dist/guest-runtime-desktop.d.ts +0 -42
  1068. package/dist/guest-runtime-desktop.js +0 -314
  1069. package/dist/guest-runtime-desktop.js.map +0 -1
  1070. package/dist/guest-runtime-main.js.map +0 -1
  1071. package/dist/guest-runtime-revision.js.map +0 -1
  1072. package/dist/guest-runtime.d.ts +0 -31
  1073. package/dist/guest-runtime.js +0 -99
  1074. package/dist/guest-runtime.js.map +0 -1
  1075. package/dist/image-evidence.d.ts +0 -2
  1076. package/dist/image-evidence.js +0 -47
  1077. package/dist/image-evidence.js.map +0 -1
  1078. package/dist/index.js.map +0 -1
  1079. package/dist/init-templates.d.ts +0 -25
  1080. package/dist/init-templates.js +0 -542
  1081. package/dist/init-templates.js.map +0 -1
  1082. package/dist/init.d.ts +0 -38
  1083. package/dist/init.js +0 -532
  1084. package/dist/init.js.map +0 -1
  1085. package/dist/key-resolution.d.ts +0 -67
  1086. package/dist/key-resolution.js +0 -375
  1087. package/dist/key-resolution.js.map +0 -1
  1088. package/dist/lab-app-runner.d.ts +0 -78
  1089. package/dist/lab-app-runner.js +0 -403
  1090. package/dist/lab-app-runner.js.map +0 -1
  1091. package/dist/lab-config.d.ts +0 -968
  1092. package/dist/lab-config.js +0 -2714
  1093. package/dist/lab-config.js.map +0 -1
  1094. package/dist/lab-engine.d.ts +0 -97
  1095. package/dist/lab-engine.js +0 -285
  1096. package/dist/lab-engine.js.map +0 -1
  1097. package/dist/lab-preflight.d.ts +0 -70
  1098. package/dist/lab-preflight.js +0 -388
  1099. package/dist/lab-preflight.js.map +0 -1
  1100. package/dist/lab-summary.d.ts +0 -57
  1101. package/dist/lab-summary.js +0 -126
  1102. package/dist/lab-summary.js.map +0 -1
  1103. package/dist/labs.d.ts +0 -59
  1104. package/dist/labs.js +0 -385
  1105. package/dist/labs.js.map +0 -1
  1106. package/dist/listen.js +0 -84
  1107. package/dist/listen.js.map +0 -1
  1108. package/dist/local-agent-claude-session.d.ts +0 -45
  1109. package/dist/local-agent-claude-session.js +0 -203
  1110. package/dist/local-agent-claude-session.js.map +0 -1
  1111. package/dist/local-agent-cli.d.ts +0 -121
  1112. package/dist/local-agent-cli.js +0 -427
  1113. package/dist/local-agent-cli.js.map +0 -1
  1114. package/dist/local-captured-inbox.d.ts +0 -6
  1115. package/dist/local-captured-inbox.js +0 -65
  1116. package/dist/local-captured-inbox.js.map +0 -1
  1117. package/dist/local-firecracker-desktop.d.ts +0 -23
  1118. package/dist/local-firecracker-desktop.js +0 -191
  1119. package/dist/local-firecracker-desktop.js.map +0 -1
  1120. package/dist/local-firecracker-study.d.ts +0 -9
  1121. package/dist/local-firecracker-study.js +0 -135
  1122. package/dist/local-firecracker-study.js.map +0 -1
  1123. package/dist/local-runtime-config.d.ts +0 -6
  1124. package/dist/local-runtime-config.js +0 -60
  1125. package/dist/local-runtime-config.js.map +0 -1
  1126. package/dist/local-runtime-host.d.ts +0 -31
  1127. package/dist/local-runtime-host.js +0 -128
  1128. package/dist/local-runtime-host.js.map +0 -1
  1129. package/dist/local-runtime-release.d.ts +0 -7
  1130. package/dist/local-runtime-release.js +0 -33
  1131. package/dist/local-runtime-release.js.map +0 -1
  1132. package/dist/local-runtime-ssh.d.ts +0 -27
  1133. package/dist/local-runtime-ssh.js +0 -91
  1134. package/dist/local-runtime-ssh.js.map +0 -1
  1135. package/dist/local-runtime.d.ts +0 -25
  1136. package/dist/local-runtime.js +0 -133
  1137. package/dist/local-runtime.js.map +0 -1
  1138. package/dist/observer-data.d.ts +0 -114
  1139. package/dist/observer-data.js +0 -224
  1140. package/dist/observer-data.js.map +0 -1
  1141. package/dist/observer-library.js +0 -191
  1142. package/dist/observer-library.js.map +0 -1
  1143. package/dist/observer-serve.d.ts +0 -99
  1144. package/dist/observer-serve.js +0 -284
  1145. package/dist/observer-serve.js.map +0 -1
  1146. package/dist/observer-static.js +0 -357
  1147. package/dist/observer-static.js.map +0 -1
  1148. package/dist/observer.d.ts +0 -118
  1149. package/dist/observer.js +0 -947
  1150. package/dist/observer.js.map +0 -1
  1151. package/dist/openai-responses-cu.d.ts +0 -158
  1152. package/dist/openai-responses-cu.js +0 -714
  1153. package/dist/openai-responses-cu.js.map +0 -1
  1154. package/dist/orientation.d.ts +0 -29
  1155. package/dist/orientation.js +0 -95
  1156. package/dist/orientation.js.map +0 -1
  1157. package/dist/oss-lab.d.ts +0 -55
  1158. package/dist/oss-lab.js +0 -344
  1159. package/dist/oss-lab.js.map +0 -1
  1160. package/dist/oss-meta-lab-scoring.d.ts +0 -15
  1161. package/dist/oss-meta-lab-scoring.js +0 -145
  1162. package/dist/oss-meta-lab-scoring.js.map +0 -1
  1163. package/dist/oss-meta-lab.d.ts +0 -287
  1164. package/dist/oss-meta-lab.js +0 -4551
  1165. package/dist/oss-meta-lab.js.map +0 -1
  1166. package/dist/oss-remote-telemetry.d.ts +0 -77
  1167. package/dist/oss-remote-telemetry.js +0 -403
  1168. package/dist/oss-remote-telemetry.js.map +0 -1
  1169. package/dist/output-token-limit.js +0 -5
  1170. package/dist/output-token-limit.js.map +0 -1
  1171. package/dist/participant-assignment.d.ts +0 -12
  1172. package/dist/participant-assignment.js +0 -15
  1173. package/dist/participant-assignment.js.map +0 -1
  1174. package/dist/persona-resolve.d.ts +0 -44
  1175. package/dist/persona-resolve.js +0 -109
  1176. package/dist/persona-resolve.js.map +0 -1
  1177. package/dist/persona.d.ts +0 -35
  1178. package/dist/persona.js +0 -141
  1179. package/dist/persona.js.map +0 -1
  1180. package/dist/pi-agent-core.d.ts +0 -77
  1181. package/dist/pi-agent-core.js +0 -207
  1182. package/dist/pi-agent-core.js.map +0 -1
  1183. package/dist/pricing.d.ts +0 -145
  1184. package/dist/pricing.js +0 -277
  1185. package/dist/pricing.js.map +0 -1
  1186. package/dist/program.d.ts +0 -96
  1187. package/dist/program.js +0 -4029
  1188. package/dist/program.js.map +0 -1
  1189. package/dist/reasoning-effort.js +0 -26
  1190. package/dist/reasoning-effort.js.map +0 -1
  1191. package/dist/reclaim.d.ts +0 -31
  1192. package/dist/reclaim.js +0 -90
  1193. package/dist/reclaim.js.map +0 -1
  1194. package/dist/redaction.d.ts +0 -90
  1195. package/dist/redaction.js +0 -294
  1196. package/dist/redaction.js.map +0 -1
  1197. package/dist/restricted-codex-analysis.d.ts +0 -15
  1198. package/dist/restricted-codex-analysis.js +0 -13
  1199. package/dist/restricted-codex-analysis.js.map +0 -1
  1200. package/dist/restricted-codex-participant-policy.d.ts +0 -44
  1201. package/dist/restricted-codex-participant-policy.js +0 -43
  1202. package/dist/restricted-codex-participant-policy.js.map +0 -1
  1203. package/dist/restricted-codex-participant-run.d.ts +0 -20
  1204. package/dist/restricted-codex-participant-run.js +0 -79
  1205. package/dist/restricted-codex-participant-run.js.map +0 -1
  1206. package/dist/restricted-codex-participant.d.ts +0 -20
  1207. package/dist/restricted-codex-participant.js +0 -250
  1208. package/dist/restricted-codex-participant.js.map +0 -1
  1209. package/dist/restricted-codex-policy.d.ts +0 -64
  1210. package/dist/restricted-codex-policy.js +0 -176
  1211. package/dist/restricted-codex-policy.js.map +0 -1
  1212. package/dist/restricted-codex-session.d.ts +0 -44
  1213. package/dist/restricted-codex-session.js +0 -657
  1214. package/dist/restricted-codex-session.js.map +0 -1
  1215. package/dist/restricted-codex-transport.d.ts +0 -69
  1216. package/dist/restricted-codex-transport.js +0 -296
  1217. package/dist/restricted-codex-transport.js.map +0 -1
  1218. package/dist/run-detail.d.ts +0 -61
  1219. package/dist/run-detail.js +0 -137
  1220. package/dist/run-detail.js.map +0 -1
  1221. package/dist/run-index.d.ts +0 -71
  1222. package/dist/run-index.js +0 -206
  1223. package/dist/run-index.js.map +0 -1
  1224. package/dist/run-narration-secrets.js +0 -68
  1225. package/dist/run-narration-secrets.js.map +0 -1
  1226. package/dist/run-paths.d.ts +0 -56
  1227. package/dist/run-paths.js +0 -355
  1228. package/dist/run-paths.js.map +0 -1
  1229. package/dist/run-projection.d.ts +0 -186
  1230. package/dist/run-projection.js +0 -350
  1231. package/dist/run-projection.js.map +0 -1
  1232. package/dist/run-status.d.ts +0 -139
  1233. package/dist/run-status.js +0 -218
  1234. package/dist/run-status.js.map +0 -1
  1235. package/dist/run.d.ts +0 -1255
  1236. package/dist/run.js +0 -5947
  1237. package/dist/run.js.map +0 -1
  1238. package/dist/sandbox-receipts.d.ts +0 -19
  1239. package/dist/sandbox-receipts.js +0 -51
  1240. package/dist/sandbox-receipts.js.map +0 -1
  1241. package/dist/screenshot-image.d.ts +0 -11
  1242. package/dist/screenshot-image.js +0 -26
  1243. package/dist/screenshot-image.js.map +0 -1
  1244. package/dist/scripted-browser-actor.d.ts +0 -297
  1245. package/dist/scripted-browser-actor.js +0 -1399
  1246. package/dist/scripted-browser-actor.js.map +0 -1
  1247. package/dist/scripted-browser-lab.d.ts +0 -121
  1248. package/dist/scripted-browser-lab.js +0 -852
  1249. package/dist/scripted-browser-lab.js.map +0 -1
  1250. package/dist/secret-prompt.js +0 -36
  1251. package/dist/secret-prompt.js.map +0 -1
  1252. package/dist/selected-output-paths.d.ts +0 -52
  1253. package/dist/selected-output-paths.js +0 -366
  1254. package/dist/selected-output-paths.js.map +0 -1
  1255. package/dist/serve-exposure.d.ts +0 -62
  1256. package/dist/serve-exposure.js +0 -129
  1257. package/dist/serve-exposure.js.map +0 -1
  1258. package/dist/serve-http.js +0 -45
  1259. package/dist/serve-http.js.map +0 -1
  1260. package/dist/serve-tunnel.js +0 -113
  1261. package/dist/serve-tunnel.js.map +0 -1
  1262. package/dist/shared-world-lab.d.ts +0 -269
  1263. package/dist/shared-world-lab.js +0 -1422
  1264. package/dist/shared-world-lab.js.map +0 -1
  1265. package/dist/source-archive.d.ts +0 -82
  1266. package/dist/source-archive.js +0 -408
  1267. package/dist/source-archive.js.map +0 -1
  1268. package/dist/stats.d.ts +0 -82
  1269. package/dist/stats.js +0 -219
  1270. package/dist/stats.js.map +0 -1
  1271. package/dist/stop-conditions.d.ts +0 -39
  1272. package/dist/stop-conditions.js +0 -66
  1273. package/dist/stop-conditions.js.map +0 -1
  1274. package/dist/study-analysis-codex-config.d.ts +0 -11
  1275. package/dist/study-analysis-codex-config.js +0 -34
  1276. package/dist/study-analysis-codex-config.js.map +0 -1
  1277. package/dist/study-analysis-engine.d.ts +0 -47
  1278. package/dist/study-analysis-engine.js +0 -327
  1279. package/dist/study-analysis-engine.js.map +0 -1
  1280. package/dist/study-analysis-evidence.d.ts +0 -27
  1281. package/dist/study-analysis-evidence.js +0 -660
  1282. package/dist/study-analysis-evidence.js.map +0 -1
  1283. package/dist/study-analysis-job.d.ts +0 -97
  1284. package/dist/study-analysis-job.js +0 -214
  1285. package/dist/study-analysis-job.js.map +0 -1
  1286. package/dist/study-analysis-provider.d.ts +0 -40
  1287. package/dist/study-analysis-provider.js +0 -170
  1288. package/dist/study-analysis-provider.js.map +0 -1
  1289. package/dist/study-analysis-service.d.ts +0 -59
  1290. package/dist/study-analysis-service.js +0 -257
  1291. package/dist/study-analysis-service.js.map +0 -1
  1292. package/dist/study-analysis-sharing.d.ts +0 -11
  1293. package/dist/study-analysis-sharing.js +0 -35
  1294. package/dist/study-analysis-sharing.js.map +0 -1
  1295. package/dist/study-analysis-store.d.ts +0 -47
  1296. package/dist/study-analysis-store.js +0 -493
  1297. package/dist/study-analysis-store.js.map +0 -1
  1298. package/dist/study-analysis-validation.d.ts +0 -618
  1299. package/dist/study-analysis-validation.js +0 -384
  1300. package/dist/study-analysis-validation.js.map +0 -1
  1301. package/dist/study-analysis.d.ts +0 -206
  1302. package/dist/study-analysis.js +0 -4
  1303. package/dist/study-analysis.js.map +0 -1
  1304. package/dist/study-costs.js +0 -114
  1305. package/dist/study-costs.js.map +0 -1
  1306. package/dist/subject-runtime.d.ts +0 -23
  1307. package/dist/subject-runtime.js +0 -70
  1308. package/dist/subject-runtime.js.map +0 -1
  1309. package/dist/tasks.d.ts +0 -90
  1310. package/dist/tasks.js +0 -144
  1311. package/dist/tasks.js.map +0 -1
  1312. package/dist/telemetry.js +0 -324
  1313. package/dist/telemetry.js.map +0 -1
  1314. package/dist/terminal-agent-actor.d.ts +0 -41
  1315. package/dist/terminal-agent-actor.js +0 -25
  1316. package/dist/terminal-agent-actor.js.map +0 -1
  1317. package/dist/terminal-encoding.js +0 -80
  1318. package/dist/terminal-encoding.js.map +0 -1
  1319. package/dist/terminal-node-bootstrap.d.ts +0 -4
  1320. package/dist/terminal-node-bootstrap.js +0 -58
  1321. package/dist/terminal-node-bootstrap.js.map +0 -1
  1322. package/dist/terminal-participant-activity.d.ts +0 -6
  1323. package/dist/terminal-participant-activity.js +0 -34
  1324. package/dist/terminal-participant-activity.js.map +0 -1
  1325. package/dist/terminal-runtime-auth.d.ts +0 -13
  1326. package/dist/terminal-runtime-auth.js +0 -24
  1327. package/dist/terminal-runtime-auth.js.map +0 -1
  1328. package/dist/terminal-runtime.d.ts +0 -16
  1329. package/dist/terminal-runtime.js +0 -42
  1330. package/dist/terminal-runtime.js.map +0 -1
  1331. package/dist/terminal-token-usage.d.ts +0 -15
  1332. package/dist/terminal-token-usage.js +0 -94
  1333. package/dist/terminal-token-usage.js.map +0 -1
  1334. package/dist/tui-actions.d.ts +0 -39
  1335. package/dist/tui-actions.js +0 -178
  1336. package/dist/tui-actions.js.map +0 -1
  1337. package/dist/tui-contract.d.ts +0 -141
  1338. package/dist/tui-contract.js +0 -54
  1339. package/dist/tui-contract.js.map +0 -1
  1340. package/dist/tui-launch.js +0 -167
  1341. package/dist/tui-launch.js.map +0 -1
  1342. package/dist/tui-project.d.ts +0 -9
  1343. package/dist/tui-project.js +0 -18
  1344. package/dist/tui-project.js.map +0 -1
  1345. package/docs/architecture/examples/state-driven-local-app/README.md +0 -80
  1346. package/docs/architecture/examples/state-driven-local-app/app.mjs +0 -48
  1347. package/docs/architecture/examples/state-driven-local-app/runner.mjs +0 -105
  1348. package/docs/architecture/local-codex-tui-actor.md +0 -213
  1349. package/docs/architecture/oss-lab-poc.md +0 -250
  1350. package/docs/architecture/runtime-broker-core.md +0 -30
  1351. package/docs/release/0.100.0-local-setup.md +0 -31
  1352. package/docs/release/0.100.1-participant-continuity.md +0 -20
  1353. package/docs/release/0.101.0-codex-ui-tools.md +0 -28
  1354. package/docs/release/0.102.0-participant-media.md +0 -43
  1355. package/docs/release/0.103.0-desktop-recording.md +0 -29
  1356. package/docs/release/0.103.1-analysis-capture-allocation.md +0 -18
  1357. package/docs/release/0.103.2-analysis-ending-context.md +0 -21
  1358. package/docs/release/0.104.0-local-captured-inboxes.md +0 -29
  1359. package/docs/release/0.104.1-recording-startup.md +0 -24
  1360. package/docs/release/0.104.2-recording-timestamps.md +0 -32
  1361. package/docs/release/0.104.3-local-smtp-catch.md +0 -15
  1362. package/docs/release/0.20.0-external-public-shared-world.md +0 -43
  1363. package/docs/release/0.84.0-redacted-evidence.md +0 -42
  1364. package/docs/release/0.84.1-portable-feedback.md +0 -20
  1365. package/docs/release/0.85.0-observer-review.md +0 -56
  1366. package/docs/release/0.85.1-observer-continuity.md +0 -20
  1367. package/docs/release/0.86.0-participant-evidence.md +0 -50
  1368. package/docs/release/0.86.1-task-preflight-saved-recordings.md +0 -40
  1369. package/docs/release/0.87.0-participant-endings-and-phone-review.md +0 -64
  1370. package/docs/release/0.88.0-study-diagnostics.md +0 -43
  1371. package/docs/release/0.88.1-completion-evidence-and-local-app.md +0 -79
  1372. package/docs/release/0.88.2-sequential-study-budgets.md +0 -57
  1373. package/docs/release/0.89.0-study-findings.md +0 -79
  1374. package/docs/release/0.89.1-analysis-finished-notice.md +0 -24
  1375. package/docs/release/0.90.0-automatic-analysis.md +0 -21
  1376. package/docs/release/0.91.0-analysis-quality-and-defaults.md +0 -33
  1377. package/docs/release/0.91.1-study-review-polish.md +0 -28
  1378. package/docs/release/0.92.0-grid-playback.md +0 -22
  1379. package/docs/release/0.93.0-global-playback.md +0 -26
  1380. package/docs/release/0.93.1-observer-review-controls.md +0 -24
  1381. package/docs/release/0.94.0-reliability.md +0 -48
  1382. package/docs/release/0.95.0-connections-setup.md +0 -38
  1383. package/docs/release/0.96.0-real-email-receiving.md +0 -47
  1384. package/docs/release/0.96.1-browser-navigation.md +0 -22
  1385. package/docs/release/0.97.0-codex-account-analysis.md +0 -45
  1386. package/docs/release/0.98.0-local-browser-studies.md +0 -31
  1387. package/docs/release/0.99.0-local-browser-mac.md +0 -34
  1388. package/docs/release/0.99.1-action-recovery.md +0 -19
  1389. /package/dist/{agent-session.d.ts → actors/agent-session.d.ts} +0 -0
  1390. /package/dist/{cua-executor-error.d.ts → actors/computer-use/executor-error.d.ts} +0 -0
  1391. /package/dist/{cua-speech.d.ts → actors/computer-use/speech.d.ts} +0 -0
  1392. /package/dist/{actor-goal-source.d.ts → actors/goal-source.d.ts} +0 -0
  1393. /package/dist/{output-token-limit.d.ts → actors/output-token-limit.d.ts} +0 -0
  1394. /package/dist/{reasoning-effort.d.ts → actors/reasoning-effort.d.ts} +0 -0
  1395. /package/dist/{secret-prompt.d.ts → cli/secret-prompt.d.ts} +0 -0
  1396. /package/dist/{telemetry.d.ts → cli/telemetry.d.ts} +0 -0
  1397. /package/dist/{browser-evidence-hygiene.d.ts → evidence/browser-hygiene.d.ts} +0 -0
  1398. /package/dist/{serve-http.d.ts → observer/http.d.ts} +0 -0
  1399. /package/dist/{observer-library.d.ts → observer/library.d.ts} +0 -0
  1400. /package/dist/{listen.d.ts → observer/listen.d.ts} +0 -0
  1401. /package/dist/{observer-static.d.ts → observer/static.d.ts} +0 -0
  1402. /package/dist/{serve-tunnel.d.ts → observer/tunnel.d.ts} +0 -0
  1403. /package/dist/{terminal-encoding.d.ts → routes/terminal/encoding.d.ts} +0 -0
  1404. /package/dist/{concurrency.d.ts → run/concurrency.d.ts} +0 -0
  1405. /package/dist/{study-costs.d.ts → run/costs.d.ts} +0 -0
  1406. /package/dist/{run-narration-secrets.d.ts → run/transient-comms-secrets.d.ts} +0 -0
  1407. /package/dist/{tui-launch.d.ts → tui/launch.d.ts} +0 -0
@@ -1,2714 +0,0 @@
1
- import { isLocalBrowserLab, localBrowserDefaults, localBrowserUnsupportedReason } from "./local-runtime-config.js";
2
- import { resolveAutomaticAnalysis } from "./automatic-analysis-config.js";
3
- // humanish.lab.v2 — a lab is a COMPOSITION over code primitives, not a hardcoded kind.
4
- //
5
- // HONEST SCOPE (read before trusting field names): the engine routes by
6
- // subject.source × execution.target (disambiguated by the actor lane where both axes
7
- // collide) and consumes a deliberately small set of fields:
8
- // subject.source/repos/appUrl/serve/env/state/clone.{depth,fanout,keep}, actors[0].count,
9
- // execution.target + execution.desktop.codexAppServer, scenario.mode,
10
- // policies.redactRepos, defaults.open.
11
- // On the computer-use routes (app-url × e2b-desktop, and clone × e2b-desktop with a
12
- // computer-use actor), `actors[0].type` IS load-bearing: it must resolve to a registered
13
- // computer-use actor, and that descriptor runs the session. Those routes also consume
14
- // actors[0].{mission,persona,laneFocus.instruction,model,reasoningEffort}, execution.timeoutMs,
15
- // execution.desktop.{browser,resolution,sandboxTimeoutMs}, and (clone)
16
- // subject.{serve,env,state,clone.depth}.
17
- // On the scripted-browser route (app-url × local-or-absent, or clone × e2b-desktop, with a
18
- // registered scripted-browser actor), `actors[0].type` is equally load-bearing, and the route
19
- // consumes scenario.ref (REQUIRED there — the committed scenario's browser steps ARE what the
20
- // actor executes), actors[0].{persona,count}, and execution.timeoutMs. On the provisioned
21
- // clone slice it also consumes subject.{repos,serve,env,state,exposure,clone.depth} and
22
- // execution.desktop.template. actors[0].{mission,laneFocus,model} are inert on that route
23
- // because no model runs, and most execution.desktop.* fields remain forward-declared (device
24
- // presets belong to the cua route — scripted surfaces are the driver's own desktop/mobile
25
- // viewports where isMobile/DSF genuinely RENDER via playwright emulation).
26
- // On the other routes those fields remain FORWARD-DECLARED and NOT yet consumed —
27
- // parseLabConfig emits a warning listing any such field that is set, so `lab inspect` shows
28
- // the truth.
29
- //
30
- // NOTE on actors[0].count: it now carries ROUTE-SPECIFIC meanings — synthetic route: simCount;
31
- // scripted-browser route: surface roster {1 = desktop, 2 = desktop + mobile}, default 1 (the
32
- // defaults-table single-lane row governs; count: 2 is the declared override); computer-use
33
- // E2B route: the HOMOGENEOUS fan-out lane count (N identical lanes, each its own E2B desktop),
34
- // capped at 16; the in-process/local-app cua route stays single lane (no E2B to fan out).
35
- //
36
- // NOTE on actors[0].lanes / actors[0].roster (computer-use E2B route, this slice): a
37
- // DIFFERENTIATED fan-out roster — each `{ id?, persona?, device?, instruction?, target? }` becomes one
38
- // independent E2B desktop (per-lane worlds, the default topology). `roster[]` is parser sugar for
39
- // repeated groups and is normalized into `lanes[]` before the engine sees it. `lanes|roster` XOR
40
- // `count` (declare a differentiated roster OR a homogeneous count, never both); `lanes|roster`
41
- // XOR `actors[0].laneFocus` (per-lane `instruction` is the roster's steer); `lanes[].device` XOR
42
- // raw `execution.desktop.resolution`. `execution.concurrency` bounds in-flight lanes (default
43
- // min(laneCount, 3); env HUMANISH_CUA_MAX_CONCURRENCY may only LOWER it — invariant 3). On every
44
- // non-cua route normalized `lanes` are inert (warned). subject.clone.fanout is REJECTED on the cua
45
- // route. `lanes[].target` is app-url × computer-use ONLY: an absolute browser URL this lane opens
46
- // instead of `subject.appUrl`; it is the generic setup-produced-target handoff, not a service
47
- // topology primitive.
48
- //
49
- // There is deliberately NO v1 compatibility: v1 had zero real users. Breaking schema changes
50
- // bump the version honestly.
51
- import { normalizeExtraExcludeEntry } from "./source-archive.js";
52
- import { actorRegistry } from "./actor-registry.js";
53
- import { containsSensitive } from "./redaction.js";
54
- import { DEVICE_PRESET_NAMES, isDevicePresetName } from "./device-presets.js";
55
- import { isReasoningEffort, reasoningEffortNames } from "./reasoning-effort.js";
56
- import { isExactRuntimeVersion } from "./terminal-runtime.js";
57
- import { isMaxOutputTokens } from "./output-token-limit.js";
58
- export const LAB_CONFIG_SCHEMA = "humanish.lab.v2";
59
- // Must start alphanumeric so an id never collides with the path-vs-id resolver heuristic
60
- // (a leading "." or "/" is read as a file path; a leading "-" collides with CLI flags).
61
- const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
62
- /**
63
- * Validate a parsed YAML object into a LabConfig. Pure: the caller owns file IO. Structural
64
- * validation only. Fields the engine does not yet consume are accepted but reported in
65
- * `warnings` so `lab inspect` never silently swallows a setting that does nothing.
66
- */
67
- export function parseLabConfig(raw) {
68
- if (!isRecord(raw)) {
69
- return invalid("Lab manifest must be a YAML object.");
70
- }
71
- if (raw.schema !== LAB_CONFIG_SCHEMA) {
72
- return invalid(`Lab schema must be ${LAB_CONFIG_SCHEMA}.`);
73
- }
74
- const id = str(raw.id);
75
- if (!id || !ID_PATTERN.test(id)) {
76
- return invalid("Lab id must be a public-safe token starting with a letter or digit (/^[A-Za-z0-9][A-Za-z0-9_.-]*$/).");
77
- }
78
- const subjectResult = parseSubject(raw.subject);
79
- if (!subjectResult.ok) {
80
- return subjectResult;
81
- }
82
- const actorsResult = parseActors(raw.actors);
83
- if (!actorsResult.ok) {
84
- return actorsResult;
85
- }
86
- const executionResult = parseExecution(raw.execution);
87
- if (!executionResult.ok) {
88
- return executionResult;
89
- }
90
- const config = {
91
- schema: LAB_CONFIG_SCHEMA,
92
- id,
93
- ...optionalStr("title", raw.title),
94
- ...optionalStr("description", raw.description),
95
- subject: subjectResult.value,
96
- actors: actorsResult.value,
97
- ...(executionResult.value ? { execution: executionResult.value } : {})
98
- };
99
- const personas = parsePersonas(raw.personas);
100
- if (personas)
101
- config.personas = personas;
102
- const scenarioResult = parseScenario(raw.scenario);
103
- if (!scenarioResult.ok) {
104
- return scenarioResult;
105
- }
106
- if (scenarioResult.value)
107
- config.scenario = scenarioResult.value;
108
- if (isRecord(raw.policies) && raw.policies.mediaPermission !== undefined && raw.policies.mediaPermission !== "prompt" && raw.policies.mediaPermission !== "granted") {
109
- return invalid("`policies.mediaPermission` must be `prompt` (the participant answers the browser's own dialog) or `granted`.");
110
- }
111
- const policies = parsePolicies(raw.policies);
112
- if (policies)
113
- config.policies = policies;
114
- const reviewResult = parseReview(raw.review);
115
- if (!reviewResult.ok)
116
- return reviewResult;
117
- if (reviewResult.value)
118
- config.review = reviewResult.value;
119
- const defaults = parseDefaults(raw.defaults);
120
- if (defaults)
121
- config.defaults = defaults;
122
- const commsResult = parseComms(raw.comms);
123
- if (!commsResult.ok)
124
- return commsResult;
125
- if (commsResult.value)
126
- config.comms = commsResult.value;
127
- if (config.comms?.email?.smtp && config.subject.topology === "shared-world") {
128
- return invalid("SMTP capture is not yet wired for shared-world studies. Use per-lane worlds for SMTP, or configure supported HTTP email capture for concurrent shared-world studies.");
129
- }
130
- const mediaReason = desktopMediaValidationReason(config);
131
- if (mediaReason)
132
- return invalid(mediaReason);
133
- const outputLimitReason = outputTokenLimitValidationReason(config);
134
- if (outputLimitReason)
135
- return invalid(outputLimitReason);
136
- // All-parallel default (#350): a multi-seat computer-use lab that does not declare
137
- // execution.concurrency runs EVERY seat at once — the declared field is a cap the author chose,
138
- // never a mode. A throttle default silently turned "N actors live" into waves of 3 in the field;
139
- // total sessions and spend are identical either way, only simultaneity differs, so the default
140
- // follows the author's roster. Resolved here at parse time so routing (sequential vs concurrent
141
- // shared-world), validation, warnings, and both engines all see one explicit number. The
142
- // sequential shared-world PoC stays available as an explicit choice: `execution.concurrency: 1`.
143
- {
144
- const seats = config.actors[0]?.lanes?.length ?? config.actors[0]?.count ?? 1;
145
- if (seats > 1 && config.execution?.concurrency === undefined && routesToComputerUse(config)) {
146
- config.execution = { ...(config.execution ?? {}), concurrency: seats };
147
- }
148
- }
149
- // Email that just works (#351): the funnel's ONLY handoff to an actor is the per-lane inbox
150
- // instruction, gated on recipients[]. Guessed lane names broke a field run — recipients copied
151
- // from a single-lane example matched nothing, so every actor was left inbox-blind with zero
152
- // signal. Omitted recipients are therefore FILLED (one deterministic address per lane); a
153
- // recipient naming an unknown lane is a hard error listing the real lane ids; declared
154
- // recipients covering zero lanes are a hard error (a guaranteed-dead funnel).
155
- const receivingReason = receivingEmailValidationReason(config);
156
- if (receivingReason)
157
- return invalid(receivingReason);
158
- if (config.comms?.email?.kind === "fake" && routesToComputerUse(config)) {
159
- const laneIds = effectiveComputerUseLaneIds(config);
160
- const email = config.comms.email;
161
- if (email.recipients === undefined) {
162
- email.recipients = laneIds.map((lane) => ({ lane, address: `${lane.toLowerCase()}@example.test` }));
163
- }
164
- else {
165
- const unknown = email.recipients.filter((recipient) => !laneIds.includes(recipient.lane));
166
- if (unknown.length > 0) {
167
- return invalid(`comms.email.recipients name lane(s) that do not exist: ${unknown.map((r) => `"${r.lane}"`).join(", ")}. This lab's lane ids are: ${laneIds.join(", ")}. A recipient's lane must match one of them exactly — the inbox instruction is injected per lane, and a mismatch disables the email funnel for that seat.`);
168
- }
169
- if (!email.recipients.some((recipient) => recipient.address !== undefined)) {
170
- return invalid("comms.email.recipients cover no lane with an address — no actor would be told an inbox exists and no captured mail could match. Give at least one recipient an address, or omit `recipients` entirely (every lane then gets a deterministic address automatically).");
171
- }
172
- }
173
- }
174
- // this-repo subjects run locally and dry-run only — there is no live execution target for the
175
- // host repo (clone/app-url provide that). Reject the mis-configs rather than silently mishandle.
176
- if (config.subject.source === "this-repo") {
177
- if (config.execution?.target) {
178
- return invalid("`execution.target` applies only to clone/app-url/local-app subjects; this-repo labs run locally.");
179
- }
180
- if (config.scenario?.mode === "live") {
181
- return invalid("this-repo labs are dry-run only; use a clone or app-url subject for a live run.");
182
- }
183
- }
184
- // local-app route: an already-running LOCAL dev server driven IN-PROCESS via a custom
185
- // CuaExecutor (no clone, no E2B desktop). Parse-validated fail-closed: a computer-use actor
186
- // only, execution.target local or absent (NEVER e2b-desktop — the whole point is to skip the
187
- // desktop), and no public-target policy (it is always loopback; the loopback shape was already
188
- // enforced in parseSubject). The actual "no buildExecutor hook supplied" case is inherently an
189
- // engine-time decision (the parser cannot know whether a library caller will pass hooks), so
190
- // it fails closed in runCuaActorLab with HUMANISH_CUA_LAB_LOCAL_APP_NO_EXECUTOR.
191
- if (config.subject.source === "local-app") {
192
- const type = config.actors[0]?.type ?? "";
193
- if (config.execution?.target !== undefined && config.execution.target !== "local") {
194
- return invalid("local-app subjects drive an in-process LOCAL dev server with NO E2B desktop — set `execution.target: local` or omit it (absent means local); `e2b-desktop` is rejected (use an app-url subject for the hosted-desktop route).");
195
- }
196
- if (!actorResolvesToComputerUse(type)) {
197
- return invalid(`actors[0].type must be a registered computer-use actor for local-app subjects (one of: ${registeredComputerUseActors().join(", ")}); the caller's custom executor runs the computer-use loop. Got "${type}".`);
198
- }
199
- if (cuaLaneCount(config) > 1) {
200
- return invalid("Multi-lane fan-out is not supported on the in-process/local-app route — fan-out provisions one independent E2B desktop per lane, which the in-process route deliberately skips; set actors[0].count to 1 and drop actors[0].lanes (use an app-url or clone subject on execution.target: e2b-desktop for fan-out).");
201
- }
202
- if (config.actors[0]?.lanes !== undefined) {
203
- return invalid("`actors[0].lanes` (fan-out roster) is not supported on the in-process/local-app route — it provisions one E2B desktop per lane, which this route skips. Use an app-url or clone subject with execution.target: e2b-desktop.");
204
- }
205
- if (config.policies?.allowPublicTargets === true) {
206
- return invalid("`policies.allowPublicTargets` is not supported on the local-app route — a local-app subject is always a loopback dev server; there is no public target to allow.");
207
- }
208
- }
209
- // app-url routes: the actor type is a REAL dispatch key (registry-resolved). The actor LANE
210
- // picks the substrate: a scripted-browser actor runs locally against the declared loopback
211
- // app; a computer-use actor drives a hosted desktop browser. Fail closed on mis-configs.
212
- if (config.subject.source === "app-url") {
213
- const type = config.actors[0]?.type ?? "";
214
- if (actorResolvesToScriptedBrowser(type)) {
215
- // Scripted-browser route (all fail-closed: invariant 6 — a field that cannot act on
216
- // this route is rejected, never silently ignored).
217
- if (config.execution?.target !== undefined && config.execution.target !== "local") {
218
- return invalid("scripted-browser actors run on the operator's machine — set `execution.target: local` or omit it (absent means local); in-sandbox scripted execution is a later slice.");
219
- }
220
- if (!config.scenario?.ref) {
221
- return invalid("scripted-browser labs require `scenario.ref` — the committed scenario's browser steps are what this actor executes; there is no built-in fallback on the lab route.");
222
- }
223
- if ((config.actors[0]?.count ?? 1) > 2) {
224
- return invalid("scripted-browser labs support actors[0].count of 1 (desktop surface) or 2 (desktop + mobile); larger fan-out is a later slice.");
225
- }
226
- if (config.policies?.redactScreenshots === true) {
227
- return invalid("`policies.redactScreenshots: true` is not implemented on the scripted-browser route yet — screenshots persist raw in gitignored .humanish; a silently ignored redaction policy would be a safety lie, so it is rejected.");
228
- }
229
- if (config.policies?.allowPublicTargets === true) {
230
- return invalid("`policies.allowPublicTargets` is not supported on the scripted-browser route — the scripted step driver enforces loopback at every navigation; public targets on this route are a later slice.");
231
- }
232
- if (!isLoopbackUrl(config.subject.appUrl ?? "")) {
233
- return invalid("`subject.appUrl` must be a loopback URL (127.0.0.1/localhost) on the scripted-browser route.");
234
- }
235
- }
236
- else {
237
- if (config.execution?.target !== "e2b-desktop" && config.execution?.target !== "local") {
238
- return invalid("app-url computer-use subjects require `execution.target: local` or `e2b-desktop`.");
239
- }
240
- if (!actorResolvesToComputerUse(type)) {
241
- return invalid(`actors[0].type must be a registered computer-use actor for app-url × e2b-desktop labs (one of: ${registeredComputerUseActors().join(", ")}); for local scripted execution use a registered scripted-browser actor (${registeredScriptedBrowserActors().join(", ")}). Got "${type}".`);
242
- }
243
- // Multi-lane fan-out is CONSUMED on this route (per-lane worlds; the shared cua-lane
244
- // cross-validation below enforces lanes/count XOR rules, the 16 cap, and the
245
- // lane-level target gates, and the allowPublicTargets+N>1 rejection for ambiguous one-target
246
- // fan-out).
247
- // Loopback by default; an owner may declare a public/preview target via policies.
248
- const laneTargets = declaredLaneTargets(config);
249
- const declaredTargets = [config.subject.appUrl ?? "", ...laneTargets];
250
- const unsafeTarget = declaredTargets.find((target) => !config.policies?.allowPublicTargets && !isLoopbackUrl(target));
251
- if (unsafeTarget !== undefined) {
252
- return invalid("`subject.appUrl` and `actors[0].lanes[].target` must be loopback URLs (127.0.0.1/localhost) unless `policies.allowPublicTargets: true` is set — set it to drive deployed/preview URLs you own.");
253
- }
254
- }
255
- }
256
- else if (actorResolvesToScriptedBrowser(config.actors[0]?.type)) {
257
- if (config.subject.source !== "clone") {
258
- return invalid("scripted-browser actors require `subject.source: app-url` (a running app at a loopback URL) or `subject.source: clone` with `execution.target: e2b-desktop` (a provisioned synthetic subject).");
259
- }
260
- if (config.execution?.target !== "e2b-desktop") {
261
- return invalid("clone subjects with scripted-browser actors require `execution.target: e2b-desktop` — the lab provisions the clone in E2B, exposes it with getHost, then drives deterministic browser steps.");
262
- }
263
- if (!config.subject.serve) {
264
- return invalid("clone subjects with scripted-browser actors require `subject.serve` (start + url) — the lab serves the app in-sandbox before the scripted browser drives it.");
265
- }
266
- if ((config.subject.repos?.length ?? 0) !== 1) {
267
- return invalid("clone scripted-browser labs require exactly one repo in subject.repos.");
268
- }
269
- const repo = config.subject.repos?.[0] ?? "";
270
- if (!REPO_SLUG_PATTERN.test(repo)) {
271
- return invalid(`subject.repos[0] must be an owner/repo slug (got "${repo}").`);
272
- }
273
- if (config.subject.topology !== undefined) {
274
- return invalid("clone scripted-browser labs do not support `subject.topology` yet — this slice provisions one synthetic subject and one deterministic scripted actor roster, not a shared-world run.");
275
- }
276
- if (config.subject.clone?.fanout !== undefined || config.subject.clone?.keep === true) {
277
- return invalid("clone scripted-browser labs do not support `subject.clone.fanout` or `subject.clone.keep` yet — the provisioned subject is always a single disposable E2B sandbox.");
278
- }
279
- if (!config.scenario?.ref) {
280
- return invalid("scripted-browser labs require `scenario.ref` — the committed scenario's browser steps are what this actor executes; there is no built-in fallback on the lab route.");
281
- }
282
- if ((config.actors[0]?.count ?? 1) > 2) {
283
- return invalid("scripted-browser labs support actors[0].count of 1 (desktop surface) or 2 (desktop + mobile); larger fan-out is a later slice.");
284
- }
285
- if (config.actors[0]?.lanes !== undefined) {
286
- return invalid("`actors[0].lanes` is not supported on the scripted-browser route yet — use actors[0].count for the deterministic surface roster.");
287
- }
288
- if (config.policies?.redactScreenshots === true) {
289
- return invalid("`policies.redactScreenshots: true` is not implemented on the scripted-browser route yet — screenshots persist raw in gitignored .humanish; a silently ignored redaction policy would be a safety lie, so it is rejected.");
290
- }
291
- if (config.policies?.allowPublicTargets === true) {
292
- return invalid("`policies.allowPublicTargets` is not supported on the clone scripted-browser route — the only external host is the harness-minted getHost URL for a provisioned synthetic subject.");
293
- }
294
- if (config.subject.exposure !== "synthetic") {
295
- return invalid("clone scripted-browser labs require `subject.exposure: synthetic` — the subject is exposed on an internet-reachable getHost URL for the run, so the author must attest it is synthetic seeded data.");
296
- }
297
- if (!config.subject.state?.seed || config.subject.state.seed.length === 0 || (config.subject.state.external?.length ?? 0) > 0) {
298
- return invalid("clone scripted-browser labs require `subject.state.seed` and do not allow `subject.state.external` — getHost-exposed subjects must be synthetic seeded data, not external/unpinned state.");
299
- }
300
- if (!config.subject.serve.start.includes("0.0.0.0")) {
301
- return invalid("clone scripted-browser labs require `subject.serve.start` to bind all interfaces (e.g. `-H 0.0.0.0` / `--host 0.0.0.0` / `HOST=0.0.0.0`) — getHost only routes to a 0.0.0.0-bound port; the readiness probe stays loopback.");
302
- }
303
- }
304
- // clone × e2b-desktop disambiguates on the actor lane: a computer-use actor means the lab
305
- // clones AND serves the subject in-sandbox, then drives it (the meta route otherwise).
306
- if (config.subject.source === "clone" && config.execution?.target === "e2b-desktop"
307
- && actorResolvesToComputerUse(config.actors[0]?.type)) {
308
- if (!config.subject.serve) {
309
- return invalid("clone subjects on the computer-use route require `subject.serve` (start + url) — the lab serves the app in-sandbox before the actor drives it.");
310
- }
311
- if ((config.subject.repos?.length ?? 0) !== 1) {
312
- return invalid("computer-use clone labs run a single lane; declare exactly one repo in subject.repos.");
313
- }
314
- const repo = config.subject.repos?.[0] ?? "";
315
- if (!REPO_SLUG_PATTERN.test(repo)) {
316
- return invalid(`subject.repos[0] must be an owner/repo slug (got "${repo}").`);
317
- }
318
- // Fan-out is CONSUMED here: N lanes each clone the SAME single repo into their own E2B
319
- // desktop (per-lane worlds). The shared cua-lane cross-validation below enforces the
320
- // lanes/count rules and the 16 cap; the single-repo rule above is unchanged.
321
- }
322
- // local-tree route: packs and uploads the operator's own working tree, then serves it exactly
323
- // like a computer-use clone subject. There is no smoke/meta/scripted equivalent for a packed
324
- // working tree in this slice, so e2b-desktop + a computer-use actor are the ONLY combination
325
- // this source supports. `subject.serve` is already required at parse time (parseSubject); the
326
- // repos/clone rejection also already happened there (local-tree never carries git slugs).
327
- if (config.subject.source === "local-tree") {
328
- if (config.execution?.target !== "e2b-desktop") {
329
- return invalid("local-tree subjects require `execution.target: e2b-desktop`: the packed working tree is provisioned and served inside a hosted desktop sandbox; there is no local/smoke route for a local-tree subject.");
330
- }
331
- if (!actorResolvesToComputerUse(config.actors[0]?.type)) {
332
- return invalid(`actors[0].type must be a registered computer-use actor for local-tree subjects (one of: ${registeredComputerUseActors().join(", ")}); the actor drives the hosted desktop that serves the packed working tree. Got "${config.actors[0]?.type ?? ""}".`);
333
- }
334
- }
335
- // Shared computer-use fan-out cross-validation (per-lane worlds, the only topology this
336
- // slice). Runs for every route that resolves to the cua backend (app-url, clone, local-app).
337
- // The in-process/local-app route already forced a single lane above, so this is a no-op there
338
- // beyond rejecting the same fields; on the E2B routes it enforces the roster contract.
339
- if (routesToComputerUse(config)) {
340
- const reason = cuaLaneValidationReason(config);
341
- if (reason) {
342
- return invalid(reason);
343
- }
344
- }
345
- // Shared-world topology cross-validation (#164). Runs whenever shared-world is DECLARED (not just
346
- // when it routes), so a half-declared shared-world fails closed with a precise reason rather than
347
- // silently downgrading to a per-lane-worlds cua run. With `execution.concurrency > 1` the
348
- // concurrent extras (synthetic-subject attestation, 0.0.0.0 serve bind, no clone.keep) also apply.
349
- if (config.subject.topology === "shared-world") {
350
- const reason = config.subject.source === "app-url"
351
- // The external-public plane (a real public deployment as the shared plane): NEVER the getHost
352
- // synthetic gate — that gate exists because getHost is internet-reachable AND harness-owned; a
353
- // public site the harness neither provisioned nor exposed has neither property.
354
- ? externalPublicSharedWorldValidationReason(config)
355
- : (config.execution?.concurrency ?? 1) > 1
356
- ? concurrentSharedWorldValidationReason(config)
357
- : sharedWorldValidationReason(config);
358
- if (reason) {
359
- return invalid(reason);
360
- }
361
- }
362
- // desktop-cli route: a computer-use participant studies a CLI/TUI the way a person does — at a
363
- // desktop, in a terminal window, by looking at it. The sibling of terminal-product, and the
364
- // distinction is the POPULATION, not the product: terminal-product sends an autonomous agent
365
- // through a pipe with stdin disabled, which is the honest way to study what an agent meets and
366
- // structurally cannot study an interactive surface. This route sends someone who can see it.
367
- //
368
- // Fail-closed on the pairing (invariant 6): a hosted desktop and a computer-use actor, because
369
- // "watch a person use a terminal" is not something the other substrates can do.
370
- if (config.subject.source === "desktop-cli") {
371
- if (config.subject.product?.name === undefined) {
372
- return invalid("desktop-cli subjects need `subject.product.name` — the CLI the participant is being asked to use.");
373
- }
374
- if (config.execution?.target !== undefined && config.execution.target !== "e2b-desktop") {
375
- return invalid("desktop-cli subjects are studied at a hosted desktop — set `execution.target: e2b-desktop` or omit it.");
376
- }
377
- if (!actorResolvesToComputerUse(config.actors[0]?.type ?? "")) {
378
- return invalid("desktop-cli subjects need a registered computer-use actor: the participant reads the screen and types, which is what makes an interactive surface studiable at all.");
379
- }
380
- const install = config.subject.product.install;
381
- if (install !== undefined && install.trim().length === 0) {
382
- return invalid("`subject.product.install` must be a non-empty command when set (omit it to study the install itself).");
383
- }
384
- }
385
- // terminal-product route: a real autonomous agent studies a CLI/product from PUBLIC surfaces
386
- // inside an E2B shell. Fail-closed (invariant 6 — a field that cannot act on this route is an
387
- // honest parse error): a registered terminal actor only, execution.target e2b-terminal or absent
388
- // (absent defaults to e2b-terminal — the only honest target for an in-sandbox agent), single
389
- // lane until fan-out lands.
390
- if (config.subject.source === "terminal-product") {
391
- const type = config.actors[0]?.type ?? "";
392
- if (config.execution?.target !== undefined && config.execution.target !== "e2b-terminal") {
393
- return invalid("terminal-product subjects run the agent inside an E2B shell — set `execution.target: e2b-terminal` or omit it (absent means e2b-terminal); `local`/`e2b-desktop` are rejected.");
394
- }
395
- if (!actorResolvesToTerminal(type)) {
396
- return invalid(`actors[0].type must be a registered terminal actor for terminal-product subjects (one of: ${registeredTerminalActors().join(", ")}). Got "${type}".`);
397
- }
398
- if ((config.actors[0]?.count ?? 1) > 1) {
399
- return invalid("Multi-lane terminal fan-out is not supported yet; set actors[0].count to 1.");
400
- }
401
- }
402
- else if (config.execution?.target === "e2b-terminal") {
403
- // e2b-terminal is the terminal-product substrate ONLY. Any other source declaring it is a
404
- // mis-config — reject, never silently mishandle (mirrors app-url's e2b-desktop pairing rule).
405
- return invalid("`execution.target: e2b-terminal` requires `subject.source: terminal-product` with a registered terminal actor.");
406
- }
407
- else if (actorResolvesToTerminal(config.actors[0]?.type)) {
408
- // A registered terminal actor on a non-terminal-product subject: rejected, never ignored (the
409
- // terminal agent only studies a declared terminal-product from public surfaces).
410
- return invalid("terminal actors require `subject.source: terminal-product` (a CLI/product the agent studies from public surfaces); other subjects are not supported on this route.");
411
- }
412
- const tasksReason = taskProtocolValidationReason(config);
413
- if (tasksReason)
414
- return invalid(tasksReason);
415
- const analysisReason = automaticAnalysisRouteReason(config);
416
- if (analysisReason)
417
- return invalid(analysisReason);
418
- const normalized = localBrowserDefaults(config);
419
- if (isLocalBrowserLab(normalized)) {
420
- const reason = localBrowserUnsupportedReason(normalized);
421
- if (reason)
422
- return invalid(reason);
423
- }
424
- return { ok: true, config: normalized, warnings: forwardDeclaredWarnings(normalized) };
425
- }
426
- // The slug interpolates into an in-sandbox shell command; the strict shape is load-bearing.
427
- const REPO_SLUG_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
428
- const ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
429
- // A lane id interpolates into per-lane evidence paths (screenshots/<id>/, actors/<id>.json), so
430
- // it must be a public-safe path token, same shape as a lab id.
431
- const LANE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
432
- const LANE_ID_MAX_CHARS = 40;
433
- const LANE_METADATA_MAX_CHARS = 80;
434
- // Hard cap on fan-out lanes (per the ratified design). No HUMANISH_MAX_LANES escape above this
435
- // until a reference panel demands it — N concurrent paid desktops is real money.
436
- export const MAX_CUA_LANES = 16;
437
- function actorResolvesToComputerUse(type) {
438
- if (!type)
439
- return false;
440
- const descriptor = actorRegistry[type];
441
- return Boolean(descriptor?.capabilities.lanes.includes("computer-use"));
442
- }
443
- function registeredComputerUseActors() {
444
- return Object.values(actorRegistry)
445
- .filter((entry) => entry.capabilities.lanes.includes("computer-use"))
446
- .map((entry) => entry.id);
447
- }
448
- function actorResolvesToScriptedBrowser(type) {
449
- if (!type)
450
- return false;
451
- const descriptor = actorRegistry[type];
452
- return Boolean(descriptor?.capabilities.lanes.includes("scripted-browser"));
453
- }
454
- function registeredScriptedBrowserActors() {
455
- return Object.values(actorRegistry)
456
- .filter((entry) => entry.capabilities.lanes.includes("scripted-browser"))
457
- .map((entry) => entry.id);
458
- }
459
- /** True when `type` resolves to a registered terminal actor (the "terminal" lane). Exported so
460
- * the engine + tests can resolve the dispatch the same way the parser does. */
461
- export function actorResolvesToTerminal(type) {
462
- if (!type)
463
- return false;
464
- const descriptor = actorRegistry[type];
465
- return Boolean(descriptor?.capabilities.lanes.includes("terminal"));
466
- }
467
- function registeredTerminalActors() {
468
- return Object.values(actorRegistry)
469
- .filter((entry) => entry.capabilities.lanes.includes("terminal"))
470
- .map((entry) => entry.id);
471
- }
472
- /**
473
- * True when this config routes to the computer-use backend: an app-url subject whose first
474
- * actor resolves to a registered computer-use actor, or a clone subject on a hosted desktop
475
- * whose first actor does. Single source of truth — selectLabBackend and the warning logic
476
- * both use it. (The app-url branch used to be unconditionally true; it narrowed when the
477
- * scripted-browser lane arrived. Behavior-preserving for every parse-valid config —
478
- * selectLabBackend keeps a bare app-url fallback to the cua backend so library-API configs
479
- * with unknown actors still hit its fail-closed ACTOR_UNSUPPORTED.)
480
- */
481
- /**
482
- * The declared fan-out lane count on the computer-use route: a `lanes[]` roster's length, else
483
- * a homogeneous `count`, else 1. The single source of truth shared by the parser, the engine,
484
- * and the pre-flight plan so the lane count is computed ONE way everywhere.
485
- */
486
- export function cuaLaneCount(config) {
487
- const actor = config.actors[0];
488
- if (actor?.lanes !== undefined) {
489
- return actor.lanes.length;
490
- }
491
- return actor?.count ?? 1;
492
- }
493
- /**
494
- * Cross-validate the computer-use fan-out declaration (per-lane worlds). Returns the failure
495
- * message, or null when valid. Enforced at parse AND re-enforced in the engine (runCuaActorLab
496
- * is itself exported npm surface). Structural lane shape (id/device validity, id uniqueness) is
497
- * already checked in parseLanes; this is the route-scoped XOR/cap/policy layer.
498
- */
499
- export function cuaLaneValidationReason(config) {
500
- const actor = config.actors[0];
501
- const lanes = actor?.lanes;
502
- const structuralReason = laneRosterStructuralValidationReason(config);
503
- if (structuralReason) {
504
- return structuralReason;
505
- }
506
- // clone.fanout is a DECLARED behavior change: rejected on the cua route (was inert-warned).
507
- // Fan-out is declared via actors[0].count/lanes; subject.clone.fanout never applied here.
508
- if (config.subject.clone?.fanout !== undefined) {
509
- return "`subject.clone.fanout` is not used on the computer-use route — declare fan-out with actors[0].count (homogeneous) or actors[0].lanes (a per-lane roster). (clone.fanout drives the OSS smoke/meta routes only.)";
510
- }
511
- if (lanes !== undefined) {
512
- if (actor?.count !== undefined) {
513
- return "Declare EITHER actors[0].count (a homogeneous lane count) OR actors[0].lanes (a differentiated roster), not both.";
514
- }
515
- if (actor?.laneFocus !== undefined) {
516
- return "actors[0].laneFocus and actors[0].lanes are mutually exclusive — a roster's per-lane `instruction` is the fan-out steer; laneFocus is the single-lane steer.";
517
- }
518
- if (config.execution?.desktop?.resolution !== undefined && lanes.some((lane) => lane.device !== undefined)) {
519
- return "actors[0].lanes[].device and a raw execution.desktop.resolution are mutually exclusive — a per-lane device preset and a single hand-set resolution cannot both govern lane geometry.";
520
- }
521
- const targeted = lanes.filter((lane) => lane.target !== undefined);
522
- if (targeted.length > 0) {
523
- if (config.subject.source !== "app-url") {
524
- return "actors[0].lanes[].target is supported only on app-url computer-use labs — clone/shared-world/local-app routes provision or own their entry URL by mechanism.";
525
- }
526
- if (lanes.some((lane) => lane.entry !== undefined)) {
527
- return "actors[0].lanes[].target and actors[0].lanes[].entry are mutually exclusive — target is an app-url fan-out browser URL; entry is a shared-world same-origin seat path.";
528
- }
529
- if (targeted.length !== lanes.length) {
530
- return "When any actors[0].lanes[].target is declared, every lane in the roster must declare target — this keeps the setup-produced target contract explicit and prevents accidental mixed worlds.";
531
- }
532
- }
533
- }
534
- const laneCount = cuaLaneCount(config);
535
- if (laneCount > MAX_CUA_LANES) {
536
- return `Computer-use fan-out is capped at ${MAX_CUA_LANES} lanes (declared ${laneCount}); N concurrent paid desktops is real spend — there is no override above the cap this slice.`;
537
- }
538
- // Public targets fan out into N independent worlds driving the SAME public app — that is an
539
- // ambiguous shared-world-ish shape, not a per-lane target swarm. Permit N>1 public runs only when
540
- // every roster lane declares its own target, making the adapter-owned topology explicit. But when
541
- // `subject.topology: shared-world` is ALSO declared, N lanes against one public target is the
542
- // EXTERNAL-PUBLIC shared-world topology (#164 phase 2) — ROUTE it there (a real public deployment
543
- // as the shared plane) instead of refusing; externalPublicSharedWorldValidationReason then applies.
544
- if (laneCount > 1 && config.policies?.allowPublicTargets === true && declaredLaneTargets(config).length === 0
545
- && config.subject.topology !== "shared-world") {
546
- return "policies.allowPublicTargets cannot be combined with multi-lane fan-out (N>1) — N lanes against one declared public target is the SHARED-WORLD topology (layer 7, #164), not per-lane worlds. Declare `subject.topology: shared-world` to run the external-public shared-world route, fan out against a loopback/provisioned subject, or run a single public-target lane.";
547
- }
548
- return null;
549
- }
550
- /**
551
- * Engine-level path-token validation for configs supplied directly through the
552
- * public TypeScript/JavaScript API instead of parseLabConfig.
553
- */
554
- export function laneRosterStructuralValidationReason(config) {
555
- const lanes = config.actors[0]?.lanes;
556
- const seenIds = new Set();
557
- if (lanes !== undefined) {
558
- if (!Array.isArray(lanes) || lanes.length === 0) {
559
- return "actors[0].lanes must be a non-empty array when set.";
560
- }
561
- for (const [index, lane] of lanes.entries()) {
562
- if (!lane || typeof lane !== "object" || Array.isArray(lane)) {
563
- return `actors[0].lanes[${index}] must be an object.`;
564
- }
565
- const id = lane.id;
566
- if (id === undefined) {
567
- continue;
568
- }
569
- if (typeof id !== "string" || !LANE_ID_PATTERN.test(id) || id.length > LANE_ID_MAX_CHARS) {
570
- return `actors[0].lanes[${index}].id must be a public-safe path token matching ${LANE_ID_PATTERN} and at most ${LANE_ID_MAX_CHARS} chars.`;
571
- }
572
- if (seenIds.has(id)) {
573
- return `actors[0].lanes ids must be unique (duplicate "${id}").`;
574
- }
575
- seenIds.add(id);
576
- }
577
- }
578
- return null;
579
- }
580
- function declaredLaneTargets(config) {
581
- return (config.actors[0]?.lanes ?? [])
582
- .map((lane) => lane.target)
583
- .filter((target) => target !== undefined);
584
- }
585
- /**
586
- * Cross-validate a `topology: shared-world` declaration (#164). Returns the failure message, or
587
- * null when valid. Enforced at parse AND re-enforced in the engine (runSharedWorldLab is exported
588
- * npm surface). The shared-world override REQUIRES: a clone or local-tree source + e2b-desktop
589
- * target + a computer-use actor + a `subject.serve` block + an `actors[0].lanes` roster of ≥2 roles (the
590
- * roster IS the role roster — no parallel roles[] field), and every role `entry` must resolve
591
- * same-origin (loopback) with serve.url. Fail-closed: a half-declared shared-world is rejected,
592
- * never silently downgraded.
593
- */
594
- export function sharedWorldValidationReason(config) {
595
- const structuralReason = laneRosterStructuralValidationReason(config);
596
- if (structuralReason) {
597
- return structuralReason;
598
- }
599
- if (config.subject.source !== "clone" && config.subject.source !== "local-tree") {
600
- return "`subject.topology: shared-world` requires `subject.source: clone` or `subject.source: local-tree` - the shared world is ONE provisioned, served, seeded plane (#164).";
601
- }
602
- if (config.execution?.target !== "e2b-desktop") {
603
- return "`subject.topology: shared-world` requires `execution.target: e2b-desktop` — the role seats drive hosted desktop browsers against one in-sandbox app.";
604
- }
605
- if (!actorResolvesToComputerUse(config.actors[0]?.type)) {
606
- return `\`subject.topology: shared-world\` requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}) — each role seat runs a computer-use session.`;
607
- }
608
- const serve = config.subject.serve;
609
- if (!serve) {
610
- return "`subject.topology: shared-world` requires `subject.serve` (start + url) — the lab serves ONE shared app in-sandbox that every role drives.";
611
- }
612
- const lanes = config.actors[0]?.lanes;
613
- if (!lanes || lanes.length < 2) {
614
- return "`subject.topology: shared-world` requires an `actors[0].lanes` roster of at least 2 roles (the roster IS the role roster — declare ≥2 lanes; a single-role shared world proves no interaction).";
615
- }
616
- if (!config.subject.state?.checkpoint || config.subject.state.checkpoint.length === 0) {
617
- return "`subject.topology: shared-world` requires `subject.state.checkpoint` (≥1 read-only digest probe) — the checkpoint timeline IS the interaction-attribution mechanism; without it the run cannot prove role B acted on role A's mutation.";
618
- }
619
- for (const lane of lanes) {
620
- if (lane.entry !== undefined && resolveSeatUrl(serve.url, lane.entry) === null) {
621
- return `actors[0].lanes role "${lane.id ?? "(unnamed)"}".entry must resolve same-origin (loopback) with subject.serve.url (${serve.url}); got "${lane.entry}".`;
622
- }
623
- }
624
- return null;
625
- }
626
- /**
627
- * The lane ids the computer-use engine will actually run: declared roster ids, else the generated
628
- * `lane-01..lane-NN` names. Mirrors the naming in cua-actor-lab.ts's laneSpecsAndPlan — a test
629
- * pins the two together — so comms recipient validation can never drift from the engine (#351).
630
- */
631
- export function effectiveComputerUseLaneIds(config) {
632
- const actor = config.actors[0];
633
- const roster = actor?.lanes;
634
- if (roster && roster.length > 0) {
635
- return roster.map((lane, index) => lane.id ?? `lane-${String(index + 1).padStart(2, "0")}`);
636
- }
637
- const count = Math.max(1, actor?.count ?? 1);
638
- return Array.from({ length: count }, (_, index) => `lane-${String(index + 1).padStart(2, "0")}`);
639
- }
640
- /**
641
- * Declared capture devices must be implemented by the selected execution route.
642
- * Unsupported backends pass false when called directly, where the declared
643
- * topology may not identify the backend that is actually executing.
644
- */
645
- export function desktopMediaValidationReason(config, supportsMedia = routesToComputerUse(config)) {
646
- if (config.execution?.desktop?.recording !== undefined
647
- && (!supportsMedia || config.subject.topology === "shared-world" || config.subject.source === "local-app")) {
648
- return "execution.desktop.recording is supported only on independent computer-use desktop lanes. Remove the declaration or select a supported route.";
649
- }
650
- const media = config.execution?.desktop?.media;
651
- if (media === undefined)
652
- return undefined;
653
- if (media.microphone !== undefined && media.microphone.source !== "speech") {
654
- return "execution.desktop.media.microphone.source must be speech. Microphone source-file injection is unsupported.";
655
- }
656
- if (config.subject.topology === "shared-world") {
657
- return "execution.desktop.media is unsupported on shared-world routes; declared capture devices would not be provisioned. Use independent computer-use browser lanes or remove the declaration.";
658
- }
659
- if (!supportsMedia || config.subject.source === "desktop-cli" || config.subject.source === "local-app") {
660
- return "execution.desktop.media is supported only on computer-use browser lanes (app-url, clone or local-tree), not this execution route. Remove the declaration or use a supported route.";
661
- }
662
- if (config.execution?.desktop?.browser === "firefox") {
663
- return "execution.desktop.media requires Chrome or Chromium; Firefox cannot receive the declared synthetic capture device. Set execution.desktop.browser: chrome or chromium.";
664
- }
665
- if (media.microphone !== undefined) {
666
- if (config.actors.some(actor => actor.type !== "local-agent" || (actor.localAgent !== undefined && actor.localAgent !== "codex"))) {
667
- return "Participant speech currently requires local-agent with Codex, on a local or hosted desktop.";
668
- }
669
- if (config.execution?.target !== "local" && media.camera !== undefined) {
670
- return "Hosted synthetic cameras replace Chromium's microphone and cannot be combined with speech. Use a local media desktop for camera and speech together, or omit the hosted camera.";
671
- }
672
- }
673
- return undefined;
674
- }
675
- export function routesToComputerUse(config) {
676
- // local-app drives the cua loop in-process (a custom executor + a non-vision provider), so it
677
- // routes to the cua backend exactly like an app-url subject with a computer-use actor.
678
- if (config.subject.source === "app-url" || config.subject.source === "local-app") {
679
- return actorResolvesToComputerUse(config.actors[0]?.type);
680
- }
681
- // desktop-cli hands the participant a terminal instead of a served page (#495), but it is the
682
- // same lane: same desktop, same actor, same prompt fields. Leaving it out of this predicate told
683
- // adopters their mission and persona were inert on the one route whose whole point is that a
684
- // person reads a screen.
685
- if (config.subject.source === "desktop-cli") {
686
- return (config.execution?.target === undefined || config.execution.target === "e2b-desktop")
687
- && actorResolvesToComputerUse(config.actors[0]?.type);
688
- }
689
- // local-tree packs+uploads the working tree, then serves it exactly like a computer-use clone
690
- // subject: same e2b-desktop + computer-use-actor gate.
691
- return (config.subject.source === "clone" || config.subject.source === "local-tree")
692
- && config.execution?.target === "e2b-desktop"
693
- && actorResolvesToComputerUse(config.actors[0]?.type);
694
- }
695
- /** Reused by direct library runners so unsupported receiving never becomes inert configuration. */
696
- export function receivingEmailValidationReason(config) {
697
- if (config.comms?.email?.kind !== "real")
698
- return undefined;
699
- if (!routesToComputerUse(config) || !["app-url", "clone", "local-tree"].includes(config.subject.source)) {
700
- return "Real email receiving requires a hosted computer-use browser study with an app-url, clone, or local-tree subject. Scripted, terminal, desktop-cli and local-app routes are unsupported.";
701
- }
702
- if (config.actors.some(actor => actor.type === "local-agent")) {
703
- return "Real email receiving is unavailable for local-agent: its host process does not isolate the inbox management credential. Use a hosted first-party computer-use actor.";
704
- }
705
- if (config.subject.topology === "shared-world" && (config.execution?.concurrency ?? 1) <= 1) {
706
- return "Real email receiving is unsupported for sequential shared-world studies. Use concurrent shared-world or independent participant desktops.";
707
- }
708
- return undefined;
709
- }
710
- /** Refuse task declarations that the selected execution path would discard (#737).
711
- * Direct runners pass their actual support rather than trusting the config's dispatch shape. */
712
- export function taskProtocolValidationReason(config, supportsTasks = routesToComputerUse(config) && !routesToSharedWorld(config)) {
713
- for (const [index, actor] of config.actors.entries()) {
714
- if (actor.tasks === undefined)
715
- continue;
716
- if (index > 0) {
717
- return `actors[${index}].tasks is unsupported: current runners consume only actors[0]. Use the first actor's supported CUA lanes for a task protocol.`;
718
- }
719
- if (!supportsTasks) {
720
- return "actors[0].tasks is unsupported on this execution path. Task protocols require a per-lane computer-use route; shared-world, terminal-product, scripted-browser, synthetic, smoke and meta routes do not consume them. Remove tasks only if a mission-only study is intended.";
721
- }
722
- }
723
- return null;
724
- }
725
- /** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
726
- export function outputTokenLimitValidationReason(config) {
727
- const actor = config.actors[0];
728
- if (actor?.maxOutputTokens === undefined)
729
- return null;
730
- if (!isMaxOutputTokens(actor.maxOutputTokens))
731
- return "actors[0].maxOutputTokens must be a positive safe integer.";
732
- if (actor.type !== "openai-computer-use" || !routesToComputerUse(config) || config.subject.source === "local-app") {
733
- return "actors[0].maxOutputTokens is supported only by first-party OpenAI computer-use routes; terminal, local-agent, scripted and custom in-process routes cannot enforce it.";
734
- }
735
- return null;
736
- }
737
- /**
738
- * True when this config routes to the SHARED-WORLD backend (#164): a clone or local-tree subject
739
- * on a hosted desktop whose first actor resolves to a computer-use actor AND that declares the
740
- * `shared-world` topology. Mirror of routesToComputerUse; the single source of truth shared by
741
- * selectLabBackend (which checks it BEFORE the cua route) and the warning logic. The same
742
- * clone/local-tree × e2b-desktop × computer-use composition WITHOUT `topology: shared-world` stays per-lane-worlds
743
- * (the cua route) — the topology declaration is the override switch.
744
- */
745
- export function routesToSharedWorld(config) {
746
- return routesToProvisionedSharedWorld(config) || routesToExternalPublicSharedWorld(config);
747
- }
748
- /** The getHost provisioned-subject shared-world shape (clone/local-tree served + exposed in-sandbox). */
749
- export function routesToProvisionedSharedWorld(config) {
750
- return (config.subject.source === "clone" || config.subject.source === "local-tree")
751
- && config.subject.topology === "shared-world"
752
- && config.execution?.target === "e2b-desktop"
753
- && actorResolvesToComputerUse(config.actors[0]?.type);
754
- }
755
- /**
756
- * The EXTERNAL-PUBLIC shared-world shape (#164 phase 2): a real PUBLIC deployment used DIRECTLY as
757
- * the shared plane — `source: app-url` + `topology: shared-world` + a computer-use actor on
758
- * e2b-desktop + `policies.allowPublicTargets: true`. NO getHost, NO clone, NO subject sandbox, NO
759
- * seed. The operator-ownership attestation `subject.publicTarget` is required (validated in
760
- * externalPublicSharedWorldValidationReason, not here — this predicate is the router only, so a
761
- * half-declared external-public config still routes here to get its precise fail-closed reason
762
- * rather than silently downgrading to the per-lane cua route). Always concurrent (concurrency > 1 is
763
- * enforced by the validation reason).
764
- */
765
- export function routesToExternalPublicSharedWorld(config) {
766
- return config.subject.source === "app-url"
767
- && config.subject.topology === "shared-world"
768
- && config.execution?.target === "e2b-desktop"
769
- && actorResolvesToComputerUse(config.actors[0]?.type)
770
- && config.policies?.allowPublicTargets === true;
771
- }
772
- /**
773
- * True when this config routes to the CONCURRENT shared-world backend (#164 phase 2): a shared-world
774
- * config with `execution.concurrency > 1` (N actor seats driving ONE plane AT ONCE). Two plane
775
- * classes: the getHost provisioned-subject shape (clone/local-tree) AND the external-public shape (a
776
- * real public deployment used directly as the plane — `source: app-url` + `allowPublicTargets`, no
777
- * getHost/clone/seed). An omitted `concurrency` is filled at parse with the seat count (all seats
778
- * live — the all-parallel default, #350), so multi-seat shared-world labs route here unless the
779
- * author explicitly declares `concurrency: 1`, which is the sequential PoC (getHost only).
780
- * selectLabBackend checks this BEFORE routesToSharedWorld.
781
- */
782
- export function routesToConcurrentSharedWorld(config) {
783
- return routesToSharedWorld(config) && (config.execution?.concurrency ?? 1) > 1;
784
- }
785
- /**
786
- * Cross-validate a CONCURRENT shared-world declaration (#164 phase 2). Returns the failure message,
787
- * or null when valid. Includes the base shared-world checks PLUS the concurrent extras: a synthetic
788
- * subject attestation (FIX-3), a 0.0.0.0 serve bind (FIX-4 — getHost only routes to a port bound on
789
- * all interfaces), and no `subject.clone.keep`/`subject.localTree.keep` (FIX-9 - either would
790
- * orphan actor sandboxes). Enforced at parse AND re-enforced in the engine (runConcurrentSharedWorld
791
- * is exported npm surface).
792
- */
793
- export function concurrentSharedWorldValidationReason(config) {
794
- const base = sharedWorldValidationReason(config);
795
- if (base) {
796
- return base;
797
- }
798
- if ((config.execution?.concurrency ?? 1) <= 1) {
799
- return "the concurrent shared-world route requires `execution.concurrency > 1` (N concurrent actor seats); concurrency 1 is the sequential PoC.";
800
- }
801
- if (config.subject.exposure !== "synthetic") {
802
- return "the concurrent shared-world route requires `subject.exposure: synthetic` — the subject is exposed on an internet-reachable getHost URL for the run, so the author must attest it is synthetic seeded data (no real/external data behind a getHost URL).";
803
- }
804
- const serve = config.subject.serve;
805
- if (!serve || !serve.start.includes("0.0.0.0")) {
806
- return "the concurrent shared-world route requires `subject.serve.start` to bind all interfaces (e.g. `-H 0.0.0.0` / `--host 0.0.0.0` / `HOST=0.0.0.0`) — getHost only routes to a 0.0.0.0-bound port; a loopback-only bind 502s. (The readiness probe stays loopback.)";
807
- }
808
- if (config.subject.clone?.keep === true || config.subject.localTree?.keep === true) {
809
- const keepField = config.subject.clone?.keep === true ? "subject.clone.keep" : "subject.localTree.keep";
810
- return `\`${keepField}\` is not supported on the concurrent shared-world route - it would orphan the N actor sandboxes (reclaimed only by server-timeout, not by id). All N+1 sandboxes are torn down by id.`;
811
- }
812
- return null;
813
- }
814
- /**
815
- * Cross-validate the EXTERNAL-PUBLIC shared-world declaration (#164 phase 2): a real PUBLIC
816
- * deployment used DIRECTLY as the shared plane (no getHost, no clone, no subject sandbox, no seed).
817
- * The honest analog of concurrentSharedWorldValidationReason for a plane the harness does NOT own:
818
- * it FORBIDS every provisioned-subject field (serve/state.seed/state.checkpoint/exposure/clone/repos
819
- * are inert with no sandbox — fail closed, never silently ignored, per invariant 6), and REQUIRES a
820
- * non-loopback appUrl + allowPublicTargets + the operator-ownership attestation subject.publicTarget +
821
- * concurrency > 1 + an actors[0].lanes roster of ≥2 with EXACTLY ONE host lane. The getHost synthetic
822
- * gate is deliberately unreachable here (there is no internet-reachable harness-owned URL to attest).
823
- * Enforced at parse AND re-enforced in the engine (runConcurrentSharedWorld is exported npm surface).
824
- */
825
- export function externalPublicSharedWorldValidationReason(config) {
826
- const structuralReason = laneRosterStructuralValidationReason(config);
827
- if (structuralReason) {
828
- return structuralReason;
829
- }
830
- if (config.subject.source !== "app-url") {
831
- return "the external-public shared-world route requires `subject.source: app-url` — a real public deployment is used directly as the shared plane (no clone, no provisioned subject).";
832
- }
833
- if (config.execution?.target !== "e2b-desktop") {
834
- return "the external-public shared-world route requires `execution.target: e2b-desktop` — the role seats drive hosted desktop browsers against the one public deployment.";
835
- }
836
- if (!actorResolvesToComputerUse(config.actors[0]?.type)) {
837
- return `the external-public shared-world route requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}) — each role seat runs a computer-use session.`;
838
- }
839
- if ((config.execution?.concurrency ?? 1) <= 1) {
840
- return "the external-public shared-world route requires `execution.concurrency > 1` (N concurrent seats sharing ONE public plane); concurrency 1 proves no shared world.";
841
- }
842
- if (config.policies?.allowPublicTargets !== true) {
843
- return "the external-public shared-world route requires `policies.allowPublicTargets: true` — the shared plane is a real non-loopback public deployment.";
844
- }
845
- const appUrl = config.subject.appUrl ?? "";
846
- if (!isHttpUrl(appUrl) || isLoopbackUrl(appUrl)) {
847
- return "the external-public shared-world route requires a non-loopback http(s) `subject.appUrl` — a loopback URL is not a shared public plane (use the getHost provisioned route for a local subject).";
848
- }
849
- // The operator-ownership attestation (the honest analog of exposure: synthetic — you cannot claim
850
- // synthetic on a real site, but you MUST attest you own/operate it). Author-trust; unverifiable.
851
- if (config.subject.publicTarget?.authorized !== true) {
852
- return "the external-public shared-world route requires `subject.publicTarget: { owner, authorized: true }` — you must attest you own/operate the public deployment used as the shared plane (author-trust; the harness cannot verify ownership).";
853
- }
854
- // FORBID every provisioned-subject field: with no sandbox they cannot act, so they are rejected
855
- // with a precise reason, never silently ignored (invariant 6). exposure: synthetic in particular
856
- // would be a LIE on a real site (the harness neither provisioned nor exposed it).
857
- if (config.subject.exposure !== undefined) {
858
- return "`subject.exposure: synthetic` is forbidden on the external-public shared-world route — you cannot attest a real public deployment is synthetic seeded data; use `subject.publicTarget` to attest ownership instead.";
859
- }
860
- if (config.subject.serve !== undefined) {
861
- return "`subject.serve` is forbidden on the external-public shared-world route — the harness does not serve the plane (it is an already-deployed public app); there is no in-sandbox serve to run.";
862
- }
863
- if (config.subject.state?.seed !== undefined || config.subject.state?.checkpoint !== undefined || config.subject.state !== undefined) {
864
- return "`subject.state` (seed/checkpoint/external) is forbidden on the external-public shared-world route — the harness neither seeds nor snapshots the plane (no in-sandbox filesystem to digest); no authoritative shared-state proof is possible on this class.";
865
- }
866
- if (config.subject.clone !== undefined || config.subject.repos !== undefined) {
867
- return "`subject.clone`/`subject.repos` are forbidden on the external-public shared-world route — nothing is cloned; the public deployment IS the plane.";
868
- }
869
- const lanes = config.actors[0]?.lanes;
870
- if (!lanes || lanes.length < 2) {
871
- return "the external-public shared-world route requires an `actors[0].lanes` roster of at least 2 roles (a single-seat shared world proves no shared session).";
872
- }
873
- if (lanes.some((lane) => lane.entry !== undefined)) {
874
- return "`actors[0].lanes[].entry` (the loopback same-origin seat path) is forbidden on the external-public shared-world route — there is no harness-served serve.url to resolve it against; seats open the public appUrl and reach the shared session through the real UI.";
875
- }
876
- const hostLanes = lanes.filter((lane) => lane.host === true);
877
- if (hostLanes.length !== 1) {
878
- return `the external-public shared-world route requires EXACTLY ONE \`host: true\` lane (the designated host seat that creates the shared session; got ${hostLanes.length}). The other ≥1 lanes are followers that join it.`;
879
- }
880
- return null;
881
- }
882
- /**
883
- * Resolve a shared-world seat's entry URL from `serve.url` + a role's `entry` (relative path or
884
- * same-origin absolute URL). Returns null when the combination is not a same-origin loopback URL
885
- * (the load-bearing public-safety boundary — a seat only ever drives the in-sandbox app).
886
- */
887
- export function resolveSeatUrl(serveUrl, entry) {
888
- if (entry === undefined || entry === "") {
889
- return isLoopbackUrl(serveUrl) ? serveUrl : null;
890
- }
891
- let base;
892
- let resolved;
893
- try {
894
- base = new URL(serveUrl);
895
- resolved = new URL(entry, serveUrl);
896
- }
897
- catch {
898
- return null;
899
- }
900
- if (resolved.origin !== base.origin) {
901
- return null;
902
- }
903
- const value = resolved.toString();
904
- return isLoopbackUrl(value) ? value : null;
905
- }
906
- /**
907
- * True when this config routes to the scripted-browser backend: an app-url subject whose
908
- * first actor resolves to a registered scripted-browser actor (execution.target local or
909
- * absent — the parse layer enforces that pairing). Mirror of routesToComputerUse; the single
910
- * source of truth for selectLabBackend and the warning logic.
911
- */
912
- export function routesToScriptedBrowser(config) {
913
- return routesToLocalScriptedBrowser(config) || routesToProvisionedScriptedBrowser(config);
914
- }
915
- export function routesToLocalScriptedBrowser(config) {
916
- return config.subject.source === "app-url"
917
- && actorResolvesToScriptedBrowser(config.actors[0]?.type);
918
- }
919
- export function routesToProvisionedScriptedBrowser(config) {
920
- return config.subject.source === "clone"
921
- && config.execution?.target === "e2b-desktop"
922
- && actorResolvesToScriptedBrowser(config.actors[0]?.type);
923
- }
924
- /**
925
- * True when this config routes to the terminal-product backend: a terminal-product subject whose
926
- * first actor resolves to a registered terminal actor (execution.target e2b-terminal or absent —
927
- * the parse layer enforces that pairing). Mirror of routesToComputerUse/routesToScriptedBrowser;
928
- * the single source of truth for selectLabBackend and the warning logic.
929
- */
930
- export function routesToTerminalProduct(config) {
931
- return config.subject.source === "terminal-product"
932
- && actorResolvesToTerminal(config.actors[0]?.type);
933
- }
934
- // Report fields that are present but not yet consumed by the engine, so a user never trusts a
935
- // setting that silently does nothing. Keeps the schema forward-correct AND honest.
936
- function forwardDeclaredWarnings(config) {
937
- const inert = [];
938
- // The computer-use routes consume the actor prompt fields, execution.timeoutMs,
939
- // execution.desktop.{resolution,sandboxTimeoutMs}, and (clone) subject.{serve,env,state,
940
- // clone.depth}; the scripted-browser route consumes scenario.ref, actors[0].{persona,count},
941
- // and execution.timeoutMs (mission/laneFocus/model are inert there: this actor runs no
942
- // model); on every other route those fields are inert.
943
- const routesToCua = routesToComputerUse(config);
944
- const routesToScripted = routesToScriptedBrowser(config);
945
- const routesToTerminal = routesToTerminalProduct(config);
946
- const routesToShared = routesToSharedWorld(config);
947
- const routesToConcurrent = routesToConcurrentSharedWorld(config);
948
- // The external-public plane (a real public deployment as the shared plane) consumes host lanes +
949
- // subject.publicTarget; it is only ever the concurrent app-url shape.
950
- const routesToExternalPublic = routesToConcurrent && config.subject.source === "app-url";
951
- const routesToHostedCuaBrowser = config.execution?.target === "e2b-desktop"
952
- && routesToCua;
953
- for (const [index, actor] of config.actors.entries()) {
954
- // Shared-world ONLY fields on the roster: per-role `entry` is inert anywhere else (invariant 6).
955
- if (actor.lanes?.some((lane) => lane.entry !== undefined) && !routesToShared) {
956
- inert.push(`actors[${index}].lanes[].entry (the per-role loopback entry is a shared-world capability; needs subject.topology: shared-world)`);
957
- }
958
- // The host-seat marker acts ONLY on the external-public shared-world route; inert elsewhere.
959
- if (actor.lanes?.some((lane) => lane.host === true) && !routesToExternalPublic) {
960
- inert.push(`actors[${index}].lanes[].host (the designated host-seat marker; needs the external-public shared-world route: app-url × topology shared-world × allowPublicTargets × concurrency > 1)`);
961
- }
962
- if (routesToCua || routesToTerminal) {
963
- // The cua + terminal routes consume mission/persona/model + laneFocus.instruction (they
964
- // compose the agent prompt + bundle provenance); laneFocus.id/label remain inert. On the
965
- // cua E2B route actors[0].lanes is CONSUMED (the fan-out roster).
966
- if (actor.laneFocus?.id)
967
- inert.push(`actors[${index}].laneFocus.id`);
968
- if (actor.laneFocus?.label)
969
- inert.push(`actors[${index}].laneFocus.label`);
970
- if (routesToTerminal && actor.lanes)
971
- inert.push(`actors[${index}].lanes (fan-out is a computer-use route capability; terminal fan-out is a later slice)`);
972
- }
973
- else if (routesToScripted) {
974
- // persona and count are consumed (trace/bundle provenance; surface roster). The prompt
975
- // fields can never act here — the scripted actor runs no model.
976
- if (actor.mission)
977
- inert.push(`actors[${index}].mission (the scripted-browser actor runs no model)`);
978
- if (actor.laneFocus)
979
- inert.push(`actors[${index}].laneFocus (the scripted-browser actor runs no model)`);
980
- if (actor.model)
981
- inert.push(`actors[${index}].model (the scripted-browser actor runs no model)`);
982
- if (actor.lanes)
983
- inert.push(`actors[${index}].lanes (the scripted-browser route fans out via actors[0].count, not a lane roster)`);
984
- }
985
- else {
986
- if (actor.mission)
987
- inert.push(`actors[${index}].mission`);
988
- if (actor.laneFocus)
989
- inert.push(`actors[${index}].laneFocus`);
990
- if (actor.persona)
991
- inert.push(`actors[${index}].persona`);
992
- if (actor.model)
993
- inert.push(`actors[${index}].model`);
994
- if (actor.lanes)
995
- inert.push(`actors[${index}].lanes`);
996
- }
997
- }
998
- if (config.subject.clone?.depth !== undefined && !routesToCua && !routesToScripted)
999
- inert.push("subject.clone.depth");
1000
- if (config.subject.serve && !routesToCua && !routesToScripted)
1001
- inert.push("subject.serve");
1002
- if (config.subject.env && !routesToCua && !routesToScripted)
1003
- inert.push("subject.env");
1004
- if (config.subject.state && !routesToCua && !routesToScripted)
1005
- inert.push("subject.state");
1006
- // topology + checkpoint act ONLY on the shared-world route (#164); a set-but-unconsumed value
1007
- // (incl. an explicit per-lane-worlds, which the cua route already is by mechanism) warns inert.
1008
- if (config.subject.topology !== undefined && !routesToShared) {
1009
- inert.push("subject.topology (drives behavior only on the shared-world route; needs subject.topology: shared-world + clone × e2b-desktop × a computer-use actor + a ≥2 lane roster)");
1010
- }
1011
- if (config.subject.state?.checkpoint !== undefined && !routesToShared) {
1012
- inert.push("subject.state.checkpoint (the shared-world state-checkpoint probe; needs subject.topology: shared-world)");
1013
- }
1014
- // publicTarget (the external-public ownership attestation) acts ONLY on the external-public
1015
- // shared-world route; inert elsewhere. (It is already parse-rejected on non-app-url sources.)
1016
- if (config.subject.publicTarget !== undefined && !routesToExternalPublic) {
1017
- inert.push("subject.publicTarget (the external-public ownership attestation; needs the external-public shared-world route: app-url × topology shared-world × allowPublicTargets × concurrency > 1)");
1018
- }
1019
- // exposure (the synthetic-subject attestation) acts ONLY on the CONCURRENT shared-world route
1020
- // (the getHost-exposed plane); inert on the sequential shared-world route (loopback) and elsewhere.
1021
- if (config.subject.exposure !== undefined && !routesToConcurrent && !(routesToScripted && config.subject.source === "clone")) {
1022
- inert.push("subject.exposure (the synthetic-subject attestation for a getHost-exposed plane; needs concurrent shared-world or clone × e2b-desktop × scripted-browser)");
1023
- }
1024
- // comms.email drives the in-sandbox email/SMS catch, which needs a subject sandbox HUMANISH
1025
- // provisions (clone or local-tree) so it holds a handle to host the catch. On an app-url /
1026
- // operator-provided subject there is no such handle, so a declared comms block would silently
1027
- // collect nothing — a false green. Warn at parse time (fires on inspect + dry-run too).
1028
- if (config.comms?.email?.kind === "fake"
1029
- && config.comms.email.external === undefined
1030
- && config.subject.source !== "clone"
1031
- && config.subject.source !== "local-tree") {
1032
- inert.push("comms.email (the in-sandbox email/SMS catch needs a harness-provisioned subject to host it — subject.source: clone or local-tree; on an app-url or operator-provided subject humanish holds no sandbox handle. Declare `comms.email.external` to run the catch yourself: humanish then points the persona at your inbox, drains your catch, and writes the same evidence — see #328)");
1033
- }
1034
- // The reverse mis-config: declaring an adopter-hosted catch on a route where humanish provisions
1035
- // the subject itself. Two catches would exist and the app would point at humanish's, so the
1036
- // declared external one would silently collect nothing.
1037
- if (config.comms?.email?.external && (config.subject.source === "clone" || config.subject.source === "local-tree")) {
1038
- inert.push("comms.email.external (this subject is harness-provisioned, so humanish hosts the catch itself and injects its URL; an adopter-hosted catch would receive nothing. Drop `external` here, or move the study to an app-url/operator-provisioned subject)");
1039
- }
1040
- // The SEQUENTIAL shared-world route has no comms wiring at all (no catch deploy, no inbox
1041
- // instruction) — a comms block there does nothing, and the actors are never told an inbox
1042
- // exists. Say so at parse time; the concurrent route (the default since #350: all seats live)
1043
- // is the one that hosts the email funnel (#351).
1044
- if (config.comms?.email && config.subject.topology === "shared-world" && (config.execution?.concurrency ?? 1) <= 1) {
1045
- inert.push("comms.email (the sequential turn-taking shared-world route has no comms wiring — no catch is deployed and no actor is told an inbox exists; remove `execution.concurrency: 1` so all seats run concurrently, which is the route that hosts the email funnel)");
1046
- }
1047
- // clone.keep IS consumed on the cua route (honored on FAILURE: the sandbox is left up to debug
1048
- // a failed install/boot; otherwise always killed). clone.fanout is REJECTED on the cua route
1049
- // (a hard parse error above), so it can never reach this warning list there.
1050
- if (!routesToCua && !routesToScripted && !routesToTerminal && config.execution?.timeoutMs !== undefined)
1051
- inert.push("execution.timeoutMs");
1052
- if (config.execution?.completionTimeoutMs !== undefined)
1053
- inert.push("execution.completionTimeoutMs");
1054
- // execution.concurrency is CONSUMED on the cua route (it bounds in-flight fan-out lanes);
1055
- // inert (warned) everywhere else.
1056
- if (config.execution?.concurrency !== undefined && !routesToCua)
1057
- inert.push("execution.concurrency");
1058
- // execution.caps is CONSUMED on the cua route (maxUsd is the fail-closed spend abort); inert
1059
- // (warned) everywhere else so a misplaced budget field is never trusted to cap a route it cannot.
1060
- if (config.execution?.caps && !routesToCua)
1061
- inert.push("execution.caps (the fail-closed spend abort is a computer-use route capability; needs a computer-use actor on e2b-desktop)");
1062
- // terminal-product consumes subject.product, scenario.caps, execution.{terminal,runtimeAuth}:
1063
- // dry-run records the contract; live execution enforces caps and command-scoped auth. On every
1064
- // OTHER route they are inert and must warn so a
1065
- // misplaced safety/budget field is never trusted to do something it cannot (invariant 6).
1066
- if (config.subject.product && !routesToTerminal && config.subject.source !== "desktop-cli")
1067
- inert.push("subject.product (needs subject.source: terminal-product or desktop-cli with the matching actor)");
1068
- if (config.scenario?.caps && !routesToTerminal)
1069
- inert.push("scenario.caps (needs subject.source: terminal-product + a registered terminal actor)");
1070
- // The study-level budget is a CUA-route capability; the terminal route is a single agent whose
1071
- // maxUsd already caps the whole run, so a maxTotalUsd there would be trusted and unenforced.
1072
- if (config.scenario?.caps?.maxTotalUsd !== undefined && routesToTerminal)
1073
- inert.push("scenario.caps.maxTotalUsd (the study-level budget is a computer-use route capability; the terminal route's maxUsd already caps the whole run)");
1074
- if (config.execution?.terminal && !routesToTerminal)
1075
- inert.push("execution.terminal (needs subject.source: terminal-product + a registered terminal actor)");
1076
- if (config.execution?.runtimeAuth !== undefined && !routesToTerminal)
1077
- inert.push("execution.runtimeAuth (needs subject.source: terminal-product + a registered terminal actor)");
1078
- if (config.execution?.runtime !== undefined && !routesToTerminal)
1079
- inert.push("execution.runtime (needs subject.source: terminal-product + a registered terminal actor)");
1080
- // execution.desktop.* stays inert on the scripted route by design: device presets belong to
1081
- // the cua desktop; scripted surfaces are the driver's fixed desktop/mobile viewports, where
1082
- // isMobile/DSF genuinely render via playwright emulation.
1083
- if (!routesToCua && config.execution?.desktop?.resolution)
1084
- inert.push("execution.desktop.resolution");
1085
- if (!routesToCua && config.execution?.desktop?.device !== undefined)
1086
- inert.push("execution.desktop.device");
1087
- if (!routesToHostedCuaBrowser && config.execution?.desktop?.browser !== undefined)
1088
- inert.push("execution.desktop.browser");
1089
- if (!routesToHostedCuaBrowser && config.execution?.desktop?.fidelity !== undefined) {
1090
- inert.push("execution.desktop.fidelity (mobile emulation is applied only to hosted Chromium computer-use lanes on execution.target: e2b-desktop)");
1091
- }
1092
- if (!routesToCua && config.execution?.desktop?.sandboxTimeoutMs !== undefined)
1093
- inert.push("execution.desktop.sandboxTimeoutMs");
1094
- // execution.desktop.template (the custom E2B desktop image) is consumed ONLY where a desktop is
1095
- // actually created via Sandbox.create — the e2b-desktop computer-use routes (cua/shared-world/
1096
- // concurrent). It is INERT on every other route (incl. the in-process local-app cua route, which
1097
- // creates no desktop, and the meta route): warn so an unconsumed template is never silently
1098
- // ignored (invariant 6).
1099
- const createsE2BDesktop = (routesToCua || (routesToScripted && config.subject.source === "clone"))
1100
- && config.execution?.target === "e2b-desktop";
1101
- if (config.execution?.desktop?.template !== undefined && !createsE2BDesktop) {
1102
- inert.push("execution.desktop.template (the custom E2B desktop image is consumed only on execution.target: e2b-desktop computer-use routes that create a desktop; needs a computer-use actor on e2b-desktop)");
1103
- }
1104
- // codexAppServer is consumed only on the e2b-desktop (meta) route; flag it when it cannot reach there.
1105
- const routesToDesktop = config.subject.source === "clone" && config.execution?.target === "e2b-desktop";
1106
- if (config.execution?.desktop?.codexAppServer !== undefined && !routesToDesktop) {
1107
- inert.push("execution.desktop.codexAppServer (needs subject.source: clone + execution.target: e2b-desktop)");
1108
- }
1109
- // scenario.ref is CONSUMED on the scripted-browser route (required there); forward-declared
1110
- // everywhere else.
1111
- if (config.scenario?.ref && !routesToScripted)
1112
- inert.push("scenario.ref");
1113
- if (config.scenario?.inline)
1114
- inert.push("scenario.inline");
1115
- // review.{scoring,milestones,vocabulary} stay forward-declared (reserved for #319) on every route.
1116
- // review.scorer (#316) IS consumed (loaded + wired, or fail-closed at load) on every scorer-capable
1117
- // route, so it does not warn there; on the scripted-browser route the actor carries no scorer seam,
1118
- // so a declared scorer is flagged inert (the run also fails closed at load).
1119
- if (config.review?.scoring)
1120
- inert.push("review.scoring (reserved for a later slice; not yet consumed)");
1121
- if (config.review?.milestones)
1122
- inert.push("review.milestones (reserved for a later slice; not yet consumed)");
1123
- if (config.review?.vocabulary)
1124
- inert.push("review.vocabulary (reserved for a later slice; not yet consumed)");
1125
- if (config.review?.scorer && routesToScripted) {
1126
- inert.push("review.scorer (the scripted-browser actor has no adopter-scorer seam; declare it on a terminal / computer-use / shared-world route)");
1127
- }
1128
- if (config.personas)
1129
- inert.push("personas");
1130
- const warnings = inert.length === 0
1131
- ? []
1132
- : [`Forward-declared fields are set but not yet consumed by the engine (planned for a later slice): ${inert.join(", ")}.`];
1133
- // A declared cap below the seat count is legal but loud: the roster promises N live actors and
1134
- // the cap delivers waves of M. Say so up front (inspect + dry-run + run) — a green run in waves
1135
- // is otherwise indistinguishable from the all-live run the author meant (#350).
1136
- {
1137
- const seats = config.actors[0]?.lanes?.length ?? config.actors[0]?.count ?? 1;
1138
- const cap = config.execution?.concurrency;
1139
- // concurrency: 1 on a shared-world lab is the SEQUENTIAL selector (turn-taking is that
1140
- // route's whole design), not a mistaken throttle — no warning there.
1141
- const sequentialSelector = config.subject.topology === "shared-world" && cap === 1;
1142
- if (routesToCua && cap !== undefined && seats > 1 && cap < seats && !sequentialSelector) {
1143
- warnings.push(`execution.concurrency ${cap} caps a ${seats}-seat roster: seats run in waves of ${cap}, never all live at once. Remove execution.concurrency (the default runs all ${seats} seats simultaneously) or set it to ${seats}; declare a lower cap only to bound simultaneous paid desktops.`);
1144
- }
1145
- }
1146
- // Partial email coverage is legal but loud (#351): a lane without an addressed recipient never
1147
- // hears an inbox exists, so an email-gated flow on that seat dead-ends by construction.
1148
- if (routesToCua && config.comms?.email?.recipients) {
1149
- const laneIds = effectiveComputerUseLaneIds(config);
1150
- const covered = new Set(config.comms.email.recipients.filter((r) => r.address !== undefined).map((r) => r.lane));
1151
- const uncovered = laneIds.filter((id) => !covered.has(id));
1152
- if (covered.size > 0 && uncovered.length > 0 && laneIds.length > 1) {
1153
- warnings.push(`comms.email covers ${covered.size} of ${laneIds.length} lanes; the uncovered lane(s) get no inbox and are never told one exists: ${uncovered.join(", ")}. Add addressed recipients for them if their flows need email.`);
1154
- }
1155
- }
1156
- return warnings;
1157
- }
1158
- function parseSubject(raw) {
1159
- if (!isRecord(raw)) {
1160
- return invalid("Lab `subject` is required and must be an object.");
1161
- }
1162
- const source = str(raw.source);
1163
- if (source !== "this-repo" && source !== "clone" && source !== "app-url" && source !== "local-app" && source !== "desktop-cli" && source !== "terminal-product" && source !== "local-tree") {
1164
- return invalid("`subject.source` must be one of: this-repo, clone, app-url, local-app, terminal-product, desktop-cli, local-tree.");
1165
- }
1166
- const subject = { source };
1167
- // topology is enum-validated everywhere; its SEMANTICS (shared-world requires clone × e2b-desktop
1168
- // × a ≥2 roster) are enforced in the shared-world cross-validation below, and a set-but-unconsumed
1169
- // topology warns as inert off the shared-world route (invariant 6).
1170
- if (raw.topology !== undefined) {
1171
- const topology = str(raw.topology);
1172
- if (topology !== "per-lane-worlds" && topology !== "shared-world") {
1173
- return invalid("`subject.topology` must be per-lane-worlds (the default) or shared-world.");
1174
- }
1175
- subject.topology = topology;
1176
- }
1177
- // exposure is enum-validated everywhere; it is REQUIRED on the concurrent shared-world route (the
1178
- // getHost synthetic-subject attestation) and warns inert elsewhere.
1179
- if (raw.exposure !== undefined) {
1180
- const exposure = str(raw.exposure);
1181
- if (exposure !== "synthetic") {
1182
- return invalid("`subject.exposure` must be `synthetic` (the author attestation that the getHost-exposed subject is synthetic seeded data).");
1183
- }
1184
- subject.exposure = exposure;
1185
- }
1186
- // `product` is terminal-product-only; reject it elsewhere (invariant 6: a field that cannot act
1187
- // on this route is an honest parse error, not silently dropped).
1188
- if (source !== "terminal-product" && source !== "desktop-cli" && raw.product !== undefined) {
1189
- return invalid("`subject.product` applies only to terminal-product and desktop-cli subjects (the CLI a participant studies from public surfaces).");
1190
- }
1191
- // appUrl is app-url/local-app-only; a terminal-product subject drives PUBLIC surfaces, not a
1192
- // single loopback app — reject appUrl on it.
1193
- if (source === "terminal-product" && raw.appUrl !== undefined) {
1194
- return invalid("`subject.appUrl` does not apply to terminal-product subjects — declare `subject.product.publicSurfaces` (the agent works from public surfaces, not one loopback app).");
1195
- }
1196
- // serve/env/state are shared between clone (cloned app) and local-tree (packed working
1197
- // tree): both routes serve a subject in-sandbox with the same install/build/start/url +
1198
- // env-name + seed/external/checkpoint shapes.
1199
- if (source !== "clone" && source !== "local-tree" && raw.serve !== undefined) {
1200
- return invalid("`subject.serve` applies only to clone subjects or local-tree subjects (the lab serves the cloned/packed app in-sandbox).");
1201
- }
1202
- if (source !== "clone" && source !== "local-tree" && raw.env !== undefined) {
1203
- return invalid("`subject.env` applies only to clone subjects or local-tree subjects (the served app's environment channel).");
1204
- }
1205
- if (source !== "clone" && source !== "local-tree" && raw.state !== undefined) {
1206
- return invalid("`subject.state` applies only to clone subjects or local-tree subjects (the lab seeds the state it serves).");
1207
- }
1208
- // repos/clone are clone-ONLY (a fresh-clone subject's git inputs). local-tree packs the
1209
- // resolution cwd itself, so it has no repo slug to clone and gets its own precise reasons
1210
- // rather than falling through to the generic clone-only message below.
1211
- if (source === "local-tree" && raw.repos !== undefined) {
1212
- return invalid("`subject.repos` does not apply to local-tree subjects. The local-tree route packs the lab resolution cwd itself; there is no owner/repo slug to clone.");
1213
- }
1214
- if (source === "local-tree" && raw.clone !== undefined) {
1215
- return invalid("`subject.clone` does not apply to local-tree subjects. Declare `subject.localTree` instead (keep/exclude/maxArchiveBytes).");
1216
- }
1217
- // Rejected, never silently dropped, on app-url/local-app/this-repo/terminal-product subjects
1218
- // too (invariant 6: a field that cannot act on this route is an honest parse error).
1219
- if (source !== "clone" && raw.repos !== undefined) {
1220
- return invalid("`subject.repos` applies only to clone subjects (the owner/repo slugs to clone).");
1221
- }
1222
- if (source !== "clone" && raw.clone !== undefined) {
1223
- return invalid("`subject.clone` applies only to clone subjects (clone depth/fanout/keep).");
1224
- }
1225
- // localTree is local-tree-ONLY (pack/upload knobs for the packed working tree).
1226
- if (source !== "local-tree" && raw.localTree !== undefined) {
1227
- return invalid("`subject.localTree` applies only to local-tree subjects (keep/exclude/maxArchiveBytes for packing the working tree).");
1228
- }
1229
- // publicTarget is app-url-ONLY (the external-public shared-world ownership attestation). It is
1230
- // meaningless without a real public deployment as the plane — reject it elsewhere (invariant 6).
1231
- if (source !== "app-url" && raw.publicTarget !== undefined) {
1232
- return invalid("`subject.publicTarget` applies only to app-url subjects on the external-public shared-world route (the operator's ownership attestation for a real public deployment used directly as the shared plane).");
1233
- }
1234
- if (source === "clone") {
1235
- const repos = strList(raw.repos);
1236
- if (!repos || repos.length === 0) {
1237
- return invalid("`subject.repos` must list at least one owner/repo slug when source is clone.");
1238
- }
1239
- subject.repos = repos;
1240
- const clone = parseClone(raw.clone);
1241
- if (clone) {
1242
- subject.clone = clone;
1243
- }
1244
- const serveResult = parseServe(raw.serve);
1245
- if (!serveResult.ok) {
1246
- return serveResult;
1247
- }
1248
- if (serveResult.value)
1249
- subject.serve = serveResult.value;
1250
- if (raw.env !== undefined) {
1251
- const env = strList(raw.env);
1252
- if (!env || env.length === 0) {
1253
- return invalid("`subject.env` must be a non-empty list of env var NAMES when set.");
1254
- }
1255
- const badName = env.find((name) => !ENV_NAME_PATTERN.test(name));
1256
- if (badName) {
1257
- return invalid(`subject.env entries must be env var NAMES like DATABASE_URL (got "${badName}"); values come from the caller's environment and are never persisted.`);
1258
- }
1259
- subject.env = env;
1260
- }
1261
- const envValuesResult = parseEnvValues(raw.envValues);
1262
- if (!envValuesResult.ok)
1263
- return envValuesResult;
1264
- if (envValuesResult.value)
1265
- subject.envValues = envValuesResult.value;
1266
- const stateResult = parseState(raw.state);
1267
- if (!stateResult.ok) {
1268
- return stateResult;
1269
- }
1270
- if (stateResult.value) {
1271
- // Semantic validation is shared with the engine (runCuaActorLab re-enforces it for
1272
- // configs that arrive through the library API without the parser).
1273
- const reason = subjectStateInvalidReason(stateResult.value, subject.env);
1274
- if (reason) {
1275
- return invalid(reason);
1276
- }
1277
- subject.state = stateResult.value;
1278
- }
1279
- }
1280
- if (source === "local-tree") {
1281
- // A local-tree subject exists to be packed and served; there is no other way to boot it, so
1282
- // serve is REQUIRED here (unlike clone, where serve is optional for the smoke/meta routes).
1283
- if (raw.serve === undefined) {
1284
- return invalid("`subject.serve` is required when source is local-tree: a local-tree subject exists to be packed and served, so declare install/build/start/url exactly like the clone route.");
1285
- }
1286
- const serveResult = parseServe(raw.serve);
1287
- if (!serveResult.ok) {
1288
- return serveResult;
1289
- }
1290
- if (serveResult.value)
1291
- subject.serve = serveResult.value;
1292
- if (raw.env !== undefined) {
1293
- const env = strList(raw.env);
1294
- if (!env || env.length === 0) {
1295
- return invalid("`subject.env` must be a non-empty list of env var NAMES when set.");
1296
- }
1297
- const badName = env.find((name) => !ENV_NAME_PATTERN.test(name));
1298
- if (badName) {
1299
- return invalid(`subject.env entries must be env var NAMES like DATABASE_URL (got "${badName}"); values come from the caller's environment and are never persisted.`);
1300
- }
1301
- subject.env = env;
1302
- }
1303
- const envValuesResult = parseEnvValues(raw.envValues);
1304
- if (!envValuesResult.ok)
1305
- return envValuesResult;
1306
- if (envValuesResult.value)
1307
- subject.envValues = envValuesResult.value;
1308
- const stateResult = parseState(raw.state);
1309
- if (!stateResult.ok) {
1310
- return stateResult;
1311
- }
1312
- if (stateResult.value) {
1313
- // Semantic validation is shared with the engine (same helper the clone route uses).
1314
- const reason = subjectStateInvalidReason(stateResult.value, subject.env);
1315
- if (reason) {
1316
- return invalid(reason);
1317
- }
1318
- subject.state = stateResult.value;
1319
- }
1320
- const localTreeResult = parseLocalTree(raw.localTree);
1321
- if (!localTreeResult.ok) {
1322
- return localTreeResult;
1323
- }
1324
- if (localTreeResult.value) {
1325
- subject.localTree = localTreeResult.value;
1326
- }
1327
- }
1328
- if (source === "app-url" || source === "local-app") {
1329
- const appUrl = str(raw.appUrl);
1330
- if (!appUrl) {
1331
- return invalid(`\`subject.appUrl\` is required when source is ${source}.`);
1332
- }
1333
- // app-url: shape-only here; the loopback-vs-public-target gate is applied in the
1334
- // cross-validation block below, where policies.allowPublicTargets is available.
1335
- // local-app: an in-process local dev server — ALWAYS loopback (no public-target option),
1336
- // so the loopback wall is enforced right here at parse.
1337
- if (source === "local-app") {
1338
- if (!isLoopbackUrl(appUrl)) {
1339
- return invalid("`subject.appUrl` must be a loopback URL (127.0.0.1/localhost) on a local-app subject — it drives an already-running LOCAL dev server in-process; public targets are not supported on this route.");
1340
- }
1341
- }
1342
- else if (!isHttpUrl(appUrl)) {
1343
- return invalid("`subject.appUrl` must be an http(s) URL.");
1344
- }
1345
- subject.appUrl = appUrl;
1346
- // publicTarget (external-public shared-world ownership attestation) is app-url-only. Shape it
1347
- // here; its REQUIRED-on-that-route semantics live in externalPublicSharedWorldValidationReason.
1348
- if (source === "app-url" && raw.publicTarget !== undefined) {
1349
- const publicTargetResult = parsePublicTarget(raw.publicTarget);
1350
- if (!publicTargetResult.ok) {
1351
- return publicTargetResult;
1352
- }
1353
- subject.publicTarget = publicTargetResult.value;
1354
- }
1355
- }
1356
- if (source === "terminal-product" || source === "desktop-cli") {
1357
- const productResult = parseProduct(raw.product);
1358
- if (!productResult.ok) {
1359
- return productResult;
1360
- }
1361
- subject.product = productResult.value;
1362
- }
1363
- return { ok: true, value: subject };
1364
- }
1365
- // The publicTarget.owner is a public-safe operator/repo label surfaced in evidence (e.g.
1366
- // "example-operator/lobby-trivia" or a bare org name). Slash allowed for the owner/repo convention.
1367
- const PUBLIC_TARGET_OWNER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_./-]*$/;
1368
- /** Parse the external-public shared-world ownership attestation ({ owner, authorized: true }). The
1369
- * harness cannot verify ownership — this is author-trust, surfaced honestly in the evidence class. */
1370
- function parsePublicTarget(raw) {
1371
- if (!isRecord(raw)) {
1372
- return invalid("`subject.publicTarget` must be an object ({ owner, authorized: true }) — the operator's ownership attestation for the external-public shared plane.");
1373
- }
1374
- const owner = str(raw.owner);
1375
- if (!owner || !PUBLIC_TARGET_OWNER_PATTERN.test(owner)) {
1376
- return invalid("`subject.publicTarget.owner` must be a public-safe operator/repo label (e.g. owner/repo); it is recorded in evidence, so it must carry no secret.");
1377
- }
1378
- if (raw.authorized !== true) {
1379
- return invalid("`subject.publicTarget.authorized` must be true — you must attest you own/operate the public deployment used as the shared plane (author-trust; the harness cannot verify ownership).");
1380
- }
1381
- return { ok: true, value: { owner, authorized: true } };
1382
- }
1383
- // The product name interpolates into evidence labels and the composed prompt; the public-safe
1384
- // token shape is the same load-bearing constraint as a lab id.
1385
- const PRODUCT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
1386
- function parseProduct(raw) {
1387
- if (!isRecord(raw)) {
1388
- return invalid("`subject.product` is required on terminal-product subjects and must be an object ({ name, publicSurfaces }).");
1389
- }
1390
- const name = str(raw.name);
1391
- if (!name || !PRODUCT_NAME_PATTERN.test(name)) {
1392
- return invalid("`subject.product.name` must be a public-safe token starting with a letter or digit (/^[A-Za-z0-9][A-Za-z0-9_.-]*$/).");
1393
- }
1394
- const workdir = str(raw.workdir);
1395
- if (raw.workdir !== undefined && (workdir === undefined || !/^[A-Za-z0-9_./-]+$/.test(workdir))) {
1396
- return invalid("`subject.product.workdir` must be a plain path (it interpolates into an in-sandbox command).");
1397
- }
1398
- const upload = str(raw.upload);
1399
- if (raw.upload !== undefined) {
1400
- if (upload === undefined || upload.trim().length === 0) {
1401
- return invalid("`subject.product.upload` must be a non-empty project-relative path when set.");
1402
- }
1403
- if (upload.startsWith("/") || /^[A-Za-z]:/.test(upload) || upload.split(/[\\/]/).includes("..")) {
1404
- return invalid("`subject.product.upload` must stay inside the project — no absolute paths and no `..` segments.");
1405
- }
1406
- }
1407
- const install = str(raw.install);
1408
- if (raw.install !== undefined && (install === undefined || install.trim().length === 0)) {
1409
- return invalid("`subject.product.install` must be a non-empty command string when set.");
1410
- }
1411
- const publicSurfaces = strList(raw.publicSurfaces);
1412
- if (!publicSurfaces || publicSurfaces.length === 0) {
1413
- return invalid("`subject.product.publicSurfaces` must list at least one public surface URL.");
1414
- }
1415
- const badSurface = publicSurfaces.find((surface) => !isHttpUrl(surface));
1416
- if (badSurface) {
1417
- return invalid(`subject.product.publicSurfaces entries must be http(s) URLs (got "${badSurface}").`);
1418
- }
1419
- return { ok: true, value: { name, publicSurfaces, ...(install === undefined ? {} : { install }), ...(workdir === undefined ? {} : { workdir }), ...(upload === undefined ? {} : { upload }) } };
1420
- }
1421
- // Public-safe stance: a computer-use actor's ENTRY URL is always an app the lab owner runs on
1422
- // loopback (inside the sandbox), never an arbitrary public site. (The constraint binds the
1423
- // entry point; a navigation watchdog for mid-session escapes is a later slice.) Exported so
1424
- // the engine re-enforces the same boundary on configs that arrive through the library API.
1425
- export function isLoopbackUrl(value) {
1426
- let url;
1427
- try {
1428
- url = new URL(value);
1429
- }
1430
- catch {
1431
- return false;
1432
- }
1433
- if (url.protocol !== "http:" && url.protocol !== "https:") {
1434
- return false;
1435
- }
1436
- const host = url.hostname.toLowerCase();
1437
- return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
1438
- }
1439
- /** A well-formed http(s) URL (any host). Shape gate before the loopback/public-target policy. */
1440
- export function isHttpUrl(value) {
1441
- try {
1442
- const url = new URL(value);
1443
- return url.protocol === "http:" || url.protocol === "https:";
1444
- }
1445
- catch {
1446
- return false;
1447
- }
1448
- }
1449
- function parseServe(raw) {
1450
- if (raw === undefined) {
1451
- return { ok: true, value: undefined };
1452
- }
1453
- if (!isRecord(raw)) {
1454
- return invalid("`subject.serve` must be an object ({ install?, build?, start, url, readyTimeoutMs? }).");
1455
- }
1456
- const start = str(raw.start);
1457
- if (!start) {
1458
- return invalid("`subject.serve.start` is required when serve is set (the long-lived command that serves the app).");
1459
- }
1460
- const url = str(raw.url);
1461
- if (!url || !isLoopbackUrl(url)) {
1462
- return invalid("`subject.serve.url` must be a loopback http(s) URL (127.0.0.1 or localhost) — the app is served INSIDE the sandbox.");
1463
- }
1464
- const serve = { start, url };
1465
- const install = str(raw.install);
1466
- if (install)
1467
- serve.install = install;
1468
- const build = str(raw.build);
1469
- if (build)
1470
- serve.build = build;
1471
- const readyTimeoutMs = posInt(raw.readyTimeoutMs);
1472
- if (readyTimeoutMs !== undefined)
1473
- serve.readyTimeoutMs = readyTimeoutMs;
1474
- const installTimeoutMs = posInt(raw.installTimeoutMs);
1475
- if (installTimeoutMs !== undefined)
1476
- serve.installTimeoutMs = installTimeoutMs;
1477
- const buildTimeoutMs = posInt(raw.buildTimeoutMs);
1478
- if (buildTimeoutMs !== undefined)
1479
- serve.buildTimeoutMs = buildTimeoutMs;
1480
- return { ok: true, value: serve };
1481
- }
1482
- /**
1483
- * Structural parse of `subject.state` into a candidate LabSubjectState. Deliberately keeps
1484
- * unrecognized `when`/`timeoutMs` values in the candidate (instead of silently dropping
1485
- * them) so subjectStateInvalidReason rejects them — a state declaration that silently does
1486
- * less than it says would violate invariant 6.
1487
- */
1488
- /**
1489
- * LITERAL non-secret subject env. Real apps need configuration before they will boot — a public base
1490
- * URL, a transport selector, a feature flag — and none of that is secret. Routing it through
1491
- * `subject.env` would force an adopter to carry a private env file just to reproduce a public study.
1492
- *
1493
- * These values ARE recorded in evidence (they are part of how the subject was configured), so a
1494
- * value that looks like a credential is refused here rather than committed to a public repo.
1495
- */
1496
- function parseEnvValues(raw) {
1497
- if (raw === undefined)
1498
- return { ok: true };
1499
- if (!isRecord(raw)) {
1500
- return invalid("`subject.envValues` must be a mapping of env var NAME to a literal non-secret value.");
1501
- }
1502
- const envValues = {};
1503
- for (const [name, rawValue] of Object.entries(raw)) {
1504
- if (!ENV_NAME_PATTERN.test(name)) {
1505
- return invalid(`subject.envValues keys must be env var NAMES like NEXT_PUBLIC_APP_URL (got "${name}").`);
1506
- }
1507
- const value = typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : str(rawValue);
1508
- if (value === undefined) {
1509
- return invalid(`\`subject.envValues.${name}\` must be a string, number, or boolean.`);
1510
- }
1511
- // Reuse the redaction module's own detector rather than inventing a second opinion about what
1512
- // a secret looks like — the two must never disagree about the same string.
1513
- if (containsSensitive(value)) {
1514
- return invalid(`\`subject.envValues.${name}\` looks like a secret or a local path, and these values are committed with the lab and recorded in evidence. Declare the NAME in \`subject.env\` instead — those values come from the caller's environment and never persist.`);
1515
- }
1516
- envValues[name] = value;
1517
- }
1518
- return { ok: true, value: envValues };
1519
- }
1520
- function parseState(raw) {
1521
- if (raw === undefined) {
1522
- return { ok: true, value: undefined };
1523
- }
1524
- if (!isRecord(raw)) {
1525
- return invalid("`subject.state` must be an object ({ seed?, external? }).");
1526
- }
1527
- const state = {};
1528
- if (raw.seed !== undefined) {
1529
- if (!Array.isArray(raw.seed) || !raw.seed.every(isRecord)) {
1530
- return invalid("`subject.state.seed` must be an array of step objects ({ name, command, when?, timeoutMs? }).");
1531
- }
1532
- state.seed = raw.seed.map((entry) => ({
1533
- name: typeof entry.name === "string" ? entry.name.trim() : "",
1534
- command: typeof entry.command === "string" ? entry.command.trim() : "",
1535
- ...(entry.when === undefined ? {} : { when: entry.when }),
1536
- ...(entry.timeoutMs === undefined ? {} : { timeoutMs: (posInt(entry.timeoutMs) ?? entry.timeoutMs) })
1537
- }));
1538
- }
1539
- if (raw.external !== undefined) {
1540
- const external = strList(raw.external);
1541
- if (!external) {
1542
- return invalid("`subject.state.external` must be a non-empty list of env var NAMES when set.");
1543
- }
1544
- state.external = external;
1545
- }
1546
- if (raw.checkpoint !== undefined) {
1547
- if (!Array.isArray(raw.checkpoint) || !raw.checkpoint.every(isRecord)) {
1548
- return invalid("`subject.state.checkpoint` must be an array of probe objects ({ name, command, redact? }).");
1549
- }
1550
- state.checkpoint = raw.checkpoint.map((probe) => ({
1551
- name: typeof probe.name === "string" ? probe.name.trim() : "",
1552
- command: typeof probe.command === "string" ? probe.command.trim() : "",
1553
- // Preserve the redact list verbatim (literal secret values may contain commas, so do NOT
1554
- // run it through the comma-splitting strList); subjectStateInvalidReason validates the shape.
1555
- ...(probe.redact === undefined ? {} : { redact: probe.redact })
1556
- }));
1557
- }
1558
- return { ok: true, value: state };
1559
- }
1560
- // The step name interpolates into in-sandbox script/status/log paths (`subject-state-<name>`);
1561
- // the strict shape is load-bearing, exactly like the repo slug.
1562
- const STATE_STEP_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
1563
- const STATE_STEP_NAME_MAX_CHARS = 40;
1564
- const STATE_STEP_WHENS = ["before-build", "before-start", "after-ready"];
1565
- /**
1566
- * Semantic validation for `subject.state`, shared by parseLabConfig and the engine
1567
- * (runCuaActorLab re-enforces it on configs that arrive through the library API). Returns
1568
- * the failure message, or null when the declaration is valid. Reads the candidate
1569
- * defensively — library callers can hand the engine arbitrarily-shaped objects.
1570
- */
1571
- export function subjectStateInvalidReason(state, env) {
1572
- const seed = state.seed;
1573
- const external = state.external;
1574
- const checkpoint = state.checkpoint;
1575
- if ((seed === undefined || seed.length === 0)
1576
- && (external === undefined || external.length === 0)
1577
- && (checkpoint === undefined || checkpoint.length === 0)) {
1578
- return "`subject.state` must declare seed steps, external env names, and/or checkpoints (an empty state block would be inert).";
1579
- }
1580
- if (seed !== undefined) {
1581
- if (!Array.isArray(seed) || seed.length === 0) {
1582
- return "`subject.state.seed` must be a non-empty array of steps when set.";
1583
- }
1584
- const names = new Set();
1585
- for (const [index, step] of seed.entries()) {
1586
- const name = typeof step?.name === "string" ? step.name : "";
1587
- if (!STATE_STEP_NAME_PATTERN.test(name) || name.length > STATE_STEP_NAME_MAX_CHARS) {
1588
- return `subject.state.seed[${index}].name must match ${STATE_STEP_NAME_PATTERN} and be at most ${STATE_STEP_NAME_MAX_CHARS} chars (it names in-sandbox file paths); got "${name}".`;
1589
- }
1590
- if (names.has(name)) {
1591
- return `subject.state.seed step names must be unique (duplicate "${name}").`;
1592
- }
1593
- names.add(name);
1594
- if (typeof step.command !== "string" || step.command.trim().length === 0) {
1595
- return `subject.state.seed[${index}].command is required (the in-sandbox shell command that seeds the state).`;
1596
- }
1597
- if (step.when !== undefined && !STATE_STEP_WHENS.includes(step.when)) {
1598
- return `subject.state.seed[${index}].when must be one of: ${STATE_STEP_WHENS.join(", ")}.`;
1599
- }
1600
- if (step.timeoutMs !== undefined && !(typeof step.timeoutMs === "number" && Number.isSafeInteger(step.timeoutMs) && step.timeoutMs >= 1)) {
1601
- return `subject.state.seed[${index}].timeoutMs must be a positive integer.`;
1602
- }
1603
- }
1604
- }
1605
- if (external !== undefined) {
1606
- if (!Array.isArray(external) || external.length === 0) {
1607
- return "`subject.state.external` must be a non-empty list of env var NAMES when set.";
1608
- }
1609
- for (const name of external) {
1610
- if (typeof name !== "string" || !ENV_NAME_PATTERN.test(name)) {
1611
- return "subject.state.external entries must be env var NAMES like DATABASE_URL; values come from the caller's environment and are never persisted.";
1612
- }
1613
- if (!env?.includes(name)) {
1614
- return "subject.state.external names must also be declared in subject.env (the declaration must name a provisioned channel).";
1615
- }
1616
- }
1617
- }
1618
- if (checkpoint !== undefined) {
1619
- if (!Array.isArray(checkpoint) || checkpoint.length === 0) {
1620
- return "`subject.state.checkpoint` must be a non-empty array of probes when set.";
1621
- }
1622
- const names = new Set();
1623
- for (const [index, probe] of checkpoint.entries()) {
1624
- const name = typeof probe?.name === "string" ? probe.name : "";
1625
- if (!STATE_STEP_NAME_PATTERN.test(name) || name.length > STATE_STEP_NAME_MAX_CHARS) {
1626
- return `subject.state.checkpoint[${index}].name must match ${STATE_STEP_NAME_PATTERN} and be at most ${STATE_STEP_NAME_MAX_CHARS} chars (it names in-sandbox file paths); got "${name}".`;
1627
- }
1628
- if (names.has(name)) {
1629
- return `subject.state.checkpoint names must be unique (duplicate "${name}").`;
1630
- }
1631
- names.add(name);
1632
- if (typeof probe.command !== "string" || probe.command.trim().length === 0) {
1633
- return `subject.state.checkpoint[${index}].command is required (the read-only digest probe command).`;
1634
- }
1635
- if (probe.redact !== undefined) {
1636
- if (!Array.isArray(probe.redact) || !probe.redact.every((value) => typeof value === "string" && value.length > 0)) {
1637
- return `subject.state.checkpoint[${index}].redact must be a list of non-empty literal strings when set.`;
1638
- }
1639
- }
1640
- }
1641
- }
1642
- return null;
1643
- }
1644
- function parseClone(raw) {
1645
- if (!isRecord(raw)) {
1646
- return undefined;
1647
- }
1648
- const clone = {};
1649
- const depth = posInt(raw.depth);
1650
- if (depth !== undefined)
1651
- clone.depth = depth;
1652
- const fanout = posInt(raw.fanout);
1653
- if (fanout !== undefined)
1654
- clone.fanout = fanout;
1655
- if (typeof raw.keep === "boolean")
1656
- clone.keep = raw.keep;
1657
- return Object.keys(clone).length > 0 ? clone : undefined;
1658
- }
1659
- /**
1660
- * Structural parse of `subject.localTree`, mirroring parseClone. Unlike parseClone (which
1661
- * silently drops an out-of-range depth/fanout), an invalid exclude/maxArchiveBytes value is
1662
- * REJECTED, never silently dropped: a caller who typed an empty exclude entry or a non-positive
1663
- * maxArchiveBytes almost certainly meant something, and the archive-size cap is a safety knob,
1664
- * not a cosmetic default.
1665
- */
1666
- function parseLocalTree(raw) {
1667
- if (raw === undefined) {
1668
- return { ok: true, value: undefined };
1669
- }
1670
- if (!isRecord(raw)) {
1671
- return invalid("`subject.localTree` must be an object ({ keep?, exclude?, maxArchiveBytes? }).");
1672
- }
1673
- const localTree = {};
1674
- if (raw.keep !== undefined) {
1675
- if (typeof raw.keep !== "boolean") {
1676
- return invalid("`subject.localTree.keep` must be a boolean (YAML true/false, not a quoted string).");
1677
- }
1678
- localTree.keep = raw.keep;
1679
- }
1680
- if (raw.exclude !== undefined) {
1681
- if (!Array.isArray(raw.exclude) || raw.exclude.some((item) => typeof item !== "string" || item.trim().length === 0)) {
1682
- return invalid("`subject.localTree.exclude` must be a list of non-empty strings (extra archive excludes on top of the always-on denylist).");
1683
- }
1684
- const exclude = strList(raw.exclude);
1685
- if (exclude) {
1686
- // Normalize/validate each entry at parse time so a mis-shaped exclude the
1687
- // author believed in can never silently no-op at packing time: absolute
1688
- // paths and glob syntax are rejected with the packing boundary's own
1689
- // reason; "./prefix" and "prefix/" normalize to the enumeration relPath
1690
- // shape.
1691
- const normalized = [];
1692
- for (const entry of exclude) {
1693
- try {
1694
- normalized.push(normalizeExtraExcludeEntry(entry));
1695
- }
1696
- catch (error) {
1697
- return invalid(`\`subject.localTree.exclude\`: ${error instanceof Error ? error.message : String(error)}`);
1698
- }
1699
- }
1700
- localTree.exclude = normalized;
1701
- }
1702
- }
1703
- if (raw.maxArchiveBytes !== undefined) {
1704
- const maxArchiveBytes = posInt(raw.maxArchiveBytes);
1705
- if (maxArchiveBytes === undefined) {
1706
- return invalid("`subject.localTree.maxArchiveBytes` must be a positive integer number of bytes when set.");
1707
- }
1708
- localTree.maxArchiveBytes = maxArchiveBytes;
1709
- }
1710
- return { ok: true, value: Object.keys(localTree).length > 0 ? localTree : undefined };
1711
- }
1712
- function parseActors(raw) {
1713
- if (!Array.isArray(raw) || raw.length === 0) {
1714
- return invalid("Lab `actors` must be a non-empty array.");
1715
- }
1716
- // Multi-actor fan-out is not wired yet (only actors[0] is consumed). Fail closed rather than
1717
- // silently ignore actors[1..]; multi-actor support lands in a later slice.
1718
- if (raw.length > 1) {
1719
- return invalid("Multiple actors are not supported yet (only the first actor runs); declare a single actor.");
1720
- }
1721
- const actors = [];
1722
- for (const [index, entry] of raw.entries()) {
1723
- if (!isRecord(entry)) {
1724
- return invalid(`actors[${index}] must be an object.`);
1725
- }
1726
- const type = str(entry.type);
1727
- if (!type) {
1728
- return invalid(`actors[${index}].type is required.`);
1729
- }
1730
- const actor = { type };
1731
- const count = posInt(entry.count);
1732
- if (count !== undefined)
1733
- actor.count = count;
1734
- if (entry.lanes !== undefined && entry.roster !== undefined) {
1735
- return invalid(`actors[${index}].lanes and actors[${index}].roster are mutually exclusive — use explicit lanes OR compact roster groups, not both.`);
1736
- }
1737
- if (entry.roster !== undefined && count !== undefined) {
1738
- return invalid(`actors[${index}].roster and actors[${index}].count are mutually exclusive — use compact differentiated groups OR a homogeneous count, not both.`);
1739
- }
1740
- if (entry.roster !== undefined && entry.laneFocus !== undefined) {
1741
- return invalid(`actors[${index}].roster and actors[${index}].laneFocus are mutually exclusive — a roster group's instruction is the per-lane steer.`);
1742
- }
1743
- const lanesResult = entry.roster !== undefined
1744
- ? parseRosterGroups(entry.roster, index)
1745
- : parseLanes(entry.lanes, index);
1746
- if (!lanesResult.ok) {
1747
- return lanesResult;
1748
- }
1749
- if (lanesResult.value)
1750
- actor.lanes = lanesResult.value;
1751
- const persona = str(entry.persona);
1752
- if (persona)
1753
- actor.persona = persona;
1754
- const mission = str(entry.mission);
1755
- if (mission)
1756
- actor.mission = mission;
1757
- const model = str(entry.model);
1758
- if (model)
1759
- actor.model = model;
1760
- if (entry.maxOutputTokens !== undefined) {
1761
- if (!isMaxOutputTokens(entry.maxOutputTokens))
1762
- return invalid(`actors[${index}].maxOutputTokens must be a positive safe integer.`);
1763
- actor.maxOutputTokens = entry.maxOutputTokens;
1764
- }
1765
- const localAgent = str(entry.localAgent);
1766
- if (entry.localAgent !== undefined) {
1767
- if (localAgent !== "codex" && localAgent !== "claude") {
1768
- return invalid(`actors[${index}].localAgent must be "codex" or "claude" (the locally signed-in CLI that drives the study).`);
1769
- }
1770
- actor.localAgent = localAgent;
1771
- }
1772
- if (entry.reasoningEffort !== undefined) {
1773
- if (!isReasoningEffort(entry.reasoningEffort)) {
1774
- return invalid(`actors[${index}].reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
1775
- }
1776
- actor.reasoningEffort = entry.reasoningEffort;
1777
- }
1778
- const stopWhenResult = parseStopWhen(entry.stopWhen, `actors[${index}].stopWhen`);
1779
- if (!stopWhenResult.ok)
1780
- return stopWhenResult;
1781
- if (stopWhenResult.value !== undefined)
1782
- actor.stopWhen = stopWhenResult.value;
1783
- const dwellResult = parseDwell(entry.dwell, `actors[${index}].dwell`);
1784
- if (!dwellResult.ok)
1785
- return dwellResult;
1786
- if (dwellResult.value !== undefined)
1787
- actor.dwell = dwellResult.value;
1788
- const tasksResult = parseTasks(entry.tasks, `actors[${index}].tasks`);
1789
- if (!tasksResult.ok)
1790
- return tasksResult;
1791
- if (tasksResult.value !== undefined)
1792
- actor.tasks = tasksResult.value;
1793
- const laneFocus = parseLaneFocus(entry.laneFocus);
1794
- if (laneFocus)
1795
- actor.laneFocus = laneFocus;
1796
- actors.push(actor);
1797
- }
1798
- return { ok: true, value: actors };
1799
- }
1800
- function parseLaneFocus(raw) {
1801
- if (!isRecord(raw)) {
1802
- return undefined;
1803
- }
1804
- const laneFocus = {};
1805
- const id = str(raw.id);
1806
- if (id)
1807
- laneFocus.id = id;
1808
- const label = str(raw.label);
1809
- if (label)
1810
- laneFocus.label = label;
1811
- const instruction = str(raw.instruction);
1812
- if (instruction)
1813
- laneFocus.instruction = instruction;
1814
- return Object.keys(laneFocus).length > 0 ? laneFocus : undefined;
1815
- }
1816
- // Like review fields, a declared lane distinction must not disappear silently (#343).
1817
- // The exhaustive records make newly added interface fields require an explicit parser decision.
1818
- const LANE_FIELDS = {
1819
- id: true, actorType: true, surface: true, caseGroup: true, persona: true,
1820
- device: true, instruction: true, stopWhen: true, dwell: true, reasoningEffort: true,
1821
- target: true, entry: true, host: true
1822
- };
1823
- const ROSTER_GROUP_FIELDS = { ...LANE_FIELDS, count: true };
1824
- const LANE_KEYS = new Set(Object.keys(LANE_FIELDS));
1825
- const ROSTER_GROUP_KEYS = new Set(Object.keys(ROSTER_GROUP_FIELDS));
1826
- /**
1827
- * Parse `actors[index].roster` compact groups into concrete lanes. This is authoring sugar for
1828
- * "N users of M adapter-owned types across S surfaces"; the runtime receives only `lanes[]`.
1829
- */
1830
- function parseRosterGroups(raw, actorIndex) {
1831
- if (raw === undefined) {
1832
- return { ok: true, value: undefined };
1833
- }
1834
- if (!Array.isArray(raw) || raw.length === 0) {
1835
- return invalid(`actors[${actorIndex}].roster must be a non-empty array of group objects ({ id, count, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry? }) when set.`);
1836
- }
1837
- const expanded = [];
1838
- const seenGroupIds = new Set();
1839
- for (const [groupIndex, entry] of raw.entries()) {
1840
- if (!isRecord(entry)) {
1841
- return invalid(`actors[${actorIndex}].roster[${groupIndex}] must be an object ({ id, count, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry? }).`);
1842
- }
1843
- const unknownKeys = Object.keys(entry).filter((key) => !ROSTER_GROUP_KEYS.has(key));
1844
- if (unknownKeys.length > 0) {
1845
- return invalid(`Unknown \`actors[${actorIndex}].roster[${groupIndex}]\` field(s): ${unknownKeys.join(", ")}. Known roster group fields: ${[...ROSTER_GROUP_KEYS].join(", ")}.`);
1846
- }
1847
- const groupId = str(entry.id);
1848
- if (groupId === undefined) {
1849
- return invalid(`actors[${actorIndex}].roster[${groupIndex}].id is required and must be a public-safe token matching ${LANE_ID_PATTERN}.`);
1850
- }
1851
- if (!LANE_ID_PATTERN.test(groupId) || groupId.length > LANE_ID_MAX_CHARS - 3) {
1852
- return invalid(`actors[${actorIndex}].roster[${groupIndex}].id must be a public-safe token matching ${LANE_ID_PATTERN} and at most ${LANE_ID_MAX_CHARS - 3} chars (generated lanes use <id>-NN); got "${groupId}".`);
1853
- }
1854
- if (seenGroupIds.has(groupId)) {
1855
- return invalid(`actors[${actorIndex}].roster group ids must be unique (duplicate "${groupId}").`);
1856
- }
1857
- seenGroupIds.add(groupId);
1858
- const count = posInt(entry.count);
1859
- if (count === undefined) {
1860
- return invalid(`actors[${actorIndex}].roster[${groupIndex}].count is required and must be a positive integer.`);
1861
- }
1862
- const groupLaneInput = { ...entry };
1863
- delete groupLaneInput.id;
1864
- delete groupLaneInput.count;
1865
- for (let i = 1; i <= count; i += 1) {
1866
- expanded.push({
1867
- ...groupLaneInput,
1868
- id: `${groupId}-${String(i).padStart(2, "0")}`
1869
- });
1870
- }
1871
- }
1872
- return parseLanes(expanded, actorIndex);
1873
- }
1874
- /**
1875
- * Parse `actors[index].lanes` into a fan-out roster (computer-use E2B route). Structural only:
1876
- * each lane is `{ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry? }`.
1877
- * Lane ids (when declared) must be public-safe path tokens and unique; lane grouping metadata
1878
- * must be public-safe tokens; a lane device must be a known preset name. The
1879
- * route-scoped cross-validation (lanes XOR count/laneFocus, device XOR raw resolution, cap 16)
1880
- * runs in parseLabConfig where the route is known.
1881
- */
1882
- function parseLanes(raw, actorIndex) {
1883
- if (raw === undefined) {
1884
- return { ok: true, value: undefined };
1885
- }
1886
- if (!Array.isArray(raw) || raw.length === 0) {
1887
- return invalid(`actors[${actorIndex}].lanes must be a non-empty array of lane objects ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry? }) when set.`);
1888
- }
1889
- const lanes = [];
1890
- const seenIds = new Set();
1891
- for (const [laneIndex, entry] of raw.entries()) {
1892
- if (!isRecord(entry)) {
1893
- return invalid(`actors[${actorIndex}].lanes[${laneIndex}] must be an object ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry? }).`);
1894
- }
1895
- const unknownKeys = Object.keys(entry).filter((key) => !LANE_KEYS.has(key));
1896
- if (unknownKeys.length > 0) {
1897
- return invalid(`Unknown \`actors[${actorIndex}].lanes[${laneIndex}]\` field(s): ${unknownKeys.join(", ")}. Known lane fields: ${[...LANE_KEYS].join(", ")}.`);
1898
- }
1899
- const lane = {};
1900
- const id = str(entry.id);
1901
- if (id !== undefined) {
1902
- if (!LANE_ID_PATTERN.test(id) || id.length > LANE_ID_MAX_CHARS) {
1903
- return invalid(`actors[${actorIndex}].lanes[${laneIndex}].id must be a public-safe token matching ${LANE_ID_PATTERN} and at most ${LANE_ID_MAX_CHARS} chars (it names per-lane evidence paths); got "${id}".`);
1904
- }
1905
- if (seenIds.has(id)) {
1906
- return invalid(`actors[${actorIndex}].lanes ids must be unique (duplicate "${id}").`);
1907
- }
1908
- seenIds.add(id);
1909
- lane.id = id;
1910
- }
1911
- const device = str(entry.device);
1912
- if (device !== undefined) {
1913
- if (!isDevicePresetName(device)) {
1914
- return invalid(`actors[${actorIndex}].lanes[${laneIndex}].device must be one of: ${DEVICE_PRESET_NAMES.join(", ")}.`);
1915
- }
1916
- lane.device = device;
1917
- }
1918
- const persona = str(entry.persona);
1919
- if (persona !== undefined)
1920
- lane.persona = persona;
1921
- const actorType = parseLaneMetadata(entry.actorType, `actors[${actorIndex}].lanes[${laneIndex}].actorType`);
1922
- if (!actorType.ok)
1923
- return actorType;
1924
- if (actorType.value !== undefined)
1925
- lane.actorType = actorType.value;
1926
- const surface = parseLaneMetadata(entry.surface, `actors[${actorIndex}].lanes[${laneIndex}].surface`);
1927
- if (!surface.ok)
1928
- return surface;
1929
- if (surface.value !== undefined)
1930
- lane.surface = surface.value;
1931
- const caseGroup = parseLaneMetadata(entry.caseGroup, `actors[${actorIndex}].lanes[${laneIndex}].caseGroup`);
1932
- if (!caseGroup.ok)
1933
- return caseGroup;
1934
- if (caseGroup.value !== undefined)
1935
- lane.caseGroup = caseGroup.value;
1936
- const instruction = str(entry.instruction);
1937
- if (instruction !== undefined)
1938
- lane.instruction = instruction;
1939
- const stopWhenResult = parseStopWhen(entry.stopWhen, `actors[${actorIndex}].lanes[${laneIndex}].stopWhen`);
1940
- if (!stopWhenResult.ok)
1941
- return stopWhenResult;
1942
- if (stopWhenResult.value !== undefined)
1943
- lane.stopWhen = stopWhenResult.value;
1944
- const dwellResult = parseDwell(entry.dwell, `actors[${actorIndex}].lanes[${laneIndex}].dwell`);
1945
- if (!dwellResult.ok)
1946
- return dwellResult;
1947
- if (dwellResult.value !== undefined)
1948
- lane.dwell = dwellResult.value;
1949
- if (entry.reasoningEffort !== undefined) {
1950
- if (!isReasoningEffort(entry.reasoningEffort)) {
1951
- return invalid(`actors[${actorIndex}].lanes[${laneIndex}].reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
1952
- }
1953
- lane.reasoningEffort = entry.reasoningEffort;
1954
- }
1955
- const target = str(entry.target);
1956
- if (target !== undefined) {
1957
- if (!isHttpUrl(target)) {
1958
- return invalid(`actors[${actorIndex}].lanes[${laneIndex}].target must be an absolute http(s) URL.`);
1959
- }
1960
- lane.target = target;
1961
- }
1962
- // `entry` is shape-captured here; the same-origin-with-serve.url check needs serve context, so
1963
- // it runs in sharedWorldValidationReason (where the route + serve.url are known).
1964
- const laneEntry = str(entry.entry);
1965
- if (laneEntry !== undefined)
1966
- lane.entry = laneEntry;
1967
- // `host` marks the designated host seat on the external-public shared-world route; the
1968
- // exactly-one-host check runs in externalPublicSharedWorldValidationReason (route context).
1969
- if (entry.host !== undefined) {
1970
- if (typeof entry.host !== "boolean") {
1971
- return invalid(`actors[${actorIndex}].lanes[${laneIndex}].host must be a boolean (marks the designated host seat on the external-public shared-world route).`);
1972
- }
1973
- if (entry.host)
1974
- lane.host = true;
1975
- }
1976
- lanes.push(lane);
1977
- }
1978
- return { ok: true, value: lanes };
1979
- }
1980
- /**
1981
- * The researcher's protocol. Each task carries what the participant is asked to do and, optionally,
1982
- * the observation that proves it happened — reusing `stopWhen`, so a criterion is exactly as
1983
- * expressive as a stop condition and an author who knows one knows the other.
1984
- *
1985
- * A task with no `success` is allowed on purpose: some things you ask a participant to do (think
1986
- * aloud, say what confused you) are not observable, and the funnel reports them as unmeasurable
1987
- * rather than quietly counting them failed.
1988
- */
1989
- function parseTasks(raw, field) {
1990
- if (raw === undefined)
1991
- return { ok: true, value: undefined };
1992
- if (!Array.isArray(raw) || raw.length === 0) {
1993
- return invalid(`\`${field}\` must be a non-empty list of tasks when set.`);
1994
- }
1995
- const tasks = [];
1996
- const seen = new Set();
1997
- for (const [index, entry] of raw.entries()) {
1998
- if (!isRecord(entry))
1999
- return invalid(`each \`${field}\` entry must be a mapping.`);
2000
- const id = str(entry.id);
2001
- if (id === undefined || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id)) {
2002
- return invalid(`\`${field}[${index}].id\` must be a short id like "sign-up".`);
2003
- }
2004
- if (seen.has(id))
2005
- return invalid(`\`${field}\` has a duplicate task id "${id}"; ids appear in evidence and must be unique.`);
2006
- seen.add(id);
2007
- const goal = str(entry.goal);
2008
- if (goal === undefined) {
2009
- return invalid(`\`${field}[${index}].goal\` is required — what the PARTICIPANT is asked to do, in their language.`);
2010
- }
2011
- const successResult = parseStopWhen(entry.success, `${field}[${index}].success`);
2012
- if (!successResult.ok)
2013
- return successResult;
2014
- tasks.push({ id, goal, ...(successResult.value === undefined ? {} : { success: successResult.value }) });
2015
- }
2016
- return { ok: true, value: tasks };
2017
- }
2018
- /** The bounds a dwell window (#510) must sit inside: at least one frame, at most an hour. */
2019
- export const DWELL_MIN_MS = 1_000;
2020
- export const DWELL_MAX_MS = 3_600_000;
2021
- export const DWELL_DEFAULT_EVERY_MS = 10_000;
2022
- function parseDwell(raw, field) {
2023
- if (raw === undefined)
2024
- return { ok: true, value: undefined };
2025
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
2026
- return invalid(`${field} must be an object with ms (and optional when, everyMs, then).`);
2027
- const entry = raw;
2028
- const ms = entry.ms;
2029
- if (typeof ms !== "number" || !Number.isInteger(ms) || ms < DWELL_MIN_MS || ms > DWELL_MAX_MS) {
2030
- return invalid(`${field}.ms must be an integer between ${DWELL_MIN_MS} and ${DWELL_MAX_MS} milliseconds.`);
2031
- }
2032
- const everyMs = entry.everyMs === undefined ? DWELL_DEFAULT_EVERY_MS : entry.everyMs;
2033
- if (typeof everyMs !== "number" || !Number.isInteger(everyMs) || everyMs < DWELL_MIN_MS || everyMs > ms) {
2034
- return invalid(`${field}.everyMs must be an integer between ${DWELL_MIN_MS} and ${field}.ms.`);
2035
- }
2036
- const then = entry.then === undefined ? "continue" : entry.then;
2037
- if (then !== "continue" && then !== "stop")
2038
- return invalid(`${field}.then must be "continue" or "stop".`);
2039
- const whenResult = parseStopWhen(entry.when, `${field}.when`);
2040
- if (!whenResult.ok)
2041
- return whenResult;
2042
- return { ok: true, value: { ...(whenResult.value === undefined ? {} : { when: whenResult.value }), ms, everyMs, then } };
2043
- }
2044
- function parseStopWhen(raw, field) {
2045
- if (raw === undefined) {
2046
- return { ok: true, value: undefined };
2047
- }
2048
- if (!isRecord(raw)) {
2049
- return invalid(`${field} must be an object ({ any: [{ id?, urlIncludes?, urlPathEquals?, textIncludes?, appStatePathEquals? }] }).`);
2050
- }
2051
- if (!Array.isArray(raw.any) || raw.any.length === 0) {
2052
- return invalid(`${field}.any must be a non-empty array of stop condition rules.`);
2053
- }
2054
- const any = [];
2055
- for (const [index, entry] of raw.any.entries()) {
2056
- if (!isRecord(entry)) {
2057
- return invalid(`${field}.any[${index}] must be an object ({ id?, urlIncludes?, urlPathEquals?, textIncludes?, appStatePathEquals? }).`);
2058
- }
2059
- const rule = {};
2060
- const id = str(entry.id);
2061
- if (id !== undefined) {
2062
- if (!LANE_ID_PATTERN.test(id) || id.length > LANE_METADATA_MAX_CHARS) {
2063
- return invalid(`${field}.any[${index}].id must be a public-safe token matching ${LANE_ID_PATTERN} and at most ${LANE_METADATA_MAX_CHARS} chars; got "${id}".`);
2064
- }
2065
- rule.id = id;
2066
- }
2067
- const urlIncludes = str(entry.urlIncludes);
2068
- if (urlIncludes !== undefined) {
2069
- rule.urlIncludes = urlIncludes;
2070
- }
2071
- const urlPathEquals = str(entry.urlPathEquals);
2072
- if (urlPathEquals !== undefined) {
2073
- if (!urlPathEquals.startsWith("/") || urlPathEquals.startsWith("//")) {
2074
- return invalid(`${field}.any[${index}].urlPathEquals must be an absolute URL path starting with one slash.`);
2075
- }
2076
- rule.urlPathEquals = urlPathEquals;
2077
- }
2078
- const textIncludes = str(entry.textIncludes);
2079
- if (textIncludes !== undefined) {
2080
- rule.textIncludes = textIncludes;
2081
- }
2082
- if (entry.appStatePathEquals !== undefined) {
2083
- const parsed = parseStopWhenAppStatePathEquals(entry.appStatePathEquals, `${field}.any[${index}].appStatePathEquals`);
2084
- if (!parsed.ok)
2085
- return parsed;
2086
- rule.appStatePathEquals = parsed.value;
2087
- }
2088
- if (rule.urlIncludes === undefined && rule.urlPathEquals === undefined && rule.textIncludes === undefined && rule.appStatePathEquals === undefined) {
2089
- return invalid(`${field}.any[${index}] must declare at least one condition: urlIncludes, urlPathEquals, textIncludes, or appStatePathEquals.`);
2090
- }
2091
- any.push(rule);
2092
- }
2093
- return { ok: true, value: { any } };
2094
- }
2095
- function parseStopWhenAppStatePathEquals(raw, field) {
2096
- if (!isRecord(raw)) {
2097
- return invalid(`${field} must be an object ({ path, equals }).`);
2098
- }
2099
- const pathValue = str(raw.path);
2100
- if (pathValue === undefined) {
2101
- return invalid(`${field}.path is required and must be a dot-separated public-safe path.`);
2102
- }
2103
- if (!/^[A-Za-z0-9_.-]+$/.test(pathValue)) {
2104
- return invalid(`${field}.path must contain only letters, digits, underscore, dash, and dot.`);
2105
- }
2106
- if (!Object.prototype.hasOwnProperty.call(raw, "equals")) {
2107
- return invalid(`${field}.equals is required.`);
2108
- }
2109
- const equals = raw.equals;
2110
- if (equals !== null && typeof equals !== "string" && typeof equals !== "number" && typeof equals !== "boolean") {
2111
- return invalid(`${field}.equals must be a string, number, boolean, or null.`);
2112
- }
2113
- return { ok: true, value: { path: pathValue, equals } };
2114
- }
2115
- function parseLaneMetadata(raw, field) {
2116
- const value = str(raw);
2117
- if (value === undefined) {
2118
- return { ok: true, value: undefined };
2119
- }
2120
- if (!LANE_ID_PATTERN.test(value) || value.length > LANE_METADATA_MAX_CHARS) {
2121
- return invalid(`${field} must be a public-safe token matching ${LANE_ID_PATTERN} and at most ${LANE_METADATA_MAX_CHARS} chars; got "${value}".`);
2122
- }
2123
- return { ok: true, value };
2124
- }
2125
- function parseExecution(raw) {
2126
- if (raw === undefined) {
2127
- return { ok: true, value: undefined };
2128
- }
2129
- if (!isRecord(raw)) {
2130
- return invalid("`execution` must be an object.");
2131
- }
2132
- const execution = {};
2133
- if (raw.target !== undefined) {
2134
- const target = str(raw.target);
2135
- if (target !== "local" && target !== "e2b-desktop" && target !== "e2b-terminal") {
2136
- return invalid("`execution.target` must be local, e2b-desktop, or e2b-terminal.");
2137
- }
2138
- execution.target = target;
2139
- }
2140
- const timeoutMs = posInt(raw.timeoutMs);
2141
- if (timeoutMs !== undefined)
2142
- execution.timeoutMs = timeoutMs;
2143
- const completionTimeoutMs = posInt(raw.completionTimeoutMs);
2144
- if (completionTimeoutMs !== undefined)
2145
- execution.completionTimeoutMs = completionTimeoutMs;
2146
- const concurrency = posInt(raw.concurrency);
2147
- if (concurrency !== undefined)
2148
- execution.concurrency = concurrency;
2149
- const desktopResult = parseDesktop(raw.desktop);
2150
- if (!desktopResult.ok) {
2151
- return desktopResult;
2152
- }
2153
- if (desktopResult.value)
2154
- execution.desktop = desktopResult.value;
2155
- // Reuse the terminal lane's caps parser (same shape, not a fork) — a malformed budget is a hard
2156
- // error, never silently dropped (a cap that silently does nothing would be a safety lie).
2157
- const capsResult = parseCaps(raw.caps);
2158
- if (!capsResult.ok) {
2159
- return capsResult;
2160
- }
2161
- if (capsResult.value)
2162
- execution.caps = capsResult.value;
2163
- const terminalResult = parseTerminal(raw.terminal);
2164
- if (!terminalResult.ok) {
2165
- return terminalResult;
2166
- }
2167
- if (terminalResult.value)
2168
- execution.terminal = terminalResult.value;
2169
- if (raw.runtime !== undefined) {
2170
- if (!isRecord(raw.runtime) || Object.keys(raw.runtime).some((key) => key !== "version") || !isExactRuntimeVersion(raw.runtime.version)) {
2171
- return invalid("`execution.runtime` must contain only an exact Codex `version` (for example 0.153.3); tags, ranges, URLs, and unknown fields are not accepted.");
2172
- }
2173
- execution.runtime = { version: raw.runtime.version };
2174
- }
2175
- if (raw.runtimeAuth !== undefined) {
2176
- const runtimeAuth = str(raw.runtimeAuth);
2177
- if (runtimeAuth !== "openai-env" && runtimeAuth !== "openai-egress") {
2178
- return invalid("`execution.runtimeAuth` must be openai-env or openai-egress (the terminal agent's runtime-auth channel).");
2179
- }
2180
- execution.runtimeAuth = runtimeAuth;
2181
- }
2182
- if (raw.egressAllow !== undefined) {
2183
- if (!Array.isArray(raw.egressAllow) || raw.egressAllow.some((h) => typeof h !== "string")) {
2184
- return invalid("`execution.egressAllow` must be an array of host strings.");
2185
- }
2186
- const hosts = raw.egressAllow.map((h) => h.trim()).filter((h) => h.length > 0);
2187
- if (hosts.length === 0) {
2188
- // An empty list would deny everything including the agent's own model endpoint, which
2189
- // fails as an unexplained hang rather than a refusal. Say so at parse time.
2190
- return invalid("`execution.egressAllow` was declared but empty. Declaring it denies all other egress, so "
2191
- + "an empty list denies everything, including the agent's own provider endpoint. Remove "
2192
- + "the field for unrestricted egress, or list the hosts the run needs.");
2193
- }
2194
- execution.egressAllow = hosts;
2195
- }
2196
- return { ok: true, value: Object.keys(execution).length > 0 ? execution : undefined };
2197
- }
2198
- function parseTerminal(raw) {
2199
- if (raw === undefined) {
2200
- return { ok: true, value: undefined };
2201
- }
2202
- if (!isRecord(raw)) {
2203
- return invalid("`execution.terminal` must be an object ({ transport?, stdin? }).");
2204
- }
2205
- const terminal = {};
2206
- if (raw.transport !== undefined) {
2207
- const transport = str(raw.transport);
2208
- if (transport !== "exec-stream") {
2209
- // "pty" is deliberately rejected: stdin is disabled, so the capture is a non-interactive
2210
- // exec stream — an interactive-PTY label would overstate the mechanism (invariant 6 + the
2211
- // goal packet's PTY ruling). True duplex PTY does not ship.
2212
- return invalid("`execution.terminal.transport` must be exec-stream — captured non-interactive exec output (stdin disabled) is not an interactive PTY; true duplex PTY transport is not supported.");
2213
- }
2214
- terminal.transport = transport;
2215
- }
2216
- if (raw.stdin !== undefined) {
2217
- const stdin = str(raw.stdin);
2218
- if (stdin !== "disabled" && stdin !== "planned" && stdin !== "sent") {
2219
- return invalid("`execution.terminal.stdin` must be disabled, planned, or sent.");
2220
- }
2221
- if (stdin === "sent") {
2222
- // Assisted input is forbidden until the interventions ledger + comparability flag + verify
2223
- // check exist (safety contract item 7) — shipping it now would let an assisted run pose as
2224
- // autonomous green proof.
2225
- return invalid("`execution.terminal.stdin: sent` (assisted input) is not supported — the current route cannot capture assisted input with a non-comparable marker. stdin is disabled by default.");
2226
- }
2227
- terminal.stdin = stdin;
2228
- }
2229
- return { ok: true, value: Object.keys(terminal).length > 0 ? terminal : undefined };
2230
- }
2231
- function parseDesktop(raw) {
2232
- if (!isRecord(raw)) {
2233
- return { ok: true, value: undefined };
2234
- }
2235
- const desktop = {};
2236
- if (raw.recording !== undefined) {
2237
- if (!isRecord(raw.recording) || typeof raw.recording.audio !== "boolean"
2238
- || Object.keys(raw.recording).some(key => key !== "audio")) {
2239
- return invalid("`execution.desktop.recording` must be { audio: true|false }; omit it for snapshots only.");
2240
- }
2241
- desktop.recording = { audio: raw.recording.audio };
2242
- }
2243
- if (raw.device !== undefined) {
2244
- const device = str(raw.device);
2245
- if (!device || !isDevicePresetName(device)) {
2246
- return invalid(`\`execution.desktop.device\` must be one of: ${DEVICE_PRESET_NAMES.join(", ")}.`);
2247
- }
2248
- desktop.device = device;
2249
- }
2250
- if (raw.resolution !== undefined) {
2251
- const resolution = raw.resolution;
2252
- if (!Array.isArray(resolution) || resolution.length !== 2 || !resolution.every((value) => Number.isInteger(value) && value > 0)) {
2253
- return invalid("`execution.desktop.resolution` must be two positive integers [width, height].");
2254
- }
2255
- desktop.resolution = [resolution[0], resolution[1]];
2256
- }
2257
- const sandboxTimeoutMs = posInt(raw.sandboxTimeoutMs);
2258
- if (sandboxTimeoutMs !== undefined)
2259
- desktop.sandboxTimeoutMs = sandboxTimeoutMs;
2260
- if (raw.browser !== undefined) {
2261
- const browser = str(raw.browser);
2262
- if (browser !== "default" && browser !== "chrome" && browser !== "chromium" && browser !== "firefox") {
2263
- return invalid("`execution.desktop.browser` must be default, chrome, chromium, or firefox.");
2264
- }
2265
- desktop.browser = browser;
2266
- }
2267
- // A custom E2B desktop template NAME or ID. Trimmed non-empty when present; deliberately NOT
2268
- // allowlisted (any string is a valid template name/id — over-restricting would reject real
2269
- // adopter images). An explicitly-set but blank/whitespace value is a mistake, not a template.
2270
- if (raw.template !== undefined) {
2271
- const template = str(raw.template);
2272
- if (template === undefined) {
2273
- return invalid("`execution.desktop.template` must be a non-empty E2B desktop template NAME or ID when set (any string is accepted; there is no allowlist).");
2274
- }
2275
- desktop.template = template;
2276
- }
2277
- if (typeof raw.codexAppServer === "boolean")
2278
- desktop.codexAppServer = raw.codexAppServer;
2279
- if (raw.fidelity !== undefined) {
2280
- if (!isRecord(raw.fidelity) || typeof raw.fidelity.mobileEmulation !== "boolean") {
2281
- return invalid("`execution.desktop.fidelity` must be an object with `mobileEmulation: true|false` (optional deviceScaleFactor, touch, userAgent).");
2282
- }
2283
- const fidelity = { mobileEmulation: raw.fidelity.mobileEmulation };
2284
- if (raw.fidelity.deviceScaleFactor !== undefined) {
2285
- const scale = raw.fidelity.deviceScaleFactor;
2286
- if (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0 || scale > 4) {
2287
- return invalid("`execution.desktop.fidelity.deviceScaleFactor` must be a number greater than 0 and at most 4.");
2288
- }
2289
- fidelity.deviceScaleFactor = scale;
2290
- }
2291
- if (raw.fidelity.touch !== undefined) {
2292
- if (typeof raw.fidelity.touch !== "boolean") {
2293
- return invalid("`execution.desktop.fidelity.touch` must be true or false.");
2294
- }
2295
- fidelity.touch = raw.fidelity.touch;
2296
- }
2297
- if (raw.fidelity.userAgent !== undefined) {
2298
- const userAgent = str(raw.fidelity.userAgent);
2299
- if (userAgent === undefined) {
2300
- return invalid("`execution.desktop.fidelity.userAgent` must be a non-empty string when set.");
2301
- }
2302
- fidelity.userAgent = userAgent;
2303
- }
2304
- desktop.fidelity = fidelity;
2305
- }
2306
- if (raw.media !== undefined) {
2307
- if (!isRecord(raw.media)) {
2308
- return invalid("`execution.desktop.media` must be an object with `camera` and/or `microphone` ({ source }).");
2309
- }
2310
- const media = {};
2311
- if (raw.media.camera !== undefined) {
2312
- const source = isRecord(raw.media.camera) ? str(raw.media.camera.source) : undefined;
2313
- if (source === undefined || (source !== "synthetic" && !source.endsWith(".y4m"))) {
2314
- return invalid("`execution.desktop.media.camera.source` must be `synthetic` or a path to a `.y4m` file (Chrome's fake video capture reads Y4M).");
2315
- }
2316
- media.camera = { source };
2317
- }
2318
- if (raw.media.microphone !== undefined) {
2319
- const source = isRecord(raw.media.microphone) ? str(raw.media.microphone.source) : undefined;
2320
- if (source !== "speech")
2321
- return invalid("`execution.desktop.media.microphone.source` must be `speech`. Microphone source-file injection is unsupported.");
2322
- media.microphone = { source };
2323
- }
2324
- if (media.camera === undefined && media.microphone === undefined) {
2325
- return invalid("`execution.desktop.media` declares neither `camera` nor `microphone`.");
2326
- }
2327
- desktop.media = media;
2328
- }
2329
- return { ok: true, value: Object.keys(desktop).length > 0 ? desktop : undefined };
2330
- }
2331
- function parsePersonas(raw) {
2332
- if (!Array.isArray(raw)) {
2333
- return undefined;
2334
- }
2335
- const personas = raw.filter(isRecord);
2336
- return personas.length > 0 ? personas : undefined;
2337
- }
2338
- function parseScenario(raw) {
2339
- if (!isRecord(raw)) {
2340
- return { ok: true, value: undefined };
2341
- }
2342
- const scenario = {};
2343
- const ref = str(raw.ref);
2344
- if (ref)
2345
- scenario.ref = ref;
2346
- if (isRecord(raw.inline))
2347
- scenario.inline = raw.inline;
2348
- const mode = str(raw.mode);
2349
- if (mode === "dry-run" || mode === "live")
2350
- scenario.mode = mode;
2351
- const capsResult = parseCaps(raw.caps);
2352
- if (!capsResult.ok) {
2353
- return capsResult;
2354
- }
2355
- if (capsResult.value)
2356
- scenario.caps = capsResult.value;
2357
- return { ok: true, value: Object.keys(scenario).length > 0 ? scenario : undefined };
2358
- }
2359
- /**
2360
- * Parse `scenario.caps`. Returns a parse failure on a malformed value rather than silently
2361
- * dropping a budget declaration (a cap that silently does nothing would be a safety lie —
2362
- * invariant 6). Each cap must be a non-negative finite number.
2363
- */
2364
- function parseCaps(raw) {
2365
- if (raw === undefined) {
2366
- return { ok: true, value: undefined };
2367
- }
2368
- if (!isRecord(raw)) {
2369
- return invalid("`scenario.caps` must be an object ({ maxUsd?, maxTotalUsd?, maxJobs?, maxMinutes? }).");
2370
- }
2371
- const caps = {};
2372
- for (const key of ["maxUsd", "maxTotalUsd", "maxJobs", "maxMinutes"]) {
2373
- if (raw[key] === undefined)
2374
- continue;
2375
- const value = nonNegNumber(raw[key]);
2376
- if (value === undefined) {
2377
- return invalid(`\`scenario.caps.${key}\` must be a non-negative number.`);
2378
- }
2379
- caps[key] = value;
2380
- }
2381
- return { ok: true, value: Object.keys(caps).length > 0 ? caps : undefined };
2382
- }
2383
- function parsePolicies(raw) {
2384
- if (!isRecord(raw)) {
2385
- return undefined;
2386
- }
2387
- const policies = {};
2388
- if (typeof raw.redactRepos === "boolean")
2389
- policies.redactRepos = raw.redactRepos;
2390
- if (typeof raw.redactScreenshots === "boolean")
2391
- policies.redactScreenshots = raw.redactScreenshots;
2392
- if (typeof raw.allowPublicTargets === "boolean")
2393
- policies.allowPublicTargets = raw.allowPublicTargets;
2394
- if (raw.mediaPermission === "prompt" || raw.mediaPermission === "granted")
2395
- policies.mediaPermission = raw.mediaPermission;
2396
- if (typeof raw.allowPrivateRepoAccess === "boolean")
2397
- policies.allowPrivateRepoAccess = raw.allowPrivateRepoAccess;
2398
- if (typeof raw.allowProviderCredentials === "boolean")
2399
- policies.allowProviderCredentials = raw.allowProviderCredentials;
2400
- if (typeof raw.allowPaymentCredentials === "boolean")
2401
- policies.allowPaymentCredentials = raw.allowPaymentCredentials;
2402
- if (typeof raw.allowGitHubMutation === "boolean")
2403
- policies.allowGitHubMutation = raw.allowGitHubMutation;
2404
- return Object.keys(policies).length > 0 ? policies : undefined;
2405
- }
2406
- // Fail-LOUD: an unrecognized `review.*` key (e.g. a `scorrer:` typo of `scorer`) is rejected rather
2407
- // than silently dropped — a gate you think you declared must not vanish silently (#316).
2408
- function parseReview(raw) {
2409
- if (raw === undefined)
2410
- return { ok: true, value: undefined };
2411
- if (!isRecord(raw))
2412
- return invalid("`review` must be a mapping.");
2413
- const knownKeys = new Set(["scoring", "milestones", "vocabulary", "scorer", "analysis"]);
2414
- const unknownKeys = Object.keys(raw).filter((key) => !knownKeys.has(key));
2415
- if (unknownKeys.length > 0) {
2416
- return invalid(`Unknown \`review\` field(s): ${unknownKeys.join(", ")}. A declared gate must not vanish silently — did you mean \`scorer\`? Known review fields: scoring, milestones, vocabulary, scorer, analysis.`);
2417
- }
2418
- const analysis = resolveAutomaticAnalysis(raw.analysis);
2419
- if (!analysis.ok)
2420
- return invalid(analysis.message);
2421
- const review = {};
2422
- if (raw.analysis !== undefined)
2423
- review.analysis = raw.analysis === false ? false : { ...raw.analysis };
2424
- const scoring = str(raw.scoring);
2425
- if (scoring)
2426
- review.scoring = scoring;
2427
- const milestones = str(raw.milestones);
2428
- if (milestones)
2429
- review.milestones = milestones;
2430
- const vocabulary = str(raw.vocabulary);
2431
- if (vocabulary)
2432
- review.vocabulary = vocabulary;
2433
- if (raw.scorer !== undefined) {
2434
- const scorer = parseReviewScorer(raw.scorer);
2435
- if (!scorer.ok)
2436
- return scorer;
2437
- review.scorer = scorer.value;
2438
- }
2439
- return { ok: true, value: Object.keys(review).length > 0 ? review : undefined };
2440
- }
2441
- function parseReviewScorer(raw) {
2442
- if (!isRecord(raw))
2443
- return invalid("`review.scorer` must be a mapping with a `ref` path (e.g. { ref: scorers/product.mjs }).");
2444
- const unknownKeys = Object.keys(raw).filter((key) => key !== "ref");
2445
- if (unknownKeys.length > 0) {
2446
- return invalid(`Unknown \`review.scorer\` field(s): ${unknownKeys.join(", ")}. review.scorer accepts only \`ref\` (a repo-relative scorer-module path).`);
2447
- }
2448
- const ref = str(raw.ref);
2449
- if (!ref)
2450
- return invalid("`review.scorer.ref` must be a non-empty repo-relative path to a scorer module (.mjs recommended; .js/.cjs accepted).");
2451
- return { ok: true, value: { ref } };
2452
- }
2453
- function parseDefaults(raw) {
2454
- if (!isRecord(raw)) {
2455
- return undefined;
2456
- }
2457
- const defaults = {};
2458
- if (typeof raw.open === "boolean")
2459
- defaults.open = raw.open;
2460
- return Object.keys(defaults).length > 0 ? defaults : undefined;
2461
- }
2462
- // Fail-loud (never silently swallow a comms setting): a malformed `comms` block returns a parse
2463
- // failure rather than being dropped.
2464
- function parseComms(raw) {
2465
- if (raw === undefined)
2466
- return { ok: true, value: undefined };
2467
- if (!isRecord(raw))
2468
- return invalid("`comms` must be a mapping.");
2469
- const unsupported = Object.keys(raw).filter(key => key !== "email");
2470
- if (unsupported.length > 0) {
2471
- return invalid(`Unsupported comms setting(s): ${unsupported.join(", ")}. Only \`comms.email\` is currently supported; SMS is not yet available.`);
2472
- }
2473
- const comms = {};
2474
- if (raw.email !== undefined) {
2475
- const email = parseCommsEmail(raw.email);
2476
- if (!email.ok)
2477
- return email;
2478
- comms.email = email.value;
2479
- }
2480
- return { ok: true, value: Object.keys(comms).length > 0 ? comms : undefined };
2481
- }
2482
- function parseCommsEmail(raw) {
2483
- if (!isRecord(raw))
2484
- return invalid("`comms.email` must be a mapping.");
2485
- if (raw.connection !== undefined) {
2486
- const unsupported = Object.keys(raw).filter(key => !["connection", "linkOrigin", "allowedOrigins"].includes(key));
2487
- if (unsupported.length)
2488
- return invalid("An email connection cannot be mixed with capture settings, kind, recipients, or provider options. Use only connection, optional linkOrigin and allowedOrigins.");
2489
- if (typeof raw.connection !== "string" || !/^[a-z][a-z0-9-]{0,47}$/.test(raw.connection))
2490
- return invalid("`comms.email.connection` must name a saved connection (lowercase letters, digits and hyphens; at most 48 characters).");
2491
- const value = { kind: "real", connection: raw.connection };
2492
- const origin = (input) => {
2493
- if (typeof input !== "string")
2494
- return undefined;
2495
- try {
2496
- const url = new URL(input);
2497
- return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash ? url.origin : undefined;
2498
- }
2499
- catch {
2500
- return undefined;
2501
- }
2502
- };
2503
- if (raw.linkOrigin !== undefined) {
2504
- const parsed = origin(raw.linkOrigin);
2505
- if (!parsed)
2506
- return invalid("`comms.email.linkOrigin` must be an exact http(s) origin without credentials, query or fragment.");
2507
- value.linkOrigin = parsed;
2508
- }
2509
- if (raw.allowedOrigins !== undefined) {
2510
- if (!Array.isArray(raw.allowedOrigins) || raw.allowedOrigins.length > 16)
2511
- return invalid("`comms.email.allowedOrigins` must contain at most 16 exact http(s) origins.");
2512
- const origins = raw.allowedOrigins.map(origin);
2513
- if (origins.some(entry => entry === undefined))
2514
- return invalid("`comms.email.allowedOrigins` accepts exact http(s) origins without credentials, query or fragment.");
2515
- value.allowedOrigins = [...new Set(origins)];
2516
- }
2517
- return { ok: true, value };
2518
- }
2519
- const unknown = Object.keys(raw).filter(key => !["kind", "injectEnv", "port", "smtp", "linkOrigin", "recipients", "external"].includes(key));
2520
- if (unknown.length)
2521
- return invalid("Unknown email capture setting. Real inboxes select a saved `comms.email.connection`.");
2522
- if (raw.kind === "real") {
2523
- return invalid("Real inboxes require `comms.email.connection` naming a saved connection; omit kind.");
2524
- }
2525
- if (raw.kind !== undefined && raw.kind !== "fake") {
2526
- return invalid("`comms.email.kind` must be `fake`.");
2527
- }
2528
- // external ingress (#328): the ADOPTER runs the catch, so there is no subject env for humanish to
2529
- // inject and `injectEnv` becomes meaningless rather than merely unused — the operator points their
2530
- // own app at their own catch. Parse it first so the injectEnv requirement can key off it.
2531
- let external;
2532
- if (raw.external !== undefined) {
2533
- if (!isRecord(raw.external))
2534
- return invalid("`comms.email.external` must be a mapping.");
2535
- const catchBaseUrl = str(raw.external.catchBaseUrl);
2536
- if (catchBaseUrl === undefined) {
2537
- return invalid("`comms.email.external.catchBaseUrl` is required — the base URL of the catch YOU run (humanish reads its GET /deliveries and your app POSTs its sends to it).");
2538
- }
2539
- for (const [field, value] of [["catchBaseUrl", catchBaseUrl], ["inboxBaseUrl", str(raw.external.inboxBaseUrl)]]) {
2540
- if (value === undefined)
2541
- continue;
2542
- let parsed;
2543
- try {
2544
- parsed = new URL(value);
2545
- }
2546
- catch {
2547
- return invalid(`\`comms.email.external.${field}\` must be an absolute http(s) URL (got "${value}").`);
2548
- }
2549
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2550
- return invalid(`\`comms.email.external.${field}\` must be an absolute http(s) URL (got "${value}").`);
2551
- }
2552
- }
2553
- const authTokenEnv = str(raw.external.authTokenEnv);
2554
- if (authTokenEnv !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(authTokenEnv)) {
2555
- return invalid(`\`comms.email.external.authTokenEnv\` must be a valid env var NAME (got "${authTokenEnv}"); the value is read at runtime and never persisted.`);
2556
- }
2557
- const inboxBaseUrl = str(raw.external.inboxBaseUrl);
2558
- external = {
2559
- catchBaseUrl,
2560
- ...(inboxBaseUrl === undefined ? {} : { inboxBaseUrl }),
2561
- ...(authTokenEnv === undefined ? {} : { authTokenEnv })
2562
- };
2563
- }
2564
- // SMTP transport, for apps that send mail through SMTP rather than a provider's HTTP API.
2565
- let smtp;
2566
- if (raw.smtp !== undefined) {
2567
- if (!isRecord(raw.smtp))
2568
- return invalid("`comms.email.smtp` must be a mapping.");
2569
- const envName = (value, field) => {
2570
- const name = str(value);
2571
- if (name === undefined)
2572
- return undefined;
2573
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `__invalid__${field}`;
2574
- };
2575
- const hostEnv = envName(raw.smtp.hostEnv, "hostEnv");
2576
- const portEnv = envName(raw.smtp.portEnv, "portEnv");
2577
- if (hostEnv === undefined || portEnv === undefined) {
2578
- return invalid("`comms.email.smtp` needs both `hostEnv` and `portEnv` — the subject-env vars your app reads for its SMTP host and port. The harness sets them to its own loopback listener.");
2579
- }
2580
- if (hostEnv.startsWith("__invalid__") || portEnv.startsWith("__invalid__")) {
2581
- return invalid("`comms.email.smtp.hostEnv` and `portEnv` must be valid env var names.");
2582
- }
2583
- let smtpPort = 2525;
2584
- if (raw.smtp.port !== undefined) {
2585
- const parsed = posInt(raw.smtp.port);
2586
- if (parsed === undefined || parsed > 65_535)
2587
- return invalid("`comms.email.smtp.port` must be a positive integer ≤ 65535.");
2588
- smtpPort = parsed;
2589
- }
2590
- const userEnv = envName(raw.smtp.userEnv, "userEnv");
2591
- const passwordEnv = envName(raw.smtp.passwordEnv, "passwordEnv");
2592
- if (userEnv?.startsWith("__invalid__") || passwordEnv?.startsWith("__invalid__")) {
2593
- return invalid("`comms.email.smtp.userEnv` and `passwordEnv` must be valid env var names.");
2594
- }
2595
- smtp = {
2596
- port: smtpPort,
2597
- hostEnv,
2598
- portEnv,
2599
- ...(userEnv === undefined ? {} : { userEnv }),
2600
- ...(passwordEnv === undefined ? {} : { passwordEnv }),
2601
- ...(str(raw.smtp.user) === undefined ? {} : { user: str(raw.smtp.user) }),
2602
- ...(str(raw.smtp.password) === undefined ? {} : { password: str(raw.smtp.password) })
2603
- };
2604
- }
2605
- const injectEnv = str(raw.injectEnv);
2606
- if (injectEnv === undefined && external === undefined && smtp === undefined) {
2607
- return invalid("`comms.email` needs a transport: `injectEnv` (the subject-env var set to the catch's HTTP base URL, e.g. RESEND_BASE_URL), or `smtp` (host/port env vars, for an app that sends over SMTP), or `external` on an adopter-hosted plane where you run the catch yourself.");
2608
- }
2609
- if (injectEnv !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(injectEnv)) {
2610
- return invalid(`\`comms.email.injectEnv\` must be a valid env var name (got "${injectEnv}").`);
2611
- }
2612
- const email = {
2613
- kind: "fake",
2614
- ...(injectEnv === undefined ? {} : { injectEnv }),
2615
- ...(smtp === undefined ? {} : { smtp }),
2616
- ...(external === undefined ? {} : { external })
2617
- };
2618
- if (raw.port !== undefined) {
2619
- const port = posInt(raw.port);
2620
- if (port === undefined)
2621
- return invalid("`comms.email.port` must be a positive integer.");
2622
- // Cap at 65534: the catch reserves port+1 for the read-only inbox listener on the shared-world route.
2623
- if (port > 65_534)
2624
- return invalid("`comms.email.port` must be ≤ 65534 (the catch reserves port+1 for the inbox listener).");
2625
- email.port = port;
2626
- }
2627
- if (raw.recipients !== undefined) {
2628
- if (!Array.isArray(raw.recipients))
2629
- return invalid("`comms.email.recipients` must be a list.");
2630
- const recipients = [];
2631
- for (const entry of raw.recipients) {
2632
- if (!isRecord(entry))
2633
- return invalid("each `comms.email.recipients` entry must be a mapping.");
2634
- const lane = str(entry.lane);
2635
- if (lane === undefined)
2636
- return invalid("each `comms.email.recipients` entry needs a `lane`.");
2637
- const address = str(entry.address);
2638
- recipients.push({ lane, ...(address === undefined ? {} : { address }) });
2639
- }
2640
- email.recipients = recipients;
2641
- }
2642
- if (raw.linkOrigin !== undefined) {
2643
- const linkOrigin = str(raw.linkOrigin);
2644
- if (linkOrigin === undefined)
2645
- return invalid("`comms.email.linkOrigin` must be a string.");
2646
- try {
2647
- new URL(linkOrigin);
2648
- }
2649
- catch {
2650
- return invalid(`\`comms.email.linkOrigin\` must be an absolute URL origin (got "${linkOrigin}").`);
2651
- }
2652
- email.linkOrigin = linkOrigin;
2653
- }
2654
- return { ok: true, value: email };
2655
- }
2656
- function invalid(message) {
2657
- return { ok: false, error: { code: "HUMANISH_LAB_INVALID", message } };
2658
- }
2659
- function isRecord(value) {
2660
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2661
- }
2662
- function str(value) {
2663
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
2664
- }
2665
- function optionalStr(key, value) {
2666
- const parsed = str(value);
2667
- return parsed === undefined ? {} : { [key]: parsed };
2668
- }
2669
- function strList(value) {
2670
- if (typeof value === "string") {
2671
- const items = value.split(",").map((item) => item.trim()).filter(Boolean);
2672
- return items.length > 0 ? items : undefined;
2673
- }
2674
- if (!Array.isArray(value)) {
2675
- return undefined;
2676
- }
2677
- const items = value.filter((item) => typeof item === "string")
2678
- .map((item) => item.trim())
2679
- .filter(Boolean);
2680
- return items.length > 0 ? items : undefined;
2681
- }
2682
- function posInt(value) {
2683
- if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1) {
2684
- return value;
2685
- }
2686
- if (typeof value === "string" && /^\d+$/.test(value)) {
2687
- const parsed = Number.parseInt(value, 10);
2688
- return Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : undefined;
2689
- }
2690
- return undefined;
2691
- }
2692
- /** A non-negative finite number (0 allowed — caps default to 0 = no-spend). Accepts a numeric
2693
- * string too, since YAML scalars can arrive as strings. */
2694
- function nonNegNumber(value) {
2695
- if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
2696
- return value;
2697
- }
2698
- if (typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim())) {
2699
- const parsed = Number.parseFloat(value.trim());
2700
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
2701
- }
2702
- return undefined;
2703
- }
2704
- /** Analysis requires a live recording producer, including supported dry-run previews. */
2705
- export function automaticAnalysisRouteReason(config) {
2706
- if (config.review?.analysis === undefined || config.review.analysis === false)
2707
- return undefined;
2708
- if (routesToComputerUse(config) || routesToScriptedBrowser(config) || routesToTerminalProduct(config)
2709
- || routesToSharedWorld(config) || routesToConcurrentSharedWorld(config)
2710
- || ["app-url", "local-app", "local-tree", "desktop-cli", "terminal-product"].includes(config.subject.source))
2711
- return undefined;
2712
- return "review.analysis requires a computer-use, scripted-browser, terminal-product or shared-world study; synthetic, smoke and meta routes do not produce eligible live recordings.";
2713
- }
2714
- //# sourceMappingURL=lab-config.js.map