humanish 0.110.4 → 0.111.1

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 (145) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTEXT.md +2 -2
  3. package/dist/actors/codex/restricted-participant-policy.d.ts +2 -0
  4. package/dist/actors/codex/restricted-participant-policy.js +30 -2
  5. package/dist/actors/codex/restricted-participant.js +1 -1
  6. package/dist/actors/computer-use/executor-diagnostic.d.ts +15 -0
  7. package/dist/actors/computer-use/executor-diagnostic.js +105 -0
  8. package/dist/actors/computer-use/executor-error.d.ts +18 -3
  9. package/dist/actors/computer-use/executor-error.js +30 -4
  10. package/dist/actors/computer-use/loop/actions.d.ts +6 -3
  11. package/dist/actors/computer-use/loop/actions.js +27 -7
  12. package/dist/actors/computer-use/loop/ending.js +9 -1
  13. package/dist/actors/computer-use/loop/types.d.ts +14 -0
  14. package/dist/actors/computer-use/loop.d.ts +1 -1
  15. package/dist/actors/computer-use/loop.js +1 -1
  16. package/dist/actors/computer-use/openai-context.d.ts +8 -2
  17. package/dist/actors/computer-use/openai-context.js +91 -26
  18. package/dist/actors/computer-use/openai-provider.js +55 -12
  19. package/dist/actors/computer-use/openai-requests.d.ts +2 -1
  20. package/dist/actors/computer-use/openai-requests.js +4 -1
  21. package/dist/actors/contract.d.ts +7 -1
  22. package/dist/actors/local-agent/claude-session.js +1 -1
  23. package/dist/actors/local-agent/cli.js +1 -1
  24. package/dist/actors/scripted-browser/actor.d.ts +2 -2
  25. package/dist/browser-control/client.js +5 -5
  26. package/dist/browser-control/protocol.d.ts +30 -1
  27. package/dist/browser-control/protocol.js +19 -2
  28. package/dist/browser-control/transport.d.ts +3 -2
  29. package/dist/browser-control/transport.js +12 -7
  30. package/dist/cli/commands/study-route-computer-use.js +2 -2
  31. package/dist/cli/commands/study-route-shared-world.js +2 -2
  32. package/dist/cli/commands/study.js +1 -4
  33. package/dist/cli/doctor.js +11 -0
  34. package/dist/evidence/encoded-text.d.ts +3 -3
  35. package/dist/evidence/encoded-text.js +10 -5
  36. package/dist/evidence/redaction.d.ts +2 -2
  37. package/dist/evidence/redaction.js +18 -7
  38. package/dist/guest/browser-tools.js +5 -1
  39. package/dist/guest/chromium-text.js +42 -18
  40. package/dist/guest/desktop-executor.js +5 -1
  41. package/dist/keys/key-resolution.d.ts +3 -0
  42. package/dist/keys/key-resolution.js +15 -2
  43. package/dist/routes/computer-use/bundle-parts.d.ts +4 -0
  44. package/dist/routes/computer-use/bundle-parts.js +7 -2
  45. package/dist/routes/computer-use/fanout-bundle.js +4 -2
  46. package/dist/routes/computer-use/local-vm.js +1 -2
  47. package/dist/routes/computer-use/participant-model.js +7 -2
  48. package/dist/routes/computer-use/plan.js +7 -10
  49. package/dist/routes/computer-use/preflight.js +1 -1
  50. package/dist/routes/computer-use/result.js +1 -1
  51. package/dist/routes/computer-use/route.js +2 -3
  52. package/dist/routes/computer-use/setup.js +2 -2
  53. package/dist/routes/computer-use/types.d.ts +4 -4
  54. package/dist/routes/preview.js +1 -0
  55. package/dist/routes/scripted/plan.js +4 -5
  56. package/dist/routes/scripted/route.js +3 -3
  57. package/dist/routes/scripted/scenario.d.ts +1 -1
  58. package/dist/routes/scripted/scenario.js +3 -3
  59. package/dist/routes/scripted/types.d.ts +2 -2
  60. package/dist/routes/shared-world/handoff.js +1 -1
  61. package/dist/routes/shared-world/participant-specs.js +1 -1
  62. package/dist/routes/shared-world/plan.js +6 -8
  63. package/dist/routes/shared-world/route.js +2 -2
  64. package/dist/routes/shared-world/setup.js +1 -0
  65. package/dist/routes/shared-world/types.d.ts +1 -1
  66. package/dist/routes/terminal/bundle.d.ts +3 -3
  67. package/dist/routes/terminal/dry-run.js +1 -0
  68. package/dist/routes/terminal/ledger.d.ts +2 -2
  69. package/dist/routes/terminal/lifetime.js +1 -1
  70. package/dist/routes/terminal/live-finish.js +25 -6
  71. package/dist/routes/terminal/live-sandbox.js +5 -1
  72. package/dist/routes/terminal/plan.js +3 -4
  73. package/dist/routes/terminal/recorder.d.ts +6 -0
  74. package/dist/routes/terminal/recorder.js +61 -6
  75. package/dist/routes/terminal/route.js +2 -3
  76. package/dist/routes/terminal/session.js +2 -1
  77. package/dist/routes/terminal/token-usage.d.ts +3 -0
  78. package/dist/routes/terminal/token-usage.js +5 -0
  79. package/dist/routes/terminal/types.d.ts +3 -2
  80. package/dist/routes/terminal/types.js +2 -1
  81. package/dist/run/dry-run.js +1 -0
  82. package/dist/run/results.d.ts +3 -1
  83. package/dist/run/run.d.ts +2 -0
  84. package/dist/run/run.js +16 -2
  85. package/dist/run/terminal-contract.d.ts +10 -0
  86. package/dist/run/terminal-contract.js +27 -9
  87. package/dist/run-study.d.ts +5 -3
  88. package/dist/run-study.js +7 -11
  89. package/dist/study/adapter-scorer-loader.d.ts +1 -1
  90. package/dist/study/adapter-scorer-loader.js +3 -3
  91. package/dist/study/composition-rules.js +36 -27
  92. package/dist/study/config.d.ts +29 -13
  93. package/dist/study/config.js +174 -157
  94. package/dist/study/discover.d.ts +1 -1
  95. package/dist/study/discover.js +1 -1
  96. package/dist/study/doctor.js +4 -5
  97. package/dist/study/keys.d.ts +250 -2
  98. package/dist/study/keys.js +32 -62
  99. package/dist/study/migrate/convert.js +3 -4
  100. package/dist/study/migrate/v2.d.ts +22 -7
  101. package/dist/study/migrate/v2.js +173 -23
  102. package/dist/study/parse/actors.d.ts +11 -32
  103. package/dist/study/parse/actors.js +11 -156
  104. package/dist/study/parse/comms.js +6 -2
  105. package/dist/study/parse/execution.d.ts +9 -4
  106. package/dist/study/parse/execution.js +4 -39
  107. package/dist/study/parse/{study-v3.d.ts → front.d.ts} +6 -17
  108. package/dist/study/parse/{study-v3.js → front.js} +24 -112
  109. package/dist/study/parse/subject.d.ts +2 -0
  110. package/dist/study/parse/subject.js +15 -18
  111. package/dist/study/parse/url-credentials.d.ts +3 -3
  112. package/dist/study/parse/url-credentials.js +155 -13
  113. package/dist/study/persona-resolve.js +7 -9
  114. package/dist/study/plan-base.d.ts +1 -1
  115. package/dist/study/plan-base.js +5 -7
  116. package/dist/study/plan-participants.d.ts +2 -2
  117. package/dist/study/plan-participants.js +3 -3
  118. package/dist/study/plan-types.d.ts +7 -5
  119. package/dist/study/plan.js +12 -6
  120. package/dist/study/preflight-probes.js +2 -2
  121. package/dist/study/preflight.d.ts +1 -1
  122. package/dist/study/preflight.js +1 -1
  123. package/dist/study/routing.d.ts +5 -5
  124. package/dist/study/routing.js +16 -16
  125. package/dist/study/run-study-options.d.ts +1 -1
  126. package/dist/study/run-study-options.js +2 -2
  127. package/dist/study/study-deps.d.ts +1 -1
  128. package/dist/study/study-fields.d.ts +4 -25
  129. package/dist/study/study-fields.js +15 -44
  130. package/dist/study/summary.js +13 -8
  131. package/dist/study/types.d.ts +51 -128
  132. package/dist/study/types.js +0 -1
  133. package/dist/study/url-credentials.js +3 -4
  134. package/dist/study/validation.d.ts +2 -10
  135. package/dist/study/validation.js +23 -53
  136. package/dist/study/warnings.d.ts +11 -3
  137. package/dist/study/warnings.js +56 -110
  138. package/dist/substrates/local/runtime-config.js +11 -13
  139. package/docs/architecture/browser-control.md +12 -2
  140. package/docs/architecture/guest-desktop.md +7 -1
  141. package/docs/architecture/terminal-product-route.md +3 -2
  142. package/docs/contracts/schemas.md +39 -9
  143. package/package.json +1 -1
  144. package/dist/study/parse/study.d.ts +0 -38
  145. package/dist/study/parse/study.js +0 -225
