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,73 +1,54 @@
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 { focusOf, rosterOf } from "./parse/actors.js";
5
- import { actorsOf, participantList, declaredParticipantCount, scenarioRefOf, } from "./study-fields.js";
6
- // The computer-use and terminal routes consume mission/persona/model and laneFocus.instruction (they
7
- // compose the agent prompt and bundle provenance); laneFocus.id/label stay inert. On the computer-use
8
- // E2B route actors[0].lanes is consumed (the fan-out roster).
4
+ import { participantList, declaredParticipantCount } from "./study-fields.js";
5
+ // The computer-use and terminal routes consume mission/persona/model (they compose the agent prompt
6
+ // and bundle provenance).
9
7
  const promptRoute = (routes) => routes.cua || routes.terminal;
10
- // The scripted-browser route consumes persona and count (trace/bundle provenance; surface roster).
11
- // The prompt fields can never act there: the scripted actor runs no model.
8
+ // The scripted-browser route consumes persona and surfaces (trace/bundle provenance). The prompt
9
+ // fields can never act there: the scripted actor runs no model.
12
10
  const scriptedRoute = (routes) => !promptRoute(routes) && routes.scripted;
13
11
  const otherRoute = (routes) => !promptRoute(routes) && !routes.scripted;
14
- /** Rows for each actor, reported as `actors[<index>].<field>`, in this order per actor. */
12
+ /** Rows for the actor and its participants, reported first, in this order. */
15
13
  const ACTOR_ROWS = [
16
14
  // Shared-world-only fields on the roster: a participant's `entry` is inert anywhere else
17
15
  // (claims match mechanism).
18
16
  {
19
- field: "lanes[].entry",
20
- reason: "the per-participant loopback entry is a shared-world capability; needs subject.topology: shared-world",
21
- applies: (actor, routes) => Boolean(rosterOf(actor)?.some((entry) => entry.entry !== undefined)) && !routes.shared,
17
+ field: "participants[].entry",
18
+ reason: "the per-participant loopback entry is a shared-world capability; needs route: shared-world",
19
+ applies: (config, routes) => Boolean(participantList(config)?.some((entry) => entry.entry !== undefined)) &&
20
+ !routes.shared,
22
21
  },
23
22
  // The host marker acts only on the external-public shared-world route; inert elsewhere.
24
23
  {
25
- field: "lanes[].host",
24
+ field: "participants[].host",
26
25
  reason: "the designated host-participant marker; needs the external-public shared-world route: app-url × topology shared-world × allowPublicTargets",
27
- applies: (actor, routes) => Boolean(rosterOf(actor)?.some((entry) => entry.host === true)) && !routes.externalPublic,
26
+ applies: (config, routes) => Boolean(participantList(config)?.some((entry) => entry.host === true)) &&
27
+ !routes.externalPublic,
28
28
  },
29
29
  {
30
- field: "laneFocus.id",
31
- applies: (actor, routes) => promptRoute(routes) && Boolean(focusOf(actor)?.id),
32
- },
33
- {
34
- field: "laneFocus.label",
35
- applies: (actor, routes) => promptRoute(routes) && Boolean(focusOf(actor)?.label),
36
- },
37
- {
38
- field: "lanes",
39
- reason: "only the computer-use route runs more than one participant",
40
- applies: (actor, routes) => routes.terminal && Boolean(rosterOf(actor)),
41
- },
42
- {
43
- field: "mission",
30
+ field: "actor.mission",
44
31
  reason: "the scripted-browser actor runs no model",
45
- applies: (actor, routes) => scriptedRoute(routes) && Boolean(actor.mission),
32
+ applies: (config, routes) => scriptedRoute(routes) && Boolean(config.actor?.mission),
46
33
  },
47
34
  {
48
- field: "laneFocus",
35
+ field: "actor.model",
49
36
  reason: "the scripted-browser actor runs no model",
50
- applies: (actor, routes) => scriptedRoute(routes) && Boolean(focusOf(actor)),
37
+ applies: (config, routes) => scriptedRoute(routes) && Boolean(config.actor?.model),
51
38
  },
39
+ // On every other route the prompt fields and persona are inert.
52
40
  {
53
- field: "model",
54
- reason: "the scripted-browser actor runs no model",
55
- applies: (actor, routes) => scriptedRoute(routes) && Boolean(actor.model),
41
+ field: "actor.mission",
42
+ applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.mission),
56
43
  },
