humanish 0.110.3 → 0.111.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/CONTEXT.md +2 -2
  3. package/dist/actors/local-agent/claude-session.js +1 -1
  4. package/dist/actors/local-agent/cli.js +1 -1
  5. package/dist/actors/scripted-browser/actor.d.ts +2 -2
  6. package/dist/actors/scripted-browser/actor.js +17 -8
  7. package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
  8. package/dist/actors/scripted-browser/launch-env.js +50 -0
  9. package/dist/analysis/automatic-completion.d.ts +2 -1
  10. package/dist/analysis/automatic-completion.js +7 -2
  11. package/dist/cli/commands/migrate.js +1 -1
  12. package/dist/cli/commands/runs.js +5 -2
  13. package/dist/cli/commands/study-route-computer-use.js +3 -2
  14. package/dist/cli/commands/study-route-preview.js +2 -1
  15. package/dist/cli/commands/study-route-shared-world.js +3 -3
  16. package/dist/cli/commands/study.js +2 -1
  17. package/dist/routes/computer-use/live-phase.js +8 -1
  18. package/dist/routes/computer-use/local-vm.js +2 -1
  19. package/dist/routes/computer-use/participant-model.js +1 -1
  20. package/dist/routes/computer-use/plan.js +9 -10
  21. package/dist/routes/computer-use/preflight.js +1 -1
  22. package/dist/routes/computer-use/result.js +1 -1
  23. package/dist/routes/computer-use/route.js +3 -2
  24. package/dist/routes/computer-use/setup.js +2 -1
  25. package/dist/routes/computer-use/types.d.ts +4 -4
  26. package/dist/routes/scripted/plan.js +5 -4
  27. package/dist/routes/scripted/route.js +6 -2
  28. package/dist/routes/scripted/scenario.d.ts +1 -1
  29. package/dist/routes/scripted/scenario.js +3 -3
  30. package/dist/routes/scripted/surface-sessions.d.ts +2 -0
  31. package/dist/routes/scripted/surface-sessions.js +2 -1
  32. package/dist/routes/scripted/types.d.ts +2 -2
  33. package/dist/routes/shared-world/handoff.js +1 -1
  34. package/dist/routes/shared-world/participant-specs.js +6 -2
  35. package/dist/routes/shared-world/plan.js +8 -8
  36. package/dist/routes/shared-world/route.js +3 -3
  37. package/dist/routes/shared-world/types.d.ts +1 -1
  38. package/dist/routes/terminal/bundle.d.ts +3 -3
  39. package/dist/routes/terminal/ledger.d.ts +2 -2
  40. package/dist/routes/terminal/lifetime.js +1 -1
  41. package/dist/routes/terminal/live-finish.js +1 -1
  42. package/dist/routes/terminal/plan.js +4 -3
  43. package/dist/routes/terminal/route.js +3 -2
  44. package/dist/routes/terminal/session.js +1 -0
  45. package/dist/routes/terminal/types.d.ts +1 -1
  46. package/dist/run/display.d.ts +10 -0
  47. package/dist/run/display.js +22 -0
  48. package/dist/run/reclaim.d.ts +4 -2
  49. package/dist/run/reclaim.js +61 -18
  50. package/dist/run/results.d.ts +1 -1
  51. package/dist/run/run.d.ts +11 -1
  52. package/dist/run/run.js +24 -5
  53. package/dist/run/status.d.ts +6 -0
  54. package/dist/run/status.js +1 -0
  55. package/dist/run/stored-runs.js +26 -11
  56. package/dist/run-study.d.ts +5 -3
  57. package/dist/run-study.js +7 -11
  58. package/dist/study/adapter-scorer-loader.d.ts +1 -1
  59. package/dist/study/adapter-scorer-loader.js +3 -3
  60. package/dist/study/composition-rules.js +49 -39
  61. package/dist/study/config.d.ts +31 -8
  62. package/dist/study/config.js +181 -155
  63. package/dist/study/discover.d.ts +1 -1
  64. package/dist/study/discover.js +1 -1
  65. package/dist/study/doctor.js +5 -4
  66. package/dist/study/keys.d.ts +250 -2
  67. package/dist/study/keys.js +32 -62
  68. package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
  69. package/dist/study/{convert.js → migrate/convert.js} +28 -31
  70. package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
  71. package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
  72. package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
  73. package/dist/study/migrate/v2.d.ts +78 -0
  74. package/dist/study/migrate/v2.js +607 -0
  75. package/dist/study/parse/actors.d.ts +34 -10
  76. package/dist/study/parse/actors.js +76 -174
  77. package/dist/study/parse/comms.js +6 -2
  78. package/dist/study/parse/execution.d.ts +9 -4
  79. package/dist/study/parse/execution.js +6 -41
  80. package/dist/study/parse/front.d.ts +36 -0
  81. package/dist/study/parse/{study-v3.js → front.js} +37 -88
  82. package/dist/study/parse/subject.d.ts +2 -0
  83. package/dist/study/parse/subject.js +15 -18
  84. package/dist/study/persona-resolve.d.ts +2 -4
  85. package/dist/study/persona-resolve.js +7 -8
  86. package/dist/study/plan-base.d.ts +1 -1
  87. package/dist/study/plan-base.js +6 -6
  88. package/dist/study/plan-participants.d.ts +2 -2
  89. package/dist/study/plan-participants.js +12 -11
  90. package/dist/study/plan-types.d.ts +5 -5
  91. package/dist/study/plan.js +5 -4
  92. package/dist/study/preflight-probes.js +4 -4
  93. package/dist/study/preflight.d.ts +1 -1
  94. package/dist/study/preflight.js +3 -3
  95. package/dist/study/routing.d.ts +5 -5
  96. package/dist/study/routing.js +17 -20
  97. package/dist/study/run-study-options.d.ts +1 -1
  98. package/dist/study/run-study-options.js +3 -3
  99. package/dist/study/study-deps.d.ts +1 -1
  100. package/dist/study/study-fields.d.ts +19 -0
  101. package/dist/study/study-fields.js +46 -0
  102. package/dist/study/summary.js +19 -18
  103. package/dist/study/types.d.ts +66 -104
  104. package/dist/study/types.js +0 -1
  105. package/dist/study/url-credentials.js +3 -3
  106. package/dist/study/validation.d.ts +4 -10
  107. package/dist/study/validation.js +54 -79
  108. package/dist/study/warnings.d.ts +3 -3
  109. package/dist/study/warnings.js +42 -110
  110. package/dist/substrates/local/runtime-config.js +14 -16
  111. package/dist/tui-app.js +3 -3
  112. package/docs/contracts/schemas.md +36 -8
  113. package/package.json +1 -1
  114. package/dist/study/parse/study-v3.d.ts +0 -21
  115. /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  // Every URL a study config declares, checked for a credential. Route planners call it, since a
