humanish 0.139.1 → 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.
- package/CHANGELOG.md +11 -0
- package/README.md +5 -4
- package/dist/analysis/automatic-completion.js +24 -0
- package/dist/analysis/automatic-config.d.ts +15 -6
- package/dist/analysis/automatic-config.js +25 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -3
- package/dist/cli/commands/study-route-scripted.js +3 -4
- package/dist/cli/commands/study-route-shared-world.js +2 -3
- package/dist/cli/commands/study-route-terminal.js +3 -4
- package/dist/cli/commands/study-run.js +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +3 -0
- package/dist/cli/findings.d.ts +17 -14
- package/dist/cli/findings.js +37 -31
- package/dist/cli/io.d.ts +5 -1
- package/dist/cli/io.js +11 -2
- package/dist/cli/run-findings.d.ts +19 -0
- package/dist/cli/run-findings.js +106 -0
- package/dist/guest/bootstrap.d.ts +4 -2
- package/dist/guest/bootstrap.js +15 -7
- package/dist/guest/display-config.d.ts +31 -0
- package/dist/guest/display-config.js +35 -0
- package/dist/guest/runtime-desktop.d.ts +9 -3
- package/dist/guest/runtime-desktop.js +59 -25
- package/dist/guest/runtime.d.ts +2 -1
- package/dist/guest/runtime.js +1 -1
- package/dist/guest-runtime-main.js +2 -1
- package/dist/routes/computer-use/e2b-desktop/fidelity.js +10 -17
- package/dist/routes/computer-use/local-vm.js +34 -1
- package/dist/routes/computer-use/plan.js +1 -1
- package/dist/routes/scripted/plan.js +1 -1
- package/dist/routes/shared-world/plan.js +1 -1
- package/dist/routes/terminal/plan.js +1 -1
- package/dist/run/route-shell.js +1 -1
- package/dist/study/doctor.js +11 -6
- package/dist/study/init-templates.js +9 -3
- package/dist/study/mobile-emulation.d.ts +27 -0
- package/dist/study/mobile-emulation.js +60 -0
- package/dist/study/plan.js +1 -1
- package/dist/study/preflight-result.js +1 -1
- package/dist/study/summary.js +1 -1
- package/dist/study/warnings.js +5 -2
- package/dist/substrates/e2b/cdp-probe.d.ts +2 -6
- package/dist/substrates/e2b/desktop-cdp.d.ts +0 -2
- package/dist/substrates/e2b/desktop-cdp.js +0 -2
- package/dist/substrates/local/firecracker-desktop.d.ts +5 -0
- package/dist/substrates/local/firecracker-desktop.js +7 -1
- package/dist/substrates/local/runtime-config.d.ts +12 -0
- package/dist/substrates/local/runtime-config.js +55 -13
- package/dist/substrates/local/runtime-release.js +8 -8
- package/dist/substrates/local/runtime.js +1 -0
- package/docs/contracts/study-analysis.md +14 -8
- package/docs/principles/invariants-and-defaults.md +1 -1
- package/docs/product/automatic-analysis.md +46 -11
- package/package.json +1 -1
- package/skills/humanish/SKILL.md +45 -26
|
@@ -3,7 +3,9 @@ import path from "node:path";
|
|
|
3
3
|
import { participantList } from "../../study/study-fields.js";
|
|
4
4
|
import { inboxRecipientFor, } from "./participant-desktop.js";
|
|
5
5
|
import { createLocalFirecrackerDesktop, } from "../../substrates/local/firecracker-desktop.js";
|
|
6
|
-
import { localBrowserDefaults, localBrowserUnsupportedReason, } from "../../substrates/local/runtime-config.js";
|
|
6
|
+
import { localBrowserDefaults, localBrowserUnsupportedReason, localGuestDisplay, } from "../../substrates/local/runtime-config.js";
|
|
7
|
+
import { mobileEmulationCommands } from "../../study/mobile-emulation.js";
|
|
8
|
+
import { declaredScreenForRender } from "../../substrates/e2b/desktop-geometry.js";
|
|
7
9
|
import { localRuntimeCapacity, prepareLocalRuntime } from "../../substrates/local/runtime.js";
|
|
8
10
|
import { dockerCommandLine, usesLima } from "../../substrates/local/runtime-host.js";
|
|
9
11
|
import { outOfMemoryMessage } from "../../substrates/local/capacity.js";
|
|
@@ -52,6 +54,30 @@ function mediaEvidence(media) {
|
|
|
52
54
|
},
|
|
53
55
|
};
|
|
54
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The geometry a local participant records when its guest emulates a phone: the requested screen
|
|
59
|
+
* and the emulation the guest applied before the first page loaded. The guest does not read back
|
|
60
|
+
* what the page reports, so the block has no `resolved` part and says so.
|
|
61
|
+
*/
|
|
62
|
+
function emulatedGeometry(run, display) {
|
|
63
|
+
const { device } = run.planned;
|
|
64
|
+
const declared = declaredScreenForRender(device.preset, device.name, device.resolution);
|
|
65
|
+
const request = display.mobileEmulation;
|
|
66
|
+
return {
|
|
67
|
+
screen: {
|
|
68
|
+
requested: { width: display.width, height: display.height },
|
|
69
|
+
...(declared === undefined ? {} : { declared }),
|
|
70
|
+
},
|
|
71
|
+
fidelity: {
|
|
72
|
+
tier: "mobile-emulated",
|
|
73
|
+
requested: request,
|
|
74
|
+
applied: mobileEmulationCommands(request).map((command) => command.method),
|
|
75
|
+
},
|
|
76
|
+
warnings: [
|
|
77
|
+
`Mobile emulation was applied for participant ${run.planned.id} in the local guest, which does not read back the page's own report; desktopGeometry.fidelity carries the request without a resolved block.`,
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
55
81
|
/** One participant's Firecracker desktop and optional captured inbox, released in finalize. */
|
|
56
82
|
function createLocalParticipantDesktop(context, run, warnings, artifactRoot) {
|
|
57
83
|
const { config, media, recording, state } = context;
|
|
@@ -71,11 +97,13 @@ function createLocalParticipantDesktop(context, run, warnings, artifactRoot) {
|
|
|
71
97
|
const runtime = await context.assets();
|
|
72
98
|
if (address && email?.kind === "fake" && email.external)
|
|
73
99
|
inbox = await startLocalCapturedInbox(email.external, address);
|
|
100
|
+
const display = localGuestDisplay(config, run.planned.device);
|
|
74
101
|
session = await createLocalFirecrackerDesktop({
|
|
75
102
|
assets: runtime,
|
|
76
103
|
...(inbox ? { inboxUrl: inbox.url } : {}),
|
|
77
104
|
...(media === undefined ? {} : { media }),
|
|
78
105
|
...(recording === undefined ? {} : { recording }),
|
|
106
|
+
...(display === undefined ? {} : { display }),
|
|
79
107
|
appUrl: run.planned.targetUrl ?? config.subject.appUrl,
|
|
80
108
|
outputRoot: path.join(context.cwd, ".humanish", "local-runtime"),
|
|
81
109
|
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
@@ -83,6 +111,11 @@ function createLocalParticipantDesktop(context, run, warnings, artifactRoot) {
|
|
|
83
111
|
state.sessions.push(session);
|
|
84
112
|
if (media !== undefined)
|
|
85
113
|
evidence.desktopBrowser = mediaEvidence(media);
|
|
114
|
+
if (display?.mobileEmulation !== undefined)
|
|
115
|
+
evidence.desktopGeometry = emulatedGeometry(run, {
|
|
116
|
+
...display,
|
|
117
|
+
mobileEmulation: display.mobileEmulation,
|
|
118
|
+
});
|
|
86
119
|
},
|
|
87
120
|
async openSession() {
|
|
88
121
|
if (!session)
|
|
@@ -343,7 +343,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
343
343
|
...(actor === undefined ? {} : { actor }),
|
|
344
344
|
},
|
|
345
345
|
});
|
|
346
|
-
const analysis = resolveAutomaticAnalysis(config.review?.analysis);
|
|
346
|
+
const analysis = resolveAutomaticAnalysis(config.review?.analysis, config.actor);
|
|
347
347
|
if (!analysis.ok)
|
|
348
348
|
return refuse("before-scope", "HUMANISH_STUDY_ANALYSIS_INVALID", analysis.message);
|
|
349
349
|
const tasksReason = taskProtocolValidationReason(config, true);
|
|
@@ -34,7 +34,7 @@ export function planScriptedStudy(config, input) {
|
|
|
34
34
|
const beforeScope = { beforeScope: true };
|
|
35
35
|
if (String(config.comms?.email?.kind) === "real")
|
|
36
36
|
return refuse("HUMANISH_SCRIPTED_SCENARIO_INVALID", "Real email receiving is unsupported on the scripted route. Use a supported hosted computer-use browser study.", beforeScope);
|
|
37
|
-
const analysis = resolveAutomaticAnalysis(config.review?.analysis);
|
|
37
|
+
const analysis = resolveAutomaticAnalysis(config.review?.analysis, config.actor);
|
|
38
38
|
if (!analysis.ok)
|
|
39
39
|
return refuse("HUMANISH_STUDY_ANALYSIS_INVALID", analysis.message, beforeScope);
|
|
40
40
|
const tasksReason = taskProtocolValidationReason(config, false);
|
|
@@ -38,7 +38,7 @@ export function planSharedWorldStudy(config, input) {
|
|
|
38
38
|
networkCaptureValidationReason(config, "shared-world");
|
|
39
39
|
if (mediaReason)
|
|
40
40
|
return refuse(invalid, mediaReason);
|
|
41
|
-
const analysis = resolveAutomaticAnalysis(config.review?.analysis);
|
|
41
|
+
const analysis = resolveAutomaticAnalysis(config.review?.analysis, config.actor);
|
|
42
42
|
if (!analysis.ok)
|
|
43
43
|
return refuse("HUMANISH_STUDY_ANALYSIS_INVALID", analysis.message);
|
|
44
44
|
const tasksReason = taskProtocolValidationReason(config, false);
|
|
@@ -39,7 +39,7 @@ export function planTerminalStudy(config, input) {
|
|
|
39
39
|
const modal = sandboxProvider === "modal" ? modalRefusal(config) : undefined;
|
|
40
40
|
if (modal !== undefined)
|
|
41
41
|
return refuse(modal.code, modal.message);
|
|
42
|
-
const analysis = resolveAutomaticAnalysis(config.review?.analysis);
|
|
42
|
+
const analysis = resolveAutomaticAnalysis(config.review?.analysis, config.actor);
|
|
43
43
|
if (!analysis.ok)
|
|
44
44
|
return refuse("HUMANISH_STUDY_ANALYSIS_INVALID", analysis.message);
|
|
45
45
|
const tasksReason = taskProtocolValidationReason(config, false);
|
package/dist/run/route-shell.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function admitRoute(shell) {
|
|
|
42
42
|
* or default analysis is recorded as skipped.
|
|
43
43
|
*/
|
|
44
44
|
export function completeRefusalAnalysis(refused, config, input) {
|
|
45
|
-
const analysis = resolveAutomaticAnalysis(config.review?.analysis);
|
|
45
|
+
const analysis = resolveAutomaticAnalysis(config.review?.analysis, config.actor);
|
|
46
46
|
return completeAutomaticAnalysis(refused, undefined, analysis.ok ? analysis.config : undefined, input, { trigger: config.review?.analysis === undefined ? "default" : "explicit" });
|
|
47
47
|
}
|
|
48
48
|
/** A result as its route's outcome. StudyOutcome pairs each route with its result type. */
|
package/dist/study/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
|
@@ -204,7 +204,7 @@ export async function studySetupChecks(args) {
|
|
|
204
204
|
if (route === "scripted")
|
|
205
205
|
checks.push(await scriptedBrowserCheck());
|
|
206
206
|
checks.push(...subjectEnvChecks(requiredSubjectEnv(requirements), args));
|
|
207
|
-
const analysis = automaticAnalysisBudget(config.review?.analysis, route);
|
|
207
|
+
const analysis = automaticAnalysisBudget(config.review?.analysis, route, undefined, config.actor);
|
|
208
208
|
if (analysis)
|
|
209
209
|
checks.push(await analysisCheck(analysis, args, checkAccount));
|
|
210
210
|
checks.push(checkScope(analysis));
|
|
@@ -231,10 +231,13 @@ async function localBrowserChecks(config, args, desktopsAtOnce) {
|
|
|
231
231
|
media: config.execution?.desktop?.media !== undefined ||
|
|
232
232
|
config.execution?.desktop?.recording !== undefined,
|
|
233
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;
|
|
234
237
|
checks.push({
|
|
235
238
|
name: "local browser runtime",
|
|
236
|
-
ok: runtime.ok,
|
|
237
|
-
message: runtime.message,
|
|
239
|
+
ok: runtime.ok && screens === undefined,
|
|
240
|
+
message: screens ?? runtime.message,
|
|
238
241
|
...(runtime.ok && !runtime.installed ? { status: "note" } : {}),
|
|
239
242
|
});
|
|
240
243
|
if (runtime.capacity !== undefined && desktopsAtOnce !== undefined)
|
|
@@ -252,7 +255,9 @@ async function localBrowserChecks(config, args, desktopsAtOnce) {
|
|
|
252
255
|
}
|
|
253
256
|
return {
|
|
254
257
|
checks,
|
|
255
|
-
...(runtime.ok && runtime.installed
|
|
258
|
+
...(runtime.ok && runtime.installed && screens === undefined
|
|
259
|
+
? {}
|
|
260
|
+
: { next: screens ?? runtime.message }),
|
|
256
261
|
};
|
|
257
262
|
}
|
|
258
263
|
/** Whether the desktops the study runs at once fit the VM or this machine, and why. */
|
|
@@ -384,7 +389,7 @@ async function analysisCheck(analysis, args, checkAccount) {
|
|
|
384
389
|
...(keyed ? {} : { status: "note" }),
|
|
385
390
|
message: keyed
|
|
386
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.`
|
|
387
|
-
: "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.",
|
|
388
393
|
};
|
|
389
394
|
}
|
|
390
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.
|
|
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
|
+
}
|
package/dist/study/plan.js
CHANGED
|
@@ -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 } : {}),
|
package/dist/study/summary.js
CHANGED
|
@@ -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);
|
package/dist/study/warnings.js
CHANGED
|
@@ -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
|
|
202
|
-
applies: (config, routes) => !routes.hostedCuaBrowser &&
|
|
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
|
-
|
|
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?.
|
|
46
|
-
desktop
|
|
47
|
-
desktop
|
|
48
|
-
|
|
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
|
|
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.
|
|
4
|
-
sha256: "
|
|
5
|
-
bytes:
|
|
6
|
-
image: "sha256:
|
|
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.
|
|
13
|
-
sha256: "
|
|
14
|
-
bytes:
|
|
15
|
-
image: "sha256:
|
|
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
|
}
|