humanish 0.139.0 → 0.140.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +5 -4
  3. package/dist/actors/codex/operator-instructions.d.ts +5 -0
  4. package/dist/actors/codex/operator-instructions.js +9 -0
  5. package/dist/actors/codex/restricted-launch.d.ts +10 -2
  6. package/dist/actors/codex/restricted-launch.js +65 -16
  7. package/dist/actors/codex/restricted-participant.d.ts +4 -0
  8. package/dist/actors/codex/restricted-participant.js +9 -0
  9. package/dist/actors/codex/restricted-policy.d.ts +5 -2
  10. package/dist/actors/codex/restricted-policy.js +90 -72
  11. package/dist/actors/codex/restricted-session.d.ts +4 -0
  12. package/dist/actors/codex/restricted-session.js +8 -0
  13. package/dist/analysis/automatic-completion.js +24 -0
  14. package/dist/analysis/automatic-config.d.ts +15 -6
  15. package/dist/analysis/automatic-config.js +25 -9
  16. package/dist/cli/commands/study-route-computer-use.js +2 -3
  17. package/dist/cli/commands/study-route-scripted.js +3 -4
  18. package/dist/cli/commands/study-route-shared-world.js +2 -3
  19. package/dist/cli/commands/study-route-terminal.js +3 -4
  20. package/dist/cli/commands/study-run.js +1 -1
  21. package/dist/cli/doctor.d.ts +2 -0
  22. package/dist/cli/doctor.js +3 -0
  23. package/dist/cli/findings.d.ts +17 -14
  24. package/dist/cli/findings.js +37 -31
  25. package/dist/cli/io.d.ts +5 -1
  26. package/dist/cli/io.js +11 -2
  27. package/dist/cli/run-findings.d.ts +19 -0
  28. package/dist/cli/run-findings.js +106 -0
  29. package/dist/guest/bootstrap.d.ts +4 -2
  30. package/dist/guest/bootstrap.js +15 -7
  31. package/dist/guest/display-config.d.ts +31 -0
  32. package/dist/guest/display-config.js +35 -0
  33. package/dist/guest/runtime-desktop.d.ts +9 -3
  34. package/dist/guest/runtime-desktop.js +59 -25
  35. package/dist/guest/runtime.d.ts +2 -1
  36. package/dist/guest/runtime.js +1 -1
  37. package/dist/guest-runtime-main.js +2 -1
  38. package/dist/routes/computer-use/e2b-desktop/fidelity.js +10 -17
  39. package/dist/routes/computer-use/local-vm.js +34 -1
  40. package/dist/routes/computer-use/plan.js +1 -1
  41. package/dist/routes/scripted/plan.js +1 -1
  42. package/dist/routes/shared-world/plan.js +1 -1
  43. package/dist/routes/terminal/plan.js +1 -1
  44. package/dist/run/route-shell.js +1 -1
  45. package/dist/study/doctor.d.ts +3 -0
  46. package/dist/study/doctor.js +22 -7
  47. package/dist/study/init-templates.js +9 -3
  48. package/dist/study/mobile-emulation.d.ts +27 -0
  49. package/dist/study/mobile-emulation.js +60 -0
  50. package/dist/study/plan.js +1 -1
  51. package/dist/study/preflight-result.js +1 -1
  52. package/dist/study/summary.js +1 -1
  53. package/dist/study/warnings.js +5 -2
  54. package/dist/substrates/e2b/cdp-probe.d.ts +2 -6
  55. package/dist/substrates/e2b/desktop-cdp.d.ts +0 -2
  56. package/dist/substrates/e2b/desktop-cdp.js +0 -2
  57. package/dist/substrates/local/firecracker-desktop.d.ts +5 -0
  58. package/dist/substrates/local/firecracker-desktop.js +7 -1
  59. package/dist/substrates/local/runtime-config.d.ts +12 -0
  60. package/dist/substrates/local/runtime-config.js +55 -13
  61. package/dist/substrates/local/runtime-release.js +8 -8
  62. package/dist/substrates/local/runtime.js +1 -0
  63. package/docs/contracts/study-analysis.md +14 -8
  64. package/docs/principles/invariants-and-defaults.md +1 -1
  65. package/docs/product/automatic-analysis.md +46 -11
  66. package/package.json +1 -1
  67. package/skills/humanish/SKILL.md +45 -26
@@ -1,10 +1,11 @@
1
1
  import path from "node:path";
2
- import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
2
+ import { isLocalBrowserStudy, localStudySetsScreens } from "../substrates/local/runtime-config.js";
3
3
  import { localRuntimeStatus } from "../substrates/local/runtime.js";
4
4
  import { capacityShortfall, describeCapacity, } from "../substrates/local/capacity.js";
5
5
  import { keyNamesOf, requiredKeys, requiredSubjectEnv } from "./requirements.js";
6
6
  import { localAgentReady, localAgentVersionRefusal, } from "../actors/local-agent/cli.js";
7
7
  import { protocolAdditionsWarning, protocolIncompatibilityMessage, } from "../actors/codex/protocol-compat.js";
8
+ import { operatorInstructionsWarning } from "../actors/codex/operator-instructions.js";
8
9
  import { admissionRule } from "../analysis/admission.js";
9
10
  import { automaticAnalysisBudget } from "../analysis/automatic-config.js";
10
11
  import { externalCatchHealthy } from "../comms/sandbox-catch.js";