2
2
  // library caller's config skips the parser that refuses these first.
3
- import { rosterOf } from "./parse/actors.js";
4
3
  import { entryCredentialReason, urlCredentialReason } from "./parse/url-credentials.js";
4
+ import { participantList } from "./study-fields.js";
5
5
  /**
6
6
  * The first URL in `config` that carries a credential, with why: the subject's app, serve and
7
7
  * public surface URLs, and each participant's target and entry.
@@ -16,14 +16,14 @@ export function studyUrlCredentialReason(config) {
16
16
  ? undefined
17
17
  : urlCredentialReason("subject.serve.url", subject.serve.url),
18
18
  ...(subject.product?.publicSurfaces ?? []).map((surface) => () => urlCredentialReason("subject.product.publicSurfaces", surface)),
19
- ...config.actors.flatMap((actor) => (rosterOf(actor) ?? []).flatMap((participant, index) => [
19
+ ...(participantList(config) ?? []).flatMap((participant, index) => [
20
20
  () => participant.target === undefined
21
21
  ? undefined
22
22
  : urlCredentialReason(`participants[${index}].target`, participant.target),
23
23
  () => participant.entry === undefined
24
24
  ? undefined
25
25
  : entryCredentialReason(`participants[${index}].entry`, participant.entry),
26
- ])),
26
+ ]),
27
27
  ];
28
28
  for (const reason of reasons) {
29
29
  const found = reason();
@@ -7,12 +7,14 @@ import type { StudyConfig } from "./types.js";
7
7
  */
8
8
  export declare function computerUseValidationReason(config: StudyConfig): string | null;
