humanish 0.111.0 → 0.111.2

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 (99) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/CONTEXT.md +1 -1
  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/browser-control/client.js +5 -5
  23. package/dist/browser-control/protocol.d.ts +30 -1
  24. package/dist/browser-control/protocol.js +19 -2
  25. package/dist/browser-control/transport.d.ts +3 -2
  26. package/dist/browser-control/transport.js +12 -7
  27. package/dist/cli/commands/study-route-computer-use.js +2 -2
  28. package/dist/cli/commands/study-route-shared-world.js +2 -2
  29. package/dist/cli/commands/study.js +1 -2
  30. package/dist/cli/doctor.js +11 -0
  31. package/dist/evidence/encoded-text.d.ts +3 -3
  32. package/dist/evidence/encoded-text.js +10 -5
  33. package/dist/evidence/redaction.d.ts +2 -2
  34. package/dist/evidence/redaction.js +18 -7
  35. package/dist/guest/browser-tools.js +5 -1
  36. package/dist/guest/chromium-text.js +42 -18
  37. package/dist/guest/desktop-executor.js +5 -1
  38. package/dist/keys/key-resolution.d.ts +3 -0
  39. package/dist/keys/key-resolution.js +15 -2
  40. package/dist/observer-app.html +6 -6
  41. package/dist/routes/computer-use/bundle-parts.d.ts +4 -0
  42. package/dist/routes/computer-use/bundle-parts.js +7 -2
  43. package/dist/routes/computer-use/fanout-bundle.js +4 -2
  44. package/dist/routes/computer-use/local-vm.js +1 -2
  45. package/dist/routes/computer-use/participant-model.js +6 -1
  46. package/dist/routes/computer-use/plan.js +3 -4
  47. package/dist/routes/computer-use/route.js +1 -2
  48. package/dist/routes/computer-use/setup.js +2 -2
  49. package/dist/routes/preview.js +1 -0
  50. package/dist/routes/scripted/plan.js +4 -5
  51. package/dist/routes/scripted/route.js +2 -2
  52. package/dist/routes/shared-world/plan.js +4 -5
  53. package/dist/routes/shared-world/route.js +2 -2
  54. package/dist/routes/shared-world/setup.js +1 -0
  55. package/dist/routes/terminal/dry-run.js +1 -0
  56. package/dist/routes/terminal/live-finish.js +24 -5
  57. package/dist/routes/terminal/live-sandbox.js +5 -1
  58. package/dist/routes/terminal/plan.js +3 -4
  59. package/dist/routes/terminal/recorder.d.ts +6 -0
  60. package/dist/routes/terminal/recorder.js +61 -6
  61. package/dist/routes/terminal/route.js +1 -2
  62. package/dist/routes/terminal/session.js +2 -1
  63. package/dist/routes/terminal/token-usage.d.ts +3 -0
  64. package/dist/routes/terminal/token-usage.js +5 -0
  65. package/dist/routes/terminal/types.d.ts +2 -1
  66. package/dist/routes/terminal/types.js +2 -1
  67. package/dist/run/dry-run.js +1 -0
  68. package/dist/run/results.d.ts +2 -0
  69. package/dist/run/run.d.ts +2 -0
  70. package/dist/run/run.js +16 -2
  71. package/dist/run/terminal-contract.d.ts +10 -0
  72. package/dist/run/terminal-contract.js +27 -9
  73. package/dist/study/composition-rules.js +17 -18
  74. package/dist/study/doctor.js +4 -5
  75. package/dist/study/migrate/v2.d.ts +5 -0
  76. package/dist/study/migrate/v2.js +23 -0
  77. package/dist/study/parse/front.js +2 -21
  78. package/dist/study/parse/url-credentials.d.ts +3 -3
  79. package/dist/study/parse/url-credentials.js +155 -13
  80. package/dist/study/persona-resolve.js +2 -2
  81. package/dist/study/plan-base.js +3 -4
  82. package/dist/study/plan-participants.js +3 -3
  83. package/dist/study/plan-types.d.ts +2 -0
  84. package/dist/study/plan.js +11 -5
  85. package/dist/study/routing.js +11 -11
  86. package/dist/study/run-study-options.js +2 -2
  87. package/dist/study/study-fields.d.ts +1 -13
  88. package/dist/study/study-fields.js +0 -24
  89. package/dist/study/summary.js +7 -7
  90. package/dist/study/types.d.ts +1 -0
  91. package/dist/study/validation.js +11 -11
  92. package/dist/study/warnings.d.ts +8 -0
  93. package/dist/study/warnings.js +22 -7
  94. package/dist/substrates/local/runtime-config.js +6 -6
  95. package/docs/architecture/browser-control.md +12 -2
  96. package/docs/architecture/guest-desktop.md +7 -1
  97. package/docs/architecture/terminal-product-route.md +3 -2
  98. package/docs/contracts/schemas.md +16 -5
  99. package/package.json +1 -1
