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.
Files changed (104) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/actors/computer-use/actor.d.ts +2 -0
  3. package/dist/actors/computer-use/actor.js +1 -0
  4. package/dist/actors/computer-use/loop/clock.d.ts +9 -8
  5. package/dist/actors/computer-use/loop/clock.js +27 -17
  6. package/dist/actors/computer-use/loop/debrief.d.ts +1 -1
  7. package/dist/actors/computer-use/loop/debrief.js +15 -5
  8. package/dist/actors/computer-use/loop/ending.d.ts +8 -5
  9. package/dist/actors/computer-use/loop/ending.js +8 -5
  10. package/dist/actors/computer-use/loop/observation.d.ts +6 -0
  11. package/dist/actors/computer-use/loop/observation.js +19 -1
  12. package/dist/actors/computer-use/loop/session.d.ts +4 -2
  13. package/dist/actors/computer-use/loop/session.js +4 -1
  14. package/dist/actors/computer-use/loop/trace.js +4 -2
  15. package/dist/actors/computer-use/loop/types.d.ts +22 -8
  16. package/dist/actors/computer-use/loop.js +5 -5
  17. package/dist/actors/contract.d.ts +31 -8
  18. package/dist/actors/contract.js +3 -0
  19. package/dist/actors/stop-cause.js +1 -0
  20. package/dist/cli/commands/study.js +8 -0
  21. package/dist/evidence/redaction.d.ts +1 -0
  22. package/dist/evidence/redaction.js +1 -0
  23. package/dist/evidence/trace-page.d.ts +6 -0
  24. package/dist/evidence/trace-page.js +90 -0
  25. package/dist/observer/feed-cursors.d.ts +14 -0
  26. package/dist/observer/feed-cursors.js +61 -0
  27. package/dist/observer/participant-items.d.ts +7 -0
  28. package/dist/observer/pinned-files.d.ts +2 -1
  29. package/dist/observer/pinned-files.js +2 -2
  30. package/dist/observer/run-routes.d.ts +1 -1
  31. package/dist/observer/run-routes.js +38 -17
  32. package/dist/observer-app.html +7 -7
  33. package/dist/routes/computer-use/bundle.js +4 -0
  34. package/dist/routes/computer-use/diagnostics.d.ts +1 -1
  35. package/dist/routes/computer-use/diagnostics.js +3 -3
  36. package/dist/routes/computer-use/e2b-desktop/desktop.js +2 -1
  37. package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +6 -0
  38. package/dist/routes/computer-use/e2b-desktop/prepare.js +47 -1
  39. package/dist/routes/computer-use/e2b-desktop/start.d.ts +2 -1
  40. package/dist/routes/computer-use/e2b-desktop/start.js +15 -3
  41. package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -1
  42. package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
  43. package/dist/routes/computer-use/fanout-records.js +4 -0
  44. package/dist/routes/computer-use/participant-deps.d.ts +1 -1
  45. package/dist/routes/computer-use/participant-desktop.d.ts +1 -1
  46. package/dist/routes/computer-use/participant-model.js +16 -6
  47. package/dist/routes/computer-use/participant-runs.d.ts +2 -2
  48. package/dist/routes/computer-use/participant-runs.js +11 -7
  49. package/dist/routes/computer-use/plan.js +39 -1
  50. package/dist/routes/computer-use/setup.d.ts +1 -0
  51. package/dist/routes/computer-use/setup.js +28 -0
  52. package/dist/routes/computer-use/single-bundle.d.ts +5 -1
  53. package/dist/routes/computer-use/single-bundle.js +2 -0
  54. package/dist/routes/computer-use/types.d.ts +16 -10
  55. package/dist/routes/shared-world/bundle-records.js +3 -0
  56. package/dist/routes/shared-world/participant-specs.js +3 -2
  57. package/dist/routes/shared-world/plan.js +5 -0
  58. package/dist/run/contained-output.d.ts +7 -3
  59. package/dist/run/contained-output.js +11 -7
  60. package/dist/run/judge.d.ts +7 -4
  61. package/dist/run/judge.js +13 -8
  62. package/dist/run/live-items.d.ts +19 -0
  63. package/dist/run/live-items.js +28 -8
  64. package/dist/run/stream-trace.d.ts +25 -4
  65. package/dist/run/stream-trace.js +55 -17
  66. package/dist/run/streams.d.ts +16 -0
  67. package/dist/study/arrivals.d.ts +32 -14
  68. package/dist/study/arrivals.js +47 -21
  69. package/dist/study/bundle-estimate.d.ts +6 -5
  70. package/dist/study/bundle-estimate.js +12 -7
  71. package/dist/study/desktop-files.d.ts +37 -0
  72. package/dist/study/desktop-files.js +174 -0
  73. package/dist/study/keys.d.ts +8 -0
  74. package/dist/study/keys.js +3 -0
  75. package/dist/study/migrate/v2.js +12 -3
  76. package/dist/study/parse/actors.d.ts +1 -1
  77. package/dist/study/parse/actors.js +28 -20
  78. package/dist/study/parse/clock.d.ts +18 -0
  79. package/dist/study/parse/clock.js +36 -0
  80. package/dist/study/plan-participants.d.ts +19 -5
  81. package/dist/study/plan-participants.js +17 -1
  82. package/dist/study/plan.js +24 -13
  83. package/dist/study/preflight.js +4 -0
  84. package/dist/study/types.d.ts +34 -5
  85. package/dist/study/validation.js +3 -0
  86. package/dist/study/warnings.d.ts +2 -2
  87. package/dist/study/warnings.js +7 -0
  88. package/dist/subject/local-tree-archive.d.ts +7 -0
  89. package/dist/subject/local-tree-archive.js +12 -0
  90. package/dist/substrates/e2b/desktop-browser.d.ts +4 -1
  91. package/dist/substrates/e2b/desktop-browser.js +6 -2
  92. package/dist/substrates/e2b/desktop-clock.d.ts +10 -0
  93. package/dist/substrates/e2b/desktop-clock.js +29 -0
  94. package/dist/substrates/e2b/sdk.d.ts +6 -0
  95. package/dist/verify/pages.d.ts +7 -0
  96. package/dist/verify/pages.js +50 -0
  97. package/dist/verify/verify.js +10 -3
  98. package/docs/architecture/actor-contract.md +2 -0
  99. package/docs/architecture/external-public-shared-world.md +4 -1
  100. package/docs/architecture/observer.md +10 -1
  101. package/docs/contracts/policy.md +7 -0
  102. package/docs/contracts/run-bundle.md +31 -1
  103. package/docs/contracts/schemas.md +72 -3
  104. 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 participants ended
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
- const browserLaunch = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, residual.execution?.desktop?.browser, [...emulationFlags, ...(mediaEvidence?.flags ?? [])], state.speech?.env ?? state.recording?.env);
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 scheduled end on the run's clock. A participant
311
- * with an end runs its session on the run's clock, so the end and the schedule read one clock.
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 { scheduledEnd } = deps;
315
- if (timeZone === undefined && scheduledEnd === 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
- ...(scheduledEnd === undefined ? {} : { end: scheduledEnd }),
330
+ ...(end === undefined ? {} : { end }),
321
331
  },
322
- ...(scheduledEnd === undefined ? {} : { now: deps.now }),
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, that end on the run's clock (`scheduledAt` is when it was
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" | "scheduledEnd">;
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, scheduledEndAt, } from "../../study/arrivals.js";
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 its
50
- * window plus room for the closing report (participantSessionMs), with the same setup and
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, that end on the run's clock (`scheduledAt` is when it was
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 = scheduledEndAt(participant, scheduledAt);
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 ? {} : { scheduledEnd: end }),
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,