@@ -90,6 +91,8 @@ function codexRecovery(readiness, fallback) {
90
91
  return codexVersionRecovery(readiness.detectedCliVersion, readiness.installation);
91
92
  case "codex_incompatible_release":
92
93
  return `${protocolIncompatibilityMessage(readiness.detectedCliVersion, readiness.protocolIncompatibilities ?? [])} ${codexInstallAdvice(readiness.installation)} Then sign in with a ChatGPT account (\`codex login\`).`;
94
+ case "codex_unsafe_configuration":
95
+ return unsafeConfigurationRecovery(readiness.refusalReason, fallback);
93
96
  case "codex_login_required":
94
97
  return "Codex is installed but not signed in. Run `codex login` and sign in with a ChatGPT account.";
95
98
  case "codex_unsupported_auth":
@@ -107,6 +110,12 @@ function codexRecovery(readiness, fallback) {
107
110
  return fallback;
108
111
  }
109
112
  }
113
+ /** What the refused setting is and how to change it; the fallback when no reason was recorded. */
114
+ function unsafeConfigurationRecovery(reason, fallback) {
115
+ if (reason === undefined)
116
+ return fallback;
117
+ return `humanish stopped the Codex launch because ${reason}. Change that setting in the Codex config (~/.codex/config.toml, a profile, or a system config) if you want hosted participants to use it, then rerun doctor.`;
118
+ }
110
119
  /** Shared read-only Codex account check for local participants in doctor and the TUI. */
111
120
  export async function localCodexParticipantCheck(args) {
112
121
  const readiness = await (args.readiness ?? codexAccountReadiness)(args.env).catch(() => ({ ready: false, errorCode: "codex_unavailable" }));
@@ -195,7 +204,7 @@ export async function studySetupChecks(args) {
195
204
  if (route === "scripted")
196
205
  checks.push(await scriptedBrowserCheck());
197
206
  checks.push(...subjectEnvChecks(requiredSubjectEnv(requirements), args));
198
- const analysis = automaticAnalysisBudget(config.review?.analysis, route);
207
+ const analysis = automaticAnalysisBudget(config.review?.analysis, route, undefined, config.actor);
199
208
  if (analysis)
200
209
  checks.push(await analysisCheck(analysis, args, checkAccount));
201
210
  checks.push(checkScope(analysis));
@@ -222,10 +231,13 @@ async function localBrowserChecks(config, args, desktopsAtOnce) {
222
231
  media: config.execution?.desktop?.media !== undefined ||
223
232
  config.execution?.desktop?.recording !== undefined,
224
233
  })))();
234
+ const screens = runtime.installed && runtime.assets?.displaySizes !== true && localStudySetsScreens(config)
235
+ ? "The installed local runtime starts every participant at 960×720, and this study sets other screen sizes. Omit device and resolution, use a hosted desktop, or set HUMANISH_LOCAL_RUNTIME_IMAGE to a runtime built from current source."
236
+ : undefined;
225
237
  checks.push({
226
238
  name: "local browser runtime",
227
- ok: runtime.ok,
228
- message: runtime.message,
239
+ ok: runtime.ok && screens === undefined,
240
+ message: screens ?? runtime.message,
229
241
  ...(runtime.ok && !runtime.installed ? { status: "note" } : {}),
230
242
  });
231
243
  if (runtime.capacity !== undefined && desktopsAtOnce !== undefined)
@@ -243,7 +255,9 @@ async function localBrowserChecks(config, args, desktopsAtOnce) {
243
255
  }
244
256
  return {
245
257
  checks,
246
- ...(runtime.ok && runtime.installed ? {} : { next: runtime.message }),
258
+ ...(runtime.ok && runtime.installed && screens === undefined
259
+ ? {}
260
+ : { next: screens ?? runtime.message }),
247
261
  };
248
262
  }
249
263
  /** Whether the desktops the study runs at once fit the VM or this machine, and why. */
@@ -320,6 +334,7 @@ async function hostedCodexParticipantCheck(args, actor) {
320
334
  ...(actor?.reasoningEffort === undefined ? {} : { reasoningEffort: actor.reasoningEffort }),
321
335
  };
322
336
  const readiness = await (args.codexParticipantReadiness ?? hostedCodexReadiness)(args.env, declared).catch(() => ({ ready: false, errorCode: "codex_unavailable" }));
337
+ const operatorNote = operatorInstructionsWarning(readiness.operatorInstructionFiles);
323
338
  const account = readiness.authentication === "api-key"
324
339
  ? "an API key, which bills its usage to that key"
325
340
  : "a ChatGPT account";
@@ -327,7 +342,7 @@ async function hostedCodexParticipantCheck(args, actor) {
327
342
  name: "local participant authentication",
328
343
  ok: readiness.ready,
329
344
  message: readiness.ready
330
- ? readyMessage(`Codex CLI ${readiness.cliVersion ?? "(unknown release)"} passed the operator handshake without a turn (initialize, config/read, account/read and an ephemeral thread/start): model ${readiness.resolvedModel ?? "(not reported)"} on ${account}. E2B supplies the desktop; no OpenAI API key is required for this participant.`, readiness)
345
+ ? readyMessage(`Codex CLI ${readiness.cliVersion ?? "(unknown release)"} passed the operator handshake without a turn (initialize, config/read, account/read and an ephemeral thread/start): model ${readiness.resolvedModel ?? "(not reported)"} on ${account}. E2B supplies the desktop; no OpenAI API key is required for this participant.${operatorNote === undefined ? "" : ` ${operatorNote}`}`, readiness)
331
346
  : `Hosted Codex participant setup is unavailable (${readiness.errorCode}). ${codexRecovery(readiness, "Check `codex login status` and the operator's Codex configuration, then rerun doctor.")} No API fallback is used.`,
332
347
  };