9
9
  /**
10
- * Cross-validate a `topology: shared-world` declaration. Returns the failure message, or
10
+ * Cross-validate a `route: shared-world` declaration. Returns the failure message, or
11
11
  * null when the one shared-world route can run it: the external-public checks for an app-url
12
12
  * subject, the provisioned checks otherwise. Enforced at parse and again by the route, since
13
13
  * runStudyWith takes a config that skipped the parser.
14
14
  */
15
15
  export declare function sharedWorldValidationReason(config: StudyConfig): string | null;
16
+ /** SMTP capture runs only where each participant has its own world, so a shared world refuses it. */
17
+ export declare function smtpValidationReason(config: StudyConfig): string | undefined;
16
18
  /**
17
19
  * Declared capture devices must be implemented by the selected execution route.
18
20
  * Unsupported backends pass false when called directly, where the declared
@@ -32,14 +34,6 @@ export declare function taskProtocolValidationReason(config: StudyConfig, suppor
32
34
  * at parse and again on the computer-use and scripted-browser routes for library callers.
33
35
  */
34
36
  export declare function cloneTargetValidationReason(config: StudyConfig): string | null;
35
- /**
36
- * Refuse a positive `scenario.caps.maxUsd` or `maxTotalUsd` on a computer-use study.
37
- * `scenario.caps` belongs to the terminal route; the computer-use route stops on `execution.caps`,
38
- * so a dollar figure here would read as a cap while the study ran uncapped. Zero spends nothing
39
- * either way and stays a warning. Enforced at parse and again on the computer-use routes for
40
- * library callers.
41
- */
42
- export declare function scenarioCapsValidationReason(config: StudyConfig): string | null;
43
37
  /** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
44
38
  export declare function outputTokenLimitValidationReason(config: StudyConfig): string | null;
45
39
  /**
@@ -58,7 +52,7 @@ export declare function concurrentSharedWorldValidationReason(config: StudyConfi
58
52
  * it rejects every provisioned-subject field (serve/state.seed/state.checkpoint/exposure/clone/repos
59
53
  * are inert with no sandbox, so they fail closed), and requires a
60
54
  * non-loopback appUrl + allowPublicTargets + the operator-ownership attestation subject.publicTarget +
61
- * concurrency >= 2 + an actors[0].lanes roster of ≥2 with exactly one host participant. The getHost synthetic
55
+ * concurrency >= 2 + a `participants` list of ≥2 with exactly one host participant. The getHost synthetic
62
56
  * gate is deliberately unreachable here (there is no internet-reachable harness-owned URL to attest).
63
57
  * Enforced at parse and re-enforced in the engine (runStudyWith takes a config that skipped the parser).
64
58
  */
@@ -1,8 +1,9 @@
1
1
  import { isMaxOutputTokens } from "../actors/output-token-limit.js";
2
- import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN, focusOf, rosterOf, } from "./parse/actors.js";
2
+ import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./parse/actors.js";
3
3
  import { isHttpUrl, isLoopbackUrl } from "./parse/subject.js";
4
4
  import { declaredTargets } from "./plan-participants.js";
