humanish 0.125.0 → 0.126.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 +16 -0
- package/dist/actors/computer-use/actor.d.ts +2 -0
- package/dist/actors/computer-use/actor.js +1 -0
- package/dist/actors/computer-use/loop/clock.d.ts +9 -8
- package/dist/actors/computer-use/loop/clock.js +27 -17
- package/dist/actors/computer-use/loop/debrief.d.ts +1 -1
- package/dist/actors/computer-use/loop/debrief.js +15 -5
- package/dist/actors/computer-use/loop/ending.d.ts +8 -5
- package/dist/actors/computer-use/loop/ending.js +8 -5
- package/dist/actors/computer-use/loop/observation.d.ts +6 -0
- package/dist/actors/computer-use/loop/observation.js +19 -1
- package/dist/actors/computer-use/loop/session.d.ts +4 -2
- package/dist/actors/computer-use/loop/session.js +4 -1
- package/dist/actors/computer-use/loop/trace.js +4 -2
- package/dist/actors/computer-use/loop/types.d.ts +22 -8
- package/dist/actors/computer-use/loop.js +5 -5
- package/dist/actors/contract.d.ts +31 -8
- package/dist/actors/contract.js +3 -0
- package/dist/actors/stop-cause.js +1 -0
- package/dist/cli/commands/study.js +8 -0
- package/dist/evidence/redaction.d.ts +1 -0
- package/dist/evidence/redaction.js +1 -0
- package/dist/evidence/trace-page.d.ts +6 -0
- package/dist/evidence/trace-page.js +90 -0
- package/dist/observer/feed-cursors.d.ts +14 -0
- package/dist/observer/feed-cursors.js +61 -0
- package/dist/observer/participant-items.d.ts +7 -0
- package/dist/observer/pinned-files.d.ts +2 -1
- package/dist/observer/pinned-files.js +2 -2
- package/dist/observer/run-routes.d.ts +1 -1
- package/dist/observer/run-routes.js +38 -17
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle.js +4 -0
- package/dist/routes/computer-use/diagnostics.d.ts +1 -1
- package/dist/routes/computer-use/diagnostics.js +3 -3
- package/dist/routes/computer-use/e2b-desktop/desktop.js +2 -1
- package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +6 -0
- package/dist/routes/computer-use/e2b-desktop/prepare.js +47 -1
- package/dist/routes/computer-use/e2b-desktop/start.d.ts +2 -1
- package/dist/routes/computer-use/e2b-desktop/start.js +15 -3
- package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -1
- package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
- package/dist/routes/computer-use/fanout-records.js +4 -0
- package/dist/routes/computer-use/participant-deps.d.ts +1 -1
- package/dist/routes/computer-use/participant-desktop.d.ts +1 -1
- package/dist/routes/computer-use/participant-model.js +16 -6
- package/dist/routes/computer-use/participant-runs.d.ts +2 -2
- package/dist/routes/computer-use/participant-runs.js +11 -7
- package/dist/routes/computer-use/plan.js +39 -1
- package/dist/routes/computer-use/setup.d.ts +1 -0
- package/dist/routes/computer-use/setup.js +28 -0
- package/dist/routes/computer-use/single-bundle.d.ts +5 -1
- package/dist/routes/computer-use/single-bundle.js +2 -0
- package/dist/routes/computer-use/types.d.ts +16 -10
- package/dist/routes/shared-world/bundle-records.js +3 -0
- package/dist/routes/shared-world/participant-specs.js +3 -2
- package/dist/routes/shared-world/plan.js +5 -0
- package/dist/run/contained-output.d.ts +7 -3
- package/dist/run/contained-output.js +11 -7
- package/dist/run/judge.d.ts +7 -4
- package/dist/run/judge.js +13 -8
- package/dist/run/live-items.d.ts +19 -0
- package/dist/run/live-items.js +28 -8
- package/dist/run/stream-trace.d.ts +25 -4
- package/dist/run/stream-trace.js +55 -17
- package/dist/run/streams.d.ts +16 -0
- package/dist/study/arrivals.d.ts +32 -14
- package/dist/study/arrivals.js +47 -21
- package/dist/study/bundle-estimate.d.ts +6 -5
- package/dist/study/bundle-estimate.js +12 -7
- package/dist/study/desktop-files.d.ts +37 -0
- package/dist/study/desktop-files.js +174 -0
- package/dist/study/keys.d.ts +8 -0
- package/dist/study/keys.js +3 -0
- package/dist/study/migrate/v2.js +12 -3
- package/dist/study/parse/actors.d.ts +1 -1
- package/dist/study/parse/actors.js +28 -20
- package/dist/study/parse/clock.d.ts +18 -0
- package/dist/study/parse/clock.js +36 -0
- package/dist/study/plan-participants.d.ts +19 -5
- package/dist/study/plan-participants.js +17 -1
- package/dist/study/plan.js +24 -13
- package/dist/study/preflight.js +4 -0
- package/dist/study/types.d.ts +34 -5
- package/dist/study/validation.js +3 -0
- package/dist/study/warnings.d.ts +2 -2
- package/dist/study/warnings.js +7 -0
- package/dist/subject/local-tree-archive.d.ts +7 -0
- package/dist/subject/local-tree-archive.js +12 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +4 -1
- package/dist/substrates/e2b/desktop-browser.js +6 -2
- package/dist/substrates/e2b/desktop-clock.d.ts +10 -0
- package/dist/substrates/e2b/desktop-clock.js +29 -0
- package/dist/substrates/e2b/sdk.d.ts +6 -0
- package/dist/verify/pages.d.ts +7 -0
- package/dist/verify/pages.js +50 -0
- package/dist/verify/verify.js +10 -3
- package/docs/architecture/actor-contract.md +2 -0
- package/docs/architecture/external-public-shared-world.md +4 -1
- package/docs/architecture/observer.md +10 -1
- package/docs/contracts/policy.md +7 -0
- package/docs/contracts/run-bundle.md +31 -1
- package/docs/contracts/schemas.md +72 -3
- package/package.json +2 -2
|
@@ -101,6 +101,10 @@ function singleParticipantArgs(base, state) {
|
|
|
101
101
|
: "e2b-desktop",
|
|
102
102
|
...(outcome?.desktopGeometry === undefined ? {} : { desktopGeometry: outcome.desktopGeometry }),
|
|
103
103
|
...(outcome?.recording === undefined ? {} : { recording: outcome.recording }),
|
|
104
|
+
...(outcome?.desktopFiles === undefined ? {} : { desktopFiles: outcome.desktopFiles }),
|
|
105
|
+
...(spec.planned.limits.timeZone === undefined
|
|
106
|
+
? {}
|
|
107
|
+
: { timeZone: spec.planned.limits.timeZone }),
|
|
104
108
|
isMobile: spec.planned.device.preset.isMobile,
|
|
105
109
|
screenshots: outcome?.screenshots ?? [],
|
|
106
110
|
captureRedaction: base.redactScreenshots ? "blurred" : "raw",
|
|
@@ -3,7 +3,7 @@ import { type ActorCompletionReason, type ActorStatus, type ActorStopCause } fro
|
|
|
3
3
|
declare const CUA_DIAGNOSTIC_CATEGORIES: readonly ["preview", "participant_outcome", "session_interrupted", "execution_error", "evidence_invalid", "mixed", "unknown"];
|
|
4
4
|
export type CuaDiagnosticCategory = (typeof CUA_DIAGNOSTIC_CATEGORIES)[number];
|
|
5
5
|
/** Closed at both projection and telemetry boundaries. No provider messages are classified. */
|
|
6
|
-
declare const CUA_DIAGNOSTIC_STOP_CAUSES: readonly ["provider_output_limit", "provider_token_limit", "time_limit", "spend_limit", "study_spend_limit", "adapter_limit", "provider_incomplete", "provider_status", "provider_refused_prompt", "harness_aborted", "usage_unreported", "scheduled_end", "unspecified_limit", "mixed", "unknown"];
|
|
6
|
+
declare const CUA_DIAGNOSTIC_STOP_CAUSES: readonly ["provider_output_limit", "provider_token_limit", "time_limit", "spend_limit", "study_spend_limit", "adapter_limit", "provider_incomplete", "provider_status", "provider_refused_prompt", "harness_aborted", "usage_unreported", "scheduled_end", "session_cap", "unspecified_limit", "mixed", "unknown"];
|
|
7
7
|
export type CuaDiagnosticStopCause = (typeof CUA_DIAGNOSTIC_STOP_CAUSES)[number];
|
|
8
8
|
export interface CuaDiagnostics {
|
|
9
9
|
category: CuaDiagnosticCategory;
|
|
@@ -30,9 +30,9 @@ export function cuaParticipantDiagnostics(input) {
|
|
|
30
30
|
const session = input.session;
|
|
31
31
|
if (!session)
|
|
32
32
|
return { category: "unknown" };
|
|
33
|
-
// A scheduled end is the participant's planned ending, so a run whose other
|
|
34
|
-
// themselves reads as one kind of ending.
|
|
35
|
-
if (session.stopCause === "scheduled_end")
|
|
33
|
+
// A scheduled end or a session cap is the participant's planned ending, so a run whose other
|
|
34
|
+
// participants ended themselves reads as one kind of ending.
|
|
35
|
+
if (session.stopCause === "scheduled_end" || session.stopCause === "session_cap")
|
|
36
36
|
return { category: "participant_outcome" };
|
|
37
37
|
if (session.stopCause !== undefined) {
|
|
38
38
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createE2BDesktopExecutor } from "../../../substrates/e2b/desktop-executor.js";
|
|
2
2
|
import { participantInbox, planParticipantComms } from "./comms.js";
|
|
3
3
|
import { participantBrowserStateObserver } from "./fidelity.js";
|
|
4
|
-
import { acquireParticipantDesktop, provisionParticipantSubject, verifyParticipantScreen, } from "./prepare.js";
|
|
4
|
+
import { acquireParticipantDesktop, placeParticipantFiles, provisionParticipantSubject, verifyParticipantScreen, } from "./prepare.js";
|
|
5
5
|
import { openParticipantSurface, startParticipantMedia, startParticipantStream } from "./start.js";
|
|
6
6
|
import { desktopEvidenceOf, newParticipantState } from "./state.js";
|
|
7
7
|
import { finishE2BDesktop } from "./teardown.js";
|
|
@@ -40,6 +40,7 @@ export function createE2BParticipantDesktop(spec, deps, warnings) {
|
|
|
40
40
|
const desktop = await acquireParticipantDesktop(ctx, state);
|
|
41
41
|
// The device claim is verified in the sandbox, and fails closed.
|
|
42
42
|
await verifyParticipantScreen(ctx, state, desktop);
|
|
43
|
+
await placeParticipantFiles(ctx, state, desktop);
|
|
43
44
|
await provisionParticipantSubject(ctx, state, desktop);
|
|
44
45
|
await startParticipantMedia(ctx, state, desktop);
|
|
45
46
|
await openParticipantSurface(ctx, state, desktop);
|
|
@@ -14,5 +14,11 @@ export declare function acquireParticipantDesktop(ctx: E2BParticipantContext, st
|
|
|
14
14
|
* requested and verified geometry side by side.
|
|
15
15
|
*/
|
|
16
16
|
export declare function verifyParticipantScreen(ctx: E2BParticipantContext, state: E2BParticipantState, desktop: E2BDesktopSandbox): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Copy the participant's files (participants[].files) into its home folder and confirm each one's
|
|
19
|
+
* size there, before the subject and the browser start. A file that does not land whole fails the
|
|
20
|
+
* participant before its first turn. The state records each confirmed file as it lands.
|
|
21
|
+
*/
|
|
22
|
+
export declare function placeParticipantFiles(ctx: E2BParticipantContext, state: E2BParticipantState, desktop: E2BDesktopSandbox): Promise<void>;
|
|
17
23
|
/** Install a desktop CLI, or clone or unpack the subject and serve it, in the sandbox. */
|
|
18
24
|
export declare function provisionParticipantSubject(ctx: E2BParticipantContext, state: E2BParticipantState, desktop: E2BDesktopSandbox): Promise<void>;
|
|
@@ -8,11 +8,12 @@ import { provisionLocalTreeSubject } from "../../../subject/local-tree.js";
|
|
|
8
8
|
import { SubjectBuildError } from "../../../subject/serve.js";
|
|
9
9
|
import { inspectDesktopScreenGeometry } from "../../../substrates/e2b/desktop-geometry.js";
|
|
10
10
|
import { observeDesktopResources } from "../../../substrates/e2b/desktop-resources.js";
|
|
11
|
-
import { acquireE2BDesktopSandbox, e2bDesktopTemplate } from "../../../substrates/e2b/sandbox.js";
|
|
11
|
+
import { acquireE2BDesktopSandbox, e2bDesktopTemplate, withOneRetryOnTransientE2BError, } from "../../../substrates/e2b/sandbox.js";
|
|
12
12
|
import { loadE2BDesktopModule } from "../../../substrates/e2b/sdk.js";
|
|
13
13
|
import { e2bShell } from "../../../substrates/e2b/shell.js";
|
|
14
14
|
import { attachReceivingInbox, participantCommsEnv, startCommsCatch } from "./comms.js";
|
|
15
15
|
import { participantSubjectEnv } from "../types.js";
|
|
16
|
+
import { plural } from "../../../run/text.js";
|
|
16
17
|
export const CUA_ACTOR_STUDY_PROVIDER_METADATA = {
|
|
17
18
|
mode: "cua-actor-lab",
|
|
18
19
|
tool: "humanish",
|
|
@@ -151,6 +152,51 @@ export async function verifyParticipantScreen(ctx, state, desktop) {
|
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Copy the participant's files (participants[].files) into its home folder and confirm each one's
|
|
157
|
+
* size there, before the subject and the browser start. A file that does not land whole fails the
|
|
158
|
+
* participant before its first turn. The state records each confirmed file as it lands.
|
|
159
|
+
*/
|
|
160
|
+
export async function placeParticipantFiles(ctx, state, desktop) {
|
|
161
|
+
const { spec, deps } = ctx;
|
|
162
|
+
const files = deps.desktopFiles?.get(spec.planned.id) ?? [];
|
|
163
|
+
if (files.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
const getInfo = desktop.files.getInfo?.bind(desktop.files);
|
|
166
|
+
if (getInfo === undefined)
|
|
167
|
+
throw new Error("The installed E2B SDK has no files.getInfo, so humanish cannot confirm that participants[].files landed. Update @e2b/desktop.");
|
|
168
|
+
const requestTimeoutMs = deps.requestTimeoutMs;
|
|
169
|
+
const startedAt = deps.now();
|
|
170
|
+
ctx.onSubjectPhase({
|
|
171
|
+
at: new Date(startedAt).toISOString(),
|
|
172
|
+
type: "cua-lab.desktop.files.started",
|
|
173
|
+
message: `copying ${plural(files.length, "file")} onto the desktop`,
|
|
174
|
+
});
|
|
175
|
+
const filesCompleted = (ok) => ctx.onSubjectPhase({
|
|
176
|
+
at: new Date(deps.now()).toISOString(),
|
|
177
|
+
type: "cua-lab.desktop.files.completed",
|
|
178
|
+
ok,
|
|
179
|
+
durationMs: Math.max(0, deps.now() - startedAt),
|
|
180
|
+
message: ok
|
|
181
|
+
? `${plural(files.length, "file")} on the desktop`
|
|
182
|
+
: `copying files onto the desktop failed after ${plural(state.desktopFiles.length, "file")}`,
|
|
183
|
+
});
|
|
184
|
+
try {
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
// A relative path lands under the sandbox user's home folder, and missing folders are made.
|
|
187
|
+
await withOneRetryOnTransientE2BError(() => desktop.files.write(file.as, file.data, { requestTimeoutMs, useOctetStream: true }));
|
|
188
|
+
const landed = await getInfo(file.as, { requestTimeoutMs });
|
|
189
|
+
if (landed.size !== file.size)
|
|
190
|
+
throw new Error(`participants[].files: ${file.as} measured ${landed.size} bytes on the desktop of participant ${spec.planned.id} after humanish copied ${file.size}, so the participant did not start.`);
|
|
191
|
+
state.desktopFiles.push({ as: file.as, size: file.size, sha256: file.sha256 });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
filesCompleted(false);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
filesCompleted(true);
|
|
199
|
+
}
|
|
154
200
|
/** Install a desktop CLI, or clone or unpack the subject and serve it, in the sandbox. */
|
|
155
201
|
export async function provisionParticipantSubject(ctx, state, desktop) {
|
|
156
202
|
const { deps } = ctx;
|
|
@@ -4,7 +4,8 @@ import type { E2BParticipantContext, E2BParticipantState } from "./state.js";
|
|
|
4
4
|
export declare function startParticipantMedia(ctx: E2BParticipantContext, state: E2BParticipantState, desktop: E2BDesktopSandbox): Promise<void>;
|
|
5
5
|
/**
|
|
6
6
|
* Open what the participant sees: the browser at the target, with any declared camera and mobile
|
|
7
|
-
* fidelity, or a terminal window on the desktop-cli route.
|
|
7
|
+
* fidelity, or a terminal window on the desktop-cli route. A participant with a time zone sees the
|
|
8
|
+
* desktop's panel clock in it; a clock that cannot be set is a warning, and it shows UTC.
|
|
8
9
|
*/
|
|
9
10
|
export declare function openParticipantSurface(ctx: E2BParticipantContext, state: E2BParticipantState, desktop: E2BDesktopSandbox): Promise<void>;
|
|
10
11
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// fills the participant's state.
|
|
4
4
|
import { redactText, toErrorMessage } from "../../../evidence/redaction.js";
|
|
5
5
|
import { CHROME_DEVTOOLS_PORT, DESKTOP_SETTLE_MS, openDesktopBrowserTarget, openDesktopTerminal, startDesktopStream, } from "../../../substrates/e2b/desktop-browser.js";
|
|
6
|
+
import { setPanelClockZone } from "../../../substrates/e2b/desktop-clock.js";
|
|
6
7
|
import { captureDesktopBrowserGeometry } from "../../../substrates/e2b/desktop-geometry.js";
|
|
7
8
|
import { prepareDesktopMedia, startE2BDesktopMedia, } from "../../../substrates/e2b/desktop-media.js";
|
|
8
9
|
import { startE2BDesktopRecording } from "../../../substrates/e2b/desktop-recording.js";
|
|
@@ -39,16 +40,23 @@ export async function startParticipantMedia(ctx, state, desktop) {
|
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
42
|
* Open what the participant sees: the browser at the target, with any declared camera and mobile
|
|
42
|
-
* fidelity, or a terminal window on the desktop-cli route.
|
|
43
|
+
* fidelity, or a terminal window on the desktop-cli route. A participant with a time zone sees the
|
|
44
|
+
* desktop's panel clock in it; a clock that cannot be set is a warning, and it shows UTC.
|
|
43
45
|
*/
|
|
44
46
|
export async function openParticipantSurface(ctx, state, desktop) {
|
|
45
47
|
const { spec, deps, warnings, targetUrl } = ctx;
|
|
46
48
|
const { residual, subject } = deps;
|
|
49
|
+
const { timeZone } = spec.planned.limits;
|
|
50
|
+
if (timeZone !== undefined) {
|
|
51
|
+
await setPanelClockZone(desktop, timeZone, deps.requestTimeoutMs).catch((error) => {
|
|
52
|
+
warnings.push(`The desktop's panel clock for participant ${spec.planned.id} could not be set to ${timeZone} and shows UTC: ${redactText(deps.scrubKnownValues(toErrorMessage(error)))}`);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
47
55
|
if (subject.kind === "desktop-cli") {
|
|
48
56
|
// A terminal window, opened the way the browser is opened on every other route: the
|
|
49
57
|
// participant arrives at a desktop with the thing they were asked to use already in front
|
|
50
58
|
// of them. They can still open another from the dock; that is the point of a desktop.
|
|
51
|
-
await openDesktopTerminal(desktop, deps.requestTimeoutMs, subject.product.workdir);
|
|
59
|
+
await openDesktopTerminal(desktop, deps.requestTimeoutMs, subject.product.workdir, timeZone);
|
|
52
60
|
await desktop.wait(DESKTOP_SETTLE_MS).catch(() => undefined);
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
@@ -59,7 +67,11 @@ export async function openParticipantSurface(ctx, state, desktop) {
|
|
|
59
67
|
? undefined
|
|
60
68
|
: await prepareDesktopMedia(desktop, requestedMedia, residual.policies?.mediaPermission ?? "prompt", deps.studyCwd, deps.requestTimeoutMs);
|
|
61
69
|
const emulationFlags = mobileLaunchFlags(residual, spec);
|
|
62
|
-
|
|
70
|
+
// The participant's time zone is TZ on the launch: the browser process and every renderer it
|
|
71
|
+
// starts read it at startup, so every tab runs in the zone from the first page load. A CDP
|
|
72
|
+
// timezone override holds only on the one tab whose DevTools session sets it, from after its load.
|
|
73
|
+
const mediaEnv = state.speech?.env ?? state.recording?.env;
|
|
74
|
+
const browserLaunch = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, residual.execution?.desktop?.browser, [...emulationFlags, ...(mediaEvidence?.flags ?? [])], timeZone === undefined ? mediaEnv : { ...mediaEnv, TZ: timeZone });
|
|
63
75
|
state.desktopBrowser =
|
|
64
76
|
mediaEvidence === undefined
|
|
65
77
|
? browserLaunch.evidence
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RunDesktopRecording } from "../../../evidence/desktop-recording-types.js";
|
|
2
2
|
import type { RunSubjectStateStepRecord } from "../../../run/bundle.js";
|
|
3
|
-
import type { RunDesktopGeometry } from "../../../run/streams.js";
|
|
3
|
+
import type { RunDesktopFile, RunDesktopGeometry } from "../../../run/streams.js";
|
|
4
4
|
import type { SubjectPhaseEvent } from "../../../subject/steps.js";
|
|
5
5
|
import type { OwnedDesktopAllocation } from "../../../substrates/desktop-session.js";
|
|
6
6
|
import type { DesktopBrowserEvidence, DesktopBrowserFamily, DesktopBrowserLaunchIdentity } from "../../../substrates/e2b/desktop-browser.js";
|
|
@@ -45,6 +45,8 @@ export interface E2BParticipantState {
|
|
|
45
45
|
readonly stateStepRecords: RunSubjectStateStepRecord[];
|
|
46
46
|
/** Completed phases only (durationMs and ok are set on completed events); bundle.events keeps these. */
|
|
47
47
|
readonly phaseRecords: SubjectPhaseEvent[];
|
|
48
|
+
/** participants[].files confirmed on the desktop, in the order they landed. */
|
|
49
|
+
readonly desktopFiles: RunDesktopFile[];
|
|
48
50
|
subjectCommit: string | undefined;
|
|
49
51
|
speech: Awaited<ReturnType<typeof startE2BDesktopMedia>> | undefined;
|
|
50
52
|
recording: Awaited<ReturnType<typeof startE2BDesktopRecording>> | undefined;
|
|
@@ -18,6 +18,7 @@ export function newParticipantState(spec) {
|
|
|
18
18
|
receivingInboxUrl: undefined,
|
|
19
19
|
stateStepRecords: [],
|
|
20
20
|
phaseRecords: [],
|
|
21
|
+
desktopFiles: [],
|
|
21
22
|
subjectCommit: undefined,
|
|
22
23
|
speech: undefined,
|
|
23
24
|
recording: undefined,
|
|
@@ -60,6 +61,7 @@ export function desktopEvidenceOf(state) {
|
|
|
60
61
|
...(state.subjectCommit === undefined ? {} : { subjectCommit: state.subjectCommit }),
|
|
61
62
|
...(state.desktopBrowser === undefined ? {} : { desktopBrowser: state.desktopBrowser }),
|
|
62
63
|
...(state.recordingEvidence === undefined ? {} : { recording: state.recordingEvidence }),
|
|
64
|
+
...(state.desktopFiles.length === 0 ? {} : { desktopFiles: state.desktopFiles }),
|
|
63
65
|
desktopGeometry: state.desktopGeometry,
|
|
64
66
|
stateStepRecords: state.stateStepRecords,
|
|
65
67
|
phaseRecords: state.phaseRecords,
|
|
@@ -120,6 +120,10 @@ function fanoutParticipantStream(args, spec, view) {
|
|
|
120
120
|
},
|
|
121
121
|
}),
|
|
122
122
|
desktopGeometry,
|
|
123
|
+
...(outcome?.desktopFiles === undefined ? {} : { desktopFiles: outcome.desktopFiles }),
|
|
124
|
+
...(spec.planned.limits.timeZone === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: { timeZone: spec.planned.limits.timeZone }),
|
|
123
127
|
...(outcome?.recording === undefined ? {} : { recording: outcome.recording }),
|
|
124
128
|
ui: {
|
|
125
129
|
route: publicTargetUrl,
|
|
@@ -36,7 +36,7 @@ interface DesktopRunFacts {
|
|
|
36
36
|
}
|
|
37
37
|
/** The fields one desktop route sets and the other does not. Undefined leaves a field out. */
|
|
38
38
|
type DesktopRouteOverrides = {
|
|
39
|
-
[K in "createDesktop" | "createProvider" | "inProcessExecutor" | "localTreeArchiveBuffer" | "externalComms" | "receiving" | "screenMismatchPolicy"]?: CuaParticipantDeps[K] | undefined;
|
|
39
|
+
[K in "createDesktop" | "createProvider" | "inProcessExecutor" | "localTreeArchiveBuffer" | "desktopFiles" | "externalComms" | "receiving" | "screenMismatchPolicy"]?: CuaParticipantDeps[K] | undefined;
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
42
|
* One run's participant deps. Call it once per run: the run budget it creates is the one ledger
|
|
@@ -22,7 +22,7 @@ export type ParticipantDesktopEvidence = {
|
|
|
22
22
|
* who did not pass.
|
|
23
23
|
*/
|
|
24
24
|
desktopFailure?: string;
|
|
25
|
-
} & Pick<ParticipantRunOutcome, "sandboxId" | "sandboxRelease" | "desktopDurationMs" | "desktopResources" | "streamUrlPresent" | "subjectCommit" | "desktopBrowser" | "desktopGeometry" | "stateStepRecords" | "phaseRecords" | "failureCode" | "commsArtifactPath" | "recording">;
|
|
25
|
+
} & Pick<ParticipantRunOutcome, "sandboxId" | "sandboxRelease" | "desktopDurationMs" | "desktopResources" | "streamUrlPresent" | "subjectCommit" | "desktopBrowser" | "desktopGeometry" | "stateStepRecords" | "phaseRecords" | "failureCode" | "commsArtifactPath" | "recording" | "desktopFiles">;
|
|
26
26
|
/**
|
|
27
27
|
* The desktop one participant runs on, owned from preparation through final evidence collection.
|
|
28
28
|
* Call methods sequentially: prepare, openSession, then finalize in a finally block.
|
|
@@ -307,18 +307,28 @@ export function makeCuaRunBudget(maxTotalUsd) {
|
|
|
307
307
|
};
|
|
308
308
|
}
|
|
309
309
|
/**
|
|
310
|
-
* The participant's clock: its time zone, and its
|
|
311
|
-
*
|
|
310
|
+
* The participant's clock: its time zone, and its end on the run's clock. A participant with an
|
|
311
|
+
* end runs its session on the run's clock, so the end and the schedule read one clock. A cap
|
|
312
|
+
* counts from here, where the session starts, so the desktop's setup does not use it.
|
|
312
313
|
*/
|
|
313
314
|
function participantClock(timeZone, deps) {
|
|
314
|
-
const
|
|
315
|
-
if (timeZone === undefined &&
|
|
315
|
+
const declared = deps.end;
|
|
316
|
+
if (timeZone === undefined && declared === undefined)
|
|
316
317
|
return {};
|
|
318
|
+
const end = declared === undefined
|
|
319
|
+
? undefined
|
|
320
|
+
: declared.kind === "session_cap"
|
|
321
|
+
? {
|
|
322
|
+
atMs: deps.now() + declared.afterMs,
|
|
323
|
+
windDownMs: declared.windDownMs,
|
|
324
|
+
kind: declared.kind,
|
|
325
|
+
}
|
|
326
|
+
: { atMs: declared.atMs, windDownMs: declared.windDownMs };
|
|
317
327
|
return {
|
|
318
328
|
clock: {
|
|
319
329
|
...(timeZone === undefined ? {} : { timeZone }),
|
|
320
|
-
...(
|
|
330
|
+
...(end === undefined ? {} : { end }),
|
|
321
331
|
},
|
|
322
|
-
...(
|
|
332
|
+
...(end === undefined ? {} : { now: deps.now }),
|
|
323
333
|
};
|
|
324
334
|
}
|
|
@@ -19,13 +19,13 @@ export declare function defaultSessionTimeoutMs(config: StudyConfig, ceilingMs:
|
|
|
19
19
|
export declare function resolveParticipantSandboxMs(config: StudyConfig, ceilingMs: number): number;
|
|
20
20
|
/**
|
|
21
21
|
* The deps one participant runs with once its start comes: its own session budget and sandbox
|
|
22
|
-
* deadline, and with a declared end
|
|
22
|
+
* deadline, and with a declared end or cap, how its session ends (`scheduledAt` is when it was
|
|
23
23
|
* due, from runOnSchedule).
|
|
24
24
|
*/
|
|
25
25
|
export declare function scheduledParticipantDeps(participant: ParticipantWindow, deps: {
|
|
26
26
|
readonly timeoutMs: number;
|
|
27
27
|
readonly sandboxMs: number;
|
|
28
|
-
}, scheduledAt: number): Pick<CuaParticipantDeps, "timeoutMs" | "sandboxMs" | "
|
|
28
|
+
}, scheduledAt: number): Pick<CuaParticipantDeps, "timeoutMs" | "sandboxMs" | "end">;
|
|
29
29
|
/**
|
|
30
30
|
* The in-flight participant bound a study declares. Defaults to the participant count: every
|
|
31
31
|
* declared participant runs at once, because a throttle nobody asked for silently turns "N actors
|
|
@@ -18,7 +18,7 @@ import { composeParticipantInstructions, DEFAULT_MISSION } from "./participant-p
|
|
|
18
18
|
import { resolveCuaRerunSelection } from "./rerun-selection.js";
|
|
19
19
|
import { plural } from "../../run/text.js";
|
|
20
20
|
import { formatDuration } from "../../run/projection.js";
|
|
21
|
-
import { describeArrivals, participantSessionMs, planArrivals,
|
|
21
|
+
import { describeArrivals, participantEnd, participantSessionMs, planArrivals, } from "../../study/arrivals.js";
|
|
22
22
|
/**
|
|
23
23
|
* The session budget of a study that declares no execution.timeoutMs. On a provisioned route it is
|
|
24
24
|
* what the `ceilingMs` sandbox ceiling leaves after provisioning, seeding and the teardown buffer.
|
|
@@ -46,9 +46,9 @@ export function resolveParticipantSandboxMs(config, ceilingMs) {
|
|
|
46
46
|
sandboxHeadroomMs(provisionedRoute ? { seed: config.subject.state?.seed ?? [] } : undefined));
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* A participant's own session budget and sandbox deadline: the study's, or with a declared end
|
|
50
|
-
* window plus room for the closing report (participantSessionMs), with the same
|
|
51
|
-
* teardown headroom the study's sandbox has over its session.
|
|
49
|
+
* A participant's own session budget and sandbox deadline: the study's, or with a declared end or
|
|
50
|
+
* cap its window or cap plus room for the closing report (participantSessionMs), with the same
|
|
51
|
+
* setup and teardown headroom the study's sandbox has over its session.
|
|
52
52
|
*/
|
|
53
53
|
function participantWindow(participant, study) {
|
|
54
54
|
const timeoutMs = participantSessionMs(participant, study.sessionBudgetMs);
|
|
@@ -56,17 +56,17 @@ function participantWindow(participant, study) {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* The deps one participant runs with once its start comes: its own session budget and sandbox
|
|
59
|
-
* deadline, and with a declared end
|
|
59
|
+
* deadline, and with a declared end or cap, how its session ends (`scheduledAt` is when it was
|
|
60
60
|
* due, from runOnSchedule).
|
|
61
61
|
*/
|
|
62
62
|
export function scheduledParticipantDeps(participant, deps, scheduledAt) {
|
|
63
|
-
const end =
|
|
63
|
+
const end = participantEnd(participant, scheduledAt);
|
|
64
64
|
return {
|
|
65
65
|
...participantWindow(participant, {
|
|
66
66
|
sessionBudgetMs: deps.timeoutMs,
|
|
67
67
|
sandboxMs: deps.sandboxMs,
|
|
68
68
|
}),
|
|
69
|
-
...(end === undefined ? {} : {
|
|
69
|
+
...(end === undefined ? {} : { end }),
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
@@ -184,6 +184,9 @@ export function participantRunsAndPlan(plan, opts = {}) {
|
|
|
184
184
|
? {}
|
|
185
185
|
: { startAfterMs: spec.planned.startAfterMs }),
|
|
186
186
|
...(spec.planned.endAfterMs === undefined ? {} : { endAfterMs: spec.planned.endAfterMs }),
|
|
187
|
+
...(spec.planned.maxSessionMs === undefined
|
|
188
|
+
? {}
|
|
189
|
+
: { maxSessionMs: spec.planned.maxSessionMs }),
|
|
187
190
|
})),
|
|
188
191
|
};
|
|
189
192
|
return { runs, participantPlan };
|
|
@@ -209,6 +212,7 @@ export function formatParticipantPlanEntry(entry) {
|
|
|
209
212
|
entry.reasoningEffort ? `effort=${entry.reasoningEffort}` : undefined,
|
|
210
213
|
entry.startAfterMs === undefined ? undefined : `start=+${formatDuration(entry.startAfterMs)}`,
|
|
211
214
|
entry.endAfterMs === undefined ? undefined : `end=+${formatDuration(entry.endAfterMs)}`,
|
|
215
|
+
entry.maxSessionMs === undefined ? undefined : `cap=${formatDuration(entry.maxSessionMs)}`,
|
|
212
216
|
].filter((part) => part !== undefined);
|
|
213
217
|
return `${entry.id}: persona=${entry.persona}${taxonomy.length > 0 ? ` ${taxonomy.join(" ")}` : ""} device=${entry.device} ${entry.resolution[0]}x${entry.resolution[1]} prompt#${entry.instructionDigest}${entry.targetDigest ? ` target#${entry.targetDigest}` : ""}`;
|
|
214
218
|
}
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
// in its order and with its codes, then the plan the run uses. What stays in the route reads
|
|
3
3
|
// external state: committed personas, a rerun's source run, keys, the local agent, subject env
|
|
4
4
|
// values, spend-cap prices and the comms catch.
|
|
5
|
+
import { DESKTOP_FILES_ROUTE_REASON } from "../../study/desktop-files.js";
|
|
5
6
|
import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
6
7
|
import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
|
|
7
8
|
import { isHttpUrl, isLoopbackUrl } from "../../study/parse/subject.js";
|
|
8
9
|
import { studyUrlCredentialReason } from "../../study/url-credentials.js";
|
|
9
10
|
import { subjectStateInvalidReason } from "../../study/parse/subject-state.js";
|
|
10
11
|
import { brainOf, callerBrainOf, planCaps, desktopRequirements, planBase, provisionedSubject, } from "../../study/plan-base.js";
|
|
11
|
-
import { computerUseParticipants, declaredTargets } from "../../study/plan-participants.js";
|
|
12
|
+
import { computerUseParticipants, declaredTargets, } from "../../study/plan-participants.js";
|
|
12
13
|
import { planStarts, windowDeadlineRefusal } from "../../study/arrivals.js";
|
|
13
14
|
import { computerUseParticipantCount, participantCountReason } from "../../study/routing.js";
|
|
14
15
|
import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
|
|
@@ -227,6 +228,40 @@ function startsOf(config, runner, limit, ceilingMs) {
|
|
|
227
228
|
function rerunPlan({ sourceRunId, participantIds, }) {
|
|
228
229
|
return { sourceRunId, ...(participantIds === undefined ? {} : { participantIds }) };
|
|
229
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Why participants[].files cannot run on this runner, or undefined. The parser refuses them off the
|
|
233
|
+
* hosted desktop; a library caller's config skips the parser, and only the hosted desktop copies
|
|
234
|
+
* files.
|
|
235
|
+
*/
|
|
236
|
+
function desktopFilesRunnerReason(desktop, participants) {
|
|
237
|
+
if (desktop === "e2b-desktop" || participants.every((entry) => entry.files === undefined))
|
|
238
|
+
return undefined;
|
|
239
|
+
return `participants[].files: ${DESKTOP_FILES_ROUTE_REASON}. This study runs on a ${desktop} desktop; remove the files or set execution.target: e2b-desktop.`;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Why a participant's time zone cannot run on this runner, or undefined. The zone also sets the
|
|
243
|
+
* zone its browser runs in, which humanish sets on a hosted desktop's browser launch only: the
|
|
244
|
+
* local runtime's guest browser and an in-process caller's browser keep their own.
|
|
245
|
+
*/
|
|
246
|
+
function timeZoneRunnerReason(desktop, participants) {
|
|
247
|
+
const zoned = participants.find((entry) => entry.limits.timeZone !== undefined);
|
|
248
|
+
if (desktop === "e2b-desktop" || zoned === undefined)
|
|
249
|
+
return undefined;
|
|
250
|
+
const declared = `Participant ${zoned.id} has the time zone ${zoned.limits.timeZone}, which sets the zone its browser runs in, and humanish sets a browser's zone only on a hosted desktop.`;
|
|
251
|
+
return desktop === "local-vm"
|
|
252
|
+
? `${declared} This study runs on the local browser runtime. Remove actor.clock and participants[].clock, or set execution.target: e2b-desktop.`
|
|
253
|
+
: `${declared} This study drives your own browser through RunStudyOptions.inProcess. Remove actor.clock and participants[].clock, and set the zone in that browser yourself, or run on a hosted desktop.`;
|
|
254
|
+
}
|
|
255
|
+
/** What the participants declare that this runner cannot do, with its code, or undefined. */
|
|
256
|
+
function runnerRefusal(desktop, participants) {
|
|
257
|
+
const files = desktopFilesRunnerReason(desktop, participants);
|
|
258
|
+
if (files)
|
|
259
|
+
return { code: "HUMANISH_COMPUTER_USE_DESKTOP_FILES_INVALID", message: files };
|
|
260
|
+
const timeZone = timeZoneRunnerReason(desktop, participants);
|
|
261
|
+
if (timeZone)
|
|
262
|
+
return { code: "HUMANISH_COMPUTER_USE_TIME_ZONE_UNSUPPORTED", message: timeZone };
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
230
265
|
/**
|
|
231
266
|
* Plan a computer-use study. It is called for any config handed to the computer-use runner, not only
|
|
232
267
|
* one routeOf sends here, so a config for another route gets this route's refusal.
|
|
@@ -311,6 +346,9 @@ export function planComputerUseStudy(config, input) {
|
|
|
311
346
|
: isLocalBrowserStudy(config)
|
|
312
347
|
? { desktop: "local-vm", brain, participants: [first, ...rest], subject: appUrlSubject }
|
|
313
348
|
: { desktop: "e2b-desktop", brain, participants: [first, ...rest], subject: hosted };
|
|
349
|
+
const unsupported = runnerRefusal(runner.desktop, participants);
|
|
350
|
+
if (unsupported)
|
|
351
|
+
return refuse("in-scope", unsupported.code, unsupported.message, actor);
|
|
314
352
|
const starts = startsOf(config, runner, input.concurrentSandboxes, ceiling.ms);
|
|
315
353
|
if (!starts.ok)
|
|
316
354
|
return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", starts.message, actor);
|
|
@@ -115,6 +115,7 @@ export declare function admitCuaRun(plan: ComputerUsePlan, input: ComputerUseRun
|
|
|
115
115
|
publicRepo: string | undefined;
|
|
116
116
|
localTreeArchive: LocalTreeArchive | undefined;
|
|
117
117
|
localTreeArchiveBuffer: ArrayBuffer | undefined;
|
|
118
|
+
desktopFiles: ReadonlyMap<string, readonly import("../../study/desktop-files.js").DesktopFile[]>;
|
|
118
119
|
};
|
|
119
120
|
}>;
|
|
120
121
|
/** Starts the run and builds what the participants and the finish read: the deps and the bundle base. */
|
|
@@ -24,6 +24,7 @@ import { projectParticipantSubjects } from "./subject-projection.js";
|
|
|
24
24
|
import { participantSubjectEnv, } from "./types.js";
|
|
25
25
|
import { capacityShortfall } from "../../substrates/local/capacity.js";
|
|
26
26
|
import { studyPersonaIds } from "../../study/persona-resolve.js";
|
|
27
|
+
import { readDesktopFiles } from "../../study/desktop-files.js";
|
|
27
28
|
import { refusedResult } from "../../run/study-result.js";
|
|
28
29
|
/** The physical project, bound before any caller hook runs. */
|
|
29
30
|
async function bindProject(cwd) {
|
|
@@ -132,6 +133,9 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
132
133
|
...subjectEnvNames.map((name) => env[name] ?? ""),
|
|
133
134
|
]);
|
|
134
135
|
sanitizeParticipantRuns(participantRuns, secrets.scrub);
|
|
136
|
+
const desktopFiles = await readStudyDesktopFiles(input.cwd, plan, participantRuns, secrets.scrub);
|
|
137
|
+
if (!desktopFiles.ok)
|
|
138
|
+
return refuse("HUMANISH_COMPUTER_USE_DESKTOP_FILES_INVALID", desktopFiles.message, descriptor.id);
|
|
135
139
|
const redactRepoLabel = plan.residual.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
|
|
136
140
|
const publicRepo = subject.kind === "clone" ? (redactRepoLabel ? "repo-01" : subject.repo) : undefined;
|
|
137
141
|
// Key-gating is route-aware: the in-process route uses the caller's own model + executor, and
|
|
@@ -195,9 +199,32 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
195
199
|
publicRepo,
|
|
196
200
|
localTreeArchive,
|
|
197
201
|
localTreeArchiveBuffer,
|
|
202
|
+
desktopFiles: desktopFiles.files,
|
|
198
203
|
},
|
|
199
204
|
};
|
|
200
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Each participant's files, read on dry runs too so a bad file is named before a live run. Paths
|
|
208
|
+
* are relative to the folder that holds the study file; a library caller's config comes from no
|
|
209
|
+
* file, so its paths are relative to the project. An `as` that holds a value `scrub` removes, such
|
|
210
|
+
* as a key from the environment, is refused: the run records `as`, and redacting it would record a
|
|
211
|
+
* different path.
|
|
212
|
+
*/
|
|
213
|
+
async function readStudyDesktopFiles(cwd, plan, participantRuns, scrub) {
|
|
214
|
+
const folder = plan.study?.path === undefined
|
|
215
|
+
? path.resolve(cwd)
|
|
216
|
+
: path.dirname(path.resolve(cwd, plan.study.path));
|
|
217
|
+
const read = await readDesktopFiles(folder, participantRuns.map((run) => run.planned));
|
|
218
|
+
if (!read.ok)
|
|
219
|
+
return read;
|
|
220
|
+
const named = [...read.files].find(([, files]) => files.some((file) => scrub(file.as) !== file.as));
|
|
221
|
+
return named === undefined
|
|
222
|
+
? read
|
|
223
|
+
: {
|
|
224
|
+
ok: false,
|
|
225
|
+
message: `Participant ${named[0]}'s files name an as path that holds a key or another value from this run's environment, which the run would record. Rename the file.`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
201
228
|
/**
|
|
202
229
|
* Whether a local study's desktops at once fit the local runtime. Read before any desktop: a
|
|
203
230
|
* desktop past the VM's memory is killed mid-session, after its participant has spent.
|
|
@@ -317,6 +344,7 @@ function cuaParticipantDeps(plan, input, admitted, run) {
|
|
|
317
344
|
createDesktop: input.localVm?.desktop,
|
|
318
345
|
...callerDriving(input, config),
|
|
319
346
|
localTreeArchiveBuffer: admitted.localTreeArchiveBuffer,
|
|
347
|
+
desktopFiles: admitted.desktopFiles.size === 0 ? undefined : admitted.desktopFiles,
|
|
320
348
|
externalComms: externalCommsConfig === undefined || externalCommsEmail === undefined
|
|
321
349
|
? undefined
|
|
322
350
|
: { email: externalCommsEmail, inboxUrl: externalInboxUrl(externalCommsConfig) },
|
|
@@ -4,7 +4,7 @@ import type { DesktopBrowserEvidence } from "../../substrates/e2b/desktop-browse
|
|
|
4
4
|
import type { ActorPersonaRef } from "../../actors/contract.js";
|
|
5
5
|
import type { CuaLoopResult } from "../../actors/computer-use/loop.js";
|
|
6
6
|
import { type BundleRun, type RunBundle, type RunFeedbackCandidate, type RunProviderResource, type RunSubjectProvenance } from "../../run/bundle.js";
|
|
7
|
-
import { type RunDesktopGeometry, type RunStream } from "../../run/streams.js";
|
|
7
|
+
import { type RunDesktopFile, type RunDesktopGeometry, type RunStream } from "../../run/streams.js";
|
|
8
8
|
import { type Verdict } from "../../run/judge.js";
|
|
9
9
|
import { type DesktopUsage } from "../../run/cost-summary.js";
|
|
10
10
|
import type { HostSuspensionReading } from "../../run/host-suspension.js";
|
|
@@ -41,6 +41,10 @@ export declare function buildSingleParticipantBundle(args: {
|
|
|
41
41
|
/** Runtime screen/window/viewport evidence. `viewport` inside this object must be measured. */
|
|
42
42
|
desktopGeometry?: RunDesktopGeometry;
|
|
43
43
|
recording?: RunDesktopRecording;
|
|
44
|
+
/** participants[].files confirmed on the participant's desktop. */
|
|
45
|
+
desktopFiles?: RunDesktopFile[];
|
|
46
|
+
/** The participant's time zone, which its turns and its hosted browser used. */
|
|
47
|
+
timeZone?: string;
|
|
44
48
|
/** Device-preset touch metadata echoed on the measured stream viewport (a prompt signal on
|
|
45
49
|
* this route, never a rendered claim); the measured width/height/DPR stay authoritative. */
|
|
46
50
|
isMobile?: boolean;
|
|
@@ -151,6 +151,8 @@ function singleStream(args, view) {
|
|
|
151
151
|
},
|
|
152
152
|
}),
|
|
153
153
|
...(desktopGeometry === undefined ? {} : { desktopGeometry }),
|
|
154
|
+
...(args.desktopFiles === undefined ? {} : { desktopFiles: args.desktopFiles }),
|
|
155
|
+
...(args.timeZone === undefined ? {} : { timeZone: args.timeZone }),
|
|
154
156
|
...(args.recording === undefined ? {} : { recording: args.recording }),
|
|
155
157
|
ui: {
|
|
156
158
|
route: publicAppUrl,
|