57
44
  {
58
- field: "lanes",
59
- reason: "the scripted-browser route fans out via actors[0].count, not an actors[0].lanes roster",
60
- applies: (actor, routes) => scriptedRoute(routes) && Boolean(rosterOf(actor)),
45
+ field: "actor.persona",
46
+ applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.persona),
61
47
  },
62
- // On every other route the prompt fields, persona and the `lanes[]` roster are inert.
63
- { field: "mission", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.mission) },
64
48
  {
65
- field: "laneFocus",
66
- applies: (actor, routes) => otherRoute(routes) && Boolean(focusOf(actor)),
49
+ field: "actor.model",
50
+ applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.model),
67
51
  },
68
- { field: "persona", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.persona) },
69
- { field: "model", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.model) },
70
- { field: "lanes", applies: (actor, routes) => otherRoute(routes) && Boolean(rosterOf(actor)) },
71
52
  ];
72
53
  const TERMINAL_ONLY = "needs subject.source: terminal-product + a registered terminal actor";
73
54
  const RESERVED = "reserved; no route reads it yet";
@@ -91,17 +72,10 @@ const CONFIG_ROWS = [
91
72
  field: "subject.state",
92
73
  applies: (config, routes) => Boolean(config.subject.state) && !routes.cua && !routes.scripted,
93
74
  },
94
- // topology + checkpoint act only on the shared-world route; a set-but-unconsumed value
95
- // (incl. an explicit per-lane-worlds, which the computer-use route already is by mechanism) warns
96
- // inert.
97
- {
98
- field: "subject.topology",
99
- reason: "drives behavior only on the shared-world route; needs subject.topology: shared-world + clone × e2b-desktop × a computer-use actor + a roster of ≥2 participants",
100
- applies: (config, routes) => config.subject.topology !== undefined && !routes.shared,
101
- },
75
+ // The checkpoint acts only on the shared-world route.
102
76
  {
103
77
  field: "subject.state.checkpoint",
104
- reason: "the shared-world state-checkpoint probe; needs subject.topology: shared-world",
78
+ reason: "the shared-world state-checkpoint probe; needs route: shared-world",
105
79
  applies: (config, routes) => config.subject.state?.checkpoint !== undefined && !routes.shared,
106
80
  },
107
81
  // publicTarget (the external-public ownership attestation) acts only on the external-public
@@ -161,18 +135,10 @@ const CONFIG_ROWS = [
161
135
  field: "execution.concurrency",
162
136
  applies: (config, routes) => config.execution?.concurrency !== undefined && !routes.cua,
163
137
  },
