humanish 0.105.0 → 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.
- package/AGENTS.md +89 -94
- package/ARCHITECTURE.md +146 -0
- package/CHANGELOG.md +502 -0
- package/CONTEXT.md +51 -0
- package/CONTRIBUTING.md +111 -9
- package/README.md +213 -124
- package/SECURITY.md +1 -1
- package/TELEMETRY.md +112 -0
- package/dist/actors/affordance.d.ts +56 -0
- package/dist/actors/affordance.js +155 -0
- package/dist/actors/agent-session.js +42 -0
- package/dist/actors/codex/app-server-actor-trace.d.ts +11 -0
- package/dist/actors/codex/app-server-actor-trace.js +211 -0
- package/dist/actors/codex/app-server-client.d.ts +39 -0
- package/dist/actors/codex/app-server-client.js +116 -0
- package/dist/actors/codex/app-server-trace.d.ts +154 -0
- package/dist/actors/codex/app-server-trace.js +424 -0
- package/dist/actors/codex/app-server-ui.d.ts +40 -0
- package/dist/actors/codex/app-server-ui.js +328 -0
- package/dist/actors/codex/app-server.d.ts +44 -0
- package/dist/actors/codex/app-server.js +268 -0
- package/dist/actors/codex/qualified-versions.d.ts +45 -0
- package/dist/actors/codex/qualified-versions.js +87 -0
- package/dist/actors/codex/restricted-executable.d.ts +17 -0
- package/dist/actors/codex/restricted-executable.js +147 -0
- package/dist/actors/codex/restricted-launch.d.ts +116 -0
- package/dist/actors/codex/restricted-launch.js +222 -0
- package/dist/actors/codex/restricted-participant-policy.d.ts +46 -0
- package/dist/actors/codex/restricted-participant-policy.js +82 -0
- package/dist/actors/codex/restricted-participant.d.ts +20 -0
- package/dist/actors/codex/restricted-participant.js +432 -0
- package/dist/actors/codex/restricted-policy.d.ts +70 -0
- package/dist/actors/codex/restricted-policy.js +326 -0
- package/dist/actors/codex/restricted-session.d.ts +65 -0
- package/dist/actors/codex/restricted-session.js +432 -0
- package/dist/actors/codex/restricted-transport.d.ts +73 -0
- package/dist/actors/codex/restricted-transport.js +335 -0
- package/dist/actors/codex/restricted-turn.d.ts +62 -0
- package/dist/actors/codex/restricted-turn.js +284 -0
- package/dist/actors/computer-use/actor.d.ts +77 -0
- package/dist/actors/computer-use/actor.js +78 -0
- package/dist/actors/computer-use/admission-limit.d.ts +12 -0
- package/dist/actors/computer-use/admission-limit.js +19 -0
- package/dist/actors/computer-use/executor-error.js +48 -0
- package/dist/actors/computer-use/loop/actions.d.ts +26 -0
- package/dist/actors/computer-use/loop/actions.js +245 -0
- package/dist/actors/computer-use/loop/backstop.d.ts +42 -0
- package/dist/actors/computer-use/loop/backstop.js +202 -0
- package/dist/actors/computer-use/loop/debrief.d.ts +19 -0
- package/dist/actors/computer-use/loop/debrief.js +164 -0
- package/dist/actors/computer-use/loop/ending.d.ts +86 -0
- package/dist/actors/computer-use/loop/ending.js +316 -0
- package/dist/actors/computer-use/loop/observation.d.ts +55 -0
- package/dist/actors/computer-use/loop/observation.js +246 -0
- package/dist/actors/computer-use/loop/output-limit.d.ts +11 -0
- package/dist/actors/computer-use/loop/output-limit.js +21 -0
- package/dist/actors/computer-use/loop/provider-call.d.ts +19 -0
- package/dist/actors/computer-use/loop/provider-call.js +208 -0
- package/dist/actors/computer-use/loop/race.d.ts +34 -0
- package/dist/actors/computer-use/loop/race.js +79 -0
- package/dist/actors/computer-use/loop/session.d.ts +77 -0
- package/dist/actors/computer-use/loop/session.js +135 -0
- package/dist/actors/computer-use/loop/spend.d.ts +30 -0
- package/dist/actors/computer-use/loop/spend.js +85 -0
- package/dist/actors/computer-use/loop/trace.d.ts +35 -0
- package/dist/actors/computer-use/loop/trace.js +140 -0
- package/dist/actors/computer-use/loop/types.d.ts +404 -0
- package/dist/actors/computer-use/loop/types.js +1 -0
- package/dist/actors/computer-use/loop/usage.d.ts +102 -0
- package/dist/actors/computer-use/loop/usage.js +355 -0
- package/dist/actors/computer-use/loop.d.ts +11 -0
- package/dist/actors/computer-use/loop.js +248 -0
- package/dist/actors/computer-use/openai-provider.d.ts +107 -0
- package/dist/actors/computer-use/openai-provider.js +472 -0
- package/dist/actors/computer-use/openai-requests.d.ts +35 -0
- package/dist/actors/computer-use/openai-requests.js +74 -0
- package/dist/actors/computer-use/openai-wire.d.ts +67 -0
- package/dist/actors/computer-use/openai-wire.js +365 -0
- package/dist/actors/computer-use/provider-error.d.ts +27 -0
- package/dist/actors/computer-use/provider-error.js +86 -0
- package/dist/actors/computer-use/speech.js +6 -0
- package/dist/actors/contract.d.ts +315 -0
- package/dist/actors/contract.js +201 -0
- package/dist/actors/goal-source.js +38 -0
- package/dist/actors/local-agent/claude-session.d.ts +43 -0
- package/dist/actors/local-agent/claude-session.js +370 -0
- package/dist/actors/local-agent/cli.d.ts +115 -0
- package/dist/actors/local-agent/cli.js +413 -0
- package/dist/actors/local-agent/readiness.d.ts +31 -0
- package/dist/actors/local-agent/readiness.js +45 -0
- package/dist/actors/openai-endpoint.d.ts +2 -0
- package/dist/actors/openai-endpoint.js +2 -0
- package/dist/actors/output-token-limit.js +4 -0
- package/dist/actors/reasoning-effort.js +33 -0
- package/dist/actors/registry.d.ts +59 -0
- package/dist/actors/registry.js +89 -0
- package/dist/actors/scripted-browser/actor.d.ts +49 -0
- package/dist/actors/scripted-browser/actor.js +428 -0
- package/dist/actors/scripted-browser/browser-command.d.ts +1 -0
- package/dist/actors/scripted-browser/browser-command.js +48 -0
- package/dist/actors/scripted-browser/journey.d.ts +9 -0
- package/dist/actors/scripted-browser/journey.js +171 -0
- package/dist/actors/scripted-browser/steps.d.ts +60 -0
- package/dist/actors/scripted-browser/steps.js +384 -0
- package/dist/actors/scripted-browser/types.d.ts +135 -0
- package/dist/actors/scripted-browser/types.js +26 -0
- package/dist/actors/stop-cause.d.ts +9 -0
- package/dist/actors/stop-cause.js +42 -0
- package/dist/actors/stop-conditions.d.ts +40 -0
- package/dist/actors/stop-conditions.js +65 -0
- package/dist/analysis/automatic-completion.d.ts +38 -0
- package/dist/analysis/automatic-completion.js +90 -0
- package/dist/analysis/automatic-config.d.ts +41 -0
- package/dist/analysis/automatic-config.js +135 -0
- package/dist/analysis/automatic.d.ts +17 -0
- package/dist/analysis/automatic.js +279 -0
- package/dist/analysis/codex-config.d.ts +8 -0
- package/dist/analysis/codex-config.js +69 -0
- package/dist/analysis/evidence-sources.d.ts +23 -0
- package/dist/analysis/evidence-sources.js +251 -0
- package/dist/analysis/evidence.d.ts +18 -0
- package/dist/analysis/evidence.js +482 -0
- package/dist/analysis/execute.d.ts +52 -0
- package/dist/analysis/execute.js +504 -0
- package/dist/analysis/image-tokens.d.ts +1 -0
- package/dist/analysis/image-tokens.js +41 -0
- package/dist/analysis/job.d.ts +97 -0
- package/dist/analysis/job.js +311 -0
- package/dist/analysis/load.d.ts +3 -0
- package/dist/analysis/load.js +11 -0
- package/dist/analysis/provider.d.ts +57 -0
- package/dist/analysis/provider.js +227 -0
- package/dist/analysis/restricted-codex.d.ts +19 -0
- package/dist/analysis/restricted-codex.js +23 -0
- package/dist/analysis/service.d.ts +72 -0
- package/dist/analysis/service.js +399 -0
- package/dist/analysis/sharing.d.ts +11 -0
- package/dist/analysis/sharing.js +44 -0
- package/dist/analysis/store-executions.d.ts +27 -0
- package/dist/analysis/store-executions.js +224 -0
- package/dist/analysis/store.d.ts +31 -0
- package/dist/analysis/store.js +315 -0
- package/dist/analysis/types.d.ts +217 -0
- package/dist/analysis/types.js +12 -0
- package/dist/analysis/validation.d.ts +225 -0
- package/dist/analysis/validation.js +562 -0
- package/dist/browser-control/client.d.ts +18 -0
- package/dist/browser-control/client.js +239 -0
- package/dist/browser-control/dispatcher.d.ts +19 -0
- package/dist/browser-control/dispatcher.js +221 -0
- package/dist/browser-control/protocol.d.ts +492 -0
- package/dist/browser-control/protocol.js +360 -0
- package/dist/browser-control/recording-transfer.d.ts +3 -0
- package/dist/browser-control/recording-transfer.js +48 -0
- package/dist/browser-control/transport.d.ts +27 -0
- package/dist/browser-control/transport.js +189 -0
- package/dist/cli/commands/analysis-signals.d.ts +7 -0
- package/dist/cli/commands/analysis-signals.js +25 -0
- package/dist/cli/commands/analyze.d.ts +3 -0
- package/dist/cli/commands/analyze.js +212 -0
- package/dist/cli/commands/codex.d.ts +3 -0
- package/dist/cli/commands/codex.js +145 -0
- package/dist/cli/commands/comms.d.ts +3 -0
- package/dist/cli/commands/comms.js +225 -0
- package/dist/cli/commands/feedback.d.ts +3 -0
- package/dist/cli/commands/feedback.js +138 -0
- package/dist/cli/commands/lab-format.d.ts +8 -0
- package/dist/cli/commands/lab-format.js +105 -0
- package/dist/cli/commands/lab-route-computer-use.d.ts +20 -0
- package/dist/cli/commands/lab-route-computer-use.js +251 -0
- package/dist/cli/commands/lab-route-open.d.ts +27 -0
- package/dist/cli/commands/lab-route-open.js +37 -0
- package/dist/cli/commands/lab-route-preview.d.ts +20 -0
- package/dist/cli/commands/lab-route-preview.js +74 -0
- package/dist/cli/commands/lab-route-run.d.ts +17 -0
- package/dist/cli/commands/lab-route-run.js +31 -0
- package/dist/cli/commands/lab-route-scripted.d.ts +19 -0
- package/dist/cli/commands/lab-route-scripted.js +50 -0
- package/dist/cli/commands/lab-route-shared-world.d.ts +20 -0
- package/dist/cli/commands/lab-route-shared-world.js +135 -0
- package/dist/cli/commands/lab-route-terminal.d.ts +19 -0
- package/dist/cli/commands/lab-route-terminal.js +49 -0
- package/dist/cli/commands/lab-run.d.ts +9 -0
- package/dist/cli/commands/lab-run.js +166 -0
- package/dist/cli/commands/lab-scorer.d.ts +30 -0
- package/dist/cli/commands/lab-scorer.js +17 -0
- package/dist/cli/commands/lab.d.ts +3 -0
- package/dist/cli/commands/lab.js +193 -0
- package/dist/cli/commands/observe.d.ts +4 -0
- package/dist/cli/commands/observe.js +345 -0
- package/dist/cli/commands/runs.d.ts +11 -0
- package/dist/cli/commands/runs.js +314 -0
- package/dist/cli/commands/setup.d.ts +11 -0
- package/dist/cli/commands/setup.js +325 -0
- package/dist/cli/commands/tui.d.ts +32 -0
- package/dist/cli/commands/tui.js +305 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.js +245 -0
- package/dist/cli/doctor.d.ts +39 -0
- package/dist/cli/doctor.js +263 -0
- package/dist/cli/first-run-path.d.ts +67 -0
- package/dist/cli/first-run-path.js +150 -0
- package/dist/cli/io.d.ts +85 -0
- package/dist/cli/io.js +232 -0
- package/dist/cli/observer-follow.d.ts +56 -0
- package/dist/cli/observer-follow.js +160 -0
- package/dist/cli/orientation.d.ts +30 -0
- package/dist/cli/orientation.js +114 -0
- package/dist/cli/program.d.ts +15 -0
- package/dist/cli/program.js +338 -0
- package/dist/cli/secret-prompt.js +39 -0
- package/dist/cli/telemetry.js +351 -0
- package/dist/cli.js +2 -4
- package/dist/comms/agentmail.d.ts +29 -0
- package/dist/comms/agentmail.js +568 -0
- package/dist/comms/capture-surface.d.ts +54 -0
- package/dist/comms/capture-surface.js +468 -0
- package/dist/comms/catch-host.d.ts +52 -0
- package/dist/comms/catch-host.js +192 -0
- package/dist/comms/connections.d.ts +20 -0
- package/dist/comms/connections.js +116 -0
- package/dist/comms/email-catch.d.ts +49 -0
- package/dist/comms/email-catch.js +236 -0
- package/dist/comms/extract.d.ts +7 -0
- package/dist/comms/extract.js +65 -0
- package/dist/comms/fake-inbox.d.ts +39 -0
- package/dist/comms/fake-inbox.js +136 -0
- package/dist/comms/images.d.ts +7 -0
- package/dist/comms/images.js +53 -0
- package/dist/comms/lease-store.d.ts +91 -0
- package/dist/comms/lease-store.js +600 -0
- package/dist/comms/providers.d.ts +30 -0
- package/dist/comms/providers.js +24 -0
- package/dist/comms/receiving-common.d.ts +17 -0
- package/dist/comms/receiving-common.js +82 -0
- package/dist/comms/receiving-evidence.d.ts +4 -0
- package/dist/comms/receiving-evidence.js +59 -0
- package/dist/comms/receiving-recovery.d.ts +22 -0
- package/dist/comms/receiving-recovery.js +100 -0
- package/dist/comms/receiving-render.d.ts +16 -0
- package/dist/comms/receiving-render.js +409 -0
- package/dist/comms/receiving-runtime.d.ts +41 -0
- package/dist/comms/receiving-runtime.js +66 -0
- package/dist/comms/receiving-surface.d.ts +8 -0
- package/dist/comms/receiving-surface.js +156 -0
- package/dist/comms/receiving-types.d.ts +122 -0
- package/dist/comms/receiving-types.js +3 -0
- package/dist/comms/receiving.d.ts +33 -0
- package/dist/comms/receiving.js +508 -0
- package/dist/comms/sandbox-catch-script.d.ts +16 -0
- package/dist/comms/sandbox-catch-script.js +331 -0
- package/dist/comms/sandbox-catch.d.ts +214 -0
- package/dist/comms/sandbox-catch.js +451 -0
- package/dist/comms/setup.d.ts +65 -0
- package/dist/comms/setup.js +259 -0
- package/dist/comms/thread-evidence.d.ts +28 -0
- package/dist/comms/thread-evidence.js +30 -0
- package/dist/comms/types.d.ts +76 -0
- package/dist/comms/types.js +8 -0
- package/dist/deprecated.d.ts +90 -0
- package/dist/deprecated.js +106 -0
- package/dist/evidence/browser-hygiene.js +42 -0
- package/dist/evidence/desktop-recorder.d.ts +39 -0
- package/dist/evidence/desktop-recorder.js +340 -0
- package/dist/evidence/desktop-recording-artifact.d.ts +5 -0
- package/dist/evidence/desktop-recording-artifact.js +53 -0
- package/dist/evidence/desktop-recording-types.d.ts +27 -0
- package/dist/evidence/desktop-recording-types.js +16 -0
- package/dist/evidence/encoded-text.d.ts +27 -0
- package/dist/evidence/encoded-text.js +243 -0
- package/dist/evidence/frame-signature.d.ts +17 -0
- package/dist/evidence/frame-signature.js +111 -0
- package/dist/evidence/image.d.ts +24 -0
- package/dist/evidence/image.js +170 -0
- package/dist/evidence/plain-text.d.ts +8 -0
- package/dist/evidence/plain-text.js +16 -0
- package/dist/evidence/redaction.d.ts +110 -0
- package/dist/evidence/redaction.js +329 -0
- package/dist/feedback/draft.d.ts +58 -0
- package/dist/feedback/draft.js +309 -0
- package/dist/feedback/export-bundle.d.ts +6 -0
- package/dist/feedback/export-bundle.js +585 -0
- package/dist/feedback/export.d.ts +50 -0
- package/dist/feedback/export.js +392 -0
- package/dist/feedback/feedback.d.ts +37 -0
- package/dist/feedback/feedback.js +282 -0
- package/dist/feedback/proof.d.ts +8 -0
- package/dist/feedback/proof.js +56 -0
- package/dist/guest/bootstrap.d.ts +55 -0
- package/dist/guest/bootstrap.js +348 -0
- package/dist/guest/browser-tools.d.ts +8 -0
- package/dist/guest/browser-tools.js +72 -0
- package/dist/guest/chromium-text.d.ts +62 -0
- package/dist/guest/chromium-text.js +386 -0
- package/dist/guest/desktop-executor.d.ts +57 -0
- package/dist/guest/desktop-executor.js +256 -0
- package/dist/guest/desktop-keys.d.ts +8 -0
- package/dist/guest/desktop-keys.js +89 -0
- package/dist/guest/desktop-media.d.ts +70 -0
- package/dist/guest/desktop-media.js +385 -0
- package/dist/guest/desktop-native.d.ts +14 -0
- package/dist/guest/desktop-native.js +156 -0
- package/dist/guest/media-config.d.ts +15 -0
- package/dist/guest/media-config.js +16 -0
- package/dist/guest/runtime-desktop.d.ts +45 -0
- package/dist/guest/runtime-desktop.js +420 -0
- package/dist/guest/runtime-processes.d.ts +28 -0
- package/dist/guest/runtime-processes.js +94 -0
- package/dist/guest/runtime.d.ts +31 -0
- package/dist/guest/runtime.js +124 -0
- package/dist/guest-media-worker.js +184 -48
- package/dist/guest-runtime-main.js +23 -10
- package/dist/guest-runtime-revision.js +1 -1
- package/dist/index.d.ts +32 -83
- package/dist/index.js +21 -42
- package/dist/keys/env-file.d.ts +15 -0
- package/dist/keys/env-file.js +109 -0
- package/dist/keys/key-resolution.d.ts +58 -0
- package/dist/keys/key-resolution.js +389 -0
- package/dist/lab/adapter-extension.d.ts +75 -0
- package/dist/lab/adapter-extension.js +253 -0
- package/dist/lab/adapter-scorer-loader.d.ts +43 -0
- package/dist/lab/adapter-scorer-loader.js +130 -0
- package/dist/lab/bag-overrides.d.ts +18 -0
- package/dist/lab/bag-overrides.js +121 -0
- package/dist/lab/composition-rules.d.ts +5 -0
- package/dist/lab/composition-rules.js +283 -0
- package/dist/lab/config.d.ts +7 -0
- package/dist/lab/config.js +208 -0
- package/dist/lab/device-presets.d.ts +97 -0
- package/dist/lab/device-presets.js +87 -0
- package/dist/lab/discover.d.ts +65 -0
- package/dist/lab/discover.js +422 -0
- package/dist/lab/doctor.d.ts +39 -0
- package/dist/lab/doctor.js +233 -0
- package/dist/lab/init-paths.d.ts +5 -0
- package/dist/lab/init-paths.js +89 -0
- package/dist/lab/init-plan.d.ts +20 -0
- package/dist/lab/init-plan.js +163 -0
- package/dist/lab/init-templates.d.ts +28 -0
- package/dist/lab/init-templates.js +416 -0
- package/dist/lab/init.d.ts +39 -0
- package/dist/lab/init.js +287 -0
- package/dist/lab/keys.d.ts +2 -0
- package/dist/lab/keys.js +264 -0
- package/dist/lab/parse/actors.d.ts +14 -0
- package/dist/lab/parse/actors.js +453 -0
- package/dist/lab/parse/comms.d.ts +12 -0
- package/dist/lab/parse/comms.js +246 -0
- package/dist/lab/parse/execution.d.ts +19 -0
- package/dist/lab/parse/execution.js +356 -0
- package/dist/lab/parse/subject-state.d.ts +30 -0
- package/dist/lab/parse/subject-state.js +179 -0
- package/dist/lab/parse/subject.d.ts +9 -0
- package/dist/lab/parse/subject.js +420 -0
- package/dist/lab/parse/values.d.ts +17 -0
- package/dist/lab/parse/values.js +65 -0
- package/dist/lab/participant-assignment.d.ts +12 -0
- package/dist/lab/participant-assignment.js +19 -0
- package/dist/lab/persona-resolve.d.ts +39 -0
- package/dist/lab/persona-resolve.js +118 -0
- package/dist/lab/persona.d.ts +39 -0
- package/dist/lab/persona.js +192 -0
- package/dist/lab/plan-base.d.ts +24 -0
- package/dist/lab/plan-base.js +113 -0
- package/dist/lab/plan-participants.d.ts +81 -0
- package/dist/lab/plan-participants.js +92 -0
- package/dist/lab/plan-types.d.ts +269 -0
- package/dist/lab/plan-types.js +5 -0
- package/dist/lab/plan.d.ts +27 -0
- package/dist/lab/plan.js +156 -0
- package/dist/lab/preflight-probes.d.ts +3 -0
- package/dist/lab/preflight-probes.js +312 -0
- package/dist/lab/preflight-result.d.ts +7 -0
- package/dist/lab/preflight-result.js +43 -0
- package/dist/lab/preflight.d.ts +95 -0
- package/dist/lab/preflight.js +117 -0
- package/dist/lab/route-inputs.d.ts +17 -0
- package/dist/lab/route-inputs.js +95 -0
- package/dist/lab/routing.d.ts +81 -0
- package/dist/lab/routing.js +214 -0
- package/dist/lab/run-lab-events.d.ts +69 -0
- package/dist/lab/run-lab-events.js +95 -0
- package/dist/lab/run-lab-options.d.ts +82 -0
- package/dist/lab/run-lab-options.js +399 -0
- package/dist/lab/summary.d.ts +60 -0
- package/dist/lab/summary.js +168 -0
- package/dist/lab/tasks.d.ts +91 -0
- package/dist/lab/tasks.js +139 -0
- package/dist/lab/types.d.ts +813 -0
- package/dist/lab/types.js +6 -0
- package/dist/lab/validation.d.ts +67 -0
- package/dist/lab/validation.js +387 -0
- package/dist/lab/warnings.d.ts +2 -0
- package/dist/lab/warnings.js +338 -0
- package/dist/observer/artifact.d.ts +23 -0
- package/dist/observer/artifact.js +235 -0
- package/dist/observer/data.d.ts +115 -0
- package/dist/observer/data.js +258 -0
- package/dist/observer/exposure.d.ts +64 -0
- package/dist/observer/exposure.js +143 -0
- package/dist/observer/http.js +50 -0
- package/dist/observer/library.js +190 -0
- package/dist/observer/listen.js +85 -0
- package/dist/observer/live.d.ts +9 -0
- package/dist/observer/live.js +25 -0
- package/dist/observer/pinned-files.d.ts +30 -0
- package/dist/observer/pinned-files.js +160 -0
- package/dist/observer/render.d.ts +69 -0
- package/dist/observer/render.js +350 -0
- package/dist/observer/run-inventory.d.ts +35 -0
- package/dist/observer/run-inventory.js +95 -0
- package/dist/observer/run-routes.d.ts +37 -0
- package/dist/observer/run-routes.js +373 -0
- package/dist/observer/serve.d.ts +97 -0
- package/dist/observer/serve.js +305 -0
- package/dist/observer/static.js +369 -0
- package/dist/observer/tunnel.js +112 -0
- package/dist/observer-app.html +9 -20
- package/dist/routes/computer-use/bundle-parts.d.ts +50 -0
- package/dist/routes/computer-use/bundle-parts.js +177 -0
- package/dist/routes/computer-use/bundle.d.ts +56 -0
- package/dist/routes/computer-use/bundle.js +184 -0
- package/dist/routes/computer-use/diagnostics.d.ts +38 -0
- package/dist/routes/computer-use/diagnostics.js +94 -0
- package/dist/routes/computer-use/e2b-desktop/comms.d.ts +65 -0
- package/dist/routes/computer-use/e2b-desktop/comms.js +225 -0
- package/dist/routes/computer-use/e2b-desktop/desktop.d.ts +3 -0
- package/dist/routes/computer-use/e2b-desktop/desktop.js +91 -0
- package/dist/routes/computer-use/e2b-desktop/fidelity.d.ts +65 -0
- package/dist/routes/computer-use/e2b-desktop/fidelity.js +156 -0
- package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +18 -0
- package/dist/routes/computer-use/e2b-desktop/prepare.js +198 -0
- package/dist/routes/computer-use/e2b-desktop/start.d.ts +14 -0
- package/dist/routes/computer-use/e2b-desktop/start.js +193 -0
- package/dist/routes/computer-use/e2b-desktop/state.d.ts +65 -0
- package/dist/routes/computer-use/e2b-desktop/state.js +69 -0
- package/dist/routes/computer-use/e2b-desktop/teardown.d.ts +6 -0
- package/dist/routes/computer-use/e2b-desktop/teardown.js +152 -0
- package/dist/routes/computer-use/external-comms.d.ts +20 -0
- package/dist/routes/computer-use/external-comms.js +55 -0
- package/dist/routes/computer-use/fanout-bundle.d.ts +11 -0
- package/dist/routes/computer-use/fanout-bundle.js +274 -0
- package/dist/routes/computer-use/fanout-lanes.d.ts +15 -0
- package/dist/routes/computer-use/fanout-lanes.js +302 -0
- package/dist/routes/computer-use/in-process-desktop.d.ts +17 -0
- package/dist/routes/computer-use/in-process-desktop.js +40 -0
- package/dist/routes/computer-use/lanes.d.ts +35 -0
- package/dist/routes/computer-use/lanes.js +321 -0
- package/dist/routes/computer-use/legacy-lane-spec.d.ts +62 -0
- package/dist/routes/computer-use/legacy-lane-spec.js +39 -0
- package/dist/routes/computer-use/live-flush.d.ts +42 -0
- package/dist/routes/computer-use/live-flush.js +154 -0
- package/dist/routes/computer-use/local-tree-pack.d.ts +27 -0
- package/dist/routes/computer-use/local-tree-pack.js +47 -0
- package/dist/routes/computer-use/local-vm.d.ts +20 -0
- package/dist/routes/computer-use/local-vm.js +218 -0
- package/dist/routes/computer-use/participant-desktop.d.ts +63 -0
- package/dist/routes/computer-use/participant-desktop.js +33 -0
- package/dist/routes/computer-use/participant-facts.d.ts +6 -0
- package/dist/routes/computer-use/participant-facts.js +22 -0
- package/dist/routes/computer-use/participant-model.d.ts +30 -0
- package/dist/routes/computer-use/participant-model.js +222 -0
- package/dist/routes/computer-use/participant-prompt.d.ts +53 -0
- package/dist/routes/computer-use/participant-prompt.js +87 -0
- package/dist/routes/computer-use/participant-runs.d.ts +67 -0
- package/dist/routes/computer-use/participant-runs.js +255 -0
- package/dist/routes/computer-use/plan.d.ts +56 -0
- package/dist/routes/computer-use/plan.js +308 -0
- package/dist/routes/computer-use/preflight.d.ts +26 -0
- package/dist/routes/computer-use/preflight.js +85 -0
- package/dist/routes/computer-use/rerun-selection.d.ts +19 -0
- package/dist/routes/computer-use/rerun-selection.js +121 -0
- package/dist/routes/computer-use/result.d.ts +7 -0
- package/dist/routes/computer-use/result.js +292 -0
- package/dist/routes/computer-use/route.d.ts +33 -0
- package/dist/routes/computer-use/route.js +145 -0
- package/dist/routes/computer-use/run-lanes.d.ts +59 -0
- package/dist/routes/computer-use/run-lanes.js +135 -0
- package/dist/routes/computer-use/self-report.d.ts +21 -0
- package/dist/routes/computer-use/self-report.js +231 -0
- package/dist/routes/computer-use/setup.d.ts +136 -0
- package/dist/routes/computer-use/setup.js +371 -0
- package/dist/routes/computer-use/single-bundle.d.ts +97 -0
- package/dist/routes/computer-use/single-bundle.js +393 -0
- package/dist/routes/computer-use/subject-projection.d.ts +51 -0
- package/dist/routes/computer-use/subject-projection.js +159 -0
- package/dist/routes/computer-use/types.d.ts +539 -0
- package/dist/routes/computer-use/types.js +37 -0
- package/dist/routes/preview.d.ts +13 -0
- package/dist/routes/preview.js +36 -0
- package/dist/routes/scripted/bundle.d.ts +47 -0
- package/dist/routes/scripted/bundle.js +195 -0
- package/dist/routes/scripted/plan.d.ts +30 -0
- package/dist/routes/scripted/plan.js +95 -0
- package/dist/routes/scripted/result.d.ts +48 -0
- package/dist/routes/scripted/result.js +176 -0
- package/dist/routes/scripted/route.d.ts +28 -0
- package/dist/routes/scripted/route.js +218 -0
- package/dist/routes/scripted/scenario.d.ts +21 -0
- package/dist/routes/scripted/scenario.js +102 -0
- package/dist/routes/scripted/session-result.d.ts +8 -0
- package/dist/routes/scripted/session-result.js +60 -0
- package/dist/routes/scripted/setup.d.ts +45 -0
- package/dist/routes/scripted/setup.js +126 -0
- package/dist/routes/scripted/subject.d.ts +49 -0
- package/dist/routes/scripted/subject.js +156 -0
- package/dist/routes/scripted/surface-records.d.ts +26 -0
- package/dist/routes/scripted/surface-records.js +88 -0
- package/dist/routes/scripted/surface-sessions.d.ts +13 -0
- package/dist/routes/scripted/surface-sessions.js +40 -0
- package/dist/routes/scripted/types.d.ts +88 -0
- package/dist/routes/scripted/types.js +3 -0
- package/dist/routes/shared-world/bundle.d.ts +20 -0
- package/dist/routes/shared-world/bundle.js +446 -0
- package/dist/routes/shared-world/checkpoints.d.ts +25 -0
- package/dist/routes/shared-world/checkpoints.js +46 -0
- package/dist/routes/shared-world/comms.d.ts +68 -0
- package/dist/routes/shared-world/comms.js +176 -0
- package/dist/routes/shared-world/external-public.d.ts +21 -0
- package/dist/routes/shared-world/external-public.js +130 -0
- package/dist/routes/shared-world/handoff.d.ts +62 -0
- package/dist/routes/shared-world/handoff.js +348 -0
- package/dist/routes/shared-world/lobby-code.d.ts +25 -0
- package/dist/routes/shared-world/lobby-code.js +152 -0
- package/dist/routes/shared-world/plan.d.ts +27 -0
- package/dist/routes/shared-world/plan.js +174 -0
- package/dist/routes/shared-world/provenance.d.ts +27 -0
- package/dist/routes/shared-world/provenance.js +65 -0
- package/dist/routes/shared-world/provisioned.d.ts +57 -0
- package/dist/routes/shared-world/provisioned.js +505 -0
- package/dist/routes/shared-world/result.d.ts +18 -0
- package/dist/routes/shared-world/result.js +327 -0
- package/dist/routes/shared-world/route.d.ts +21 -0
- package/dist/routes/shared-world/route.js +277 -0
- package/dist/routes/shared-world/seat-records.d.ts +20 -0
- package/dist/routes/shared-world/seat-records.js +213 -0
- package/dist/routes/shared-world/seats.d.ts +38 -0
- package/dist/routes/shared-world/seats.js +208 -0
- package/dist/routes/shared-world/setup.d.ts +48 -0
- package/dist/routes/shared-world/setup.js +200 -0
- package/dist/routes/shared-world/types.d.ts +354 -0
- package/dist/routes/shared-world/types.js +29 -0
- package/dist/routes/terminal/adapter.d.ts +37 -0
- package/dist/routes/terminal/adapter.js +201 -0
- package/dist/routes/terminal/bundle.d.ts +79 -0
- package/dist/routes/terminal/bundle.js +305 -0
- package/dist/routes/terminal/credentials.d.ts +34 -0
- package/dist/routes/terminal/credentials.js +102 -0
- package/dist/routes/terminal/dry-run.d.ts +24 -0
- package/dist/routes/terminal/dry-run.js +157 -0
- package/dist/routes/terminal/encoding.js +79 -0
- package/dist/routes/terminal/ledger.d.ts +49 -0
- package/dist/routes/terminal/ledger.js +170 -0
- package/dist/routes/terminal/lifetime.d.ts +10 -0
- package/dist/routes/terminal/lifetime.js +22 -0
- package/dist/routes/terminal/live-finish.d.ts +31 -0
- package/dist/routes/terminal/live-finish.js +215 -0
- package/dist/routes/terminal/live-sandbox.d.ts +69 -0
- package/dist/routes/terminal/live-sandbox.js +452 -0
- package/dist/routes/terminal/persona.d.ts +20 -0
- package/dist/routes/terminal/persona.js +34 -0
- package/dist/routes/terminal/plan.d.ts +21 -0
- package/dist/routes/terminal/plan.js +115 -0
- package/dist/routes/terminal/recorder.d.ts +23 -0
- package/dist/routes/terminal/recorder.js +77 -0
- package/dist/routes/terminal/result.d.ts +47 -0
- package/dist/routes/terminal/result.js +114 -0
- package/dist/routes/terminal/route.d.ts +23 -0
- package/dist/routes/terminal/route.js +170 -0
- package/dist/routes/terminal/runtime-auth.d.ts +12 -0
- package/dist/routes/terminal/runtime-auth.js +23 -0
- package/dist/routes/terminal/runtime.d.ts +14 -0
- package/dist/routes/terminal/runtime.js +47 -0
- package/dist/routes/terminal/sandbox.d.ts +35 -0
- package/dist/routes/terminal/sandbox.js +126 -0
- package/dist/routes/terminal/session.d.ts +25 -0
- package/dist/routes/terminal/session.js +199 -0
- package/dist/routes/terminal/token-usage.d.ts +15 -0
- package/dist/routes/terminal/token-usage.js +93 -0
- package/dist/routes/terminal/trace.d.ts +40 -0
- package/dist/routes/terminal/trace.js +172 -0
- package/dist/routes/terminal/types.d.ts +342 -0
- package/dist/routes/terminal/types.js +31 -0
- package/dist/run/bundle-shape.d.ts +7 -0
- package/dist/run/bundle-shape.js +369 -0
- package/dist/run/bundle.d.ts +547 -0
- package/dist/run/bundle.js +44 -0
- package/dist/run/concurrency.js +24 -0
- package/dist/run/contained-output.d.ts +50 -0
- package/dist/run/contained-output.js +351 -0
- package/dist/run/cost-summary.d.ts +28 -0
- package/dist/run/cost-summary.js +213 -0
- package/dist/run/costs.js +140 -0
- package/dist/run/detail.d.ts +62 -0
- package/dist/run/detail.js +139 -0
- package/dist/run/dry-run-inputs.d.ts +9 -0
- package/dist/run/dry-run-inputs.js +90 -0
- package/dist/run/dry-run.d.ts +6 -0
- package/dist/run/dry-run.js +362 -0
- package/dist/run/feedback-shape.d.ts +7 -0
- package/dist/run/feedback-shape.js +89 -0
- package/dist/run/git-state.d.ts +35 -0
- package/dist/run/git-state.js +316 -0
- package/dist/run/git-workspace.d.ts +32 -0
- package/dist/run/git-workspace.js +450 -0
- package/dist/run/judge.d.ts +210 -0
- package/dist/run/judge.js +240 -0
- package/dist/run/locate.d.ts +22 -0
- package/dist/run/locate.js +122 -0
- package/dist/run/outcomes.d.ts +31 -0
- package/dist/run/outcomes.js +210 -0
- package/dist/run/participant-records.d.ts +28 -0
- package/dist/run/participant-records.js +63 -0
- package/dist/run/participant.d.ts +29 -0
- package/dist/run/participant.js +12 -0
- package/dist/run/paths.d.ts +79 -0
- package/dist/run/paths.js +356 -0
- package/dist/run/preflight-receipts.d.ts +46 -0
- package/dist/run/preflight-receipts.js +173 -0
- package/dist/run/pricing.d.ts +124 -0
- package/dist/run/pricing.js +298 -0
- package/dist/run/project.d.ts +8 -0
- package/dist/run/project.js +103 -0
- package/dist/run/projection.d.ts +184 -0
- package/dist/run/projection.js +356 -0
- package/dist/run/reclaim.d.ts +41 -0
- package/dist/run/reclaim.js +183 -0
- package/dist/run/results.d.ts +88 -0
- package/dist/run/results.js +3 -0
- package/dist/run/run-index.d.ts +107 -0
- package/dist/run/run-index.js +243 -0
- package/dist/run/run.d.ts +103 -0
- package/dist/run/run.js +199 -0
- package/dist/run/sandbox-receipts.d.ts +31 -0
- package/dist/run/sandbox-receipts.js +49 -0
- package/dist/run/shared-world-evidence.d.ts +220 -0
- package/dist/run/shared-world-evidence.js +49 -0
- package/dist/run/shared-world-shape.d.ts +26 -0
- package/dist/run/shared-world-shape.js +181 -0
- package/dist/run/stats.d.ts +82 -0
- package/dist/run/stats.js +258 -0
- package/dist/run/status.d.ts +111 -0
- package/dist/run/status.js +176 -0
- package/dist/run/stored-runs.d.ts +38 -0
- package/dist/run/stored-runs.js +273 -0
- package/dist/run/stream-shape.d.ts +4 -0
- package/dist/run/stream-shape.js +160 -0
- package/dist/run/streams.d.ts +257 -0
- package/dist/run/streams.js +12 -0
- package/dist/run/study-files.d.ts +35 -0
- package/dist/run/study-files.js +164 -0
- package/dist/run/terminal-contract.d.ts +25 -0
- package/dist/run/terminal-contract.js +47 -0
- package/dist/run/text.d.ts +2 -0
- package/dist/run/text.js +4 -0
- package/dist/run/transient-comms-secrets.js +72 -0
- package/dist/run/type-guards.d.ts +6 -0
- package/dist/run/type-guards.js +18 -0
- package/dist/run-lab.d.ts +110 -0
- package/dist/run-lab.js +148 -0
- package/dist/subject/clone.d.ts +37 -0
- package/dist/subject/clone.js +62 -0
- package/dist/subject/desktop-cli.d.ts +17 -0
- package/dist/subject/desktop-cli.js +51 -0
- package/dist/subject/local-tree-archive.d.ts +82 -0
- package/dist/subject/local-tree-archive.js +431 -0
- package/dist/subject/local-tree.d.ts +28 -0
- package/dist/subject/local-tree.js +58 -0
- package/dist/subject/node-bootstrap.d.ts +12 -0
- package/dist/subject/node-bootstrap.js +66 -0
- package/dist/subject/runtime.d.ts +13 -0
- package/dist/subject/runtime.js +50 -0
- package/dist/subject/serve.d.ts +39 -0
- package/dist/subject/serve.js +249 -0
- package/dist/subject/state.d.ts +23 -0
- package/dist/subject/state.js +58 -0
- package/dist/subject/steps.d.ts +37 -0
- package/dist/subject/steps.js +39 -0
- package/dist/substrates/command-failure.d.ts +23 -0
- package/dist/substrates/command-failure.js +45 -0
- package/dist/substrates/desktop-session.d.ts +41 -0
- package/dist/substrates/desktop-session.js +59 -0
- package/dist/substrates/detached.d.ts +55 -0
- package/dist/substrates/detached.js +139 -0
- package/dist/substrates/e2b/cdp-probe.d.ts +99 -0
- package/dist/substrates/e2b/cdp-probe.js +556 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +81 -0
- package/dist/substrates/e2b/desktop-browser.js +410 -0
- package/dist/substrates/e2b/desktop-cdp.d.ts +77 -0
- package/dist/substrates/e2b/desktop-cdp.js +211 -0
- package/dist/substrates/e2b/desktop-executor.d.ts +92 -0
- package/dist/substrates/e2b/desktop-executor.js +302 -0
- package/dist/substrates/e2b/desktop-geometry.d.ts +83 -0
- package/dist/substrates/e2b/desktop-geometry.js +331 -0
- package/dist/substrates/e2b/desktop-media.d.ts +29 -0
- package/dist/substrates/e2b/desktop-media.js +151 -0
- package/dist/substrates/e2b/desktop-recording.d.ts +18 -0
- package/dist/substrates/e2b/desktop-recording.js +238 -0
- package/dist/substrates/e2b/desktop-resources.d.ts +11 -0
- package/dist/substrates/e2b/desktop-resources.js +35 -0
- package/dist/substrates/e2b/desktop-screenshot-cleanup.d.ts +15 -0
- package/dist/substrates/e2b/desktop-screenshot-cleanup.js +66 -0
- package/dist/substrates/e2b/lifetime.d.ts +12 -0
- package/dist/substrates/e2b/lifetime.js +18 -0
- package/dist/substrates/e2b/sandbox.d.ts +102 -0
- package/dist/substrates/e2b/sandbox.js +202 -0
- package/dist/substrates/e2b/sdk.d.ts +194 -0
- package/dist/substrates/e2b/sdk.js +168 -0
- package/dist/substrates/e2b/shell.d.ts +10 -0
- package/dist/substrates/e2b/shell.js +61 -0
- package/dist/substrates/local/captured-inbox.d.ts +6 -0
- package/dist/substrates/local/captured-inbox.js +87 -0
- package/dist/substrates/local/firecracker-desktop.d.ts +25 -0
- package/dist/substrates/local/firecracker-desktop.js +315 -0
- package/dist/substrates/local/runtime-config.d.ts +6 -0
- package/dist/substrates/local/runtime-config.js +91 -0
- package/dist/substrates/local/runtime-host.d.ts +30 -0
- package/dist/substrates/local/runtime-host.js +143 -0
- package/dist/substrates/local/runtime-release.d.ts +5 -0
- package/dist/substrates/local/runtime-release.js +32 -0
- package/dist/substrates/local/runtime-ssh.d.ts +9 -0
- package/dist/substrates/local/runtime-ssh.js +129 -0
- package/dist/substrates/local/runtime.d.ts +25 -0
- package/dist/substrates/local/runtime.js +190 -0
- package/dist/substrates/shell.d.ts +44 -0
- package/dist/substrates/shell.js +22 -0
- package/dist/tui/actions.d.ts +39 -0
- package/dist/tui/actions.js +203 -0
- package/dist/tui/contract.d.ts +144 -0
- package/dist/tui/contract.js +52 -0
- package/dist/tui/launch.js +172 -0
- package/dist/tui/project.d.ts +10 -0
- package/dist/tui/project.js +17 -0
- package/dist/tui-app.js +125 -125
- package/dist/verify/actor.d.ts +27 -0
- package/dist/verify/actor.js +292 -0
- package/dist/verify/artifacts.d.ts +47 -0
- package/dist/verify/artifacts.js +373 -0
- package/dist/verify/costs.d.ts +30 -0
- package/dist/verify/costs.js +129 -0
- package/dist/verify/liveness.d.ts +7 -0
- package/dist/verify/liveness.js +72 -0
- package/dist/verify/rerun.d.ts +3 -0
- package/dist/verify/rerun.js +46 -0
- package/dist/verify/shared-world-concurrent.d.ts +11 -0
- package/dist/verify/shared-world-concurrent.js +346 -0
- package/dist/verify/shared-world.d.ts +8 -0
- package/dist/verify/shared-world.js +295 -0
- package/dist/verify/subject.d.ts +17 -0
- package/dist/verify/subject.js +123 -0
- package/dist/verify/verify.d.ts +34 -0
- package/dist/verify/verify.js +333 -0
- package/docs/README.md +67 -0
- package/docs/architecture/actor-contract.md +215 -174
- package/docs/architecture/browser-control.md +51 -37
- package/docs/architecture/comms-inbox.md +8 -8
- package/docs/architecture/desktop-recording.md +2 -2
- package/docs/architecture/desktop-sessions.md +33 -21
- package/docs/architecture/external-public-shared-world.md +80 -64
- package/docs/architecture/github-feedback-loop.md +40 -27
- package/docs/architecture/guest-desktop.md +22 -10
- package/docs/architecture/local-browser-runtime.md +30 -21
- package/docs/architecture/observer-review.md +9 -9
- package/docs/architecture/observer.md +53 -57
- package/docs/architecture/participant-media.md +3 -3
- package/docs/architecture/project-layout.md +33 -137
- package/docs/architecture/real-email-receiving.md +7 -6
- package/docs/architecture/restricted-codex-analysis.md +211 -25
- package/docs/architecture/serve.md +89 -87
- package/docs/architecture/state-driven-executor.md +79 -64
- package/docs/architecture/task-protocol-support.md +18 -18
- package/docs/architecture/terminal-product-lane.md +92 -86
- package/docs/contracts/adapter-admission.md +8 -4
- package/docs/contracts/adapter-fixtures.md +6 -6
- package/docs/contracts/core.md +22 -24
- package/docs/contracts/feedback.md +33 -23
- package/docs/contracts/policy.md +28 -31
- package/docs/contracts/run-bundle.md +168 -82
- package/docs/contracts/schemas.md +563 -379
- package/docs/contracts/study-analysis.md +2 -2
- package/docs/contracts/study-costs.md +12 -12
- package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +36 -0
- package/docs/decisions/0002-observer-is-one-self-contained-file.md +30 -0
- package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +35 -0
- package/docs/decisions/0004-participant-desktops-stay-in-the-route.md +31 -0
- package/docs/decisions/README.md +16 -0
- package/docs/goals/current.md +22 -50
- package/docs/principles/actor-fidelity.md +37 -40
- package/docs/principles/engineering.md +81 -0
- package/docs/principles/invariants-and-defaults.md +48 -45
- package/docs/principles/self-driving-harness.md +11 -10
- package/docs/principles/three-roles.md +12 -13
- package/docs/product/automatic-analysis.md +39 -9
- package/docs/product/lobby-trivia-3player-external-public.md +14 -13
- package/docs/product/open-source-install-experience.md +34 -38
- package/docs/ramp/README.md +91 -218
- package/docs/release/open-source-readiness.md +5 -2
- package/docs/release/public-readiness-standard.md +1 -1
- package/examples/README.md +11 -0
- package/examples/participant/README.md +80 -0
- package/examples/participant/app.mjs +55 -0
- package/examples/participant/run.mjs +116 -0
- package/examples/scorer/README.md +29 -0
- package/examples/scorer/run.mjs +63 -0
- package/examples/scorer/scorer.mjs +20 -0
- package/package.json +61 -57
- package/skills/humanish/SKILL.md +72 -60
- package/skills/humanish/agents/openai.yaml +2 -2
- package/dist/actor-contract.d.ts +0 -296
- package/dist/actor-contract.js +0 -340
- package/dist/actor-contract.js.map +0 -1
- package/dist/actor-goal-source.js +0 -32
- package/dist/actor-goal-source.js.map +0 -1
- package/dist/actor-registry.d.ts +0 -77
- package/dist/actor-registry.js +0 -106
- package/dist/actor-registry.js.map +0 -1
- package/dist/actor-stop-cause.d.ts +0 -9
- package/dist/actor-stop-cause.js +0 -42
- package/dist/actor-stop-cause.js.map +0 -1
- package/dist/adapter-extension.d.ts +0 -76
- package/dist/adapter-extension.js +0 -273
- package/dist/adapter-extension.js.map +0 -1
- package/dist/adapter-scorer-loader.d.ts +0 -42
- package/dist/adapter-scorer-loader.js +0 -129
- package/dist/adapter-scorer-loader.js.map +0 -1
- package/dist/affordance.d.ts +0 -53
- package/dist/affordance.js +0 -145
- package/dist/affordance.js.map +0 -1
- package/dist/agent-session.js +0 -43
- package/dist/agent-session.js.map +0 -1
- package/dist/argv.d.ts +0 -1
- package/dist/argv.js +0 -8
- package/dist/argv.js.map +0 -1
- package/dist/artifact-reference.d.ts +0 -22
- package/dist/artifact-reference.js +0 -47
- package/dist/artifact-reference.js.map +0 -1
- package/dist/automatic-analysis-completion.d.ts +0 -25
- package/dist/automatic-analysis-completion.js +0 -56
- package/dist/automatic-analysis-completion.js.map +0 -1
- package/dist/automatic-analysis-config.d.ts +0 -38
- package/dist/automatic-analysis-config.js +0 -68
- package/dist/automatic-analysis-config.js.map +0 -1
- package/dist/automatic-study-analysis.d.ts +0 -17
- package/dist/automatic-study-analysis.js +0 -195
- package/dist/automatic-study-analysis.js.map +0 -1
- package/dist/browser-control-client.d.ts +0 -18
- package/dist/browser-control-client.js +0 -176
- package/dist/browser-control-client.js.map +0 -1
- package/dist/browser-control-dispatcher.d.ts +0 -19
- package/dist/browser-control-dispatcher.js +0 -147
- package/dist/browser-control-dispatcher.js.map +0 -1
- package/dist/browser-control-protocol.d.ts +0 -472
- package/dist/browser-control-protocol.js +0 -185
- package/dist/browser-control-protocol.js.map +0 -1
- package/dist/browser-control-recording-transfer.d.ts +0 -4
- package/dist/browser-control-recording-transfer.js +0 -42
- package/dist/browser-control-recording-transfer.js.map +0 -1
- package/dist/browser-control-transport.d.ts +0 -27
- package/dist/browser-control-transport.js +0 -177
- package/dist/browser-control-transport.js.map +0 -1
- package/dist/browser-evidence-hygiene.js +0 -43
- package/dist/browser-evidence-hygiene.js.map +0 -1
- package/dist/chrome-cdp-probe.d.ts +0 -97
- package/dist/chrome-cdp-probe.js +0 -536
- package/dist/chrome-cdp-probe.js.map +0 -1
- package/dist/claude-agent-sdk.d.ts +0 -94
- package/dist/claude-agent-sdk.js +0 -367
- package/dist/claude-agent-sdk.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/codex-app-server-ui.d.ts +0 -39
- package/dist/codex-app-server-ui.js +0 -357
- package/dist/codex-app-server-ui.js.map +0 -1
- package/dist/codex-app-server.d.ts +0 -144
- package/dist/codex-app-server.js +0 -754
- package/dist/codex-app-server.js.map +0 -1
- package/dist/command-failure.d.ts +0 -43
- package/dist/command-failure.js +0 -73
- package/dist/command-failure.js.map +0 -1
- package/dist/comms-agentmail.d.ts +0 -25
- package/dist/comms-agentmail.js +0 -418
- package/dist/comms-agentmail.js.map +0 -1
- package/dist/comms-catch-host.d.ts +0 -52
- package/dist/comms-catch-host.js +0 -178
- package/dist/comms-catch-host.js.map +0 -1
- package/dist/comms-connections.d.ts +0 -52
- package/dist/comms-connections.js +0 -123
- package/dist/comms-connections.js.map +0 -1
- package/dist/comms-email-catch.d.ts +0 -54
- package/dist/comms-email-catch.js +0 -212
- package/dist/comms-email-catch.js.map +0 -1
- package/dist/comms-evidence.d.ts +0 -27
- package/dist/comms-evidence.js +0 -31
- package/dist/comms-evidence.js.map +0 -1
- package/dist/comms-fake-inbox.d.ts +0 -45
- package/dist/comms-fake-inbox.js +0 -184
- package/dist/comms-fake-inbox.js.map +0 -1
- package/dist/comms-images.d.ts +0 -7
- package/dist/comms-images.js +0 -39
- package/dist/comms-images.js.map +0 -1
- package/dist/comms-inbox.d.ts +0 -66
- package/dist/comms-inbox.js +0 -389
- package/dist/comms-inbox.js.map +0 -1
- package/dist/comms-lease-store.d.ts +0 -91
- package/dist/comms-lease-store.js +0 -495
- package/dist/comms-lease-store.js.map +0 -1
- package/dist/comms-receiving-evidence.d.ts +0 -46
- package/dist/comms-receiving-evidence.js +0 -35
- package/dist/comms-receiving-evidence.js.map +0 -1
- package/dist/comms-receiving-inbox.d.ts +0 -16
- package/dist/comms-receiving-inbox.js +0 -410
- package/dist/comms-receiving-inbox.js.map +0 -1
- package/dist/comms-receiving-runtime.d.ts +0 -25
- package/dist/comms-receiving-runtime.js +0 -45
- package/dist/comms-receiving-runtime.js.map +0 -1
- package/dist/comms-receiving-types.d.ts +0 -98
- package/dist/comms-receiving-types.js +0 -2
- package/dist/comms-receiving-types.js.map +0 -1
- package/dist/comms-receiving.d.ts +0 -58
- package/dist/comms-receiving.js +0 -554
- package/dist/comms-receiving.js.map +0 -1
- package/dist/comms-sandbox-catch.d.ts +0 -229
- package/dist/comms-sandbox-catch.js +0 -737
- package/dist/comms-sandbox-catch.js.map +0 -1
- package/dist/comms-setup.d.ts +0 -40
- package/dist/comms-setup.js +0 -121
- package/dist/comms-setup.js.map +0 -1
- package/dist/comms-types.d.ts +0 -72
- package/dist/comms-types.js +0 -10
- package/dist/comms-types.js.map +0 -1
- package/dist/computer-use-actor.d.ts +0 -76
- package/dist/computer-use-actor.js +0 -68
- package/dist/computer-use-actor.js.map +0 -1
- package/dist/computer-use.d.ts +0 -429
- package/dist/computer-use.js +0 -1751
- package/dist/computer-use.js.map +0 -1
- package/dist/concurrency.js +0 -27
- package/dist/concurrency.js.map +0 -1
- package/dist/concurrent-shared-world-lab.d.ts +0 -186
- package/dist/concurrent-shared-world-lab.js +0 -2181
- package/dist/concurrent-shared-world-lab.js.map +0 -1
- package/dist/core/git-state.d.ts +0 -33
- package/dist/core/git-state.js +0 -277
- package/dist/core/git-state.js.map +0 -1
- package/dist/core/git-workspace.d.ts +0 -31
- package/dist/core/git-workspace.js +0 -447
- package/dist/core/git-workspace.js.map +0 -1
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.js +0 -3
- package/dist/core/index.js.map +0 -1
- package/dist/core/run-primitives.d.ts +0 -66
- package/dist/core/run-primitives.js +0 -120
- package/dist/core/run-primitives.js.map +0 -1
- package/dist/cua-actor-lab.d.ts +0 -855
- package/dist/cua-actor-lab.js +0 -3543
- package/dist/cua-actor-lab.js.map +0 -1
- package/dist/cua-admission-limit.d.ts +0 -12
- package/dist/cua-admission-limit.js +0 -20
- package/dist/cua-admission-limit.js.map +0 -1
- package/dist/cua-desktop-lane.d.ts +0 -35
- package/dist/cua-desktop-lane.js +0 -13
- package/dist/cua-desktop-lane.js.map +0 -1
- package/dist/cua-diagnostics.d.ts +0 -38
- package/dist/cua-diagnostics.js +0 -68
- package/dist/cua-diagnostics.js.map +0 -1
- package/dist/cua-executor-error.js +0 -48
- package/dist/cua-executor-error.js.map +0 -1
- package/dist/cua-provider-error.d.ts +0 -16
- package/dist/cua-provider-error.js +0 -35
- package/dist/cua-provider-error.js.map +0 -1
- package/dist/cua-speech.js +0 -2
- package/dist/cua-speech.js.map +0 -1
- package/dist/desktop-recorder.d.ts +0 -39
- package/dist/desktop-recorder.js +0 -226
- package/dist/desktop-recorder.js.map +0 -1
- package/dist/desktop-recording-artifact.d.ts +0 -5
- package/dist/desktop-recording-artifact.js +0 -54
- package/dist/desktop-recording-artifact.js.map +0 -1
- package/dist/desktop-recording-types.d.ts +0 -24
- package/dist/desktop-recording-types.js +0 -14
- package/dist/desktop-recording-types.js.map +0 -1
- package/dist/desktop-session.d.ts +0 -41
- package/dist/desktop-session.js +0 -47
- package/dist/desktop-session.js.map +0 -1
- package/dist/device-presets.d.ts +0 -67
- package/dist/device-presets.js +0 -55
- package/dist/device-presets.js.map +0 -1
- package/dist/doctor-lab.d.ts +0 -38
- package/dist/doctor-lab.js +0 -126
- package/dist/doctor-lab.js.map +0 -1
- package/dist/e2b-cua-desktop.d.ts +0 -3
- package/dist/e2b-cua-desktop.js +0 -708
- package/dist/e2b-cua-desktop.js.map +0 -1
- package/dist/e2b-cua-provisioning.d.ts +0 -314
- package/dist/e2b-cua-provisioning.js +0 -1219
- package/dist/e2b-cua-provisioning.js.map +0 -1
- package/dist/e2b-desktop-executor.d.ts +0 -101
- package/dist/e2b-desktop-executor.js +0 -297
- package/dist/e2b-desktop-executor.js.map +0 -1
- package/dist/e2b-desktop-launch.d.ts +0 -243
- package/dist/e2b-desktop-launch.js +0 -234
- package/dist/e2b-desktop-launch.js.map +0 -1
- package/dist/e2b-desktop-media.d.ts +0 -14
- package/dist/e2b-desktop-media.js +0 -58
- package/dist/e2b-desktop-media.js.map +0 -1
- package/dist/e2b-desktop-recording.d.ts +0 -16
- package/dist/e2b-desktop-recording.js +0 -162
- package/dist/e2b-desktop-recording.js.map +0 -1
- package/dist/e2b-desktop-resources.d.ts +0 -11
- package/dist/e2b-desktop-resources.js +0 -33
- package/dist/e2b-desktop-resources.js.map +0 -1
- package/dist/e2b-desktop-screenshot-cleanup.d.ts +0 -15
- package/dist/e2b-desktop-screenshot-cleanup.js +0 -67
- package/dist/e2b-desktop-screenshot-cleanup.js.map +0 -1
- package/dist/e2b-desktop-session.d.ts +0 -7
- package/dist/e2b-desktop-session.js +0 -29
- package/dist/e2b-desktop-session.js.map +0 -1
- package/dist/e2b-detached.d.ts +0 -53
- package/dist/e2b-detached.js +0 -130
- package/dist/e2b-detached.js.map +0 -1
- package/dist/e2b-terminal-lab.d.ts +0 -383
- package/dist/e2b-terminal-lab.js +0 -2000
- package/dist/e2b-terminal-lab.js.map +0 -1
- package/dist/env-file.d.ts +0 -14
- package/dist/env-file.js +0 -108
- package/dist/env-file.js.map +0 -1
- package/dist/export-bundle.d.ts +0 -7
- package/dist/export-bundle.js +0 -441
- package/dist/export-bundle.js.map +0 -1
- package/dist/export.d.ts +0 -49
- package/dist/export.js +0 -308
- package/dist/export.js.map +0 -1
- package/dist/feedback-proof.d.ts +0 -8
- package/dist/feedback-proof.js +0 -44
- package/dist/feedback-proof.js.map +0 -1
- package/dist/feedback.d.ts +0 -76
- package/dist/feedback.js +0 -496
- package/dist/feedback.js.map +0 -1
- package/dist/first-run-path.d.ts +0 -55
- package/dist/first-run-path.js +0 -136
- package/dist/first-run-path.js.map +0 -1
- package/dist/frame-signature.d.ts +0 -24
- package/dist/frame-signature.js +0 -128
- package/dist/frame-signature.js.map +0 -1
- package/dist/guest-bootstrap.d.ts +0 -55
- package/dist/guest-bootstrap.js +0 -285
- package/dist/guest-bootstrap.js.map +0 -1
- package/dist/guest-browser-tools.d.ts +0 -8
- package/dist/guest-browser-tools.js +0 -66
- package/dist/guest-browser-tools.js.map +0 -1
- package/dist/guest-chromium-text.d.ts +0 -27
- package/dist/guest-chromium-text.js +0 -281
- package/dist/guest-chromium-text.js.map +0 -1
- package/dist/guest-desktop-executor.d.ts +0 -22
- package/dist/guest-desktop-executor.js +0 -178
- package/dist/guest-desktop-executor.js.map +0 -1
- package/dist/guest-desktop-media.d.ts +0 -33
- package/dist/guest-desktop-media.js +0 -281
- package/dist/guest-desktop-media.js.map +0 -1
- package/dist/guest-desktop-native.d.ts +0 -14
- package/dist/guest-desktop-native.js +0 -131
- package/dist/guest-desktop-native.js.map +0 -1
- package/dist/guest-media-config.d.ts +0 -15
- package/dist/guest-media-config.js +0 -8
- package/dist/guest-media-config.js.map +0 -1
- package/dist/guest-media-worker.js.map +0 -1
- package/dist/guest-runtime-desktop.d.ts +0 -42
- package/dist/guest-runtime-desktop.js +0 -314
- package/dist/guest-runtime-desktop.js.map +0 -1
- package/dist/guest-runtime-main.js.map +0 -1
- package/dist/guest-runtime-revision.js.map +0 -1
- package/dist/guest-runtime.d.ts +0 -31
- package/dist/guest-runtime.js +0 -99
- package/dist/guest-runtime.js.map +0 -1
- package/dist/image-evidence.d.ts +0 -2
- package/dist/image-evidence.js +0 -47
- package/dist/image-evidence.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/init-templates.d.ts +0 -25
- package/dist/init-templates.js +0 -541
- package/dist/init-templates.js.map +0 -1
- package/dist/init.d.ts +0 -38
- package/dist/init.js +0 -532
- package/dist/init.js.map +0 -1
- package/dist/key-resolution.d.ts +0 -67
- package/dist/key-resolution.js +0 -375
- package/dist/key-resolution.js.map +0 -1
- package/dist/lab-app-runner.d.ts +0 -78
- package/dist/lab-app-runner.js +0 -403
- package/dist/lab-app-runner.js.map +0 -1
- package/dist/lab-config.d.ts +0 -968
- package/dist/lab-config.js +0 -2714
- package/dist/lab-config.js.map +0 -1
- package/dist/lab-engine.d.ts +0 -97
- package/dist/lab-engine.js +0 -285
- package/dist/lab-engine.js.map +0 -1
- package/dist/lab-preflight.d.ts +0 -70
- package/dist/lab-preflight.js +0 -388
- package/dist/lab-preflight.js.map +0 -1
- package/dist/lab-summary.d.ts +0 -57
- package/dist/lab-summary.js +0 -126
- package/dist/lab-summary.js.map +0 -1
- package/dist/labs.d.ts +0 -66
- package/dist/labs.js +0 -401
- package/dist/labs.js.map +0 -1
- package/dist/listen.js +0 -84
- package/dist/listen.js.map +0 -1
- package/dist/local-agent-claude-session.d.ts +0 -45
- package/dist/local-agent-claude-session.js +0 -203
- package/dist/local-agent-claude-session.js.map +0 -1
- package/dist/local-agent-cli.d.ts +0 -121
- package/dist/local-agent-cli.js +0 -427
- package/dist/local-agent-cli.js.map +0 -1
- package/dist/local-captured-inbox.d.ts +0 -6
- package/dist/local-captured-inbox.js +0 -65
- package/dist/local-captured-inbox.js.map +0 -1
- package/dist/local-firecracker-desktop.d.ts +0 -23
- package/dist/local-firecracker-desktop.js +0 -191
- package/dist/local-firecracker-desktop.js.map +0 -1
- package/dist/local-firecracker-study.d.ts +0 -9
- package/dist/local-firecracker-study.js +0 -135
- package/dist/local-firecracker-study.js.map +0 -1
- package/dist/local-runtime-config.d.ts +0 -6
- package/dist/local-runtime-config.js +0 -60
- package/dist/local-runtime-config.js.map +0 -1
- package/dist/local-runtime-host.d.ts +0 -31
- package/dist/local-runtime-host.js +0 -128
- package/dist/local-runtime-host.js.map +0 -1
- package/dist/local-runtime-release.d.ts +0 -7
- package/dist/local-runtime-release.js +0 -33
- package/dist/local-runtime-release.js.map +0 -1
- package/dist/local-runtime-ssh.d.ts +0 -27
- package/dist/local-runtime-ssh.js +0 -91
- package/dist/local-runtime-ssh.js.map +0 -1
- package/dist/local-runtime.d.ts +0 -25
- package/dist/local-runtime.js +0 -133
- package/dist/local-runtime.js.map +0 -1
- package/dist/observer-data.d.ts +0 -114
- package/dist/observer-data.js +0 -224
- package/dist/observer-data.js.map +0 -1
- package/dist/observer-library.js +0 -191
- package/dist/observer-library.js.map +0 -1
- package/dist/observer-serve.d.ts +0 -99
- package/dist/observer-serve.js +0 -284
- package/dist/observer-serve.js.map +0 -1
- package/dist/observer-static.js +0 -357
- package/dist/observer-static.js.map +0 -1
- package/dist/observer.d.ts +0 -118
- package/dist/observer.js +0 -947
- package/dist/observer.js.map +0 -1
- package/dist/openai-responses-cu.d.ts +0 -158
- package/dist/openai-responses-cu.js +0 -714
- package/dist/openai-responses-cu.js.map +0 -1
- package/dist/orientation.d.ts +0 -29
- package/dist/orientation.js +0 -95
- package/dist/orientation.js.map +0 -1
- package/dist/oss-lab.d.ts +0 -55
- package/dist/oss-lab.js +0 -344
- package/dist/oss-lab.js.map +0 -1
- package/dist/oss-meta-lab-scoring.d.ts +0 -15
- package/dist/oss-meta-lab-scoring.js +0 -145
- package/dist/oss-meta-lab-scoring.js.map +0 -1
- package/dist/oss-meta-lab.d.ts +0 -287
- package/dist/oss-meta-lab.js +0 -4551
- package/dist/oss-meta-lab.js.map +0 -1
- package/dist/oss-remote-telemetry.d.ts +0 -77
- package/dist/oss-remote-telemetry.js +0 -403
- package/dist/oss-remote-telemetry.js.map +0 -1
- package/dist/output-token-limit.js +0 -5
- package/dist/output-token-limit.js.map +0 -1
- package/dist/participant-assignment.d.ts +0 -12
- package/dist/participant-assignment.js +0 -15
- package/dist/participant-assignment.js.map +0 -1
- package/dist/persona-resolve.d.ts +0 -44
- package/dist/persona-resolve.js +0 -99
- package/dist/persona-resolve.js.map +0 -1
- package/dist/persona.d.ts +0 -39
- package/dist/persona.js +0 -144
- package/dist/persona.js.map +0 -1
- package/dist/pi-agent-core.d.ts +0 -77
- package/dist/pi-agent-core.js +0 -207
- package/dist/pi-agent-core.js.map +0 -1
- package/dist/pricing.d.ts +0 -145
- package/dist/pricing.js +0 -277
- package/dist/pricing.js.map +0 -1
- package/dist/program.d.ts +0 -96
- package/dist/program.js +0 -4030
- package/dist/program.js.map +0 -1
- package/dist/reasoning-effort.js +0 -26
- package/dist/reasoning-effort.js.map +0 -1
- package/dist/reclaim.d.ts +0 -31
- package/dist/reclaim.js +0 -90
- package/dist/reclaim.js.map +0 -1
- package/dist/redaction.d.ts +0 -90
- package/dist/redaction.js +0 -294
- package/dist/redaction.js.map +0 -1
- package/dist/restricted-codex-analysis.d.ts +0 -15
- package/dist/restricted-codex-analysis.js +0 -13
- package/dist/restricted-codex-analysis.js.map +0 -1
- package/dist/restricted-codex-participant-policy.d.ts +0 -44
- package/dist/restricted-codex-participant-policy.js +0 -43
- package/dist/restricted-codex-participant-policy.js.map +0 -1
- package/dist/restricted-codex-participant-run.d.ts +0 -20
- package/dist/restricted-codex-participant-run.js +0 -79
- package/dist/restricted-codex-participant-run.js.map +0 -1
- package/dist/restricted-codex-participant.d.ts +0 -20
- package/dist/restricted-codex-participant.js +0 -250
- package/dist/restricted-codex-participant.js.map +0 -1
- package/dist/restricted-codex-policy.d.ts +0 -64
- package/dist/restricted-codex-policy.js +0 -176
- package/dist/restricted-codex-policy.js.map +0 -1
- package/dist/restricted-codex-session.d.ts +0 -44
- package/dist/restricted-codex-session.js +0 -657
- package/dist/restricted-codex-session.js.map +0 -1
- package/dist/restricted-codex-transport.d.ts +0 -69
- package/dist/restricted-codex-transport.js +0 -296
- package/dist/restricted-codex-transport.js.map +0 -1
- package/dist/run-detail.d.ts +0 -61
- package/dist/run-detail.js +0 -137
- package/dist/run-detail.js.map +0 -1
- package/dist/run-index.d.ts +0 -71
- package/dist/run-index.js +0 -206
- package/dist/run-index.js.map +0 -1
- package/dist/run-narration-secrets.js +0 -68
- package/dist/run-narration-secrets.js.map +0 -1
- package/dist/run-paths.d.ts +0 -56
- package/dist/run-paths.js +0 -355
- package/dist/run-paths.js.map +0 -1
- package/dist/run-projection.d.ts +0 -186
- package/dist/run-projection.js +0 -350
- package/dist/run-projection.js.map +0 -1
- package/dist/run-status.d.ts +0 -139
- package/dist/run-status.js +0 -218
- package/dist/run-status.js.map +0 -1
- package/dist/run.d.ts +0 -1255
- package/dist/run.js +0 -5949
- package/dist/run.js.map +0 -1
- package/dist/sandbox-receipts.d.ts +0 -19
- package/dist/sandbox-receipts.js +0 -51
- package/dist/sandbox-receipts.js.map +0 -1
- package/dist/screenshot-image.d.ts +0 -11
- package/dist/screenshot-image.js +0 -26
- package/dist/screenshot-image.js.map +0 -1
- package/dist/scripted-browser-actor.d.ts +0 -297
- package/dist/scripted-browser-actor.js +0 -1399
- package/dist/scripted-browser-actor.js.map +0 -1
- package/dist/scripted-browser-lab.d.ts +0 -121
- package/dist/scripted-browser-lab.js +0 -852
- package/dist/scripted-browser-lab.js.map +0 -1
- package/dist/secret-prompt.js +0 -36
- package/dist/secret-prompt.js.map +0 -1
- package/dist/selected-output-paths.d.ts +0 -52
- package/dist/selected-output-paths.js +0 -366
- package/dist/selected-output-paths.js.map +0 -1
- package/dist/serve-exposure.d.ts +0 -62
- package/dist/serve-exposure.js +0 -129
- package/dist/serve-exposure.js.map +0 -1
- package/dist/serve-http.js +0 -45
- package/dist/serve-http.js.map +0 -1
- package/dist/serve-tunnel.js +0 -113
- package/dist/serve-tunnel.js.map +0 -1
- package/dist/shared-world-lab.d.ts +0 -269
- package/dist/shared-world-lab.js +0 -1424
- package/dist/shared-world-lab.js.map +0 -1
- package/dist/source-archive.d.ts +0 -82
- package/dist/source-archive.js +0 -408
- package/dist/source-archive.js.map +0 -1
- package/dist/stats.d.ts +0 -82
- package/dist/stats.js +0 -219
- package/dist/stats.js.map +0 -1
- package/dist/stop-conditions.d.ts +0 -39
- package/dist/stop-conditions.js +0 -66
- package/dist/stop-conditions.js.map +0 -1
- package/dist/study-analysis-codex-config.d.ts +0 -11
- package/dist/study-analysis-codex-config.js +0 -34
- package/dist/study-analysis-codex-config.js.map +0 -1
- package/dist/study-analysis-engine.d.ts +0 -47
- package/dist/study-analysis-engine.js +0 -327
- package/dist/study-analysis-engine.js.map +0 -1
- package/dist/study-analysis-evidence.d.ts +0 -27
- package/dist/study-analysis-evidence.js +0 -660
- package/dist/study-analysis-evidence.js.map +0 -1
- package/dist/study-analysis-job.d.ts +0 -97
- package/dist/study-analysis-job.js +0 -214
- package/dist/study-analysis-job.js.map +0 -1
- package/dist/study-analysis-provider.d.ts +0 -40
- package/dist/study-analysis-provider.js +0 -170
- package/dist/study-analysis-provider.js.map +0 -1
- package/dist/study-analysis-service.d.ts +0 -59
- package/dist/study-analysis-service.js +0 -257
- package/dist/study-analysis-service.js.map +0 -1
- package/dist/study-analysis-sharing.d.ts +0 -11
- package/dist/study-analysis-sharing.js +0 -35
- package/dist/study-analysis-sharing.js.map +0 -1
- package/dist/study-analysis-store.d.ts +0 -47
- package/dist/study-analysis-store.js +0 -493
- package/dist/study-analysis-store.js.map +0 -1
- package/dist/study-analysis-validation.d.ts +0 -618
- package/dist/study-analysis-validation.js +0 -384
- package/dist/study-analysis-validation.js.map +0 -1
- package/dist/study-analysis.d.ts +0 -206
- package/dist/study-analysis.js +0 -4
- package/dist/study-analysis.js.map +0 -1
- package/dist/study-costs.js +0 -114
- package/dist/study-costs.js.map +0 -1
- package/dist/subject-runtime.d.ts +0 -23
- package/dist/subject-runtime.js +0 -70
- package/dist/subject-runtime.js.map +0 -1
- package/dist/tasks.d.ts +0 -90
- package/dist/tasks.js +0 -144
- package/dist/tasks.js.map +0 -1
- package/dist/telemetry.js +0 -324
- package/dist/telemetry.js.map +0 -1
- package/dist/terminal-agent-actor.d.ts +0 -41
- package/dist/terminal-agent-actor.js +0 -25
- package/dist/terminal-agent-actor.js.map +0 -1
- package/dist/terminal-encoding.js +0 -80
- package/dist/terminal-encoding.js.map +0 -1
- package/dist/terminal-node-bootstrap.d.ts +0 -4
- package/dist/terminal-node-bootstrap.js +0 -58
- package/dist/terminal-node-bootstrap.js.map +0 -1
- package/dist/terminal-participant-activity.d.ts +0 -6
- package/dist/terminal-participant-activity.js +0 -34
- package/dist/terminal-participant-activity.js.map +0 -1
- package/dist/terminal-runtime-auth.d.ts +0 -13
- package/dist/terminal-runtime-auth.js +0 -24
- package/dist/terminal-runtime-auth.js.map +0 -1
- package/dist/terminal-runtime.d.ts +0 -16
- package/dist/terminal-runtime.js +0 -42
- package/dist/terminal-runtime.js.map +0 -1
- package/dist/terminal-token-usage.d.ts +0 -15
- package/dist/terminal-token-usage.js +0 -94
- package/dist/terminal-token-usage.js.map +0 -1
- package/dist/tui-actions.d.ts +0 -39
- package/dist/tui-actions.js +0 -178
- package/dist/tui-actions.js.map +0 -1
- package/dist/tui-contract.d.ts +0 -141
- package/dist/tui-contract.js +0 -54
- package/dist/tui-contract.js.map +0 -1
- package/dist/tui-launch.js +0 -167
- package/dist/tui-launch.js.map +0 -1
- package/dist/tui-project.d.ts +0 -9
- package/dist/tui-project.js +0 -18
- package/dist/tui-project.js.map +0 -1
- package/docs/architecture/examples/state-driven-local-app/README.md +0 -80
- package/docs/architecture/examples/state-driven-local-app/app.mjs +0 -48
- package/docs/architecture/examples/state-driven-local-app/runner.mjs +0 -105
- package/docs/architecture/local-codex-tui-actor.md +0 -213
- package/docs/architecture/oss-lab-poc.md +0 -250
- package/docs/architecture/runtime-broker-core.md +0 -30
- package/docs/release/0.100.0-local-setup.md +0 -31
- package/docs/release/0.100.1-participant-continuity.md +0 -20
- package/docs/release/0.101.0-codex-ui-tools.md +0 -28
- package/docs/release/0.102.0-participant-media.md +0 -43
- package/docs/release/0.103.0-desktop-recording.md +0 -29
- package/docs/release/0.103.1-analysis-capture-allocation.md +0 -18
- package/docs/release/0.103.2-analysis-ending-context.md +0 -21
- package/docs/release/0.104.0-local-captured-inboxes.md +0 -29
- package/docs/release/0.104.1-recording-startup.md +0 -24
- package/docs/release/0.104.2-recording-timestamps.md +0 -32
- package/docs/release/0.104.3-local-smtp-catch.md +0 -15
- package/docs/release/0.105.0-participant-backgrounds.md +0 -50
- package/docs/release/0.20.0-external-public-shared-world.md +0 -43
- package/docs/release/0.84.0-redacted-evidence.md +0 -42
- package/docs/release/0.84.1-portable-feedback.md +0 -20
- package/docs/release/0.85.0-observer-review.md +0 -56
- package/docs/release/0.85.1-observer-continuity.md +0 -20
- package/docs/release/0.86.0-participant-evidence.md +0 -50
- package/docs/release/0.86.1-task-preflight-saved-recordings.md +0 -40
- package/docs/release/0.87.0-participant-endings-and-phone-review.md +0 -64
- package/docs/release/0.88.0-study-diagnostics.md +0 -43
- package/docs/release/0.88.1-completion-evidence-and-local-app.md +0 -79
- package/docs/release/0.88.2-sequential-study-budgets.md +0 -57
- package/docs/release/0.89.0-study-findings.md +0 -79
- package/docs/release/0.89.1-analysis-finished-notice.md +0 -24
- package/docs/release/0.90.0-automatic-analysis.md +0 -21
- package/docs/release/0.91.0-analysis-quality-and-defaults.md +0 -33
- package/docs/release/0.91.1-study-review-polish.md +0 -28
- package/docs/release/0.92.0-grid-playback.md +0 -22
- package/docs/release/0.93.0-global-playback.md +0 -26
- package/docs/release/0.93.1-observer-review-controls.md +0 -24
- package/docs/release/0.94.0-reliability.md +0 -48
- package/docs/release/0.95.0-connections-setup.md +0 -38
- package/docs/release/0.96.0-real-email-receiving.md +0 -47
- package/docs/release/0.96.1-browser-navigation.md +0 -22
- package/docs/release/0.97.0-codex-account-analysis.md +0 -45
- package/docs/release/0.98.0-local-browser-studies.md +0 -31
- package/docs/release/0.99.0-local-browser-mac.md +0 -34
- package/docs/release/0.99.1-action-recovery.md +0 -19
- /package/dist/{agent-session.d.ts → actors/agent-session.d.ts} +0 -0
- /package/dist/{cua-executor-error.d.ts → actors/computer-use/executor-error.d.ts} +0 -0
- /package/dist/{cua-speech.d.ts → actors/computer-use/speech.d.ts} +0 -0
- /package/dist/{actor-goal-source.d.ts → actors/goal-source.d.ts} +0 -0
- /package/dist/{output-token-limit.d.ts → actors/output-token-limit.d.ts} +0 -0
- /package/dist/{reasoning-effort.d.ts → actors/reasoning-effort.d.ts} +0 -0
- /package/dist/{secret-prompt.d.ts → cli/secret-prompt.d.ts} +0 -0
- /package/dist/{telemetry.d.ts → cli/telemetry.d.ts} +0 -0
- /package/dist/{browser-evidence-hygiene.d.ts → evidence/browser-hygiene.d.ts} +0 -0
- /package/dist/{serve-http.d.ts → observer/http.d.ts} +0 -0
- /package/dist/{observer-library.d.ts → observer/library.d.ts} +0 -0
- /package/dist/{listen.d.ts → observer/listen.d.ts} +0 -0
- /package/dist/{observer-static.d.ts → observer/static.d.ts} +0 -0
- /package/dist/{serve-tunnel.d.ts → observer/tunnel.d.ts} +0 -0
- /package/dist/{terminal-encoding.d.ts → routes/terminal/encoding.d.ts} +0 -0
- /package/dist/{concurrency.d.ts → run/concurrency.d.ts} +0 -0
- /package/dist/{study-costs.d.ts → run/costs.d.ts} +0 -0
- /package/dist/{run-narration-secrets.d.ts → run/transient-comms-secrets.d.ts} +0 -0
- /package/dist/{tui-launch.d.ts → tui/launch.d.ts} +0 -0
package/dist/lab-config.js
DELETED
|
@@ -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
|