333
348
  }
@@ -374,7 +389,7 @@ async function analysisCheck(analysis, args, checkAccount) {
374
389
  ...(keyed ? {} : { status: "note" }),
375
390
  message: keyed
376
391
  ? `OPENAI_API_KEY is present for the separate automatic analysis request; model access and quota are not tested. The analysis is refused before it starts if ${admissionRule(`$${analysis.maxCostUsd}`)}; this is not a billing cap. Participant readiness is independent.`
377
- : "Will be skipped: OPENAI_API_KEY is missing. The participant may run, but there will be no automatic findings report. Add an OpenAI API key or set review.analysis: false deliberately.",
392
+ : "Will be skipped: OPENAI_API_KEY is missing. The participant may run, but there will be no automatic findings report. Add an OpenAI API key, or set review.analysis.provider: codex to have a signed-in Codex account analyze the run instead, or set review.analysis: false deliberately.",
378
393
  };
379
394
  }
380
395
  /** What doctor checked and what it did not. */
@@ -62,6 +62,7 @@ Studies:
62
62
  - private or machine-local studies live in ignored .humanish/studies/*.yaml or
63
63
  .humanish/local/studies/*.yaml;
64
64
  - run a study with \`${command} run <study>\`, or \`${command} watch <study>\` to stay attached;
65
+ - a live run ends with findings from a separate analyst; \`${command} review --run latest\` shows them;
65
66
  - files under humanish/labs/ move there with \`${command} migrate\`.
66
67
 
67
68
  Format standard:
@@ -79,12 +80,17 @@ Format standard:
79
80
  */
80
81
  function tryLiveStudy(actor, localAgent = "codex") {
81
82
  const account = actor === "local-agent";
83
+ const analyst = localAgent === "codex"
84
+ ? `The analysis after the run uses the same Codex account. Set review.analysis: false to skip
85
+ it.`
86
+ : `When OPENAI_API_KEY is set, the analysis after the run bills it: humanish refuses that request
87
+ before it starts if its estimate is over $${DEFAULT_ANALYSIS_MAX_COST_USD}. With Codex also signed
88
+ in, review.analysis.provider: codex has its account analyze the run instead. Set
89
+ review.analysis: false to skip it.`;
82
90
  const needs = account
83
91
  ? ` Needs E2B_API_KEY and the coding agent signed in on this machine. Cost: one small task, a few
84
92
  E2B desktop minutes and the agent's account usage. humanish cannot put a dollar cap on account
85
- usage, so the ten-minute session limit below bounds the run. When OPENAI_API_KEY is set, the
86
- analysis after the run bills it: humanish refuses that request before it starts if its estimate
87
- is over $${DEFAULT_ANALYSIS_MAX_COST_USD}. Set review.analysis: false to skip it.`
93
+ usage, so the ten-minute session limit below bounds the run. ${analyst}`
88
94
  : ` Needs E2B_API_KEY and OPENAI_API_KEY. Cost: one small task with a $2 cap on estimated model
89
95
  spend. The run stops before its next model request once the estimate passes $2, so the last
90
96
  request can go slightly over, and hosted desktop time is billed separately. The analysis after
@@ -0,0 +1,27 @@
1
+ import type { DevicePreset } from "./device-presets.js";
2
+ import type { StudyDesktopFidelity } from "./types.js";
3
+ /** The CDP Emulation overrides one participant's Chromium pages get. */
4
+ export interface MobileEmulationRequest {
5
+ width: number;
6
+ height: number;
7
+ deviceScaleFactor: number;
8
+ touch: boolean;
9
+ userAgent: string;
10
+ }
11
+ /**
12
+ * The emulation a participant gets, or undefined. Only a participant on a mobile preset is
13
+ * emulated: a run-wide `mobileEmulation` must not hand a desktop or tablet participant beside a
14
+ * phone participant an iPhone user agent. Hosted and local desktops both read this.
15
+ */
16
+ export declare function participantMobileEmulation(fidelity: StudyDesktopFidelity | undefined, preset: DevicePreset): MobileEmulationRequest | undefined;
17
+ /** Chromium flags that carry the user agent and touch support to every page and worker. */
18
+ export declare function mobileEmulationLaunchFlags(request: MobileEmulationRequest | undefined): string[];
19
+ /**
20
+ * The CDP commands that apply a request to one page, in order. The local guest sends these over
21
+ * its own page session; the hosted desktop's in-sandbox probe sends the same Emulation domain.
22
+ * Touch adds mouse-to-touch conversion, since participants drive a desktop pointer.
23
+ */
24
+ export declare function mobileEmulationCommands(request: MobileEmulationRequest): {
25
+ method: string;
26
+ params: Record<string, unknown>;
27
+ }[];
@@ -0,0 +1,60 @@
1
+ /** The user agent a mobile-emulated participant presents unless the study sets its own. */
2
+ const DEFAULT_MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
3
+ /**
4
+ * The emulation a participant gets, or undefined. Only a participant on a mobile preset is
5
+ * emulated: a run-wide `mobileEmulation` must not hand a desktop or tablet participant beside a
6
+ * phone participant an iPhone user agent. Hosted and local desktops both read this.
7
+ */
8
+ export function participantMobileEmulation(fidelity, preset) {
9
+ if (!fidelity?.mobileEmulation || !preset.isMobile)
10
+ return undefined;
11
+ return {
12
+ width: preset.width,
13
+ height: preset.height,
14
+ deviceScaleFactor: fidelity.deviceScaleFactor ?? preset.deviceScaleFactor,
15
+ touch: fidelity.touch ?? true,
16
+ userAgent: fidelity.userAgent ?? DEFAULT_MOBILE_USER_AGENT,
17
+ };
18
+ }
19
+ /** Chromium flags that carry the user agent and touch support to every page and worker. */
20
+ export function mobileEmulationLaunchFlags(request) {
21
+ if (request === undefined)
22
+ return [];
23
+ return [
24
+ `--user-agent=${request.userAgent}`,
25
+ ...(request.touch ? ["--touch-events=enabled"] : []),
26
+ ];
27
+ }
28
+ /**
29
+ * The CDP commands that apply a request to one page, in order. The local guest sends these over
30
+ * its own page session; the hosted desktop's in-sandbox probe sends the same Emulation domain.
31
+ * Touch adds mouse-to-touch conversion, since participants drive a desktop pointer.
32
+ */
33
+ export function mobileEmulationCommands(request) {
34
+ return [
35
+ {
36
+ method: "Emulation.setDeviceMetricsOverride",
37
+ params: {
38
+ width: request.width,
39
+ height: request.height,
40
+ deviceScaleFactor: request.deviceScaleFactor,
41
+ mobile: true,
42
+ screenWidth: request.width,
43
+ screenHeight: request.height,
44
+ },
45
+ },
46
+ ...(request.touch
47
+ ? [
48
+ {
49
+ method: "Emulation.setTouchEmulationEnabled",
50
+ params: { enabled: true, maxTouchPoints: 5 },
51
+ },
52
+ {
53
+ method: "Emulation.setEmitTouchEventsForMouse",
54
+ params: { enabled: true, configuration: "mobile" },
55
+ },
56
+ ]
57
+ : []),
58
+ { method: "Emulation.setUserAgentOverride", params: { userAgent: request.userAgent } },
59
+ ];
60
+ }
@@ -41,7 +41,7 @@ function planPreview(config, options, input) {
41
41
  });