@@ -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()) {
@@ -44,7 +43,7 @@ export function isNonEmpty(values) {
44
43
  return values.length > 0;
45
44
  }
46
45
  export function planCaps(config) {
47
- const caps = capsOf(config);
46
+ const caps = config.caps;
48
47
  return {
49
48
  ...(caps?.maxUsd === undefined ? {} : { maxUsd: caps.maxUsd }),
50
49
  ...(caps?.maxTotalUsd === undefined ? {} : { maxTotalUsd: caps.maxTotalUsd }),
@@ -86,7 +85,7 @@ export function desktopRequirements(config, args) {
86
85
  return requirements;
87
86
  }
88
87
  export function brainOf(config, callerProvider) {
89
- const actor = actorOf(config);
88
+ const actor = config.actor;
90
89
  if (callerProvider)
91
90
  return callerBrainOf(config);
92
91
  const declared = declaredModelOf(config);
@@ -99,7 +98,7 @@ export function callerBrainOf(config) {
99
98
  return { kind: "caller", ...declaredModelOf(config) };
100
99
  }
101
100
  function declaredModelOf(config) {
102
- const model = actorOf(config)?.model;
101
+ const model = config.actor?.model;
103
102
  return model === undefined ? {} : { declaredModel: model };
104
103
  }
105
104
  /** The model a participant's spend is priced at when its provider reports none. */
@@ -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
@@ -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;
@@ -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) {
@@ -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;
@@ -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
@@ -105,9 +105,9 @@ 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
@@ -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
@@ -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 };
@@ -1,19 +1,7 @@
1
- import type { StudyActor, StudyCaps, StudyConfig, StudyParticipantEntry } from "./types.js";
2
- /** The actor that runs. */
3
- export declare function actorOf(config: StudyConfig): StudyActor | undefined;
1
+ import type { StudyConfig, StudyParticipantEntry } from "./types.js";
4
2
  /** The participants listed one by one: a `participants` list. Undefined when none is listed. */
5
3
  export declare function participantList(config: StudyConfig): readonly StudyParticipantEntry[] | undefined;
6
4
  /** The declared number of identical participants: `participants: <n>` or `participants.count`. */
7
5
  export declare function declaredParticipantCount(config: StudyConfig): number | undefined;
8
6
  /** The steer each of a counted group of participants gets: `participants.instruction`. */
9
7
  export declare function participantInstruction(config: StudyConfig): string | undefined;
10
- /** How many scripted surfaces run, 1 for desktop and 2 with mobile. */
11
- export declare function surfaceCount(config: StudyConfig): number | undefined;
12
- /** The declared run mode. */
13
- export declare function modeOf(config: StudyConfig): StudyConfig["mode"];
14
- /** The scripted route's scenario id or path. */
15
- export declare function scenarioRefOf(config: StudyConfig): string | undefined;
16
- /** The spend caps the route enforces. parseStudy refuses a key the route does not read. */
17
- export declare function capsOf(config: StudyConfig): StudyCaps | undefined;
18
- /** True when the participants share one app: `route: shared-world`. */
19
- export declare function declaresSharedWorld(config: StudyConfig): boolean;
@@ -2,10 +2,6 @@
2
2
  // study declares them as a count, as `{ count, instruction }` or as a list, and the accessors below
3
3
  // read each form. tests/surface/study-config-reads.test.ts refuses a direct read of a humanish.lab.v2
4
4
  // field anywhere outside migrate.
5
- /** The actor that runs. */
6
- export function actorOf(config) {
7
- return config.actor;
8
- }
9
5
  /** The participants listed one by one: a `participants` list. Undefined when none is listed. */
10
6
  export function participantList(config) {
11
7
  return Array.isArray(config.participants) ? config.participants : undefined;
@@ -21,26 +17,6 @@ export function declaredParticipantCount(config) {
21
17
  export function participantInstruction(config) {
22
18
  return isGroup(config.participants) ? config.participants.instruction : undefined;
23
19
  }
24
- /** How many scripted surfaces run, 1 for desktop and 2 with mobile. */
25
- export function surfaceCount(config) {
26
- return config.surfaces?.length;
27
- }
28
- /** The declared run mode. */
29
- export function modeOf(config) {
30
- return config.mode;
31
- }
32
- /** The scripted route's scenario id or path. */
33
- export function scenarioRefOf(config) {
34
- return config.scenario;
35
- }
36
- /** The spend caps the route enforces. parseStudy refuses a key the route does not read. */
37
- export function capsOf(config) {
38
- return config.caps;
39
- }
40
- /** True when the participants share one app: `route: shared-world`. */
41
- export function declaresSharedWorld(config) {
42
- return config.route === "shared-world";
43
- }
44
20
  function isGroup(participants) {
45
21
  return typeof participants === "object" && participants !== null && !Array.isArray(participants);
46
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, surfaceCount, } 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,7 +47,7 @@ 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) ?? [])
@@ -56,7 +56,7 @@ function participantsOf(config) {
56
56
  const personas = rosterPersonas.length > 0 ? rosterPersonas : actor.persona === undefined ? [] : [actor.persona];
57
57
  // A scripted study's participants are its surfaces, one replay on each.
58
58
  const count = declaredParticipantCount(config) ??
59
- surfaceCount(config) ??
59
+ config.surfaces?.length ??
60
60
  participantList(config)?.length ??
61
61
  personas.length ??
62
62
  1;
@@ -71,7 +71,7 @@ function participantsOf(config) {
71
71
  function summaryCapsOf(config) {
72
72
  if (!isComputerUseComposition(config))
73
73
  return {};
74
- const caps = capsOf(config);
74
+ const caps = config.caps;
75
75
  return {
76
76
  ...(typeof caps?.maxUsd === "number" ? { laneUsd: caps.maxUsd } : {}),
77
77
  ...(typeof caps?.maxTotalUsd === "number" ? { studyUsd: caps.maxTotalUsd } : {}),
@@ -139,7 +139,7 @@ export async function readStudySummary(cwd, study, options = {}) {
139
139
  : undefined;
140
140
  const participantReadiness = options.checkKeys === true &&
141
141
  isLocalBrowserStudy(inspected.config) &&
142
- actorOf(inspected.config)?.type === "local-agent"
142
+ inspected.config.actor?.type === "local-agent"
143
143
  ? await localCodexParticipantCheck({ env: options.env ?? process.env })
144
144
  : undefined;
145
145
  return {
@@ -166,7 +166,7 @@ export async function readStudySummary(cwd, study, options = {}) {
166
166
  ...(typeof config.description === "string" ? { description: config.description.trim() } : {}),
167
167
  ...(subject === undefined ? {} : { subject }),
168
168
  ...(participants === undefined ? {} : { participants }),
169
- model: actorOf(inspected.config)?.model ?? DEFAULT_OPENAI_CU_MODEL,
169
+ model: inspected.config.actor?.model ?? DEFAULT_OPENAI_CU_MODEL,
170
170
  reasoningEffort: reasoningEffortOf(inspected.config),
171
171
  caps: summaryCapsOf(inspected.config),
172
172
  ...(keysReady === undefined ? {} : { keysReady }),
@@ -511,6 +511,7 @@ export interface StudyExecution {
511
511
  };
512
512
  /**
513
513
  * `terminal-product` route: outbound routing allowlist passed to E2B with a deny-all fallback.
514
+ * Every other route leaves egress open and ignores it with a warning; 0.112.0 refuses it there.
514
515
  * Domain filtering is a routing control, not strict destination isolation on shared hosting.
515
516
  * It does not constrain spending through an allowed runtime provider, including when
516
517
  * openai-egress keeps the raw runtime key outside the sandbox.
@@ -3,7 +3,7 @@ import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./parse/actors
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
+ import { participantList } from "./study-fields.js";
7
7
  /**
8
8
  * Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
9
9
  * message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
@@ -53,7 +53,7 @@ export function computerUseValidationReason(config) {
53
53
  if (participantCount > 1 &&
54
54
  config.policies?.allowPublicTargets === true &&
55
55
  declaredTargets(config).length === 0 &&
56
- !declaresSharedWorld(config)) {
56
+ config.route !== "shared-world") {
57
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.";
58
58
  }
59
59
  return null;
@@ -119,7 +119,7 @@ function provisionedSharedWorldStructureReason(config) {
119
119
  if (config.execution?.target !== "e2b-desktop") {
120
120
  return "`route: shared-world` requires `execution.target: e2b-desktop`: the role participants drive hosted desktop browsers against one in-sandbox app.";
121
121
  }
122
- if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
122
+ if (!actorResolvesToComputerUse(config.actor?.type)) {
123
123
  return `\`route: shared-world\` requires a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}); each role participant runs a computer-use session.`;
124
124
  }
125
125
  const serve = config.subject.serve;
@@ -142,7 +142,7 @@ function provisionedSharedWorldStructureReason(config) {
142
142
  }
143
143
  /** SMTP capture runs only where each participant has its own world, so a shared world refuses it. */
144
144
  export function smtpValidationReason(config) {
145
- return config.comms?.email?.smtp && declaresSharedWorld(config)
145
+ return config.comms?.email?.smtp && config.route === "shared-world"
146
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
147
  : undefined;
148
148
  }
@@ -153,7 +153,7 @@ export function smtpValidationReason(config) {
153
153
  */
154
154
  export function desktopMediaValidationReason(config, supportsMedia = isComputerUseComposition(config)) {
155
155
  if (config.execution?.desktop?.recording !== undefined &&
156
- (!supportsMedia || declaresSharedWorld(config) || config.subject.source === "local-app")) {
156
+ (!supportsMedia || config.route === "shared-world" || config.subject.source === "local-app")) {
157
157
  return "execution.desktop.recording is supported only for computer-use participants on independent desktops. Remove the declaration or select a supported route.";
158
158
  }
159
159
  const media = config.execution?.desktop?.media;
@@ -162,7 +162,7 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
162
162
  if (media.microphone !== undefined && media.microphone.source !== "speech") {
163
163
  return "execution.desktop.media.microphone.source must be speech. Microphone source-file injection is unsupported.";
164
164
  }
165
- if (declaresSharedWorld(config)) {
165
+ if (config.route === "shared-world") {
166
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.";
167
167
  }
168
168
  if (!supportsMedia ||
@@ -174,7 +174,7 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
174
174
  return "execution.desktop.media requires Chrome or Chromium; Firefox cannot receive the declared synthetic capture device. Set execution.desktop.browser: chrome or chromium.";
175
175
  }
176
176
  if (media.microphone !== undefined) {
177
- const actor = actorOf(config);
177
+ const actor = config.actor;
178
178
  if (actor !== undefined &&
179
179
  (actor.type !== "local-agent" ||
180
180
  (actor.localAgent !== undefined && actor.localAgent !== "codex"))) {
@@ -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 (actorOf(config)?.type === "local-agent") {
197
+ if (config.actor?.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,7 +202,7 @@ 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
- if (actorOf(config)?.tasks !== undefined && !supportsTasks) {
205
+ if (config.actor?.tasks !== undefined && !supportsTasks) {
206
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.";
207
207
  }
208
208
  return null;
@@ -223,7 +223,7 @@ export function cloneTargetValidationReason(config) {
223
223
  }
224
224
  /** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
225
225
  export function outputTokenLimitValidationReason(config) {
226
- const actor = actorOf(config);
226
+ const actor = config.actor;
227
227
  if (actor?.maxOutputTokens === undefined)
228
228
  return null;
229
229
  if (!isMaxOutputTokens(actor.maxOutputTokens))
@@ -302,7 +302,7 @@ export function externalPublicSharedWorldValidationReason(config) {
302
302
  if (config.execution?.target !== "e2b-desktop") {
303
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.";
304
304
  }
305
- if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
305
+ if (!actorResolvesToComputerUse(config.actor?.type)) {
306
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.`;
307
307
  }
308
308
  const roster = participantList(config);
@@ -9,4 +9,12 @@ export declare function inertFieldLabels(config: StudyConfig): string[];
9
9
  * `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
10
10
  */
11
11
  export declare function inertFieldPaths(config: StudyConfig): string[];
12
+ /**
13
+ * The warning for `execution.egressAllow` on a route that ignores it, or undefined. Only the
14
+ * terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
15
+ * study author would trust a list that blocks nothing. `humanish study check` and every run print
16
+ * it, and the run records it in its bundle.
17
+ * Removed in 0.112.0: deprecated on 2026-10-05. 0.112.0 refuses the field off the terminal route.
18
+ */
19
+ export declare function egressAllowIgnoredWarning(config: StudyConfig): string | undefined;
12
20
  export declare function forwardDeclaredWarnings(config: StudyConfig): string[];
@@ -1,7 +1,7 @@
1
1
  import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
2
2
  import { declaredParticipantIds } from "./plan-participants.js";
3
3
  import { addressedRecipients } from "./parse/comms.js";
4
- import { actorOf, participantList, declaredParticipantCount, scenarioRefOf, } from "./study-fields.js";
4
+ import { participantList, declaredParticipantCount } from "./study-fields.js";
5
5
  // The computer-use and terminal routes consume mission/persona/model (they compose the agent prompt
6
6
  // and bundle provenance).
7
7
  const promptRoute = (routes) => routes.cua || routes.terminal;
@@ -29,25 +29,25 @@ const ACTOR_ROWS = [
29
29
  {
30
30
  field: "actor.mission",
31
31
  reason: "the scripted-browser actor runs no model",
32
- applies: (config, routes) => scriptedRoute(routes) && Boolean(actorOf(config)?.mission),
32
+ applies: (config, routes) => scriptedRoute(routes) && Boolean(config.actor?.mission),
33
33
  },
34
34
  {
35
35
  field: "actor.model",
36
36
  reason: "the scripted-browser actor runs no model",
37
- applies: (config, routes) => scriptedRoute(routes) && Boolean(actorOf(config)?.model),
37
+ applies: (config, routes) => scriptedRoute(routes) && Boolean(config.actor?.model),
38
38
  },
39
39
  // On every other route the prompt fields and persona are inert.
40
40
  {
41
41
  field: "actor.mission",
42
- applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.mission),
42
+ applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.mission),
43
43
  },
44
44
  {
45
45
  field: "actor.persona",
46
- applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.persona),
46
+ applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.persona),
47
47
  },
48
48
  {
49
49
  field: "actor.model",
50
- applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.model),
50
+ applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.model),
51
51
  },
52
52
  ];
53
53
  const TERMINAL_ONLY = "needs subject.source: terminal-product + a registered terminal actor";
@@ -205,7 +205,7 @@ const CONFIG_ROWS = [
205
205
  // everywhere else.
206
206
  {
207
207
  field: "scenario",
208
- applies: (config, routes) => Boolean(scenarioRefOf(config)) && !routes.scripted,
208
+ applies: (config, routes) => Boolean(config.scenario) && !routes.scripted,
209
209
  },
210
210
  // review.{scoring,milestones,vocabulary} stay forward-declared on every route.
211
211
  // review.scorer is consumed (loaded + wired, or fail-closed at load) on every scorer-capable
@@ -265,6 +265,18 @@ export function inertFieldLabels(config) {
265
265
  export function inertFieldPaths(config) {
266
266
  return inertRows(config).map((row) => row.field);
267
267
  }
268
+ /**
269
+ * The warning for `execution.egressAllow` on a route that ignores it, or undefined. Only the
270
+ * terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
271
+ * study author would trust a list that blocks nothing. `humanish study check` and every run print
272
+ * it, and the run records it in its bundle.
273
+ * Removed in 0.112.0: deprecated on 2026-10-05. 0.112.0 refuses the field off the terminal route.
274
+ */
275
+ export function egressAllowIgnoredWarning(config) {
276
+ if (config.execution?.egressAllow === undefined || config.route === "terminal")
277
+ return undefined;
278
+ return `\`execution.egressAllow\` is ignored on route: ${config.route}. Only route: terminal creates its sandbox with that outbound allowlist, and this route's browser desktops can reach any site. Remove the field: humanish 0.112.0 refuses it on every route except terminal.`;
279
+ }
268
280
  // Report fields that are present but not yet consumed by the engine, so a user never trusts a
269
281
  // setting that silently does nothing.
270
282
  export function forwardDeclaredWarnings(config) {
@@ -275,6 +287,9 @@ export function forwardDeclaredWarnings(config) {
275
287
  : [
276
288
  `These fields are set, but no route reads them yet, so they have no effect: ${inert.join(", ")}.`,
277
289
  ];
290
+ const egressIgnored = egressAllowIgnoredWarning(config);
291
+ if (egressIgnored !== undefined)
292
+ warnings.push(egressIgnored);
278
293
  // A declared cap below the participant count is legal but loud: the roster promises N live actors
279
294
  // and the cap delivers waves of M. Say so up front (inspect + dry-run + run): a green run in
280
295
  // waves is otherwise indistinguishable from the all-live run the author meant.
@@ -1,15 +1,15 @@
1
- import { actorOf, capsOf, participantList } from "../../study/study-fields.js";
1
+ import { participantList } from "../../study/study-fields.js";
2
2
  // The runtime image enforces a 30-minute lifetime; reserve setup/teardown room.
3
3
  export const LOCAL_BROWSER_LIFETIME_MS = 30 * 60_000;
4
4
  const MAX_SESSION_MS = 20 * 60_000;
5
5
  export function isLocalBrowserStudy(config) {
6
6
  return (config.subject.source === "app-url" &&
7
7
  config.execution?.target === "local" &&
8
- ["local-agent", "openai-computer-use"].includes(actorOf(config)?.type ?? ""));
8
+ ["local-agent", "openai-computer-use"].includes(config.actor?.type ?? ""));
9
9
  }
10
10
  /** Defaults for the explicitly selected local substrate; hosted configurations are untouched. */
11
11
  export function localBrowserDefaults(config) {
12
- const actor = actorOf(config);
12
+ const actor = config.actor;
13
13
  if (!isLocalBrowserStudy(config) || actor === undefined)
14
14
  return config;
15
15
  return {
@@ -33,7 +33,7 @@ export function localBrowserDefaults(config) {
33
33
  };
34
34
  }
35
35
  export function localBrowserUnsupportedReason(config) {
36
- const actor = actorOf(config);
36
+ const actor = config.actor;
37
37
  const desktop = config.execution?.desktop;
38
38
  if ((config.execution?.timeoutMs ?? MAX_SESSION_MS) > MAX_SESSION_MS) {
39
39
  return "Local browser sessions currently support at most 20 minutes, within the runtime's 30-minute lifetime.";
@@ -63,8 +63,8 @@ export function localBrowserUnsupportedReason(config) {
63
63
  actor.reasoningEffort !== "low" ||
64
64
  participantList(config)?.some((entry) => entry.reasoningEffort !== undefined && entry.reasoningEffort !== "low") ||
65
65
  actor.maxOutputTokens !== undefined ||
66
- capsOf(config)?.maxUsd !== undefined ||
67
- capsOf(config)?.maxTotalUsd !== undefined)) {
66
+ config.caps?.maxUsd !== undefined ||
67
+ config.caps?.maxTotalUsd !== undefined)) {
68
68
  return "Local Codex participants currently use gpt-6-astra at low effort. Account dollar/output-token caps are unavailable; use API participants for those controls.";
69
69
  }
70
70
  for (const target of [
@@ -62,8 +62,15 @@ runtime-management method. Initial target navigation remains an adapter-owned
62
62
  operation.
63
63
 
64
64
  Replies acknowledge completion or contain a finite `ComputerUseExecutorError` code and
65
- `not_dispatched` / `outcome_uncertain` disposition. They never include raw
65
+ `not_dispatched` / `outcome_uncertain` disposition. An `action_rejected` /
66
+ `not_dispatched` refusal may add a `reason` from a fixed list (`extra_tab`), which
67
+ the study loop turns into a hint the participant can act on. They never include raw
66
68
  exception prose, typed text, page URLs, or browser errors in their error fields.
69
+ A failure the driver did not declare may add a `diagnostic` of two fixed words: the
70
+ step that failed (such as `insert_text`) and a category (`target_closed`,
71
+ `detached`, `navigation`, `timeout`, `protocol_error`, `channel_closed` or
72
+ `unknown`). The guest picks the category from the error's class and known message
73
+ shapes and then drops the message; the host prints a fixed sentence for each word.
67
74
  A generic driver exception after invocation is uncertain; only a genuine typed
68
75
  driver declaration can attest that input was never dispatched.
69
76
 
@@ -96,7 +103,10 @@ Observation requires a PNG and state signature. It may include bounded URL,
96
103
  title, text, fractional scroll position and heard speech; those remain
97
104
  runtime-only under the existing loop contract. Arbitrary `appState` is refused
98
105
  because v1 has no closed schema for it. The protocol does not truncate strings,
99
- round coordinates, or silently drop unsupported state.
106
+ round coordinates, or silently drop unsupported state. A Codex participant's
107
+ `humanish_ui` call is checked before it reaches the protocol: a wait longer than 30 seconds is
108
+ shortened to 30 seconds there, and the trace records a `wait shortened` notice with both
109
+ durations.
100
110
 
101
111
  Click, double-click, move, scroll and drag may carry `heldKeys`, which the
102
112
  OpenAI provider maps from its computer tool's `keys`. The Codex participant's
@@ -38,6 +38,11 @@ world checks actual document focus and binds preparation to the editable element
38
38
  page changes, additional tabs, dialogs, iframe focus and ambiguous focus are
39
39
  refused. The owner acquires the one page and native window before untrusted
40
40
  navigation. The driver never chooses another tab or brings one to the front.
41
+ Playwright emulates focus in every page it drives, so the focus probe also passes
42
+ in a background tab and cannot tell which tab is in front; text therefore needs the
43
+ owned page to be the context's only page. That refusal carries the fixed reason
44
+ `extra_tab` over browser control, and the study loop tells the participant to close
45
+ the other tab.
41
46
  Focus can change between protocol messages; this is not atomic element-targeted
42
47
  input or a general multi-window driver.
43
48
 
@@ -56,7 +61,8 @@ text paths likewise need application readback to establish task outcomes.
56
61
  Native children use fixed paths and a minimal explicit environment. Operator
57
62
  credentials and inherited Xauthority are not forwarded. Native address-bar text travels over the
58
63
  helper's stdin; web text travels over the private browser pipe, never a shell. Helpers have bounded deadlines and output;
59
- raw diagnostic text is never returned as an executor error. Only an acquired,
64
+ raw diagnostic text is never returned as an executor error. A text port failure the
65
+ driver did not declare carries only a fixed step and category. Only an acquired,
60
66
  still-live child handle authorizes termination. If capture-helper exit cannot
61
67
  be confirmed, its private files stay for runtime-owner reclamation.
62
68
 
@@ -104,8 +104,9 @@ execution:
104
104
  Codex command receives the nonsecret value `humanish-egress-auth-placeholder`
105
105
  under `CODEX_API_KEY`, plus `CODEX_CA_CERTIFICATE` pointing at E2B's existing
106
106
  system CA bundle (`/etc/ssl/certs/ca-certificates.crt`) so TLS verification trusts
107
- the platform's proxy CA. humanish does not disable TLS verification or download
108
- an unauthenticated CA. The sandbox receives no raw runtime key in command env, sandbox env,
107
+ the platform's proxy CA. `humanish doctor` run inside the sandbox reports that
108
+ `CODEX_API_KEY` holds this placeholder and that no usable key is available.
109
+ humanish does not disable TLS verification or download an unauthenticated CA. The sandbox receives no raw runtime key in command env, sandbox env,
109
110
  files, metadata, or captured evidence. The host still scrubs the actual key from
110
111
  output and errors, including errors during sandbox creation.
111
112