164
- // execution.caps is consumed on the computer-use route (maxUsd is the fail-closed spend abort);
165
- // inert (warned) everywhere else so a misplaced budget field is never trusted to cap a route it
166
- // cannot.
167
- {
168
- field: "execution.caps",
169
- reason: "only the computer-use route enforces it, with a computer-use actor on e2b-desktop",
170
- applies: (config, routes) => Boolean(config.execution?.caps) && !routes.cua,
171
- },
172
- // terminal-product consumes subject.product, scenario.caps, execution.{terminal,runtimeAuth}:
173
- // dry-run records the contract; live execution enforces caps and command-scoped auth. On every
174
- // other route they are inert and must warn so a
175
- // misplaced safety/budget field is never trusted to do something it cannot.
138
+ // parse/front.ts refuses a `caps` key the declared route does not read, so no row lists `caps`.
139
+ // terminal-product consumes subject.product and execution.{terminal,runtimeAuth}: dry-run records
140
+ // the contract; live execution enforces command-scoped auth. On every other route they are inert
141
+ // and must warn so a misplaced safety field is never trusted to do something it cannot.
176
142
  {
177
143
  field: "subject.product",
178
144
  reason: "needs subject.source: terminal-product or desktop-cli with the matching actor",
@@ -180,18 +146,6 @@ const CONFIG_ROWS = [
180
146
  !routes.terminal &&
181
147
  config.subject.source !== "desktop-cli",
182
148
  },
183
- {
184
- field: "scenario.caps",
185
- reason: TERMINAL_ONLY,
186
- applies: (config, routes) => Boolean(config.scenario?.caps) && !routes.terminal,
187
- },
188
- // The study-level budget is a CUA-route capability; the terminal route is a single agent whose
189
- // maxUsd already caps the whole run, so a maxTotalUsd there would be trusted and unenforced.
190
- {
191
- field: "scenario.caps.maxTotalUsd",
192
- reason: "the study-level budget is a computer-use route capability; the terminal route's maxUsd already caps the whole run",
193
- applies: (config, routes) => config.scenario?.caps?.maxTotalUsd !== undefined && routes.terminal,
194
- },
195
149
  {
196
150
  field: "execution.terminal",
197
151
  reason: TERMINAL_ONLY,
@@ -247,13 +201,12 @@ const CONFIG_ROWS = [
247
201
  reason: "no current route reads it",
248
202
  applies: (config) => config.execution?.desktop?.codexAppServer !== undefined,
249
203
  },
250
- // scenario.ref is consumed on the scripted-browser route (required there); forward-declared
204
+ // `scenario` is consumed on the scripted-browser route (required there); forward-declared
251
205
  // everywhere else.
252
206
  {
253
- field: "scenario.ref",
254
- applies: (config, routes) => Boolean(scenarioRefOf(config)) && !routes.scripted,
207
+ field: "scenario",
208
+ applies: (config, routes) => Boolean(config.scenario) && !routes.scripted,
255
209
  },
256
- { field: "scenario.inline", applies: (config) => Boolean(config.scenario?.inline) },
257
210
  // review.{scoring,milestones,vocabulary} stay forward-declared on every route.
258
211
  // review.scorer is consumed (loaded + wired, or fail-closed at load) on every scorer-capable
259
212
  // route, so it does not warn there; on the scripted-browser route the actor carries no scorer seam,
@@ -278,7 +231,6 @@ const CONFIG_ROWS = [
278
231
  reason: "the scripted-browser actor has no adopter-scorer seam; declare it on a terminal / computer-use / shared-world route",
279
232
  applies: (config, routes) => Boolean(config.review?.scorer) && routes.scripted,
280
233
  },
281
- { field: "personas", applies: (config) => Boolean(config.personas) },
282
234
  ];
283
235
  function routesOf(config) {
284
236
  const cua = isComputerUseComposition(config);
@@ -295,44 +247,35 @@ function routesOf(config) {
295
247
  config.execution?.target === "e2b-desktop",
296
248
  };
297
249
  }
298
- function rowLabel(row, prefix = "") {
299
- return row.reason === undefined
300
- ? `${prefix}${row.field}`
301
- : `${prefix}${row.field} (${row.reason})`;
250
+ function inertRows(config) {
251
+ const routes = routesOf(config);
252
+ return [...ACTOR_ROWS, ...CONFIG_ROWS].filter((row) => row.applies(config, routes));
302
253
  }
303
254
  /**
304
- * The fields a config sets that its route does not read, each as `field` or `field (reason)`. A v2
305
- * file gets them as one warning; a v3 study is refused for any of them.
255
+ * The fields a config sets that its route does not read, each as `field` or `field (reason)`.
256
+ * parseStudy refuses a study for any of them.
306
257
  */
307
258
  export function inertFieldLabels(config) {
308
- const routes = routesOf(config);
309
- const inert = [];
310
- for (const [index, actor] of actorsOf(config).entries()) {
311
- for (const row of ACTOR_ROWS)
312
- if (row.applies(actor, routes))
313
- inert.push(rowLabel(row, `actors[${index}].`));
314
- }
315
- for (const row of CONFIG_ROWS)
316
- if (row.applies(config, routes))
317
- inert.push(rowLabel(row));
318
- return inert;
259
+ return inertRows(config).map((row) => row.reason === undefined ? row.field : `${row.field} (${row.reason})`);
319
260
  }
320
261
  /**
321
262
  * The same fields as `inertFieldLabels`, as paths without their reasons, such as
322
- * `actors[0].lanes[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
263
+ * `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
323
264
  */
324
265
  export function inertFieldPaths(config) {
325
- const routes = routesOf(config);
326
- const paths = [];
327
- for (const [index, actor] of actorsOf(config).entries()) {
328
- for (const row of ACTOR_ROWS)
329
- if (row.applies(actor, routes))
330
- paths.push(`actors[${index}].${row.field}`);
331
- }
332
- for (const row of CONFIG_ROWS)
333
- if (row.applies(config, routes))
334
- paths.push(row.field);
335
- return paths;
266
+ return inertRows(config).map((row) => row.field);
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.`;
336
279
  }
337
280
  // Report fields that are present but not yet consumed by the engine, so a user never trusts a
338
281
  // setting that silently does nothing.
@@ -344,6 +287,9 @@ export function forwardDeclaredWarnings(config) {
344
287
  : [
345
288
  `These fields are set, but no route reads them yet, so they have no effect: ${inert.join(", ")}.`,
346
289
  ];
290
+ const egressIgnored = egressAllowIgnoredWarning(config);
291
+ if (egressIgnored !== undefined)
292
+ warnings.push(egressIgnored);
347
293
  // A declared cap below the participant count is legal but loud: the roster promises N live actors
348
294
  // and the cap delivers waves of M. Say so up front (inspect + dry-run + run): a green run in
349
295
  // waves is otherwise indistinguishable from the all-live run the author meant.
@@ -1,23 +1,24 @@
1
- import { actorOf, actorsOf, 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
- if (!isLocalBrowserStudy(config))
12
+ const actor = config.actor;
13
+ if (!isLocalBrowserStudy(config) || actor === undefined)
13
14
  return config;
14
15
  return {
15
16
  ...config,
16
- actors: actorsOf(config).map((actor) => ({
17
+ actor: {
17
18
  ...actor,
18
19
  model: actor.model ?? "gpt-6-astra",
19
20
  reasoningEffort: actor.reasoningEffort ?? "low",
20
- })),
21
+ },
21
22
  execution: {
22
23
  ...config.execution,
23
24
  timeoutMs: config.execution?.timeoutMs ?? MAX_SESSION_MS,
@@ -26,19 +27,18 @@ export function localBrowserDefaults(config) {
26
27
  resolution: config.execution?.desktop?.resolution ?? [960, 720],
27
28
  },
28
29
  },
29
- ...(config.review?.analysis === undefined && actorOf(config)?.type === "local-agent"
30
+ ...(config.review?.analysis === undefined && actor.type === "local-agent"
30
31
  ? { review: { ...config.review, analysis: { provider: "codex" } } }
31
32
  : {}),
32
33
  };
33
34
  }
34
35
  export function localBrowserUnsupportedReason(config) {
35
- const actor = actorOf(config);
36
+ const actor = config.actor;
36
37
  const desktop = config.execution?.desktop;
37
38
  if ((config.execution?.timeoutMs ?? MAX_SESSION_MS) > MAX_SESSION_MS) {
38
39
  return "Local browser sessions currently support at most 20 minutes, within the runtime's 30-minute lifetime.";
39
40
  }
40
- if (actorsOf(config).length !== 1 ||
41
- !actor ||
41
+ if (!actor ||
42
42
  (actor.type === "local-agent" && actor.localAgent !== undefined && actor.localAgent !== "codex")) {
43
43
  return "Local browser studies support only Codex local-agent or openai-computer-use participants.";
44
44
  }
@@ -63,10 +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
- config.execution?.caps?.maxUsd !== undefined ||
67
- config.execution?.caps?.maxTotalUsd !== undefined ||
68
- config.scenario?.caps?.maxUsd !== undefined ||
69
- config.scenario?.caps?.maxTotalUsd !== undefined)) {
66
+ config.caps?.maxUsd !== undefined ||
67
+ config.caps?.maxTotalUsd !== undefined)) {
70
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.";
71
69
  }
72
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
 
@@ -59,10 +59,19 @@ workflow without leaking private upstream truth into core.
59
59
 
60
60
  ## Study File
61
61
 
62
- Schema: `humanish.study.v3`. `src/study/parse/study-v3.ts` rewrites a v3 document into the
63
- normalized config in `src/study/types.ts`, which `src/study/config.ts` parses. humanish refuses a
64
- `humanish.lab.v2` file with `HUMANISH_STUDY_V2_UNSUPPORTED`, whose message names
65
- `humanish migrate <path>`; migrate converts the file to v3.
62
+ Schema: `humanish.study.v3`. `src/study/config.ts` parses a v3 document into `StudyConfig`
63
+ (`src/study/types.ts`), which has the file's keys; `src/study/parse/front.ts` checks the route and
64
+ the forms of `participants`, `surfaces` and `caps` it takes. humanish refuses a `humanish.lab.v2`
65
+ file with `HUMANISH_STUDY_V2_UNSUPPORTED`, whose message names `humanish migrate <path>`; migrate
66
+ converts the file to v3.
67
+
68
+ The parsed config is the file with defaults filled: a `participants` entry with a `count` expanded
69
+ into its participants, `comms.email.recipients` and a shared world's `execution.concurrency`
70
+ filled, `kind: real` added beside an email `connection`, and a local browser study's model,
71
+ effort, timeout and resolution. `humanish study show --json` prints that config, and `parseStudy`
72
+ reads it back to the same config. Before 0.111.0, `StudyConfig` had the `humanish.lab.v2` keys
73
+ (`actors`, `execution.caps`, the `scenario` object, `subject.topology`); the 0.111.0 release notes
74
+ map each one to its v3 key.
66
75
 
67
76
  A study declares its route and composes code primitives; it is not a hardcoded kind. The
68
77
  top-level keys:
@@ -969,14 +978,25 @@ Core-owned fields:
969
978
  kept it through `previous_response_id`) or `explicit_context` (the server kept
970
979
  none). `explicitReason` says why: `configured` (`zeroDataRetention`) or
971
980
  `zdr_rejection` (the organization rejected server-side state; `switchedAt` and
972
- `switchedAtRequest` say when). In `explicit_context` each request carries the
981
+ `switchedAtRequest` say when). With `zdr_rejection`, `rejection` names the
982
+ answer that made the switch: `zero_data_retention` (the error named it),
983
+ `previous_response` (a 400 that refused or could not find the
984
+ `previous_response_id`) or `stored_item` (a 404 for an item the server never
985
+ kept, "Item with id '...' not found"). In `explicit_context` each request carries the
973
986
  conversation from the client, with `store: false` and the reasoning returned
974
987
  in encrypted form. It carries the opening message, every exchange of a reply
975
988
  with the outputs that answered it, and the latest reply. Past an estimated
976
- 64,000 input tokens the opening screenshot is dropped first, then the oldest
977
- exchanges become lines of a note, sent as an assistant message, that keeps
978
- their reasoning summaries, messages and actions as text. The two newest
979
- exchanges are always carried whole, even when they alone pass the budget. `summarizedTurns` counts the earlier exchanges the latest
989
+ 64,000 input tokens it is cut down to 32,000 in one step: the opening
990
+ screenshot is dropped first, then the oldest exchanges become lines of a note,
991
+ sent as an assistant message, that keeps their reasoning summaries, messages
992
+ and actions as text, with any text that came back with their screenshots.
993
+ Past 32,000 characters the note keeps its first four lines and its newest
994
+ ones, and names the turns between them as not listed.
995
+ The two newest exchanges are always carried whole, even when they alone pass
996
+ the budget. Between cuts each request starts with the whole previous request,
997
+ so the provider's prompt cache can serve all of it; the request after a cut is
998
+ billed as new input. `tokenUsage.turns` has each request's `cachedInput` and
999
+ `cacheWriteInput`. `summarizedTurns` counts the earlier exchanges the latest
980
1000
  `explicit_context` request no longer carried whole: lines of the note, or
981
1001
  only counted once the note reached its cap.
982
1002
  `requests[]` has one entry per participant request: its `mode` and, in
@@ -1552,6 +1572,16 @@ Refusals:
1552
1572
  a preview or scripted study, `createProvider` off computer use, or `prepareDesktop` where no E2B
1553
1573
  desktop exists. The message names the option and the route. The same code refuses a field
1554
1574
  `RunStudyOptions` no longer has (below); that message names the field and where its job went.
1575
+ - `HUMANISH_STUDY_V2_UNSUPPORTED`: the config is `humanish.lab.v2`, or it still sets a
1576
+ `StudyConfig` field of humanish 0.110 that 0.111.0 renamed: `actors`, a `scenario` object,
1577
+ `execution.caps`, `subject.topology`, `personas`, or `count`, `lanes`, `roster` or `laneFocus` on
1578
+ `actor`. The message names each one's v3 field. The
1579
+ planners read only the v3 fields, so a budget left in `execution.caps` would otherwise run
1580
+ uncapped.
1581
+ - `HUMANISH_STUDY_INVALID`: the config has no `actor` object, or its `route` is missing or is not
1582
+ the route its subject and actor take. The message is the one `parseStudy` gives.
1583
+
1584
+ `runStudy` checks the config before the options, so a config refusal comes first.
1555
1585
 
1556
1586
  `rerun.laneIds`, the older name of `rerun.participantIds`, was removed in 0.107.0. A caller that
1557
1587
  still passes it is refused with `HUMANISH_STUDY_OPTION_UNSUPPORTED`; use `rerun.participantIds`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.110.4",
3
+ "version": "0.111.1",
4
4
  "description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,38 +0,0 @@
1
- import { type StudyConfig, type StudyParseFailure, type StudyScenarioCaps, type StudyV3 } from "../types.js";
2
- interface StudyV3Options {
3
- /**
4
- * `report`, for `humanish migrate`: list the fields the route does not read in `inert`, as v2
5
- * paths, in place of refusing the study for them, and give the checks' messages as they wrote
6
- * them, in the v2 spelling of the file migrate converts. A shared world may then give
7
- * `participants` as a count or leave it out, as a v2 file could (readStudyFront).
8
- */
9
- readonly inert?: "refuse" | "report";
10
- }
11
- type StudyV3ParseResult = {
12
- ok: true;
13
- config: StudyV3;
14
- warnings: string[];
15
- inert?: string[];
16
- } | StudyParseFailure;
17
- type Parsed<T> = {
18
- ok: true;
19
- value: T;
20
- } | StudyParseFailure;
21
- /**
22
- * Validate a parsed humanish.study.v3 document into a StudyV3, with the messages parseStudy gives.
23
- * Pure: the caller owns file IO. parseStudy refuses a humanish.lab.v2 document before this runs.
24
- */
25
- export declare function parseStudyV3(raw: unknown, options?: StudyV3Options): StudyV3ParseResult;
26
- /**
27
- * A v3 study in the v2 spelling: the actor holds its participants, `caps` moves under `execution`
28
- * or `scenario`, and a shared world is `subject.topology: shared-world`. The checks that read more
29
- * than one section run on this view until StudyConfig has the v3 shape.
30
- */
31
- export declare function toLegacy(study: StudyV3): StudyConfig;
32
- /**
33
- * A caps block's values. A value that is not a non-negative number refuses the study: a cap that
34
- * does nothing must not look like one that holds. readStudyFront has checked the keys against the
35
- * route; migrate checks a v2 file's two blocks with it.
36
- */
37
- export declare function parseCaps(raw: unknown): Parsed<StudyScenarioCaps | undefined>;
38
- export {};