humanish 0.139.1 → 0.141.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 +23 -0
- package/README.md +5 -4
- package/dist/actors/codex/restricted-participant.js +1 -1
- package/dist/actors/computer-use/external-handoff.d.ts +22 -0
- package/dist/actors/computer-use/external-handoff.js +83 -0
- package/dist/actors/computer-use/loop/observation.d.ts +7 -0
- package/dist/actors/computer-use/loop/observation.js +17 -1
- package/dist/actors/computer-use/loop/types.d.ts +6 -0
- package/dist/analysis/analysis-limits.d.ts +6 -0
- package/dist/analysis/analysis-limits.js +6 -0
- package/dist/analysis/analysis-lock.d.ts +8 -0
- package/dist/analysis/analysis-lock.js +129 -0
- package/dist/analysis/automatic-completion.js +31 -0
- package/dist/analysis/automatic-config.d.ts +15 -6
- package/dist/analysis/automatic-config.js +25 -9
- package/dist/analysis/execute.js +1 -1
- package/dist/analysis/job.d.ts +1 -0
- package/dist/analysis/job.js +6 -2
- package/dist/analysis/recording-ended.d.ts +16 -0
- package/dist/analysis/recording-ended.js +29 -0
- package/dist/analysis/service.d.ts +1 -3
- package/dist/analysis/service.js +23 -50
- package/dist/analysis/source.d.ts +1 -1
- package/dist/browser-control/protocol.d.ts +8 -0
- package/dist/browser-control/protocol.js +21 -0
- package/dist/cli/commands/analysis-signals.d.ts +10 -4
- package/dist/cli/commands/analysis-signals.js +28 -6
- package/dist/cli/commands/runs.js +8 -2
- package/dist/cli/commands/study-route-computer-use.js +4 -5
- package/dist/cli/commands/study-route-scripted.js +5 -6
- package/dist/cli/commands/study-route-shared-world.js +4 -5
- package/dist/cli/commands/study-route-terminal.js +5 -6
- 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 +40 -14
- package/dist/cli/findings.js +117 -35
- package/dist/cli/io.d.ts +5 -1
- package/dist/cli/io.js +12 -2
- package/dist/cli/run-findings.d.ts +19 -0
- package/dist/cli/run-findings.js +106 -0
- package/dist/evidence/browser-hygiene.d.ts +12 -1
- package/dist/evidence/browser-hygiene.js +45 -2
- package/dist/feedback/export-bundle.js +1 -1
- 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/external-links.d.ts +14 -0
- package/dist/guest/external-links.js +48 -0
- package/dist/guest/runtime-desktop.d.ts +9 -3
- package/dist/guest/runtime-desktop.js +64 -27
- 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/observer/data.d.ts +4 -0
- package/dist/observer/run-routes.js +12 -3
- package/dist/observer-app.html +2 -2
- package/dist/routes/computer-use/e2b-desktop/desktop.js +27 -17
- package/dist/routes/computer-use/e2b-desktop/fidelity.js +10 -17
- package/dist/routes/computer-use/e2b-desktop/teardown.js +1 -1
- package/dist/routes/computer-use/in-process-desktop.js +2 -2
- package/dist/routes/computer-use/live-phase.js +1 -1
- package/dist/routes/computer-use/local-vm.js +34 -1
- package/dist/routes/computer-use/plan.js +1 -1
- package/dist/routes/computer-use/setup.js +2 -2
- package/dist/routes/scripted/plan.js +1 -1
- package/dist/routes/shared-world/plan.js +1 -1
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/terminal/plan.js +1 -1
- package/dist/run/display.d.ts +6 -1
- package/dist/run/display.js +11 -0
- package/dist/run/host-suspension.js +4 -1
- package/dist/run/preflight-receipts.d.ts +3 -12
- package/dist/run/preflight-receipts.js +7 -67
- package/dist/run/process-owner.d.ts +28 -0
- package/dist/run/process-owner.js +83 -0
- package/dist/run/projection.js +1 -1
- package/dist/run/reclaim.d.ts +6 -0
- package/dist/run/reclaim.js +32 -2
- package/dist/run/route-shell.js +2 -2
- package/dist/run/run-index.d.ts +3 -1
- package/dist/run/run-index.js +23 -11
- package/dist/run/run.d.ts +16 -5
- package/dist/run/run.js +25 -10
- package/dist/run/status.d.ts +57 -2
- package/dist/run/status.js +87 -1
- package/dist/run/stored-runs.d.ts +3 -0
- package/dist/run/stored-runs.js +9 -4
- 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/run-study-events.d.ts +7 -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-browser.js +3 -1
- package/dist/substrates/e2b/desktop-cdp.d.ts +0 -2
- package/dist/substrates/e2b/desktop-cdp.js +0 -2
- package/dist/substrates/e2b/desktop-executor.d.ts +7 -0
- package/dist/substrates/e2b/desktop-executor.js +3 -0
- package/dist/substrates/e2b/desktop-handoff.d.ts +22 -0
- package/dist/substrates/e2b/desktop-handoff.js +86 -0
- 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/dist/tui-app.js +115 -115
- package/docs/architecture/observer.md +7 -3
- package/docs/contracts/schemas.md +24 -3
- package/docs/contracts/study-analysis.md +15 -9
- package/docs/principles/invariants-and-defaults.md +1 -1
- package/docs/product/automatic-analysis.md +75 -18
- package/package.json +1 -1
- package/skills/humanish/SKILL.md +153 -31
package/dist/guest/bootstrap.js
CHANGED
|
@@ -3,9 +3,10 @@ import { validateBrowserControlIdentity, sameBrowserControlIdentity, } from "../
|
|
|
3
3
|
import { ComputerUseExecutorError } from "../actors/computer-use/executor-error.js";
|
|
4
4
|
import { createBrowserControlClient } from "../browser-control/client.js";
|
|
5
5
|
import { guestMediaConfigSchema } from "./media-config.js";
|
|
6
|
+
import { guestDisplayConfigSchema } from "./display-config.js";
|
|
6
7
|
export const GUEST_BOOTSTRAP_VERSION = 1;
|
|
7
8
|
export const GUEST_BOOTSTRAP_LIMITS = Object.freeze({
|
|
8
|
-
bytes: 65_536 +
|
|
9
|
+
bytes: 65_536 + 2048,
|
|
9
10
|
readyBytes: 1024,
|
|
10
11
|
initialUrlBytes: 65_536,
|
|
11
12
|
admissionMs: 5000,
|
|
@@ -42,7 +43,7 @@ export function guestReadyTimeoutMs(initialUrl) {
|
|
|
42
43
|
? 0
|
|
43
44
|
: GUEST_BOOTSTRAP_LIMITS.navigationMs + GUEST_BOOTSTRAP_LIMITS.paintMs));
|
|
44
45
|
}
|
|
45
|
-
function canonical(identity, ready, initialUrl, media, recording) {
|
|
46
|
+
function canonical(identity, ready, initialUrl, media, recording, display) {
|
|
46
47
|
const { generation, challenge, runtimeRevision } = validateBrowserControlIdentity(identity);
|
|
47
48
|
const fixed = { generation, challenge, runtimeRevision };
|
|
48
49
|
if (initialUrl !== undefined && (ready || validateGuestInitialUrl(initialUrl) !== initialUrl))
|
|
@@ -52,6 +53,8 @@ function canonical(identity, ready, initialUrl, media, recording) {
|
|
|
52
53
|
if (recording !== undefined &&
|
|
53
54
|
(ready || !desktopRecordingConfigSchema.safeParse(recording).success))
|
|
54
55
|
throw refused();
|
|
56
|
+
if (display !== undefined && (ready || !guestDisplayConfigSchema.safeParse(display).success))
|
|
57
|
+
throw refused();
|
|
55
58
|
return JSON.stringify(ready
|
|
56
59
|
? { version: 1, ready: true, identity: fixed }
|
|
57
60
|
: {
|
|
@@ -62,10 +65,11 @@ function canonical(identity, ready, initialUrl, media, recording) {
|
|
|
62
65
|
...(recording === undefined
|
|
63
66
|
? {}
|
|
64
67
|
: { recording: desktopRecordingConfigSchema.parse(recording) }),
|
|
68
|
+
...(display === undefined ? {} : { display: guestDisplayConfigSchema.parse(display) }),
|
|
65
69
|
});
|
|
66
70
|
}
|
|
67
|
-
export function encodeGuestBootstrap(identity, ready = false, initialUrl, media, recording) {
|
|
68
|
-
const bytes = Buffer.from(canonical(identity, ready, initialUrl, media, recording));
|
|
71
|
+
export function encodeGuestBootstrap(identity, ready = false, initialUrl, media, recording, display) {
|
|
72
|
+
const bytes = Buffer.from(canonical(identity, ready, initialUrl, media, recording, display));
|
|
69
73
|
if (bytes.length > (ready ? GUEST_BOOTSTRAP_LIMITS.readyBytes : GUEST_BOOTSTRAP_LIMITS.bytes))
|
|
70
74
|
throw refused();
|
|
71
75
|
const frame = Buffer.alloc(4 + bytes.length);
|
|
@@ -89,16 +93,18 @@ function parseBootstrap(bytes, revision, ready) {
|
|
|
89
93
|
const initialUrl = "initialUrl" in value ? value.initialUrl : undefined;
|
|
90
94
|
const media = "media" in value ? guestMediaConfigSchema.parse(value.media) : undefined;
|
|
91
95
|
const recording = "recording" in value ? desktopRecordingConfigSchema.parse(value.recording) : undefined;
|
|
96
|
+
const display = "display" in value ? guestDisplayConfigSchema.parse(value.display) : undefined;
|
|
92
97
|
if (initialUrl !== undefined && typeof initialUrl !== "string")
|
|
93
98
|
throw refused();
|
|
94
99
|
if (identity.runtimeRevision !== revision ||
|
|
95
|
-
canonical(identity, ready, initialUrl, media, recording) !== bytes.toString("ascii"))
|
|
100
|
+
canonical(identity, ready, initialUrl, media, recording, display) !== bytes.toString("ascii"))
|
|
96
101
|
throw refused();
|
|
97
102
|
return {
|
|
98
103
|
identity,
|
|
99
104
|
...(initialUrl === undefined ? {} : { initialUrl }),
|
|
100
105
|
...(media === undefined ? {} : { media }),
|
|
101
106
|
...(recording === undefined ? {} : { recording }),
|
|
107
|
+
...(display === undefined ? {} : { display }),
|
|
102
108
|
};
|
|
103
109
|
}
|
|
104
110
|
catch {
|
|
@@ -116,6 +122,7 @@ export class GuestBootstrapReader {
|
|
|
116
122
|
initialUrl;
|
|
117
123
|
media;
|
|
118
124
|
recording;
|
|
125
|
+
display;
|
|
119
126
|
resolve;
|
|
120
127
|
reject;
|
|
121
128
|
header = Buffer.alloc(4);
|
|
@@ -190,6 +197,7 @@ export class GuestBootstrapReader {
|
|
|
190
197
|
this.initialUrl = parsed.initialUrl;
|
|
191
198
|
this.media = parsed.media;
|
|
192
199
|
this.recording = parsed.recording;
|
|
200
|
+
this.display = parsed.display;
|
|
193
201
|
this.admitted = true;
|
|
194
202
|
clearTimeout(this.timer);
|
|
195
203
|
this.resolve(parsed.identity);
|
|
@@ -244,9 +252,9 @@ export class GuestBootstrapReader {
|
|
|
244
252
|
}
|
|
245
253
|
}
|
|
246
254
|
/** Fixed preface on an already acquired Firecracker stream; no path discovery/retry. */
|
|
247
|
-
export async function connectGuestBootstrap(stream, identity, signal, initialUrl, media, recording) {
|
|
255
|
+
export async function connectGuestBootstrap(stream, identity, signal, initialUrl, media, recording, display) {
|
|
248
256
|
validateBrowserControlIdentity(identity);
|
|
249
|
-
const request = encodeGuestBootstrap(identity, false, initialUrl, media, recording);
|
|
257
|
+
const request = encodeGuestBootstrap(identity, false, initialUrl, media, recording, display);
|
|
250
258
|
await new Promise((resolve, reject) => {
|
|
251
259
|
let bytes = Buffer.alloc(0), done = false;
|
|
252
260
|
const timer = setTimeout(() => finish(new ComputerUseExecutorError("deadline_exceeded", "not_dispatched")), GUEST_BOOTSTRAP_LIMITS.connectMs);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The screen a guest starts when the bootstrap names none. */
|
|
3
|
+
export declare const DEFAULT_GUEST_SCREEN: Readonly<{
|
|
4
|
+
width: 960;
|
|
5
|
+
height: 720;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* The screens a guest admits. The narrowest is the narrowest window Chromium renders; the widest
|
|
9
|
+
* is the widest device preset. At 1920×1200 an uncompressed 24-bit frame is 6.9 MB, so every
|
|
10
|
+
* capture fits the 8 MiB browser-control PNG limit. Below 480 pixels high the page area under
|
|
11
|
+
* Chromium's toolbar is too short to use.
|
|
12
|
+
*/
|
|
13
|
+
export declare const GUEST_SCREEN_LIMITS: Readonly<{
|
|
14
|
+
minWidth: 500;
|
|
15
|
+
maxWidth: 1920;
|
|
16
|
+
minHeight: 480;
|
|
17
|
+
maxHeight: 1200;
|
|
18
|
+
}>;
|
|
19
|
+
/** One participant's Xvfb screen, and the mobile emulation its pages get, if any. */
|
|
20
|
+
export declare const guestDisplayConfigSchema: z.ZodObject<{
|
|
21
|
+
width: z.ZodNumber;
|
|
22
|
+
height: z.ZodNumber;
|
|
23
|
+
mobileEmulation: z.ZodOptional<z.ZodObject<{
|
|
24
|
+
width: z.ZodNumber;
|
|
25
|
+
height: z.ZodNumber;
|
|
26
|
+
deviceScaleFactor: z.ZodNumber;
|
|
27
|
+
touch: z.ZodBoolean;
|
|
28
|
+
userAgent: z.ZodString;
|
|
29
|
+
}, z.core.$strict>>;
|
|
30
|
+
}, z.core.$strict>;
|
|
31
|
+
export type GuestDisplayConfig = z.infer<typeof guestDisplayConfigSchema>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MIN_DESKTOP_RENDER_WIDTH } from "../study/device-presets.js";
|
|
3
|
+
/** The screen a guest starts when the bootstrap names none. */
|
|
4
|
+
export const DEFAULT_GUEST_SCREEN = Object.freeze({ width: 960, height: 720 });
|
|
5
|
+
/**
|
|
6
|
+
* The screens a guest admits. The narrowest is the narrowest window Chromium renders; the widest
|
|
7
|
+
* is the widest device preset. At 1920×1200 an uncompressed 24-bit frame is 6.9 MB, so every
|
|
8
|
+
* capture fits the 8 MiB browser-control PNG limit. Below 480 pixels high the page area under
|
|
9
|
+
* Chromium's toolbar is too short to use.
|
|
10
|
+
*/
|
|
11
|
+
export const GUEST_SCREEN_LIMITS = Object.freeze({
|
|
12
|
+
minWidth: MIN_DESKTOP_RENDER_WIDTH,
|
|
13
|
+
maxWidth: 1920,
|
|
14
|
+
minHeight: 480,
|
|
15
|
+
maxHeight: 1200,
|
|
16
|
+
});
|
|
17
|
+
const side = (max) => z.number().int().min(1).max(max);
|
|
18
|
+
/** One participant's Xvfb screen, and the mobile emulation its pages get, if any. */
|
|
19
|
+
export const guestDisplayConfigSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
width: z.number().int().min(GUEST_SCREEN_LIMITS.minWidth).max(GUEST_SCREEN_LIMITS.maxWidth),
|
|
22
|
+
height: z.number().int().min(GUEST_SCREEN_LIMITS.minHeight).max(GUEST_SCREEN_LIMITS.maxHeight),
|
|
23
|
+
mobileEmulation: z
|
|
24
|
+
.object({
|
|
25
|
+
width: side(GUEST_SCREEN_LIMITS.maxWidth),
|
|
26
|
+
height: side(GUEST_SCREEN_LIMITS.maxHeight),
|
|
27
|
+
deviceScaleFactor: z.number().positive().max(4),
|
|
28
|
+
touch: z.boolean(),
|
|
29
|
+
// The bootstrap frame is ASCII; a launch flag carries it too.
|
|
30
|
+
userAgent: z.string().regex(/^[\x20-\x7e]{1,512}$/),
|
|
31
|
+
})
|
|
32
|
+
.strict()
|
|
33
|
+
.optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CuaExecutor } from "../actors/computer-use/loop.js";
|
|
2
|
+
/** Where the image keeps the stand-ins. */
|
|
3
|
+
export declare const GUEST_EXTERNAL_LINK_BIN = "/opt/humanish/control/bin";
|
|
4
|
+
/**
|
|
5
|
+
* Write the browser profile's Preferences before the first launch, so the initial app's origin
|
|
6
|
+
* opens tel:, sms: and the other app schemes without Chromium's prompt. No initial URL, no file.
|
|
7
|
+
*/
|
|
8
|
+
export declare function prepareGuestBrowserProfile(profileDir: string, initialUrl: string | undefined): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* The executor with each observation carrying the links appended to `log` since the previous
|
|
11
|
+
* observation, at most EXTERNAL_HANDOFF_LIMITS.perObservation. A line still being written waits
|
|
12
|
+
* for the next observation. Every other property of the executor is kept.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withExternalHandoffs(executor: CuaExecutor, log: string): CuaExecutor;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Links the guest browser hands to another app (mailto:, tel:, sms:). The guest has no email,
|
|
2
|
+
// phone or messaging app. The image's stand-ins for xdg-open and xdg-email, first on the guest's
|
|
3
|
+
// search path, append each such link to a log in the runtime directory, and every observation the
|
|
4
|
+
// executor returns carries the links appended since the previous one.
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { EXTERNAL_HANDOFF_LIMITS, externalHandoffOf, } from "../actors/computer-use/external-handoff.js";
|
|
7
|
+
import { externalAppPreferences } from "../evidence/browser-hygiene.js";
|
|
8
|
+
/** Where the image keeps the stand-ins. */
|
|
9
|
+
export const GUEST_EXTERNAL_LINK_BIN = "/opt/humanish/control/bin";
|
|
10
|
+
/**
|
|
11
|
+
* Write the browser profile's Preferences before the first launch, so the initial app's origin
|
|
12
|
+
* opens tel:, sms: and the other app schemes without Chromium's prompt. No initial URL, no file.
|
|
13
|
+
*/
|
|
14
|
+
export async function prepareGuestBrowserProfile(profileDir, initialUrl) {
|
|
15
|
+
const preferences = externalAppPreferences(initialUrl);
|
|
16
|
+
if (preferences === undefined)
|
|
17
|
+
return;
|
|
18
|
+
await mkdir(`${profileDir}/Default`, { recursive: true, mode: 0o700 });
|
|
19
|
+
await writeFile(`${profileDir}/Default/Preferences`, JSON.stringify(preferences), {
|
|
20
|
+
mode: 0o600,
|
|
21
|
+
flag: "wx",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The executor with each observation carrying the links appended to `log` since the previous
|
|
26
|
+
* observation, at most EXTERNAL_HANDOFF_LIMITS.perObservation. A line still being written waits
|
|
27
|
+
* for the next observation. Every other property of the executor is kept.
|
|
28
|
+
*/
|
|
29
|
+
export function withExternalHandoffs(executor, log) {
|
|
30
|
+
let consumed = 0;
|
|
31
|
+
return {
|
|
32
|
+
...executor,
|
|
33
|
+
observe: async () => {
|
|
34
|
+
const observation = await executor.observe();
|
|
35
|
+
const text = await readFile(log, "utf8").catch(() => "");
|
|
36
|
+
const lines = text
|
|
37
|
+
.split("\n")
|
|
38
|
+
.slice(0, -1)
|
|
39
|
+
.slice(consumed, consumed + EXTERNAL_HANDOFF_LIMITS.perObservation);
|
|
40
|
+
consumed += lines.length;
|
|
41
|
+
const externalHandoffs = lines.flatMap((line) => {
|
|
42
|
+
const handoff = externalHandoffOf(line);
|
|
43
|
+
return handoff === undefined ? [] : [handoff];
|
|
44
|
+
});
|
|
45
|
+
return externalHandoffs.length === 0 ? observation : { ...observation, externalHandoffs };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Stats } from "node:fs";
|
|
2
2
|
import { type BrowserContext, type Page } from "playwright-core";
|
|
3
|
+
import { type GuestDisplayConfig } from "./display-config.js";
|
|
3
4
|
import type { GuestRuntimeDesktop } from "./runtime.js";
|
|
4
5
|
import type { GuestMediaConfig } from "./media-config.js";
|
|
5
6
|
import { type GuestDesktopMediaOptions } from "./desktop-media.js";
|
|
@@ -10,7 +11,7 @@ export declare const GUEST_RUNTIME_PATHS: Readonly<{
|
|
|
10
11
|
run: "/run/humanish";
|
|
11
12
|
home: "/home/humanish";
|
|
12
13
|
}>;
|
|
13
|
-
export type GuestRuntimePhase = "layout" | "xauthority" | "display" | "window_manager" | "media" | "recording" | "browser" | "sandbox" | "focus" | "fixture" | "navigation";
|
|
14
|
+
export type GuestRuntimePhase = "layout" | "xauthority" | "display" | "window_manager" | "media" | "recording" | "browser" | "sandbox" | "emulation" | "focus" | "fixture" | "navigation";
|
|
14
15
|
/** Initial document only: do not wait for app data, subresources or network idle. */
|
|
15
16
|
export declare function navigateGuestInitialPage(page: Pick<Page, "goto" | "waitForFunction">, initialUrl: string, signal: AbortSignal): Promise<void>;
|
|
16
17
|
/** /run/humanish and /home/humanish: real directories private to the guest user. */
|
|
@@ -19,7 +20,11 @@ export declare function isPrivateGuestDirectory(stat: Pick<Stats, "isDirectory"
|
|
|
19
20
|
export declare function isPinnedOpenboxConfig(stat: Pick<Stats, "isFile" | "uid" | "gid" | "nlink" | "size" | "mode">): boolean;
|
|
20
21
|
/** chrome://sandbox must report every namespace and seccomp feature and the overall verdict. */
|
|
21
22
|
export declare function isAdequatelySandboxed(report: string): boolean;
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The window leaves 20 pixels above and below it on the participant's screen. A mobile participant's
|
|
25
|
+
* user agent and touch flags reach every page and worker; CDP emulation sets the viewport.
|
|
26
|
+
*/
|
|
27
|
+
export declare function chromiumLaunchArgs(media: GuestMediaConfig | undefined, display?: GuestDisplayConfig): string[];
|
|
23
28
|
/** The devices to start; the permission policy only changes Chromium's flags. */
|
|
24
29
|
export declare function desktopMediaRequest(media: GuestMediaConfig): GuestDesktopMediaOptions["media"];
|
|
25
30
|
/** Audio needs Pulse, which only the microphone path starts. */
|
|
@@ -27,7 +32,7 @@ export declare function recorderAudio(recording: DesktopRecordingConfig, media:
|
|
|
27
32
|
audioSources: DesktopRecordingAudioSource[];
|
|
28
33
|
pulseReady: boolean;
|
|
29
34
|
};
|
|
30
|
-
/** Fixed guest layout with an optional admitted initial loopback app URL. */
|
|
35
|
+
/** Fixed guest layout with an optional admitted initial loopback app URL and participant screen. */
|
|
31
36
|
export declare function createGuestRuntimeDesktop(options: {
|
|
32
37
|
signal: AbortSignal;
|
|
33
38
|
onTerminal(): void;
|
|
@@ -35,6 +40,7 @@ export declare function createGuestRuntimeDesktop(options: {
|
|
|
35
40
|
initialUrl?: string;
|
|
36
41
|
media?: GuestMediaConfig;
|
|
37
42
|
recording?: DesktopRecordingConfig;
|
|
43
|
+
display?: GuestDisplayConfig;
|
|
38
44
|
}): Promise<GuestRuntimeDesktop & {
|
|
39
45
|
readonly owner: {
|
|
40
46
|
context: BrowserContext;
|
|
@@ -3,6 +3,8 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { lstat, mkdir, open, rm } from "node:fs/promises";
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
5
|
import { chromium } from "playwright-core";
|
|
6
|
+
import { DEFAULT_GUEST_SCREEN } from "./display-config.js";
|
|
7
|
+
import { mobileEmulationCommands, mobileEmulationLaunchFlags, } from "../study/mobile-emulation.js";
|
|
6
8
|
import { createGuestDesktopNativeTools } from "./desktop-native.js";
|
|
7
9
|
import { createGuestChromiumText } from "./chromium-text.js";
|
|
8
10
|
import { createGuestBrowserTools } from "./browser-tools.js";
|
|
@@ -12,6 +14,7 @@ import { GUEST_BOOTSTRAP_LIMITS, validateGuestInitialUrl } from "./bootstrap.js"
|
|
|
12
14
|
import { startDesktopMedia, } from "./desktop-media.js";
|
|
13
15
|
import { startDesktopRecorder } from "../evidence/desktop-recorder.js";
|
|
14
16
|
import { createGuestProcesses, setupFailed } from "./runtime-processes.js";
|
|
17
|
+
import { GUEST_EXTERNAL_LINK_BIN, prepareGuestBrowserProfile, withExternalHandoffs, } from "./external-links.js";
|
|
15
18
|
/** The guest image's fixed layout. Tests derive guest paths from it rather than spelling them. */
|
|
16
19
|
export const GUEST_RUNTIME_PATHS = Object.freeze({
|
|
17
20
|
root: "/opt/humanish/control",
|
|
@@ -19,7 +22,8 @@ export const GUEST_RUNTIME_PATHS = Object.freeze({
|
|
|
19
22
|
home: "/home/humanish",
|
|
20
23
|
});
|
|
21
24
|
const GUEST_RUNTIME_ENV = Object.freeze({
|
|
22
|
-
|
|
25
|
+
// The stand-ins come first, so the browser hands a mailto: or tel: link to them.
|
|
26
|
+
PATH: `${GUEST_EXTERNAL_LINK_BIN}:/usr/bin:/bin`,
|
|
23
27
|
HOME: "/home/humanish",
|
|
24
28
|
USER: "humanish",
|
|
25
29
|
LOGNAME: "humanish",
|
|
@@ -33,16 +37,18 @@ const GUEST_RUNTIME_ENV = Object.freeze({
|
|
|
33
37
|
TMPDIR: "/tmp",
|
|
34
38
|
});
|
|
35
39
|
const CONFIG_SHA = "4ae1c52eab748a3624b3948ce792647be258caba70c8c72038b9a43be6459552";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
function xvfbArgs(screen) {
|
|
41
|
+
return [
|
|
42
|
+
":0",
|
|
43
|
+
"-screen",
|
|
44
|
+
"0",
|
|
45
|
+
`${screen.width}x${screen.height}x24`,
|
|
46
|
+
"-nolisten",
|
|
47
|
+
"tcp",
|
|
48
|
+
"-auth",
|
|
49
|
+
GUEST_RUNTIME_ENV.XAUTHORITY,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
46
52
|
const SANDBOX_FEATURES = ["PID namespaces", "Network namespaces", "Seccomp-BPF sandbox"];
|
|
47
53
|
const RECORDING_PATH = `${GUEST_RUNTIME_PATHS.home}/desktop-recording.mp4`;
|
|
48
54
|
/** Initial document only: do not wait for app data, subresources or network idle. */
|
|
@@ -86,10 +92,16 @@ export function isAdequatelySandboxed(report) {
|
|
|
86
92
|
return (SANDBOX_FEATURES.every((label) => new RegExp(label + "\\s+Yes").test(report)) &&
|
|
87
93
|
report.includes("You are adequately sandboxed"));
|
|
88
94
|
}
|
|
89
|
-
|
|
95
|
+
/**
|
|
96
|
+
* The window leaves 20 pixels above and below it on the participant's screen. A mobile participant's
|
|
97
|
+
* user agent and touch flags reach every page and worker; CDP emulation sets the viewport.
|
|
98
|
+
*/
|
|
99
|
+
export function chromiumLaunchArgs(media, display) {
|
|
100
|
+
const screen = display ?? DEFAULT_GUEST_SCREEN;
|
|
90
101
|
return [
|
|
91
|
-
|
|
102
|
+
`--window-size=${screen.width},${screen.height - 40}`,
|
|
92
103
|
"--window-position=0,20",
|
|
104
|
+
...mobileEmulationLaunchFlags(display?.mobileEmulation),
|
|
93
105
|
"--disable-background-networking",
|
|
94
106
|
"--disable-component-update",
|
|
95
107
|
"--no-first-run",
|
|
@@ -162,12 +174,12 @@ async function waitForDisplay(processes, signal, check) {
|
|
|
162
174
|
throw setupFailed();
|
|
163
175
|
}
|
|
164
176
|
/** A recorder that fails to start leaves the desktop usable; finishRecording reports it. */
|
|
165
|
-
async function startGuestRecorder(recording, media, env, signal) {
|
|
177
|
+
async function startGuestRecorder(recording, media, screen, env, signal) {
|
|
166
178
|
try {
|
|
167
179
|
return await startDesktopRecorder({
|
|
168
180
|
display: ":0",
|
|
169
|
-
width:
|
|
170
|
-
height:
|
|
181
|
+
width: screen.width,
|
|
182
|
+
height: screen.height,
|
|
171
183
|
outputPath: RECORDING_PATH,
|
|
172
184
|
env,
|
|
173
185
|
signal,
|
|
@@ -178,7 +190,7 @@ async function startGuestRecorder(recording, media, env, signal) {
|
|
|
178
190
|
return undefined;
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
|
-
async function launchGuestBrowser(state, media, check) {
|
|
193
|
+
async function launchGuestBrowser(state, media, display, check) {
|
|
182
194
|
state.pendingContext = chromium.launchPersistentContext(`${GUEST_RUNTIME_PATHS.home}/browser`, {
|
|
183
195
|
executablePath: "/usr/bin/chromium",
|
|
184
196
|
headless: false,
|
|
@@ -186,7 +198,7 @@ async function launchGuestBrowser(state, media, check) {
|
|
|
186
198
|
viewport: null,
|
|
187
199
|
env: state.recorder?.env ?? state.media?.env ?? GUEST_RUNTIME_ENV,
|
|
188
200
|
timeout: 25_000,
|
|
189
|
-
args: chromiumLaunchArgs(media),
|
|
201
|
+
args: chromiumLaunchArgs(media, display),
|
|
190
202
|
});
|
|
191
203
|
// A browser that finishes launching after the stop is closed here; teardown may not see it.
|
|
192
204
|
void state.pendingContext.then((browser) => {
|
|
@@ -216,6 +228,25 @@ async function verifyBrowserSandbox(context, page, check) {
|
|
|
216
228
|
check();
|
|
217
229
|
return report;
|
|
218
230
|
}
|
|
231
|
+
/** Sends the emulation commands over a page session that stays attached, so they hold. */
|
|
232
|
+
async function emulateOn(context, page, request) {
|
|
233
|
+
const session = await context.newCDPSession(page);
|
|
234
|
+
for (const { method, params } of mobileEmulationCommands(request))
|
|
235
|
+
// Playwright types send by protocol method; these names come from the fixed list.
|
|
236
|
+
await session.send(method, params);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Emulate a phone on the launch page before it navigates, so the first document loads with the
|
|
240
|
+
* phone's viewport, touch and user agent. Tabs opened later get the same overrides when they open;
|
|
241
|
+
* a tab that closes before that is left alone.
|
|
242
|
+
*/
|
|
243
|
+
async function emulateMobile(browser, request, check) {
|
|
244
|
+
await emulateOn(browser.context, browser.page, request);
|
|
245
|
+
check();
|
|
246
|
+
browser.context.on("page", (page) => {
|
|
247
|
+
void emulateOn(browser.context, page, request).catch(() => { });
|
|
248
|
+
});
|
|
249
|
+
}
|
|
219
250
|
/** Text actions run only while the browser window that setup saw still has focus. */
|
|
220
251
|
function focusGuard(native, window) {
|
|
221
252
|
return async (actionSignal) => {
|
|
@@ -223,7 +254,7 @@ function focusGuard(native, window) {
|
|
|
223
254
|
throw new ComputerUseExecutorError("action_rejected", "not_dispatched");
|
|
224
255
|
};
|
|
225
256
|
}
|
|
226
|
-
async function createGuestExecutorFor(state, browser, signal, onTerminal, check) {
|
|
257
|
+
async function createGuestExecutorFor(state, browser, screen, signal, onTerminal, check) {
|
|
227
258
|
const native = createGuestDesktopNativeTools({
|
|
228
259
|
display: ":0",
|
|
229
260
|
temporaryDirectory: `${GUEST_RUNTIME_PATHS.run}/capture`,
|
|
@@ -236,8 +267,8 @@ async function createGuestExecutorFor(state, browser, signal, onTerminal, check)
|
|
|
236
267
|
assertFocusedWindow: focusGuard(native, window),
|
|
237
268
|
});
|
|
238
269
|
return createGuestDesktopExecutor({
|
|
239
|
-
width:
|
|
240
|
-
height:
|
|
270
|
+
width: screen.width,
|
|
271
|
+
height: screen.height,
|
|
241
272
|
tools: createGuestBrowserTools(native, state.content),
|
|
242
273
|
authoritySignal: signal,
|
|
243
274
|
onTerminal,
|
|
@@ -329,10 +360,11 @@ function closeGuestRuntimeDesktop(state, processes, controller, detach) {
|
|
|
329
360
|
controller.abort();
|
|
330
361
|
return state.closing;
|
|
331
362
|
}
|
|
332
|
-
/** Fixed guest layout with an optional admitted initial loopback app URL. */
|
|
363
|
+
/** Fixed guest layout with an optional admitted initial loopback app URL and participant screen. */
|
|
333
364
|
export async function createGuestRuntimeDesktop(options) {
|
|
334
365
|
if (options.initialUrl !== undefined)
|
|
335
366
|
validateGuestInitialUrl(options.initialUrl);
|
|
367
|
+
const screen = options.display ?? DEFAULT_GUEST_SCREEN;
|
|
336
368
|
const controller = new AbortController();
|
|
337
369
|
const signal = AbortSignal.any([options.signal, controller.signal]);
|
|
338
370
|
const state = { stopping: false };
|
|
@@ -360,10 +392,11 @@ export async function createGuestRuntimeDesktop(options) {
|
|
|
360
392
|
progress("layout");
|
|
361
393
|
check();
|
|
362
394
|
await verifyGuestLayout(check);
|
|
395
|
+
await prepareGuestBrowserProfile(`${GUEST_RUNTIME_PATHS.home}/browser`, options.initialUrl);
|
|
363
396
|
progress("xauthority");
|
|
364
397
|
await authorizeDisplay(processes, check);
|
|
365
398
|
progress("display");
|
|
366
|
-
processes.spawn("/usr/bin/Xvfb",
|
|
399
|
+
processes.spawn("/usr/bin/Xvfb", xvfbArgs(screen), undefined, true);
|
|
367
400
|
await waitForDisplay(processes, signal, check);
|
|
368
401
|
progress("window_manager");
|
|
369
402
|
processes.spawn("/usr/bin/openbox", ["--config-file", `${GUEST_RUNTIME_PATHS.root}/openbox.xml`], undefined, true);
|
|
@@ -381,15 +414,19 @@ export async function createGuestRuntimeDesktop(options) {
|
|
|
381
414
|
if (options.recording !== undefined) {
|
|
382
415
|
progress("recording");
|
|
383
416
|
const env = state.media?.env ?? GUEST_RUNTIME_ENV;
|
|
384
|
-
state.recorder = await startGuestRecorder(options.recording, options.media, env, signal);
|
|
417
|
+
state.recorder = await startGuestRecorder(options.recording, options.media, screen, env, signal);
|
|
385
418
|
check();
|
|
386
419
|
}
|
|
387
420
|
progress("browser");
|
|
388
|
-
const browser = await launchGuestBrowser(state, options.media, check);
|
|
421
|
+
const browser = await launchGuestBrowser(state, options.media, options.display, check);
|
|
389
422
|
progress("sandbox");
|
|
390
423
|
const sandboxReport = await verifyBrowserSandbox(browser.context, browser.page, check);
|
|
424
|
+
if (options.display?.mobileEmulation !== undefined) {
|
|
425
|
+
progress("emulation");
|
|
426
|
+
await emulateMobile(browser, options.display.mobileEmulation, check);
|
|
427
|
+
}
|
|
391
428
|
progress("focus");
|
|
392
|
-
const executor = await createGuestExecutorFor(state, browser, signal, options.onTerminal, check);
|
|
429
|
+
const executor = await createGuestExecutorFor(state, browser, screen, signal, options.onTerminal, check);
|
|
393
430
|
if (options.initialUrl !== undefined) {
|
|
394
431
|
progress("navigation");
|
|
395
432
|
await navigateGuestInitialPage(browser.page, options.initialUrl, signal);
|
|
@@ -400,7 +437,7 @@ export async function createGuestRuntimeDesktop(options) {
|
|
|
400
437
|
await openGuestFixture(browser.page, check);
|
|
401
438
|
}
|
|
402
439
|
return {
|
|
403
|
-
executor: state.media?.wrap(executor) ?? executor,
|
|
440
|
+
executor: withExternalHandoffs(state.media?.wrap(executor) ?? executor, `${GUEST_RUNTIME_ENV.XDG_RUNTIME_DIR}/humanish-links.log`),
|
|
404
441
|
close,
|
|
405
442
|
...(options.recording === undefined
|
|
406
443
|
? {}
|
package/dist/guest/runtime.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Duplex, Readable } from "node:stream";
|
|
2
2
|
import type { CuaExecutor } from "../actors/computer-use/loop.js";
|
|
3
3
|
import type { GuestMediaConfig } from "./media-config.js";
|
|
4
|
+
import type { GuestDisplayConfig } from "./display-config.js";
|
|
4
5
|
import type { DesktopRecordingConfig, DesktopRecordingMetadata } from "../evidence/desktop-recording-types.js";
|
|
5
6
|
export interface GuestRuntimeDesktop {
|
|
6
7
|
executor: CuaExecutor;
|
|
@@ -18,7 +19,7 @@ export interface GuestRuntimeOptions {
|
|
|
18
19
|
signal: AbortSignal;
|
|
19
20
|
/** Fixed private supervision channel, not browser-control output. */
|
|
20
21
|
marker(value: "A" | "R"): void;
|
|
21
|
-
createDesktop(signal: AbortSignal, onTerminal: () => void, initialUrl?: string, media?: GuestMediaConfig, recording?: DesktopRecordingConfig): Promise<GuestRuntimeDesktop>;
|
|
22
|
+
createDesktop(signal: AbortSignal, onTerminal: () => void, initialUrl?: string, media?: GuestMediaConfig, recording?: DesktopRecordingConfig, display?: GuestDisplayConfig): Promise<GuestRuntimeDesktop>;
|
|
22
23
|
}
|
|
23
24
|
/** One admitted desktop and one dispatcher. The owner retains physical teardown. */
|
|
24
25
|
export declare function runGuestRuntime(options: GuestRuntimeOptions): Promise<{
|
package/dist/guest/runtime.js
CHANGED
|
@@ -83,7 +83,7 @@ export async function runGuestRuntime(options) {
|
|
|
83
83
|
void close();
|
|
84
84
|
}, guestReadyTimeoutMs(reader.initialUrl));
|
|
85
85
|
options.marker("A");
|
|
86
|
-
preparing = options.createDesktop(authority, terminal, reader.initialUrl, reader.media, reader.recording);
|
|
86
|
+
preparing = options.createDesktop(authority, terminal, reader.initialUrl, reader.media, reader.recording, reader.display);
|
|
87
87
|
desktop = await Promise.race([
|
|
88
88
|
preparing,
|
|
89
89
|
new Promise((_, reject) => {
|
|
@@ -27,12 +27,13 @@ try {
|
|
|
27
27
|
if (writeSync(Number(markerFd), value) !== 1)
|
|
28
28
|
throw new Error();
|
|
29
29
|
},
|
|
30
|
-
createDesktop: (signal, onTerminal, initialUrl, media, recording) => createGuestRuntimeDesktop({
|
|
30
|
+
createDesktop: (signal, onTerminal, initialUrl, media, recording, display) => createGuestRuntimeDesktop({
|
|
31
31
|
signal,
|
|
32
32
|
onTerminal,
|
|
33
33
|
...(initialUrl === undefined ? {} : { initialUrl }),
|
|
34
34
|
...(media === undefined ? {} : { media }),
|
|
35
35
|
...(recording === undefined ? {} : { recording }),
|
|
36
|
+
...(display === undefined ? {} : { display }),
|
|
36
37
|
}),
|
|
37
38
|
});
|
|
38
39
|
const result = await runtime.closed;
|
package/dist/observer/data.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface ObserverData {
|
|
|
23
23
|
state: "running" | "finished" | "interrupted" | "unknown";
|
|
24
24
|
observedAt: string;
|
|
25
25
|
source: "local-run-status";
|
|
26
|
+
/** Present while the run's own process analyzes it after the run ended: when it started. */
|
|
27
|
+
analysis?: {
|
|
28
|
+
startedAt: string;
|
|
29
|
+
};
|
|
26
30
|
};
|
|
27
31
|
run: {
|
|
28
32
|
runId: string;
|
|
@@ -15,7 +15,7 @@ import { displayedTraces, loadStreamTraces, } from "../run/stream-trace.js";
|
|
|
15
15
|
import { isNodeError, isRecord } from "../run/type-guards.js";
|
|
16
16
|
import { MAX_NOTE_FILE_BYTES, readRunNotes, RUN_NOTES_DIR, } from "../run/note-files.js";
|
|
17
17
|
import { analysisCostOf, runCost, runCostLabel } from "../run/run-cost.js";
|
|
18
|
-
import { RUN_STATUS_FILE, RUN_STATUS_STALE_MS, isRunStatusRecord, } from "../run/status.js";
|
|
18
|
+
import { RUN_STATUS_FILE, RUN_STATUS_STALE_MS, runAnalysisPhase, isRunStatusRecord, } from "../run/status.js";
|
|
19
19
|
import { renderObserverHtml } from "./artifact.js";
|
|
20
20
|
import { parseFeedCursors } from "./feed-cursors.js";
|
|
21
21
|
import { observerFeedBody } from "./page-data.js";
|
|
@@ -98,7 +98,9 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
|
|
|
98
98
|
const derivedRoot = cleanedRelativePath.split("/")[0];
|
|
99
99
|
const derivedLeaf = path.posix.basename(cleanedRelativePath);
|
|
100
100
|
// Local-only files (raw sandbox ids, the recording pid) are read in process, never served.
|
|
101
|
-
if (
|
|
101
|
+
if (
|
|
102
|
+
// `.analysis-lock-stale-*` is a lock whose dead owner's next writer moved it aside.
|
|
103
|
+
derivedRoot?.startsWith(".analysis-lock") === true ||
|
|
102
104
|
derivedLeaf.startsWith(".humanish-write-") ||
|
|
103
105
|
isAnalysisRecordPath(cleanedRelativePath) ||
|
|
104
106
|
isLocalOnlyRunFile(cleanedRelativePath)) {
|
|
@@ -330,9 +332,16 @@ function withLocalRunStatus(record, input, outcome) {
|
|
|
330
332
|
state = "running";
|
|
331
333
|
}
|
|
332
334
|
}
|
|
335
|
+
// The run's own process analyzes it after the run ended; the bundle outcome does not say so.
|
|
336
|
+
const analysis = timestampsValid ? runAnalysisPhase(record, now) : undefined;
|
|
333
337
|
return {
|
|
334
338
|
...data,
|
|
335
|
-
runtime: {
|
|
339
|
+
runtime: {
|
|
340
|
+
state,
|
|
341
|
+
observedAt: new Date(now).toISOString(),
|
|
342
|
+
source: "local-run-status",
|
|
343
|
+
...(analysis?.state === "running" ? { analysis: { startedAt: analysis.startedAt } } : {}),
|
|
344
|
+
},
|
|
336
345
|
};
|
|
337
346
|
}
|
|
338
347
|
/** internal: exported for the stream lifecycle tests. */
|