42
42
  if (String(config.comms?.email?.kind) === "real")
43
43
  return refuse("HUMANISH_STUDY_COMMS_UNSUPPORTED", "Real email receiving is unsupported on the preview route. Use a supported hosted computer-use study.");
44
- const analysis = resolveAutomaticAnalysis(config.review?.analysis);
44
+ const analysis = resolveAutomaticAnalysis(config.review?.analysis, config.actor);
45
45
  if (!analysis.ok)
46
46
  return refuse("HUMANISH_STUDY_ANALYSIS_INVALID", analysis.message);
47
47
  const unsupported = automaticAnalysisRouteReason(config);
@@ -7,7 +7,7 @@ import { computerUseParticipantCount } from "./routing.js";
7
7
  export const STUDY_CHECK_SCHEMA = "humanish.study-check.v1";
8
8
  export function finalize(ctx, args) {
9
9
  const checks = [...ctx.checks, ...(args?.check ? [args.check] : []), ...(args?.checks ?? [])];
10
- const analysis = automaticAnalysisBudget(ctx.config.review?.analysis, ctx.route, computerUseParticipantCount(ctx.config));
10
+ const analysis = automaticAnalysisBudget(ctx.config.review?.analysis, ctx.route, computerUseParticipantCount(ctx.config), ctx.config.actor);
11
11
  return {
12
12
  schema: STUDY_CHECK_SCHEMA,
13
13
  ...(analysis ? { analysis } : {}),
@@ -123,7 +123,7 @@ export async function readStudySummary(cwd, study, options = {}) {
123
123
  missingKeys = missing;
124
124
  }
125
125
  // Computed once: a test-then-use pair reads as though the two calls could differ.
126
- const analysis = automaticAnalysisBudget(inspected.config.review?.analysis, route, computerUseParticipantCount(inspected.config));
126
+ const analysis = automaticAnalysisBudget(inspected.config.review?.analysis, route, computerUseParticipantCount(inspected.config), inspected.config.actor);
127
127
  const subject = subjectOf(config);
128
128
  const participants = participantsOf(inspected.config);
129
129
  const caps = summaryCapsOf(inspected.config);
@@ -1,6 +1,7 @@
1
1
  import { cli } from "../cli/invocation.js";
2
2
  import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
3
3
  import { DESKTOP_FILES_ROUTE_REASON } from "./desktop-files.js";
4
+ import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
4
5
  import { computerUseParticipants, declaredParticipantIds, sharedWorldParticipants, } from "./plan-participants.js";
5
6
  import { addressedRecipients } from "./parse/comms.js";
6
7
  import { participantList, participantInstruction, declaredParticipantCount, } from "./study-fields.js";
@@ -198,8 +199,10 @@ const CONFIG_ROWS = [
198
199
  },
199
200
  {
200
201
  field: "execution.desktop.fidelity",
201
- reason: "mobile emulation is applied only to computer-use participants in hosted Chromium on execution.target: e2b-desktop",
202
- applies: (config, routes) => !routes.hostedCuaBrowser && config.execution?.desktop?.fidelity !== undefined,
202
+ reason: "mobile emulation is applied only to computer-use participants in Chromium on a hosted desktop or execution.target: local",
203
+ applies: (config, routes) => !routes.hostedCuaBrowser &&
204
+ !isLocalBrowserStudy(config) &&
205
+ config.execution?.desktop?.fidelity !== undefined,
203
206
  },
204
207
  {
205
208
  field: "execution.desktop.sandboxTimeoutMs",
@@ -1,4 +1,5 @@
1
1
  import { type ChromeNetworkCaptureRequest } from "./cdp-network-capture.js";
2
+ import type { MobileEmulationRequest } from "../../study/mobile-emulation.js";
2
3
  /** Which target to attribute when the endpoint lists several pages. */
3
4
  export type ChromeCdpPagePreference = "pinned" | "active";
4
5
  export interface ChromeCdpProbeArgs {
@@ -37,12 +38,7 @@ export interface ChromeCdpProbeArgs {
37
38
  network?: ChromeNetworkCaptureRequest;
38
39
  }
39
40
  /** Mobile emulation request: the CDP Emulation domain applied to one page target. */
40
- export interface ChromeMobileEmulationRequest {
41
- width: number;
42
- height: number;
43
- deviceScaleFactor: number;
44
- touch: boolean;
45
- userAgent: string;
41
+ export interface ChromeMobileEmulationRequest extends MobileEmulationRequest {
46
42
  platform?: string;
47
43
  }
48
44
  /** What the page reports after emulation: the proof, never copied from the request. */
@@ -66,8 +66,6 @@ export declare function makeChromeBrowserStateObserver(desktop: E2BDesktopSandbo
66
66
  text?: string;
67
67
  scrollY?: number;
68
68
  }>;
69
- /** The user agent a mobile-emulated participant presents unless the study sets its own. */
70
- export declare const DEFAULT_MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
71
69
  /**
72
70
  * Apply mobile emulation to the participant's launch page and read back what the page reports.
73
71
  * Fails closed: a request that cannot be applied throws, because a desktop run labelled mobile is
@@ -97,8 +97,6 @@ export function makeChromeBrowserStateObserver(desktop, requestTimeoutMs, endpoi
97
97
  };
98
98
  };
99
99
  }
100
- /** The user agent a mobile-emulated participant presents unless the study sets its own. */
101
- export const DEFAULT_MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1";
102
100
  /**
103
101
  * Apply mobile emulation to the participant's launch page and read back what the page reports.
104
102
  * Fails closed: a request that cannot be applied throws, because a desktop run labelled mobile is
@@ -2,11 +2,14 @@ import type { Writable } from "node:stream";
2
2
  import { type DesktopRecordingConfig, type DesktopRecordingMetadata } from "../../evidence/desktop-recording-types.js";
3
3
  import { type DesktopSession } from "../desktop-session.js";
4
4
  import { type GuestMediaConfig } from "../../guest/media-config.js";
5
+ import { type GuestDisplayConfig } from "../../guest/display-config.js";
5
6
  /** The installed runtime image; `humanish runtime setup` chooses it, never a participant. */
6
7
  export interface LocalFirecrackerAssets {
7
8
  image: string;
8
9
  runtimeRevision: string;
9
10
  media?: boolean;
11
+ /** The guest starts the screen and emulation a bootstrap names; older images are 960×720 only. */
12
+ displaySizes?: boolean;
10
13
  }
11
14
  export interface LocalFirecrackerDesktop extends DesktopSession {
12
15
  finishRecording(destination: Writable): Promise<DesktopRecordingMetadata>;
@@ -26,6 +29,8 @@ interface LocalFirecrackerOptions {
26
29
  signal?: AbortSignal;
27
30
  media?: GuestMediaConfig;
28
31
  recording?: DesktopRecordingConfig;
32
+ /** The participant's screen and mobile emulation; absent is the guest's 960×720 default. */
33
+ display?: GuestDisplayConfig;
29
34
  inboxUrl?: string;
30
35
  }
31
36
  /** One VM with opaque TCP forwards to the selected app and optional scoped inbox. */
@@ -9,6 +9,7 @@ import { ownDesktopAllocation, } from "../desktop-session.js";
9
9
  import { runtimeDocker, runtimeExec, usesLima } from "./runtime-host.js";
10
10
  import { openLimaTunnel } from "./runtime-ssh.js";
11
11
  import { guestMediaConfigSchema } from "../../guest/media-config.js";
12
+ import { guestDisplayConfigSchema } from "../../guest/display-config.js";
12
13
  import { cli } from "../../cli/invocation.js";
13
14
  import { DESKTOP_RESERVATION } from "./capacity.js";
14
15
  const docker = async (args) => (await runtimeDocker(args, {}, 60_000)).stdout.trim();
@@ -55,6 +56,11 @@ function checkDesktopOptions(options) {
55
56
  if (options.assets.media !== true)
56
57
  throw new Error(`This runtime does not include the recorder. Run ${cli("runtime setup --media")}.`);
57
58
  }
59
+ if (options.display !== undefined) {
60
+ const { width, height } = guestDisplayConfigSchema.parse(options.display);
61
+ if (options.assets.displaySizes !== true)
62
+ throw new Error(`This local runtime starts every participant at 960×720, so it cannot start the ${width}×${height} screen this participant needs. Omit device and resolution, use a hosted desktop, or set HUMANISH_LOCAL_RUNTIME_IMAGE to a runtime built from current source.`);
63
+ }
58
64
  let url;
59
65
  try {
60
66
  url = new URL(validateGuestInitialUrl(options.appUrl));
@@ -261,7 +267,7 @@ async function bootstrapGuest(stream, options, url, signal) {
261
267
  runtimeRevision: options.assets.runtimeRevision,
262
268
  };
263
269
  try {
264
- return await connectGuestBootstrap(stream, identity, signal, url.href, options.media, options.recording);
270
+ return await connectGuestBootstrap(stream, identity, signal, url.href, options.media, options.recording, options.display);
265
271
  }
266
272
  catch (error) {
267
273
  signal.throwIfAborted();
@@ -1,6 +1,18 @@
1
1
  import type { StudyConfig } from "../../study/types.js";
2
+ import { type DevicePreset } from "../../study/device-presets.js";
3
+ import { type GuestDisplayConfig } from "../../guest/display-config.js";
2
4
  export declare const LOCAL_BROWSER_LIFETIME_MS: number;
3
5
  export declare function isLocalBrowserStudy(config: StudyConfig): boolean;
4
6
  /** Defaults for the explicitly selected local substrate; hosted configurations are untouched. */
5
7
  export declare function localBrowserDefaults(config: StudyConfig): StudyConfig;
6
8
  export declare function localBrowserUnsupportedReason(config: StudyConfig): string | undefined;
9
+ /**
10
+ * The screen a local participant's guest starts, with mobile emulation where hosted desktops apply
11
+ * it. Undefined is the guest's 960×720 default, which every installed runtime supports.
12
+ */
13
+ export declare function localGuestDisplay(config: StudyConfig, device: {
14
+ preset: DevicePreset;
15
+ resolution: readonly [number, number];
16
+ }): GuestDisplayConfig | undefined;
17
+ /** Whether any participant of a local study needs a screen other than the guest's default. */
18
+ export declare function localStudySetsScreens(config: StudyConfig): boolean;
@@ -1,4 +1,7 @@
1
1
  import { participantList } from "../../study/study-fields.js";
2
+ import { DEVICE_PRESET_NAMES, resolveParticipantDevice, } from "../../study/device-presets.js";
3
+ import { participantMobileEmulation } from "../../study/mobile-emulation.js";
4
+ import { DEFAULT_GUEST_SCREEN, GUEST_SCREEN_LIMITS, } from "../../guest/display-config.js";
2
5
  // The runtime image enforces a 30-minute lifetime; reserve setup/teardown room.
3
6
  export const LOCAL_BROWSER_LIFETIME_MS = 30 * 60_000;
4
7
  const MAX_SESSION_MS = 20 * 60_000;
@@ -24,12 +27,18 @@ export function localBrowserDefaults(config) {
24
27
  timeoutMs: config.execution?.timeoutMs ?? MAX_SESSION_MS,
25
28
  desktop: {
26
29
  ...config.execution?.desktop,
27
- resolution: config.execution?.desktop?.resolution ?? [960, 720],
30
+ // A named device governs the screen when the study sets one; resolution would override it.
31
+ ...(config.execution?.desktop?.device !== undefined ||
32
+ participantList(config)?.some((entry) => entry.device !== undefined)
33
+ ? {}
34
+ : {
35
+ resolution: config.execution?.desktop?.resolution ?? [
36
+ DEFAULT_GUEST_SCREEN.width,
37
+ DEFAULT_GUEST_SCREEN.height,
38
+ ],
39
+ }),
28
40
  },
29
41
  },
30
- ...(config.review?.analysis === undefined && actor.type === "local-agent"
31
- ? { review: { ...config.review, analysis: { provider: "codex" } } }
32
- : {}),
33
42
  };
34
43
  }
35
44
  export function localBrowserUnsupportedReason(config) {
@@ -42,20 +51,19 @@ export function localBrowserUnsupportedReason(config) {
42
51
  (actor.type === "local-agent" && actor.localAgent !== undefined && actor.localAgent !== "codex")) {
43
52
  return "Local browser studies support only Codex local-agent or openai-computer-use participants.";
44
53
  }
45
- if (desktop?.resolution?.[0] !== 960 ||
46
- desktop.resolution[1] !== 720 ||
47
- desktop.device !== undefined ||
48
- (desktop.browser !== undefined && !["default", "chromium"].includes(desktop.browser)) ||
49
- participantList(config)?.some((entry) => entry.device !== undefined) ||
50
- desktop.template !== undefined ||
51
- desktop.sandboxTimeoutMs !== undefined) {
52
- return "The local browser runtime currently uses a 960×720 Chromium desktop. Omit hosted templates, device presets and sandboxTimeoutMs.";
54
+ if ((desktop?.browser !== undefined && !["default", "chromium"].includes(desktop.browser)) ||
55
+ desktop?.template !== undefined ||
56
+ desktop?.sandboxTimeoutMs !== undefined) {
57
+ return "The local browser runtime runs Chromium and has no hosted templates or sandboxTimeoutMs. Remove execution.desktop.browser, template and sandboxTimeoutMs, or use a hosted desktop.";
53
58
  }
59
+ const screenReason = localScreenReason(config);
60
+ if (screenReason)
61
+ return screenReason;
54
62
  if (config.comms !== undefined &&
55
63
  (config.comms.email?.kind !== "fake" || !config.comms.email.external)) {
56
64
  return "Local browser inboxes require captured email with comms.email.external.catchBaseUrl. Run humanish comms catch and point your app's email sends at it. Real receiving requires a supported hosted route.";
57
65
  }
58
- if (desktop.media?.camera !== undefined && desktop.media.camera.source !== "synthetic") {
66
+ if (desktop?.media?.camera !== undefined && desktop.media.camera.source !== "synthetic") {
59
67
  return "Local cameras currently use source: synthetic. Camera files remain supported on hosted desktops.";
60
68
  }
61
69
  if (actor.type === "local-agent" &&
@@ -87,3 +95,37 @@ export function localBrowserUnsupportedReason(config) {
87
95
  }
88
96
  return undefined;
89
97
  }
98
+ /** Why a participant's screen does not fit the local guest, or undefined. */
99
+ function localScreenReason(config) {
100
+ const limits = GUEST_SCREEN_LIMITS;
101
+ for (const entry of participantList(config) ?? [{}]) {
102
+ const [width, height] = resolveParticipantDevice(config, entry.device).resolution;
103
+ if (width < limits.minWidth ||
104
+ width > limits.maxWidth ||
105
+ height < limits.minHeight ||
106
+ height > limits.maxHeight)
107
+ return `Local browser desktops support screens up to ${limits.maxWidth}×${limits.maxHeight} and at least ${limits.minHeight} pixels high; this study asks for ${width}×${height}. Use a device preset (${DEVICE_PRESET_NAMES.join(", ")}), a resolution within those bounds, or a hosted desktop.`;
108
+ }
109
+ const userAgent = config.execution?.desktop?.fidelity?.userAgent;
110
+ if (userAgent !== undefined && !/^[\x20-\x7e]{1,512}$/.test(userAgent))
111
+ return "Local browser desktops take an execution.desktop.fidelity.userAgent of at most 512 printable ASCII characters. Shorten it or remove non-ASCII characters.";
112
+ return undefined;
113
+ }
114
+ /**
115
+ * The screen a local participant's guest starts, with mobile emulation where hosted desktops apply
116
+ * it. Undefined is the guest's 960×720 default, which every installed runtime supports.
117
+ */
118
+ export function localGuestDisplay(config, device) {
119
+ const mobileEmulation = participantMobileEmulation(config.execution?.desktop?.fidelity, device.preset);
120
+ const [width, height] = device.resolution;
121
+ if (mobileEmulation === undefined &&
122
+ width === DEFAULT_GUEST_SCREEN.width &&
123
+ height === DEFAULT_GUEST_SCREEN.height)
124
+ return undefined;
125
+ return { width, height, ...(mobileEmulation === undefined ? {} : { mobileEmulation }) };
126
+ }
127
+ /** Whether any participant of a local study needs a screen other than the guest's default. */
128
+ export function localStudySetsScreens(config) {
129
+ const study = localBrowserDefaults(config);
130
+ return (participantList(study) ?? [{}]).some((entry) => localGuestDisplay(study, resolveParticipantDevice(study, entry.device)) !== undefined);
131
+ }
@@ -1,18 +1,18 @@
1
1
  /** Updated with the verified, public runtime artifact before release. */
2
2
  const LOCAL_RUNTIME_RELEASE = {
3
- url: "https://github.com/danielgwilson/humanish/releases/download/runtime-2026.09.28.1/runtime-linux-amd64.tar.gz",
4
- sha256: "224be52467d6a808c79f156c6537d8a34c2dfb7b71daa836d34c8611fbbb6647",
5
- bytes: 596142100,
6
- image: "sha256:89cec77ea8097d32f3ef14bf2bdb0a93ffd25c3975913f8c5ffc6a2f360482a2",
3
+ url: "https://github.com/danielgwilson/humanish/releases/download/runtime-2026.10.21.1/runtime-linux-amd64.tar.gz",
4
+ sha256: "1de53db000f661e91ff6ea9a2eb8198b08b850621cf63d8ce7c1b046f5035faf",
5
+ bytes: 597029770,
6
+ image: "sha256:c805c310b7b086276efa74ea5735e8b559457c7edba85c5c9e3ac7934e8760f2",
7
7
  };
8
8
  /** Architecture-specific artifacts; each retains matching sources and notices. */
9
9
  export const LOCAL_RUNTIME_RELEASES = {
10
10
  amd64: LOCAL_RUNTIME_RELEASE,
11
11
  arm64: {
12
- url: "https://github.com/danielgwilson/humanish/releases/download/runtime-2026.09.28.2/runtime-linux-arm64.tar.gz",
13
- sha256: "d03e1b6335bea76a22bf88ca9aa8dc1f4fe0af16fba02c654a333766e27cd2ea",
14
- bytes: 582661785,
15
- image: "sha256:c3d584d9586d2e86833ee8d050c43774d456c6c4f8f3c9a53194bd90910fa938",
12
+ url: "https://github.com/danielgwilson/humanish/releases/download/runtime-2026.10.21.2/runtime-linux-arm64.tar.gz",
13
+ sha256: "ea7a42e12eae3b4d9140fff41e4e3512930202f9ccbb688497b146634d1cef10",
14
+ bytes: 583555960,
15
+ image: "sha256:e21c33b4745afdb3bac6a69c76606814348032ee661228c9bfdd9c7d06591a28",
16
16
  },
17
17
  };
18
18
  /** Media assets are separate so ordinary browser studies keep the smaller download. */
@@ -164,6 +164,7 @@ async function engineStatus(options, architecture, lima) {
164
164
  image: image.Id,
165
165
  runtimeRevision: revision,
166
166
  ...(labels["to.humanish.runtime.media"] === "1" ? { media: true } : {}),
167
+ ...(labels["to.humanish.runtime.display-sizes"] === "1" ? { displaySizes: true } : {}),
167
168
  },
168
169
  };
169
170
  }
@@ -3,15 +3,16 @@
3
3
  Study analysis is an independent interpretation of retained participant evidence.
4
4
  It is separate from the participant's account, recorded outcome, and the run's
5
5
  deterministic review verdict. Opening an Observer never starts a provider request.
6
- Supported live runs request analysis on completion by default, with a separate
7
- $3 cap on its expected cost. Set `review.analysis: false` to disable that request;
8
- see [automatic analysis](../product/automatic-analysis.md).
6
+ Supported live runs request analysis on completion by default. A study that declares no analyst
7
+ gets the Codex account analyst when its participant is a Codex `local-agent`, and the OpenAI
8
+ analyst with a separate $10 cap on its expected cost otherwise. Set `review.analysis: false` to
9
+ disable that request; see [automatic analysis](../product/automatic-analysis.md).
9
10
 
10
11
  ## Invocation
11
12
 
12
13
  ```bash
13
- humanish analyze --run latest --max-cost 3 --dry-run --json
14
- humanish analyze --run latest --max-cost 3 --json
14
+ humanish analyze --run latest --max-cost 10 --dry-run --json
15
+ humanish analyze --run latest --max-cost 10 --json
15
16
  humanish observe --run latest
16
17
  humanish analyze list --run latest --json
17
18
  humanish analyze show --run latest --json
@@ -116,9 +117,11 @@ the current merge instructions, billed $1.92, most of its input served from the
116
117
  provider's prompt cache, and each of its 466 citations resolved. Two runs do not
117
118
  calibrate the merge request's estimate.
118
119
 
119
- ## Explicit Codex account analysis
120
+ ## Codex account analysis
120
121
 
121
- An existing completed recording can use a separate restricted Codex analyst:
122
+ A study whose participant is a Codex `local-agent` uses this analyst unless it declares another,
123
+ and any study can select it with `review.analysis.provider: codex`. An existing completed
124
+ recording can use it too:
122
125
 
123
126
  ```bash
124
127
  humanish analyze --run latest --provider codex --dry-run --json
@@ -486,7 +489,10 @@ The states without findings:
486
489
  - `skipped`: the automatic analysis was skipped or refused, for example without
487
490
  `OPENAI_API_KEY`, or the run's study file sets `review.analysis: false`
488
491
  (`AUTOMATIC_ANALYSIS_DISABLED`, read from the file when the view is built). `next` is
489
- `analyze --max-cost 3`, which runs an analysis anyway. For an analysis refused for its
492
+ `analyze --max-cost 10`, which runs an analysis anyway. For a skip without `OPENAI_API_KEY`
493
+ on a machine where Codex is signed in with a ChatGPT account (read when the view is built),
494
+ `next` is `analyze --provider codex` and the message names `review.analysis.provider: codex`
495
+ in the run's study file; otherwise the message says to set the key. For an analysis refused for its
490
496
  cost, the message gives the expected cost, the worst case and the cap, and `next` sets
491
497
  `--max-cost` to the worst case rounded up.
492
498
  - `failed`: the latest attempt failed or was cancelled and no usable version exists. `next`
@@ -90,7 +90,7 @@ silently drifting from one is not.
90
90
  | Default | Why it is the default | Legitimate override |
91
91
  | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
92
92
  | Dry-run | Spend safety (the live-spend rule sets the floor; dry-run keeps it far away) | `mode: live` |
93
- | Post-run analysis on supported live studies | Findings accompany the recording; a separate $3 cap on its expected cost is visible before execution | `review.analysis: false` disables it; an explicit mapping sets another analysis budget. Dry-run and unsupported routes never dispatch; missing default credentials produce a recorded skip |
93
+ | Post-run analysis on supported live studies | Findings accompany the recording; a Codex participant's run is analyzed by the same account, and otherwise a separate $10 cap on its expected cost is visible before execution | `review.analysis: false` disables it; an explicit mapping sets another analysis budget. Dry-run and unsupported routes never dispatch; missing default credentials produce a recorded skip |
94
94
  | Separate worlds (`per-lane-worlds`) | Isolation, attribution, reproducibility | `route: shared-world`: N participants against one mutable plane for scenarios that are about interaction between participants. Participants run at once, as many as `execution.concurrency` (two or more) and the E2B plan's limit allow, unless the study declares when each one starts (`participants[].startAfterMs`). The plane is one getHost-exposed subject sandbox with synthetic seeded data, or an operator-owned public deployment (external-public), driven by N actor sandboxes. The bundle declares the weaker `attributionClass: shared-world` and a verify-enforced `attributionLimits` ceiling (`best-effort-causal-attribution` and the rest of the concurrent set). Sequential turns at `execution.concurrency: 1` were removed in 0.106.0. |
95
95
  | External key placement | Smallest blast radius: when the keyed process (e.g. a computer-use provider loop) runs outside the sandbox, its key never enters | In-sandbox placement when the keyed process runs inside (an agent harness under test); declared per actor type, with a spend budget |
96
96
  | Loopback entry URLs | Public-safety: never drive third-party sites unbidden | `policies.allowPublicTargets` for an owner-declared deployment/preview (a Vercel preview of your own app). Multi-participant public/preview fan-out needs explicit `participants[].target` for every participant, so the adapter-owned topology is declared rather than inferred. Provisioned clone subjects always serve in-sandbox on loopback |