5
5
  import { actorResolvesToComputerUse, computerUseParticipantCount, MAX_COMPUTER_USE_PARTICIPANTS, registeredComputerUseActors, resolveEntryUrl, isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
6
+ import { actorOf, participantList, declaresSharedWorld } from "./study-fields.js";
6
7
  /**
7
8
  * Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
8
9
  * message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
@@ -10,39 +11,32 @@ import { actorResolvesToComputerUse, computerUseParticipantCount, MAX_COMPUTER_U
10
11
  * validity, unique ids) first, then the route-scoped XOR, cap and policy checks.
11
12
  */
12
13
  export function computerUseValidationReason(config) {
13
- const actor = config.actors[0];
14
- const roster = rosterOf(actor);
14
+ const roster = participantList(config);
15
15
  const structuralReason = rosterStructuralValidationReason(config);
16
16
  if (structuralReason) {
17
17
  return structuralReason;
18
18
  }
19
19
  // clone.fanout is a declared behavior change: rejected on the computer-use route (was
20
- // inert-warned). Fan-out is declared via `actors[0].count` or `actors[0].lanes`;
21
- // subject.clone.fanout never applied here.
20
+ // inert-warned). Fan-out is declared with `participants`; subject.clone.fanout never applied
21
+ // here.
22
22
  if (config.subject.clone?.fanout !== undefined) {
23
- 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 roster of participants). (No current route reads clone.fanout.)";
23
+ return "`subject.clone.fanout` is not used on the computer-use route: declare fan-out with participants (homogeneous) or participants (a roster of participants). (No current route reads clone.fanout.)";
24
24
  }
25
25
  if (roster !== undefined) {
26
- if (actor?.count !== undefined) {
27
- return "Set either `actors[0].count` (identical participants) or `actors[0].lanes` (a roster of distinct participants), not both.";
28
- }
29
- if (focusOf(actor) !== undefined) {
30
- return "actors[0].laneFocus and actors[0].lanes are mutually exclusive: each roster entry's `instruction` is the fan-out steer; laneFocus is the steer for a single participant.";
31
- }
32
26
  if (config.execution?.desktop?.resolution !== undefined &&
33
27
  roster.some((entry) => entry.device !== undefined)) {
34
- return "actors[0].lanes[].device and a raw execution.desktop.resolution are mutually exclusive: a per-participant device preset and a single hand-set resolution cannot both govern participant geometry.";
28
+ return "participants[].device and a raw execution.desktop.resolution are mutually exclusive: a per-participant device preset and a single hand-set resolution cannot both govern participant geometry.";
35
29
  }
36
30
  const targeted = roster.filter((entry) => entry.target !== undefined);
37
31
  if (targeted.length > 0) {
38
32
  if (config.subject.source !== "app-url") {
39
- return "`actors[0].lanes[].target` works only on app-url computer-use studies. Clone, shared-world and local-app studies set each participant's entry URL themselves; remove `target`.";
33
+ return "`participants[].target` works only on app-url computer-use studies. Clone, shared-world and local-app studies set each participant's entry URL themselves; remove `target`.";
40
34
  }
41
35
  if (roster.some((entry) => entry.entry !== undefined)) {
42
- 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 participant path.";
36
+ return "participants[].target and participants[].entry are mutually exclusive: target is an app-url fan-out browser URL; entry is a shared-world same-origin participant path.";
43
37
  }
44
38
  if (targeted.length !== roster.length) {
45
- return "When any actors[0].lanes[].target is declared, every participant in the roster must declare target; this keeps the setup-produced target contract explicit and prevents accidental mixed worlds.";
39
+ return "When any participants[].target is declared, every participant in the roster must declare target; this keeps the setup-produced target contract explicit and prevents accidental mixed worlds.";
46
40
  }
47
41
  }
48
42
  }
@@ -53,14 +47,14 @@ export function computerUseValidationReason(config) {
53
47
  // Public targets fan out into N independent worlds driving the same public app, which is an
54
48
  // ambiguous shared-world-ish shape, not a per-participant target swarm. Permit N>1 public runs only
55
49
  // when every roster entry declares its own target, making the adapter-owned topology explicit. But when
56
- // `subject.topology: shared-world` is also declared, N participants against one public target is the
50
+ // `route: shared-world` is also declared, N participants against one public target is the
57
51
  // external-public shared-world topology, so route it there (a real public deployment
58
52
  // as the shared plane) instead of refusing; externalPublicSharedWorldValidationReason then applies.
59
53
  if (participantCount > 1 &&
60
54
  config.policies?.allowPublicTargets === true &&
61
55
  declaredTargets(config).length === 0 &&
62
- config.subject.topology !== "shared-world") {
63
- return "`policies.allowPublicTargets` with more than one participant sends them all to one public app, which is a shared world. Set `subject.topology: shared-world` to run them together there, give each `actors[0].lanes[]` entry its own `target`, or run one participant.";
56
+ !declaresSharedWorld(config)) {
57
+ return "`policies.allowPublicTargets` with more than one participant sends them all to one public app, which is a shared world. Set `route: shared-world` to run them together there, give each `participants[]` entry its own `target`, or run one participant.";
64
58
  }
65
59
  return null;
66
60
  }
@@ -69,15 +63,15 @@ export function computerUseValidationReason(config) {
69
63
  * public TypeScript/JavaScript API instead of parseStudy.
70
64
  */
71
65
  function rosterStructuralValidationReason(config) {
72
- const roster = rosterOf(config.actors[0]);
66
+ const roster = participantList(config);
73
67
  const seenIds = new Set();
74
68
  if (roster !== undefined) {
75
69
  if (!Array.isArray(roster) || roster.length === 0) {
76
- return "actors[0].lanes must be a non-empty array when set.";
70
+ return "participants must be a non-empty array when set.";
77
71
  }
78
72
  for (const [index, entry] of roster.entries()) {
79
73
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
80
- return `actors[0].lanes[${index}] must be an object.`;
74
+ return `participants[${index}] must be an object.`;
81
75
  }
82
76
  const id = entry.id;
83
77
  if (id === undefined) {
@@ -86,10 +80,10 @@ function rosterStructuralValidationReason(config) {
86
80
  if (typeof id !== "string" ||
87
81
  !PARTICIPANT_ID_PATTERN.test(id) ||
88
82
  id.length > PARTICIPANT_ID_MAX_CHARS) {
89
- return `actors[0].lanes[${index}].id must be a public-safe path token matching ${PARTICIPANT_ID_PATTERN} and at most ${PARTICIPANT_ID_MAX_CHARS} chars.`;
83
+ return `participants[${index}].id must be a public-safe path token matching ${PARTICIPANT_ID_PATTERN} and at most ${PARTICIPANT_ID_MAX_CHARS} chars.`;
90
84
  }
91
85
  if (seenIds.has(id)) {
92
- return `actors[0].lanes ids must be unique (duplicate "${id}").`;
86
+ return `participants ids must be unique (duplicate "${id}").`;
93
87
  }
94
88
  seenIds.add(id);
95
89
  }
@@ -97,7 +91,7 @@ function rosterStructuralValidationReason(config) {
97
91
  return null;
98
92
  }
99
93
  /**
100
- * Cross-validate a `topology: shared-world` declaration. Returns the failure message, or
94
+ * Cross-validate a `route: shared-world` declaration. Returns the failure message, or
101
95
  * null when the one shared-world route can run it: the external-public checks for an app-url
102
96
  * subject, the provisioned checks otherwise. Enforced at parse and again by the route, since
103
97
  * runStudyWith takes a config that skipped the parser.
@@ -109,8 +103,8 @@ export function sharedWorldValidationReason(config) {
109
103
  }
110
104
  /**
111
105
  * The structural checks every provisioned shared world shares. It requires: a clone or local-tree source + e2b-desktop
112
- * target + a computer-use actor + a `subject.serve` block + an `actors[0].lanes` roster of ≥2 roles (the
113
- * roster is the role roster; there is no separate roles[] field), and every role `entry` must resolve
106
+ * target + a computer-use actor + a `subject.serve` block + a `participants` list of ≥2 (the list
107
+ * is the role roster; there is no separate roles[] field), and every role `entry` must resolve
114
108
  * same-origin (loopback) with serve.url. Fail-closed: a half-declared shared-world is rejected,
115
109
  * never silently downgraded.
116
110
  */
@@ -120,32 +114,38 @@ function provisionedSharedWorldStructureReason(config) {
120
114
  return structuralReason;
121
115
  }
122
116
  if (config.subject.source !== "clone" && config.subject.source !== "local-tree") {
123
- return "`subject.topology: shared-world` needs `subject.source: clone` or `local-tree`, which humanish serves as one seeded app every participant uses, or `app-url` for a public deployment you own.";
117
+ return "`route: shared-world` needs `subject.source: clone` or `local-tree`, which humanish serves as one seeded app every participant uses, or `app-url` for a public deployment you own.";
124
118
  }
125
119
  if (config.execution?.target !== "e2b-desktop") {
126
- return "`subject.topology: shared-world` requires `execution.target: e2b-desktop`: the role participants drive hosted desktop browsers against one in-sandbox app.";
120
+ return "`route: shared-world` requires `execution.target: e2b-desktop`: the role participants drive hosted desktop browsers against one in-sandbox app.";
127
121
  }
128
- if (!actorResolvesToComputerUse(config.actors[0]?.type)) {
129
- return `\`subject.topology: shared-world\` requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}); each role participant runs a computer-use session.`;
122
+ if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
123
+ return `\`route: shared-world\` requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}); each role participant runs a computer-use session.`;
130
124
  }
131
125
  const serve = config.subject.serve;
132
126
  if (!serve) {
133
- return "`subject.topology: shared-world` needs `subject.serve` (start and url): humanish starts one copy of the app in the sandbox, and every participant uses it.";
127
+ return "`route: shared-world` needs `subject.serve` (start and url): humanish starts one copy of the app in the sandbox, and every participant uses it.";
134
128
  }
135
- const roster = rosterOf(config.actors[0]);
129
+ const roster = participantList(config);
136
130
  if (!roster || roster.length < 2) {
137
- return "`subject.topology: shared-world` needs an `actors[0].lanes` roster of at least 2 participants: with one participant there is nobody to interact with.";
131
+ return "`route: shared-world` needs a `participants` list of at least 2 participants: with one participant there is nobody to interact with.";
138
132
  }
139
133
  if (!config.subject.state?.checkpoint || config.subject.state.checkpoint.length === 0) {
140
- return "`subject.topology: shared-world` needs at least one read-only `subject.state.checkpoint` probe: the checkpoints are how the run shows that participants changed the shared app.";
134
+ return "`route: shared-world` needs at least one read-only `subject.state.checkpoint` probe: the checkpoints are how the run shows that participants changed the shared app.";
141
135
  }
142
136
  for (const entry of roster) {
143
137
  if (entry.entry !== undefined && resolveEntryUrl(serve.url, entry.entry) === null) {
144
- return `actors[0].lanes role "${entry.id ?? "(unnamed)"}".entry must resolve same-origin (loopback) with subject.serve.url (${serve.url}); got "${entry.entry}".`;
138
+ return `participants role "${entry.id ?? "(unnamed)"}".entry must resolve same-origin (loopback) with subject.serve.url (${serve.url}); got "${entry.entry}".`;
145
139
  }
146
140
  }
147
141
  return null;
148
142
  }
143
+ /** SMTP capture runs only where each participant has its own world, so a shared world refuses it. */
144
+ export function smtpValidationReason(config) {
145
+ return config.comms?.email?.smtp && declaresSharedWorld(config)
146
+ ? "SMTP capture is not supported yet for shared-world studies. Use the default per-lane-worlds topology for SMTP, or configure supported HTTP email capture for concurrent shared-world studies."
147
+ : undefined;
148
+ }
149
149
  /**
150
150
  * Declared capture devices must be implemented by the selected execution route.
151
151
  * Unsupported backends pass false when called directly, where the declared
@@ -153,9 +153,7 @@ function provisionedSharedWorldStructureReason(config) {
153
153
  */
154
154
  export function desktopMediaValidationReason(config, supportsMedia = isComputerUseComposition(config)) {
155
155
  if (config.execution?.desktop?.recording !== undefined &&
156
- (!supportsMedia ||
157
- config.subject.topology === "shared-world" ||
158
- config.subject.source === "local-app")) {
156
+ (!supportsMedia || declaresSharedWorld(config) || config.subject.source === "local-app")) {
159
157
  return "execution.desktop.recording is supported only for computer-use participants on independent desktops. Remove the declaration or select a supported route.";
160
158
  }
161
159
  const media = config.execution?.desktop?.media;
@@ -164,7 +162,7 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
164
162
  if (media.microphone !== undefined && media.microphone.source !== "speech") {
165
163
  return "execution.desktop.media.microphone.source must be speech. Microphone source-file injection is unsupported.";
166
164
  }
167
- if (config.subject.topology === "shared-world") {
165
+ if (declaresSharedWorld(config)) {
168
166
  return "execution.desktop.media is unsupported on shared-world routes; declared capture devices would not be provisioned. Use independent computer-use browser participants or remove the declaration.";
169
167
  }
170
168
  if (!supportsMedia ||
@@ -176,8 +174,10 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
176
174
  return "execution.desktop.media requires Chrome or Chromium; Firefox cannot receive the declared synthetic capture device. Set execution.desktop.browser: chrome or chromium.";
177
175
  }
178
176
  if (media.microphone !== undefined) {
179
- if (config.actors.some((actor) => actor.type !== "local-agent" ||
180
- (actor.localAgent !== undefined && actor.localAgent !== "codex"))) {
177
+ const actor = actorOf(config);
178
+ if (actor !== undefined &&
179
+ (actor.type !== "local-agent" ||
180
+ (actor.localAgent !== undefined && actor.localAgent !== "codex"))) {
181
181
  return "Participant speech currently requires local-agent with Codex, on a local or hosted desktop.";
182
182
  }
183
183
  if (config.execution?.target !== "local" && media.camera !== undefined) {
@@ -194,7 +194,7 @@ export function receivingEmailValidationReason(config) {
194
194
  !["app-url", "clone", "local-tree"].includes(config.subject.source)) {
195
195
  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.";
196
196
  }
197
- if (config.actors.some((actor) => actor.type === "local-agent")) {
197
+ if (actorOf(config)?.type === "local-agent") {
198
198
  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.";
199
199
  }
200
200
  return undefined;
@@ -202,15 +202,8 @@ export function receivingEmailValidationReason(config) {
202
202
  /** Refuse task declarations that the selected execution path would discard.
203
203
  * Direct runners pass their actual support rather than trusting the config's dispatch shape. */
204
204
  export function taskProtocolValidationReason(config, supportsTasks = isComputerUseComposition(config) && !isSharedWorldComposition(config)) {
205
- for (const [index, actor] of config.actors.entries()) {
206
- if (actor.tasks === undefined)
207
- continue;
208
- if (index > 0) {
209
- return `actors[${index}].tasks is unsupported: current runners consume only actors[0]. Use the first actor's computer-use participants for a task protocol.`;
210
- }
211
- if (!supportsTasks) {
212
- return "actors[0].tasks is unsupported on this execution path. Task protocols require the computer-use route, with one world per participant; shared-world, terminal-product, scripted-browser and synthetic routes do not consume them. Remove tasks only if a mission-only study is intended.";
213
- }
205
+ if (actorOf(config)?.tasks !== undefined && !supportsTasks) {
206
+ return "actor.tasks is unsupported on this execution path. Task protocols require the computer-use route, with one world per participant; shared-world, terminal-product, scripted-browser and synthetic routes do not consume them. Remove tasks only if a mission-only study is intended.";
214
207
  }
215
208
  return null;
216
209
  }
@@ -228,35 +221,17 @@ export function cloneTargetValidationReason(config) {
228
221
  const got = target === undefined ? "it is absent" : `got "${target}"`;
229
222
  return `clone subjects require \`execution.target: e2b-desktop\` (${got}): humanish clones and serves the repo inside a hosted desktop sandbox. \`execution.target: local\` applies to app-url and local-app subjects.`;
230
223
  }
231
- /**
232
- * Refuse a positive `scenario.caps.maxUsd` or `maxTotalUsd` on a computer-use study.
233
- * `scenario.caps` belongs to the terminal route; the computer-use route stops on `execution.caps`,
234
- * so a dollar figure here would read as a cap while the study ran uncapped. Zero spends nothing
235
- * either way and stays a warning. Enforced at parse and again on the computer-use routes for
236
- * library callers.
237
- */
238
- export function scenarioCapsValidationReason(config) {
239
- if (!isComputerUseComposition(config))
240
- return null;
241
- for (const key of ["maxUsd", "maxTotalUsd"]) {
242
- const value = config.scenario?.caps?.[key];
243
- if (value === undefined || value <= 0)
244
- continue;
245
- return `scenario.caps.${key} (${value}) does not cap a computer-use study: this route stops on execution.caps.${key}. Move the value to execution.caps.${key}; scenario.caps applies only to terminal-product studies.`;
246
- }
247
- return null;
248
- }
249
224
  /** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
250
225
  export function outputTokenLimitValidationReason(config) {
251
- const actor = config.actors[0];
226
+ const actor = actorOf(config);
252
227
  if (actor?.maxOutputTokens === undefined)
253
228
  return null;
254
229
  if (!isMaxOutputTokens(actor.maxOutputTokens))
255
- return "actors[0].maxOutputTokens must be a positive safe integer.";
230
+ return "actor.maxOutputTokens must be a positive safe integer.";
256
231
  if (actor.type !== "openai-computer-use" ||
257
232
  !isComputerUseComposition(config) ||
258
233
  config.subject.source === "local-app") {
259
- 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.";
234
+ return "actor.maxOutputTokens is supported only by first-party OpenAI computer-use routes; terminal, local-agent, scripted and custom in-process routes cannot enforce it.";
260
235
  }
261
236
  return null;
262
237
  }
@@ -270,7 +245,7 @@ export function outputTokenLimitValidationReason(config) {
270
245
  function sharedWorldConcurrencyReason(config) {
271
246
  // Direct library callers skip the parser, so an omitted value defaults here exactly as the
272
247
  // route does: to the participant count. A missing roster reads as 0 and is refused.
273
- const participants = rosterOf(config.actors[0])?.length ?? 0;
248
+ const participants = participantList(config)?.length ?? 0;
274
249
  const concurrency = config.execution?.concurrency ?? participants;
275
250
  if (concurrency >= 2)
276
251
  return null;
@@ -312,7 +287,7 @@ export function concurrentSharedWorldValidationReason(config) {
312
287
  * it rejects every provisioned-subject field (serve/state.seed/state.checkpoint/exposure/clone/repos
313
288
  * are inert with no sandbox, so they fail closed), and requires a
314
289
  * non-loopback appUrl + allowPublicTargets + the operator-ownership attestation subject.publicTarget +
315
- * concurrency >= 2 + an actors[0].lanes roster of ≥2 with exactly one host participant. The getHost synthetic
290
+ * concurrency >= 2 + a `participants` list of ≥2 with exactly one host participant. The getHost synthetic
316
291
  * gate is deliberately unreachable here (there is no internet-reachable harness-owned URL to attest).
317
292
  * Enforced at parse and re-enforced in the engine (runStudyWith takes a config that skipped the parser).
318
293
  */
@@ -327,12 +302,12 @@ export function externalPublicSharedWorldValidationReason(config) {
327
302
  if (config.execution?.target !== "e2b-desktop") {
328
303
  return "the external-public shared-world route requires `execution.target: e2b-desktop`: the role participants drive hosted desktop browsers against the one public deployment.";
329
304
  }
330
- if (!actorResolvesToComputerUse(config.actors[0]?.type)) {
305
+ if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
331
306
  return `the external-public shared-world route requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}); each role participant runs a computer-use session.`;
332
307
  }
333
- const roster = rosterOf(config.actors[0]);
308
+ const roster = participantList(config);
334
309
  if (!roster || roster.length < 2) {
335
- return "the external-public shared-world route requires an `actors[0].lanes` roster of at least 2 roles (a single-participant shared world proves no shared session).";
310
+ return "the external-public shared-world route requires a `participants` list of at least 2 roles (a single-participant shared world proves no shared session).";
336
311
  }
337
312
  const sharedWorldConcurrency = sharedWorldConcurrencyReason(config);
338
313
  if (sharedWorldConcurrency)
@@ -367,7 +342,7 @@ export function externalPublicSharedWorldValidationReason(config) {
367
342
  return "`subject.clone` and `subject.repos` do not apply to an external-public shared-world study: the participants use the public deployment, so nothing is cloned. Remove them.";
368
343
  }
369
344
  if (roster.some((entry) => entry.entry !== undefined)) {
370
- return "`actors[0].lanes[].entry` (the loopback same-origin participant path) is forbidden on the external-public shared-world route: there is no harness-served serve.url to resolve it against; participants open the public appUrl and reach the shared session through the real UI.";
345
+ return "`participants[].entry` (the loopback same-origin participant path) is forbidden on the external-public shared-world route: there is no harness-served serve.url to resolve it against; participants open the public appUrl and reach the shared session through the real UI.";
371
346
  }
372
347
  const hostEntries = roster.filter((entry) => entry.host === true);
373
348
  if (hostEntries.length !== 1) {
@@ -1,12 +1,12 @@
1
1
  import type { StudyConfig } from "./types.js";
2
2
  /**
3
- * The fields a config sets that its route does not read, each as `field` or `field (reason)`. A v2
4
- * file gets them as one warning; a v3 study is refused for any of them.
3
+ * The fields a config sets that its route does not read, each as `field` or `field (reason)`.
4
+ * parseStudy refuses a study for any of them.
5
5
  */
6
6
  export declare function inertFieldLabels(config: StudyConfig): string[];
7
7
  /**
8
8
  * The same fields as `inertFieldLabels`, as paths without their reasons, such as
9
- * `actors[0].lanes[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
9
+ * `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
10
10
  */
11
11
  export declare function inertFieldPaths(config: StudyConfig): string[];
12
12
  export declare function forwardDeclaredWarnings(config: StudyConfig): string[];