@@ -1,7 +1,6 @@
1
1
  // What every route planner shares: the plan fields common to all routes, the frozen residual config,
2
2
  // and the requirement and subject helpers.
3
3
  import { DEFAULT_OPENAI_CU_MODEL } from "../actors/computer-use/openai-provider.js";
4
- import { actorOf, capsOf } from "./study-fields.js";
5
4
  // A YAML alias can make a parsed record contain itself (an inline persona that names its own
6
5
  // anchor), and structuredClone keeps the cycle, so each object is frozen once.
7
6
  function deepFreeze(value, frozen = new WeakSet()) {
@@ -14,11 +13,10 @@ function deepFreeze(value, frozen = new WeakSet()) {
14
13
  return value;
15
14
  }
16
15
  function residualOf(config) {
17
- const { comms, policies, personas, defaults, review } = config;
16
+ const { comms, policies, defaults, review } = config;
18
17
  return deepFreeze(structuredClone({
19
18
  ...(comms === undefined ? {} : { comms }),
20
19
  ...(policies === undefined ? {} : { policies }),
21
- ...(personas === undefined ? {} : { personas }),
22
20
  ...(defaults === undefined ? {} : { defaults }),
23
21
  ...(review === undefined ? {} : { review }),
24
22
  ...(config.execution?.desktop === undefined && config.execution?.target === undefined
@@ -44,8 +42,8 @@ function residualOf(config) {
44
42
  export function isNonEmpty(values) {
45
43
  return values.length > 0;
46
44
  }
47
- export function planCaps(config, route) {
48
- const caps = capsOf(config, route);
45
+ export function planCaps(config) {
46
+ const caps = config.caps;
49
47
  return {
50
48
  ...(caps?.maxUsd === undefined ? {} : { maxUsd: caps.maxUsd }),
51
49
  ...(caps?.maxTotalUsd === undefined ? {} : { maxTotalUsd: caps.maxTotalUsd }),
@@ -87,7 +85,7 @@ export function desktopRequirements(config, args) {
87
85
  return requirements;
88
86
  }
89
87
  export function brainOf(config, callerProvider) {
90
- const actor = actorOf(config);
88
+ const actor = config.actor;
91
89
  if (callerProvider)
92
90
  return callerBrainOf(config);
93
91
  const declared = declaredModelOf(config);
@@ -100,7 +98,7 @@ export function callerBrainOf(config) {
100
98
  return { kind: "caller", ...declaredModelOf(config) };
101
99
  }
102
100
  function declaredModelOf(config) {
103
- const model = actorOf(config)?.model;
101
+ const model = config.actor?.model;
104
102
  return model === undefined ? {} : { declaredModel: model };
105
103
  }
106
104
  /** The model a participant's spend is priced at when its provider reports none. */
@@ -9,7 +9,7 @@ export interface Participant {
9
9
  readonly id: string;
10
10
  /** 0-based position in the roster. Bundle `sim-NNN` and `stream-NNN` ids derive from it. */
11
11
  readonly index: number;
12
- /** `lanes[i].persona ?? actors[0].persona`. The route compiles the committed persona file. */
12
+ /** `participants[i].persona ?? actor.persona`. The route compiles the committed persona file. */
13
13
  readonly personaId: string | undefined;
14
14
  /** The declared mission and per-participant focus. An absent mission takes the route's default. */
15
15
  readonly assignment: {
@@ -30,7 +30,7 @@ interface DesktopParticipant extends Participant {
30
30
  readonly preset: DevicePreset;
31
31
  readonly resolution: [number, number];
32
32
  };
33
- /** The `lanes[]` entry's value, else actor value, else absent (the provider default is recorded in the trace). */
33
+ /** The `participants` entry's value, else actor value, else absent (the provider default is recorded in the trace). */
34
34
  readonly limits: {
35
35
  readonly stopWhen?: StopWhen;
36
36
  readonly dwell?: DwellWindow;
@@ -2,9 +2,9 @@
2
2
  // Computer-use and shared-world participants share the desktop fields; each kind adds its own.
3
3
  import { resolveParticipantDevice } from "./device-presets.js";
4
4
  import { isSharedWorldComposition, participantIdAt } from "./routing.js";
5
- import { actorOf, participantList, declaredParticipantCount, participantInstruction, } from "./study-fields.js";
5
+ import { participantList, declaredParticipantCount, participantInstruction, } from "./study-fields.js";
6
6
  function desktopParticipant(config, entry, index, kind, focus) {
7
- const actor = actorOf(config);
7
+ const actor = config.actor;
8
8
  const device = resolveParticipantDevice(config, entry?.device);
9
9
  const personaId = entry?.persona ?? actor?.persona;
10
10
  const mission = actor?.mission;
@@ -39,7 +39,7 @@ function desktopParticipant(config, entry, index, kind, focus) {
39
39
  * override (`--count`) applies only when no roster is declared.
40
40
  */
41
41
  export function computerUseParticipants(config, countOverride) {
42
- const actor = actorOf(config);
42
+ const actor = config.actor;
43
43
  const roster = participantList(config);
44
44
  const count = roster
45
45
  ? roster.length
@@ -8,7 +8,7 @@ import type { RunStudyProvenance } from "../run/study-provenance.js";
8
8
  import type { ComputerUseRefusal } from "../routes/computer-use/plan.js";
9
9
  import type { SharedWorldRefusal } from "../routes/shared-world/plan.js";
10
10
  import type { ComputerUseParticipant, ExternalPublicParticipant, ProvisionedParticipant } from "./plan-participants.js";
11
- import type { StudyConfig, StudyExecutionTerminal, StudyRuntimeAuth, StudyScenarioCaps, StudySubjectProduct, StudySubjectServe, StudySubjectState, StudySubjectStateCheckpoint } from "./types.js";
11
+ import type { StudyConfig, StudyExecutionTerminal, StudyRuntimeAuth, StudyCaps, StudySubjectProduct, StudySubjectServe, StudySubjectState, StudySubjectStateCheckpoint } from "./types.js";
12
12
  export type NonEmpty<T> = readonly [T, ...T[]];
13
13
  type AtLeastTwo<T> = readonly [T, T, ...T[]];
14
14
  /**
@@ -48,7 +48,7 @@ export type Requirement = {
48
48
  * VM study read it before a plan exists. Shared world, terminal and scripted still read some config
49
49
  * fields at run time.
50
50
  */
51
- export type ResidualConfig = Pick<StudyConfig, "comms" | "policies" | "personas" | "defaults" | "review"> & {
51
+ export type ResidualConfig = Pick<StudyConfig, "comms" | "policies" | "defaults" | "review"> & {
52
52
  readonly execution?: Pick<NonNullable<StudyConfig["execution"]>, "desktop" | "target">;
53
53
  readonly subject: Pick<StudyConfig["subject"], "clone" | "localTree" | "repos" | "envValues">;
54
54
  };
@@ -63,6 +63,8 @@ interface PlanBase {
63
63
  /** The study's declared title, which bundles record. */
64
64
  readonly title?: string;
65
65
  readonly study?: RunStudyProvenance;
66
+ /** Warnings about the study's own fields, which the run records in its bundle as warn events. */
67
+ readonly warnings?: readonly string[];
66
68
  /** A frozen copy owned by the plan. */
67
69
  readonly residual: Readonly<ResidualConfig>;
68
70
  readonly dryRun: boolean;
@@ -83,7 +85,7 @@ interface PreviewPlan extends PlanBase {
83
85
  }
84
86
  /**
85
87
  * The model driving a desktop participant. `caller` is the library caller's createProvider.
86
- * `declaredModel` is actors[0].model as the study wrote it, absent when undeclared: the providers
88
+ * `declaredModel` is actor.model as the study wrote it, absent when undeclared: the providers
87
89
  * take it as written, and spend is priced at it, else the default (pricedModel). An openai
88
90
  * brain's `model` is the one its provider runs, with that default applied.
89
91
  */
@@ -229,10 +231,10 @@ export type TerminalPlan = PlanBase & {
229
231
  readonly stdin?: NonNullable<StudyExecutionTerminal["stdin"]>;
230
232
  } & ({
231
233
  readonly dryRun: true;
232
- readonly caps?: StudyScenarioCaps;
234
+ readonly caps?: StudyCaps;
233
235
  } | {
234
236
  readonly dryRun: false;
235
- readonly caps: StudyScenarioCaps & {
237
+ readonly caps: StudyCaps & {
236
238
  readonly maxUsd: number;
237
239
  readonly maxMinutes: number;
238
240
  };
@@ -1,5 +1,5 @@
1
1
  // The route decision. A study's route follows from its composition (subject.source,
2
- // execution.target, the first actor's registered run kind, subject.topology), never from a
2
+ // execution.target, the actor's registered run kind, `route: shared-world`), never from a
3
3
  // declared kind. This is the only function that decides it.
4
4
  import { resolveAutomaticAnalysis } from "../analysis/automatic-config.js";
5
5
  import { callerDrivingOf, planComputerUseStudy } from "../routes/computer-use/plan.js";
@@ -11,17 +11,18 @@ import { THIS_REPO_DRY_RUN_ONLY } from "./composition-rules.js";
11
11
  import { planBase } from "./plan-base.js";
12
12
  import { routeOf } from "./routing.js";
13
13
  import { automaticAnalysisRouteReason, taskProtocolValidationReason } from "./validation.js";
14
- import { declaredParticipantCount, modeOf } from "./study-fields.js";
14
+ import { declaredParticipantCount } from "./study-fields.js";
15
+ import { egressAllowIgnoredWarning } from "./warnings.js";
15
16
  export { routeOf } from "./routing.js";
16
17
  /** Resolve dry-run: explicit override wins, else the scenario mode, else the given fallback. */
17
18
  export function resolveStudyDryRun(config, override, fallback) {
18
19
  if (override !== undefined) {
19
20
  return override;
20
21
  }
21
- if (modeOf(config) === "live") {
22
+ if (config.mode === "live") {
22
23
  return false;
23
24
  }
24
- if (modeOf(config) === "dry-run") {
25
+ if (config.mode === "dry-run") {
25
26
  return true;
26
27
  }
27
28
  return fallback;
@@ -73,8 +74,13 @@ export function planStudy(config, options, deps = {}) {
73
74
  if (!result.ok)
74
75
  return result;
75
76
  // The manifest the CLI resolved enters the plan here and nowhere else; the routes read
76
- // plan.study for the run's status record and bundle.
77
- const plan = options.study === undefined ? result.plan : { ...result.plan, study: options.study };
77
+ // plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
78
+ const warning = egressAllowIgnoredWarning(study);
79
+ const plan = {
80
+ ...result.plan,
81
+ ...(options.study === undefined ? {} : { study: options.study }),
82
+ ...(warning === undefined ? {} : { warnings: [warning] }),
83
+ };
78
84
  return { ok: true, planned: { plan } };
79
85
  }
80
86
  function planRoute(route, study, options, input, deps) {
@@ -25,7 +25,7 @@ export async function runPublicPreviewPreflight(ctx) {
25
25
  { name: "route", ok: false, message: routeError },
26
26
  ]);
27
27
  }
28
- const rosterTargets = ctx.targets.filter((target) => target.kind === "actors[0].lanes[].target");
28
+ const rosterTargets = ctx.targets.filter((target) => target.kind === "participants[].target");
29
29
  const publicTargets = rosterTargets.length > 0
30
30
  ? rosterTargets
31
31
  : ctx.targets.filter((target) => target.kind === "subject.appUrl");
@@ -270,7 +270,7 @@ function targetUrlFor(config, target) {
270
270
  if (target.kind === "subject.appUrl" && config.subject.appUrl) {
271
271
  return config.subject.appUrl;
272
272
  }
273
- if (target.kind === "actors[0].lanes[].target") {
273
+ if (target.kind === "participants[].target") {
274
274
  const rosterTarget = participantList(config)?.find((entry) => entry.target && digest(entry.target) === target.targetDigest)?.target;
275
275
  if (rosterTarget)
276
276
  return rosterTarget;
@@ -12,7 +12,7 @@ export interface StudyPreflightCheck {
12
12
  }
13
13
  export interface StudyPreflightTarget {
14
14
  label: string;
15
- kind: "subject.appUrl" | "actors[0].lanes[].target" | "subject.serve.url" | "subject.product.publicSurface";
15
+ kind: "subject.appUrl" | "participants[].target" | "subject.serve.url" | "subject.product.publicSurface";
16
16
  targetDigest: string;
17
17
  originDigest?: string;
18
18
  loopback: boolean;
@@ -84,7 +84,7 @@ function collectTargets(config) {
84
84
  }
85
85
  for (const [index, entry] of (participantList(config) ?? []).entries()) {
86
86
  if (entry.target) {
87
- targets.push(makeTarget(`actors[0].lanes[${index}].target`, "actors[0].lanes[].target", entry.target));
87
+ targets.push(makeTarget(`participants[${index}].target`, "participants[].target", entry.target));
88
88
  }
89
89
  }
90
90
  if (config.subject.serve?.url) {
@@ -9,8 +9,8 @@ export declare function registeredScriptedBrowserActors(): string[];
9
9
  export declare function actorResolvesToTerminal(type: string | undefined): boolean;
10
10
  export declare function registeredTerminalActors(): string[];
11
11
  /**
12
- * The declared fan-out participant count on the computer-use route: a `lanes[]` roster's length, else
13
- * a homogeneous `count`, else 1. The single source of truth shared by the parser, the engine,
12
+ * The declared fan-out participant count on the computer-use route: a `participants` list's
13
+ * length, else a participant count, else 1. The single source of truth shared by the parser, the engine,
14
14
  * and the pre-flight plan so the participant count is computed the same way everywhere.
15
15
  */
16
16
  export declare function computerUseParticipantCount(config: StudyConfig): number;
@@ -36,15 +36,15 @@ export declare function isComputerUseComposition(config: StudyConfig): boolean;
36
36
  * `shared-world` topology. Mirror of isComputerUseComposition; the single source of truth shared by
37
37
  * routeOf (which checks it before the computer-use route) and the warning logic. Every
38
38
  * shared-world study runs its participants at once (`execution.concurrency` >= 2). The same
39
- * clone/local-tree × e2b-desktop × computer-use composition without `topology: shared-world` stays per-lane-worlds
40
- * (the computer-use route); the topology declaration is the override switch.
39
+ * clone/local-tree × e2b-desktop × computer-use composition without `route: shared-world` stays per-lane-worlds
40
+ * (the computer-use route); the route declaration is the override switch.
41
41
  */
42
42
  export declare function isSharedWorldComposition(config: StudyConfig): boolean;
43
43
  /** The getHost provisioned-subject shared-world shape (clone/local-tree served + exposed in-sandbox). */
44
44
  export declare function isProvisionedSharedWorldComposition(config: StudyConfig): boolean;
45
45
  /**
46
46
  * The external-public shared-world shape: a real public deployment used directly as
47
- * the shared plane: `source: app-url` + `topology: shared-world` + a computer-use actor on
47
+ * the shared plane: `source: app-url` + `route: shared-world` + a computer-use actor on
48
48
  * e2b-desktop + `policies.allowPublicTargets: true`. No getHost, no clone, no subject sandbox, no
49
49
  * seed. The operator-ownership attestation `subject.publicTarget` is required (validated in
50
50
  * externalPublicSharedWorldValidationReason, not here; this predicate is the router only, so a
@@ -1,6 +1,6 @@
1
1
  import { actorRegistry } from "../actors/registry.js";
2
2
  import { isLoopbackUrl } from "./parse/subject.js";
3
- import { actorOf, participantList, declaredParticipantCount, declaresSharedWorld, } from "./study-fields.js";
3
+ import { participantList, declaredParticipantCount } from "./study-fields.js";
4
4
  // Hard cap on computer-use participants. No setting raises it: each participant is a paid desktop,
5
5
  // and they all run at once.
6
6
  export const MAX_COMPUTER_USE_PARTICIPANTS = 16;
@@ -46,8 +46,8 @@ export function registeredTerminalActors() {
46
46
  return registeredActorsOn("terminal");
47
47
  }
48
48
  /**
49
- * The declared fan-out participant count on the computer-use route: a `lanes[]` roster's length, else
50
- * a homogeneous `count`, else 1. The single source of truth shared by the parser, the engine,
49
+ * The declared fan-out participant count on the computer-use route: a `participants` list's
50
+ * length, else a participant count, else 1. The single source of truth shared by the parser, the engine,
51
51
  * and the pre-flight plan so the participant count is computed the same way everywhere.
52
52
  */
53
53
  export function computerUseParticipantCount(config) {
@@ -74,7 +74,7 @@ export function isComputerUseComposition(config) {
74
74
  // local-app drives the cua loop in-process (a custom executor + a non-vision provider), so it
75
75
  // routes to the computer-use route exactly like an app-url subject with a computer-use actor.
76
76
  if (config.subject.source === "app-url" || config.subject.source === "local-app") {
77
- return actorResolvesToComputerUse(actorOf(config)?.type);
77
+ return actorResolvesToComputerUse(config.actor?.type);
78
78
  }
79
79
  // desktop-cli hands the participant a terminal instead of a served page, but it is the
80
80
  // same route: same desktop, same actor, same prompt fields. Leaving it out of this predicate told
@@ -82,13 +82,13 @@ export function isComputerUseComposition(config) {
82
82
  // person reads a screen.
83
83
  if (config.subject.source === "desktop-cli") {
84
84
  return ((config.execution?.target === undefined || config.execution.target === "e2b-desktop") &&
85
- actorResolvesToComputerUse(actorOf(config)?.type));
85
+ actorResolvesToComputerUse(config.actor?.type));
86
86
  }
87
87
  // local-tree packs+uploads the working tree, then serves it exactly like a computer-use clone
88
88
  // subject: same e2b-desktop + computer-use-actor gate.
89
89
  return ((config.subject.source === "clone" || config.subject.source === "local-tree") &&
90
90
  config.execution?.target === "e2b-desktop" &&
91
- actorResolvesToComputerUse(actorOf(config)?.type));
91
+ actorResolvesToComputerUse(config.actor?.type));
92
92
  }
93
93
  /**
94
94
  * True when this config routes to the shared-world route: a clone or local-tree subject
@@ -96,8 +96,8 @@ export function isComputerUseComposition(config) {
96
96
  * `shared-world` topology. Mirror of isComputerUseComposition; the single source of truth shared by
97
97
  * routeOf (which checks it before the computer-use route) and the warning logic. Every
98
98
  * shared-world study runs its participants at once (`execution.concurrency` >= 2). The same
99
- * clone/local-tree × e2b-desktop × computer-use composition without `topology: shared-world` stays per-lane-worlds
100
- * (the computer-use route); the topology declaration is the override switch.
99
+ * clone/local-tree × e2b-desktop × computer-use composition without `route: shared-world` stays per-lane-worlds
100
+ * (the computer-use route); the route declaration is the override switch.
101
101
  */
102
102
  export function isSharedWorldComposition(config) {
103
103
  return (isProvisionedSharedWorldComposition(config) || isExternalPublicSharedWorldComposition(config));
@@ -105,13 +105,13 @@ export function isSharedWorldComposition(config) {
105
105
  /** The getHost provisioned-subject shared-world shape (clone/local-tree served + exposed in-sandbox). */
106
106
  export function isProvisionedSharedWorldComposition(config) {
107
107
  return ((config.subject.source === "clone" || config.subject.source === "local-tree") &&
108
- declaresSharedWorld(config) &&
108
+ config.route === "shared-world" &&
109
109
  config.execution?.target === "e2b-desktop" &&
110
- actorResolvesToComputerUse(actorOf(config)?.type));
110
+ actorResolvesToComputerUse(config.actor?.type));
111
111
  }
112
112
  /**
113
113
  * The external-public shared-world shape: a real public deployment used directly as
114
- * the shared plane: `source: app-url` + `topology: shared-world` + a computer-use actor on
114
+ * the shared plane: `source: app-url` + `route: shared-world` + a computer-use actor on
115
115
  * e2b-desktop + `policies.allowPublicTargets: true`. No getHost, no clone, no subject sandbox, no
116
116
  * seed. The operator-ownership attestation `subject.publicTarget` is required (validated in
117
117
  * externalPublicSharedWorldValidationReason, not here; this predicate is the router only, so a
@@ -120,9 +120,9 @@ export function isProvisionedSharedWorldComposition(config) {
120
120
  */
121
121
  export function isExternalPublicSharedWorldComposition(config) {
122
122
  return (config.subject.source === "app-url" &&
123
- declaresSharedWorld(config) &&
123
+ config.route === "shared-world" &&
124
124
  config.execution?.target === "e2b-desktop" &&
125
- actorResolvesToComputerUse(actorOf(config)?.type) &&
125
+ actorResolvesToComputerUse(config.actor?.type) &&
126
126
  config.policies?.allowPublicTargets === true);
127
127
  }
128
128
  /**
@@ -159,12 +159,12 @@ export function isScriptedBrowserComposition(config) {
159
159
  return (isLocalScriptedBrowserComposition(config) || isProvisionedScriptedBrowserComposition(config));
160
160
  }
161
161
  function isLocalScriptedBrowserComposition(config) {
162
- return (config.subject.source === "app-url" && actorResolvesToScriptedBrowser(actorOf(config)?.type));
162
+ return config.subject.source === "app-url" && actorResolvesToScriptedBrowser(config.actor?.type);
163
163
  }
164
164
  export function isProvisionedScriptedBrowserComposition(config) {
165
165
  return (config.subject.source === "clone" &&
166
166
  config.execution?.target === "e2b-desktop" &&
167
- actorResolvesToScriptedBrowser(actorOf(config)?.type));
167
+ actorResolvesToScriptedBrowser(config.actor?.type));
168
168
  }
169
169
  /**
170
170
  * True when this config routes to the terminal-product backend: a terminal-product subject whose
@@ -173,7 +173,7 @@ export function isProvisionedScriptedBrowserComposition(config) {
173
173
  * the single source of truth for routeOf and the warning logic.
174
174
  */
175
175
  export function isTerminalProductComposition(config) {
176
- return (config.subject.source === "terminal-product" && actorResolvesToTerminal(actorOf(config)?.type));
176
+ return (config.subject.source === "terminal-product" && actorResolvesToTerminal(config.actor?.type));
177
177
  }
178
178
  /**
179
179
  * The route a config takes. It never refuses: a config no route can run still gets the route
@@ -4,7 +4,7 @@ import type { StudyConfig } from "./types.js";
4
4
  import { type StudyEvent } from "./run-study-events.js";
5
5
  type Refusal = {
6
6
  ok: false;
7
- code: "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED";
7
+ code: "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID";
8
8
  message: string;
9
9
  };
10
10
  type Normalized = {
@@ -7,7 +7,7 @@ import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
7
7
  import { resolveStudyDryRun } from "./plan.js";
8
8
  import { knownSecretValues, studyEventEmitter } from "./run-study-events.js";
9
9
  import { studyResultIdentity } from "../run/study-result.js";
10
- import { actorOf, participantList } from "./study-fields.js";
10
+ import { participantList } from "./study-fields.js";
11
11
  const unsupported = (option, route, reason) => ({
12
12
  ok: false,
13
13
  code: "HUMANISH_STUDY_OPTION_UNSUPPORTED",
@@ -105,7 +105,7 @@ export function normalizeRunStudyOptions(config, route, options) {
105
105
  export function optionRefusalOutcome(config, route, options, refusal) {
106
106
  const cwd = path.resolve(options.cwd);
107
107
  const error = { code: refusal.code, message: refusal.message };
108
- const actor = actorOf(config)?.type ?? "";
108
+ const actor = config.actor?.type ?? "";
109
109
  const dryRun = resolveStudyDryRun(config, options.dryRun, true) ?? true;
110
110
  const runId = options.runId ?? "not-created";
111
111
  const common = { ok: false, cwd, actor, dryRun, runId, warnings: [], error };
@@ -27,7 +27,7 @@ export interface StudyDeps {
27
27
  readonly subjectPhaseSink?: (event: SubjectPhaseEvent, participant?: ParticipantRef) => void;
28
28
  /**
29
29
  * Shared world: runs each participant's computer-use session in place of the actor's own. The
30
- * planner reads whether it is set: a custom runner cannot enforce actors[0].maxOutputTokens.
30
+ * planner reads whether it is set: a custom runner cannot enforce actor.maxOutputTokens.
31
31
  */
32
32
  readonly runSession?: (options: CuaActorSessionOptions) => Promise<CuaLoopResult>;
33
33
  /**
@@ -1,28 +1,7 @@
1
- import type { StudyRoute } from "./routing.js";
2
- import type { StudyActor, StudyConfig, StudyParticipantEntry, StudyScenario, StudyScenarioCaps } from "./types.js";
3
- /** The actor that runs. A v3 file declares it as `actor`. */
4
- export declare function actorOf(config: StudyConfig): StudyActor | undefined;
5
- /**
6
- * Every declared actor. parseStudy accepts exactly one; a library config may declare more, and the
7
- * checks that refuse a second actor's settings read them all.
8
- */
9
- export declare function actorsOf(config: StudyConfig): readonly StudyActor[];
10
- /** The participants listed one by one: a v3 `participants` list. Undefined when none is listed. */
1
+ import type { StudyConfig, StudyParticipantEntry } from "./types.js";
2
+ /** The participants listed one by one: a `participants` list. Undefined when none is listed. */
11
3
  export declare function participantList(config: StudyConfig): readonly StudyParticipantEntry[] | undefined;
12
- /** The declared number of identical participants: v3 `participants: <n>` or `participants.count`. */
4
+ /** The declared number of identical participants: `participants: <n>` or `participants.count`. */
13
5
  export declare function declaredParticipantCount(config: StudyConfig): number | undefined;
14
- /** The steer each of a counted group of participants gets: v3 `participants.instruction`. */
6
+ /** The steer each of a counted group of participants gets: `participants.instruction`. */
15
7
  export declare function participantInstruction(config: StudyConfig): string | undefined;
16
- /** How many scripted surfaces run, 1 for desktop and 2 with mobile. A v3 file lists `surfaces`. */
17
- export declare function surfaceCount(config: StudyConfig): number | undefined;
18
- /** The declared run mode. A v3 file declares it as `mode`. */
19
- export declare function modeOf(config: StudyConfig): StudyScenario["mode"];
20
- /** The scripted route's scenario id or path. A v3 file declares it as `scenario`. */
21
- export declare function scenarioRefOf(config: StudyConfig): string | undefined;
22
- /**
23
- * The spend caps a route enforces. A v3 file declares them as `caps`. The v2 shape keeps them in
24
- * `execution.caps` on computer use and shared world, and in `scenario.caps` on terminal.
25
- */
26
- export declare function capsOf(config: StudyConfig, route: Extract<StudyRoute, "computer-use" | "shared-world" | "terminal">): StudyScenarioCaps | undefined;
27
- /** True when the participants share one app. A v3 file declares `route: shared-world`. */
28
- export declare function declaresSharedWorld(config: StudyConfig): boolean;
@@ -1,51 +1,22 @@
1
- // The study's fields, read in one place. StudyConfig still has the humanish.lab.v2 shape
2
- // (`actors[0]`, `execution.caps`, `scenario.mode`), while a humanish.study.v3 file names the same
3
- // values `actor`, `participants`, `surfaces`, `caps`, `mode`, `scenario` and `route`. Code outside
4
- // the parser reads them through these functions, and tests/surface/study-config-reads.test.ts
5
- // refuses a direct read, so the config can take the v3 shape by changing this file.
6
- /** The actor that runs. A v3 file declares it as `actor`. */
7
- export function actorOf(config) {
8
- return config.actors[0];
9
- }
10
- /**
11
- * Every declared actor. parseStudy accepts exactly one; a library config may declare more, and the
12
- * checks that refuse a second actor's settings read them all.
13
- */
14
- export function actorsOf(config) {
15
- return config.actors;
16
- }
17
- /** The participants listed one by one: a v3 `participants` list. Undefined when none is listed. */
1
+ // The study's fields, read in one place. Participants are the one read that is not a field: a v3
2
+ // study declares them as a count, as `{ count, instruction }` or as a list, and the accessors below
3
+ // read each form. tests/surface/study-config-reads.test.ts refuses a direct read of a humanish.lab.v2
4
+ // field anywhere outside migrate.
5
+ /** The participants listed one by one: a `participants` list. Undefined when none is listed. */
18
6
  export function participantList(config) {
19
- return config.actors[0]?.lanes;
7
+ return Array.isArray(config.participants) ? config.participants : undefined;
20
8
  }
21
- /** The declared number of identical participants: v3 `participants: <n>` or `participants.count`. */
9
+ /** The declared number of identical participants: `participants: <n>` or `participants.count`. */
22
10
  export function declaredParticipantCount(config) {
23
- return config.actors[0]?.count;
11
+ const { participants } = config;
12
+ if (typeof participants === "number")
13
+ return participants;
14
+ return isGroup(participants) ? participants.count : undefined;
24
15
  }
25
- /** The steer each of a counted group of participants gets: v3 `participants.instruction`. */
16
+ /** The steer each of a counted group of participants gets: `participants.instruction`. */
26
17
  export function participantInstruction(config) {
27
- return config.actors[0]?.laneFocus?.instruction;
28
- }
29
- /** How many scripted surfaces run, 1 for desktop and 2 with mobile. A v3 file lists `surfaces`. */
30
- export function surfaceCount(config) {
31
- return config.actors[0]?.count;
32
- }
33
- /** The declared run mode. A v3 file declares it as `mode`. */
34
- export function modeOf(config) {
35
- return config.scenario?.mode;
36
- }
37
- /** The scripted route's scenario id or path. A v3 file declares it as `scenario`. */
38
- export function scenarioRefOf(config) {
39
- return config.scenario?.ref;
40
- }
41
- /**
42
- * The spend caps a route enforces. A v3 file declares them as `caps`. The v2 shape keeps them in
43
- * `execution.caps` on computer use and shared world, and in `scenario.caps` on terminal.
44
- */
45
- export function capsOf(config, route) {
46
- return route === "terminal" ? config.scenario?.caps : config.execution?.caps;
18
+ return isGroup(config.participants) ? config.participants.instruction : undefined;
47
19
  }
48
- /** True when the participants share one app. A v3 file declares `route: shared-world`. */
49
- export function declaresSharedWorld(config) {
50
- return config.subject.topology === "shared-world";
20
+ function isGroup(participants) {
21
+ return typeof participants === "object" && participants !== null && !Array.isArray(participants);
51
22
  }
@@ -18,7 +18,7 @@ import { inspectStudyManifest } from "./discover.js";
18
18
  import { isComputerUseComposition } from "./routing.js";
19
19
  import { probeKeySources } from "../keys/key-resolution.js";
20
20
  import { receivingRequiredKey } from "../comms/setup.js";
21
- import { actorOf, participantList, declaredParticipantCount, capsOf } from "./study-fields.js";
21
+ import { participantList, declaredParticipantCount } from "./study-fields.js";
22
22
  export const STUDY_SUMMARY_SCHEMA = "humanish.study-summary.v1";
23
23
  /**
24
24
  * The effort every participant will run at, or `"per-lane"` when they differ. A participant that declares nothing
@@ -26,7 +26,7 @@ export const STUDY_SUMMARY_SCHEMA = "humanish.study-summary.v1";
26
26
  * reported as the resolved value, exactly as `model` reports its default rather than hiding it.
27
27
  */
28
28
  function reasoningEffortOf(config) {
29
- const fallback = actorOf(config)?.reasoningEffort ?? DEFAULT_OPENAI_CU_REASONING_EFFORT;
29
+ const fallback = config.actor?.reasoningEffort ?? DEFAULT_OPENAI_CU_REASONING_EFFORT;
30
30
  const roster = participantList(config) ?? [];
31
31
  const resolved = new Set(roster.map((entry) => entry.reasoningEffort ?? fallback));
32
32
  if (resolved.size > 1)
@@ -47,14 +47,19 @@ function subjectOf(config) {
47
47
  }
48
48
  /** How many participants, and who; collapsed when they are all the same persona. */
49
49
  function participantsOf(config) {
50
- const actor = actorOf(config);
50
+ const actor = config.actor;
51
51
  if (actor === undefined)
52
52
  return undefined;
53
53
  const rosterPersonas = (participantList(config) ?? [])
54
54
  .map((entry) => entry.persona)
55
55
  .filter((persona) => typeof persona === "string");
56
56
  const personas = rosterPersonas.length > 0 ? rosterPersonas : actor.persona === undefined ? [] : [actor.persona];
57
- const count = declaredParticipantCount(config) ?? participantList(config)?.length ?? personas.length ?? 1;
57
+ // A scripted study's participants are its surfaces, one replay on each.
58
+ const count = declaredParticipantCount(config) ??
59
+ config.surfaces?.length ??
60
+ participantList(config)?.length ??
61
+ personas.length ??
62
+ 1;
58
63
  const unique = [...new Set(personas)];
59
64
  if (unique.length === 0)
60
65
  return `${count} participant${count === 1 ? "" : "s"}`;
@@ -62,11 +67,11 @@ function participantsOf(config) {
62
67
  // are named, because which personas are in a study is the study's design.
63
68
  return unique.length === 1 ? `${count} × ${unique[0]}` : unique.join(" · ");
64
69
  }
65
- /** The computer-use caps; `execution.caps` is inert on other routes, so none is drawn there. */
70
+ /** The computer-use caps. Other routes draw none. */
66
71
  function summaryCapsOf(config) {
67
72
  if (!isComputerUseComposition(config))
68
73
  return {};
69
- const caps = capsOf(config, "computer-use");
74
+ const caps = config.caps;
70
75
  return {
71
76
  ...(typeof caps?.maxUsd === "number" ? { laneUsd: caps.maxUsd } : {}),
72
77
  ...(typeof caps?.maxTotalUsd === "number" ? { studyUsd: caps.maxTotalUsd } : {}),
@@ -134,7 +139,7 @@ export async function readStudySummary(cwd, study, options = {}) {
134
139
  : undefined;
135
140
  const participantReadiness = options.checkKeys === true &&
136
141
  isLocalBrowserStudy(inspected.config) &&
137
- actorOf(inspected.config)?.type === "local-agent"
142
+ inspected.config.actor?.type === "local-agent"
138
143
  ? await localCodexParticipantCheck({ env: options.env ?? process.env })
139
144
  : undefined;
140
145
  return {
@@ -161,7 +166,7 @@ export async function readStudySummary(cwd, study, options = {}) {
161
166
  ...(typeof config.description === "string" ? { description: config.description.trim() } : {}),
162
167
  ...(subject === undefined ? {} : { subject }),
163
168
  ...(participants === undefined ? {} : { participants }),
164
- model: actorOf(inspected.config)?.model ?? DEFAULT_OPENAI_CU_MODEL,
169
+ model: inspected.config.actor?.model ?? DEFAULT_OPENAI_CU_MODEL,
165
170
  reasoningEffort: reasoningEffortOf(inspected.config),
166
171
  caps: summaryCapsOf(inspected.config),
167
172
  ...(keysReady === undefined ? {} : { keysReady }),