humanish 0.125.0 → 0.127.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 +29 -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 +10 -5
- package/dist/actors/computer-use/loop/ending.js +12 -7
- 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/computer-use/openai-provider.js +1 -1
- package/dist/actors/contract.d.ts +31 -8
- package/dist/actors/contract.js +3 -0
- package/dist/actors/goal-source.d.ts +32 -2
- package/dist/actors/goal-source.js +41 -2
- package/dist/actors/stop-cause.js +1 -0
- package/dist/analysis/evidence-sources.js +2 -2
- package/dist/analysis/types.d.ts +2 -2
- package/dist/analysis/validation.js +2 -1
- 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/data.js +8 -5
- 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-bundle.js +3 -0
- package/dist/routes/computer-use/fanout-records.js +5 -1
- 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-execution.js +6 -2
- package/dist/routes/computer-use/participant-facts.js +4 -1
- package/dist/routes/computer-use/participant-model.js +16 -6
- package/dist/routes/computer-use/participant-runs.d.ts +10 -10
- package/dist/routes/computer-use/participant-runs.js +54 -84
- package/dist/routes/computer-use/plan.d.ts +2 -0
- package/dist/routes/computer-use/plan.js +65 -17
- package/dist/routes/computer-use/rerun-selection.js +8 -5
- package/dist/routes/computer-use/setup.d.ts +1 -0
- package/dist/routes/computer-use/setup.js +29 -1
- package/dist/routes/computer-use/single-bundle.d.ts +5 -1
- package/dist/routes/computer-use/single-bundle.js +11 -1
- package/dist/routes/computer-use/types.d.ts +18 -13
- package/dist/routes/computer-use/types.js +0 -3
- package/dist/routes/shared-world/bundle-records.js +4 -1
- package/dist/routes/shared-world/external-public.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +3 -2
- package/dist/routes/shared-world/plan.d.ts +2 -0
- package/dist/routes/shared-world/plan.js +8 -3
- package/dist/routes/shared-world/provisioned.js +1 -1
- package/dist/run/bundle.d.ts +11 -1
- package/dist/run/contained-output.d.ts +7 -3
- package/dist/run/contained-output.js +11 -7
- package/dist/run/judge.d.ts +14 -4
- package/dist/run/judge.js +17 -12
- package/dist/run/live-items.d.ts +19 -0
- package/dist/run/live-items.js +28 -8
- package/dist/run/notes.js +1 -1
- package/dist/run/outcomes.d.ts +8 -2
- package/dist/run/outcomes.js +56 -14
- 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 +105 -34
- package/dist/study/arrivals.js +179 -74
- package/dist/study/bundle-estimate.d.ts +12 -10
- package/dist/study/bundle-estimate.js +138 -72
- 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 -10
- package/dist/study/plan-participants.js +13 -9
- package/dist/study/plan-types.d.ts +6 -1
- package/dist/study/plan.d.ts +2 -1
- package/dist/study/plan.js +28 -15
- package/dist/study/preflight.js +4 -0
- package/dist/study/types.d.ts +35 -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/lifetime.d.ts +22 -8
- package/dist/substrates/e2b/lifetime.js +54 -10
- 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 +93 -11
- package/package.json +2 -2
|
@@ -9,6 +9,8 @@ import { invalid, str } from "./values.js";
|
|
|
9
9
|
import { isRecord } from "../../run/type-guards.js";
|
|
10
10
|
import { scanEncodedText } from "../../evidence/encoded-text.js";
|
|
11
11
|
import { isStartAfterMs, startAfterMessage, windowReason } from "../arrivals.js";
|
|
12
|
+
import { parseParticipantFiles } from "../desktop-files.js";
|
|
13
|
+
import { parseClock } from "./clock.js";
|
|
12
14
|
// A participant id interpolates into its evidence paths (screenshots/<id>/, actors/<id>.json), so
|
|
13
15
|
// it must be a public-safe path token, same shape as a study id.
|
|
14
16
|
export const PARTICIPANT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
@@ -78,12 +80,11 @@ export function parseActorFields(entry, path) {
|
|
|
78
80
|
actor.maxWaitMs = entry.maxWaitMs;
|
|
79
81
|
if (typeof entry.idleWaitMs === "number")
|
|
80
82
|
actor.idleWaitMs = entry.idleWaitMs;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
actor.clock =
|
|
86
|
-
}
|
|
83
|
+
const clock = parseClock(entry.clock, `${path}.clock`);
|
|
84
|
+
if (!clock.ok)
|
|
85
|
+
return clock;
|
|
86
|
+
if (clock.value !== undefined)
|
|
87
|
+
actor.clock = clock.value;
|
|
87
88
|
const localAgent = str(entry.localAgent);
|
|
88
89
|
if (entry.localAgent !== undefined) {
|
|
89
90
|
if (localAgent !== "codex" && localAgent !== "claude") {
|
|
@@ -118,7 +119,7 @@ export function parseActorFields(entry, path) {
|
|
|
118
119
|
* Parse a participant list into a fan-out roster (computer-use E2B route): `participants` in a
|
|
119
120
|
* study, `actors[0].lanes` in the v2 file migrate reads, each named in messages by `paths`.
|
|
120
121
|
* Structural only: each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?,
|
|
121
|
-
* instruction?, target?, entry?, host?, startAfterMs?, endAfterMs?, windDownMs?, idleWaitMs?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
122
|
+
* instruction?, target?, entry?, host?, startAfterMs?, endAfterMs?, windDownMs?, idleWaitMs?, maxSessionMs?, clock?, reasoningEffort?, stopWhen?, dwell?, files? }`.
|
|
122
123
|
* Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
|
|
123
124
|
* must be public-safe tokens; a device must be a known preset name. The
|
|
124
125
|
* route-scoped cross-validation (device XOR raw resolution, targets, the participant bound) runs in parseStudy
|
|
@@ -129,13 +130,13 @@ export function parseParticipantEntries(raw, paths) {
|
|
|
129
130
|
return { ok: true, value: undefined };
|
|
130
131
|
}
|
|
131
132
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
132
|
-
return invalid(`${paths.list} must be a non-empty array of participant objects ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, startAfterMs?, endAfterMs?, windDownMs?, idleWaitMs?, reasoningEffort?, stopWhen?, dwell? }) when set.`);
|
|
133
|
+
return invalid(`${paths.list} must be a non-empty array of participant objects ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, startAfterMs?, endAfterMs?, windDownMs?, idleWaitMs?, maxSessionMs?, clock?, reasoningEffort?, stopWhen?, dwell?, files? }) when set.`);
|
|
133
134
|
}
|
|
134
135
|
const entries = [];
|
|
135
136
|
const seenIds = new Set();
|
|
136
137
|
for (const [entryIndex, entry] of raw.entries()) {
|
|
137
138
|
if (!isRecord(entry)) {
|
|
138
|
-
return invalid(`${paths.entry(entryIndex)} must be an object ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, startAfterMs?, endAfterMs?, windDownMs?, idleWaitMs?, reasoningEffort?, stopWhen?, dwell? }).`);
|
|
139
|
+
return invalid(`${paths.entry(entryIndex)} must be an object ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, startAfterMs?, endAfterMs?, windDownMs?, idleWaitMs?, maxSessionMs?, clock?, reasoningEffort?, stopWhen?, dwell?, files? }).`);
|
|
139
140
|
}
|
|
140
141
|
const parsedEntry = {};
|
|
141
142
|
const id = str(entry.id);
|
|
@@ -205,6 +206,11 @@ export function parseParticipantEntries(raw, paths) {
|
|
|
205
206
|
return invalid(credential);
|
|
206
207
|
parsedEntry.target = target;
|
|
207
208
|
}
|
|
209
|
+
const files = parseParticipantFiles(entry.files, `${paths.entry(entryIndex)}.files`);
|
|
210
|
+
if (!files.ok)
|
|
211
|
+
return files;
|
|
212
|
+
if (files.value !== undefined)
|
|
213
|
+
parsedEntry.files = files.value;
|
|
208
214
|
// `entry` is shape-captured here; the same-origin-with-serve.url check needs serve context, so
|
|
209
215
|
// it runs in sharedWorldValidationReason (where the route + serve.url are known).
|
|
210
216
|
const entryPath = str(entry.entry);
|
|
@@ -228,11 +234,18 @@ export function parseParticipantEntries(raw, paths) {
|
|
|
228
234
|
return invalid(startAfterMessage(`${paths.entry(entryIndex)}.startAfterMs`));
|
|
229
235
|
parsedEntry.startAfterMs = entry.startAfterMs;
|
|
230
236
|
}
|
|
231
|
-
const window = windowReason({
|
|
237
|
+
const window = windowReason({
|
|
238
|
+
...parsedEntry,
|
|
239
|
+
endAfterMs: entry.endAfterMs,
|
|
240
|
+
maxSessionMs: entry.maxSessionMs,
|
|
241
|
+
windDownMs: entry.windDownMs,
|
|
242
|
+
}, paths.entry(entryIndex));
|
|
232
243
|
if (window !== undefined)
|
|
233
244
|
return invalid(window);
|
|
234
245
|
if (typeof entry.endAfterMs === "number")
|
|
235
246
|
parsedEntry.endAfterMs = entry.endAfterMs;
|
|
247
|
+
if (typeof entry.maxSessionMs === "number")
|
|
248
|
+
parsedEntry.maxSessionMs = entry.maxSessionMs;
|
|
236
249
|
if (typeof entry.windDownMs === "number")
|
|
237
250
|
parsedEntry.windDownMs = entry.windDownMs;
|
|
238
251
|
// Checked against actor.maxWaitMs once the actor is read (waitLimitValidationReason).
|
|
@@ -241,6 +254,11 @@ export function parseParticipantEntries(raw, paths) {
|
|
|
241
254
|
return invalid(idleWait);
|
|
242
255
|
if (typeof entry.idleWaitMs === "number")
|
|
243
256
|
parsedEntry.idleWaitMs = entry.idleWaitMs;
|
|
257
|
+
const clock = parseClock(entry.clock, `${paths.entry(entryIndex)}.clock`);
|
|
258
|
+
if (!clock.ok)
|
|
259
|
+
return clock;
|
|
260
|
+
if (clock.value !== undefined)
|
|
261
|
+
parsedEntry.clock = clock.value;
|
|
244
262
|
entries.push(parsedEntry);
|
|
245
263
|
}
|
|
246
264
|
return { ok: true, value: entries };
|
|
@@ -412,13 +430,3 @@ function parseEntryMetadata(raw, field) {
|
|
|
412
430
|
}
|
|
413
431
|
return { ok: true, value };
|
|
414
432
|
}
|
|
415
|
-
/** Whether Intl knows `name` as a time zone. */
|
|
416
|
-
function isTimeZone(name) {
|
|
417
|
-
try {
|
|
418
|
-
new Intl.DateTimeFormat("en-GB", { timeZone: name });
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
catch {
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StudyParseFailure } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A participant's clock, `actor.clock` or `participants[].clock`: `{ timeZone }`. Each turn gives the
|
|
4
|
+
* participant the time in the zone, and a hosted desktop launches its browser with TZ set to it.
|
|
5
|
+
* Chrome reads TZ through ICU, which takes only a tz database name spelled with its capitals: on
|
|
6
|
+
* TZ=+05:00 it runs in UTC, and on TZ=europe/london it reports Etc/Unknown, though Intl here
|
|
7
|
+
* accepts both. So the zone must be a name Intl knows that is not a UTC offset and is spelled with
|
|
8
|
+
* capitals. Intl gives no case-sensitive check: it spells europe/belfast as Europe/London, and its
|
|
9
|
+
* supportedValuesOf omits aliases such as Europe/Belfast. So the rule is the one every tz database name
|
|
10
|
+
* keeps: each part between slashes starts with a capital, and a case variant of Intl's spelling is
|
|
11
|
+
* refused. A miscased letter after the first in a part of an alias (Europe/BELFAST) still passes.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseClock(raw: unknown, path: string): {
|
|
14
|
+
ok: true;
|
|
15
|
+
value: {
|
|
16
|
+
timeZone: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
} | StudyParseFailure;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isRecord } from "../../run/type-guards.js";
|
|
2
|
+
import { invalid } from "./values.js";
|
|
3
|
+
/**
|
|
4
|
+
* A participant's clock, `actor.clock` or `participants[].clock`: `{ timeZone }`. Each turn gives the
|
|
5
|
+
* participant the time in the zone, and a hosted desktop launches its browser with TZ set to it.
|
|
6
|
+
* Chrome reads TZ through ICU, which takes only a tz database name spelled with its capitals: on
|
|
7
|
+
* TZ=+05:00 it runs in UTC, and on TZ=europe/london it reports Etc/Unknown, though Intl here
|
|
8
|
+
* accepts both. So the zone must be a name Intl knows that is not a UTC offset and is spelled with
|
|
9
|
+
* capitals. Intl gives no case-sensitive check: it spells europe/belfast as Europe/London, and its
|
|
10
|
+
* supportedValuesOf omits aliases such as Europe/Belfast. So the rule is the one every tz database name
|
|
11
|
+
* keeps: each part between slashes starts with a capital, and a case variant of Intl's spelling is
|
|
12
|
+
* refused. A miscased letter after the first in a part of an alias (Europe/BELFAST) still passes.
|
|
13
|
+
*/
|
|
14
|
+
export function parseClock(raw, path) {
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return { ok: true, value: undefined };
|
|
17
|
+
const timeZone = isRecord(raw) ? raw.timeZone : undefined;
|
|
18
|
+
const resolved = typeof timeZone === "string" ? intlSpelling(timeZone) : undefined;
|
|
19
|
+
if (typeof timeZone !== "string" || resolved === undefined)
|
|
20
|
+
return invalid(`${path} must be { timeZone }, with timeZone a time zone name such as "Europe/London": the participant's local time, which each turn tells it and its hosted browser runs in.`);
|
|
21
|
+
if (/^[+-]/.test(resolved))
|
|
22
|
+
return invalid(`${path}.timeZone "${timeZone}" is a UTC offset, and a browser's time zone takes only a time zone name. Write the name of a place in that zone, such as "Asia/Karachi" for +05:00.`);
|
|
23
|
+
if (/(^|\/)[^A-Z]/.test(timeZone) ||
|
|
24
|
+
(resolved !== timeZone && resolved.toLowerCase() === timeZone.toLowerCase()))
|
|
25
|
+
return invalid(`${path}.timeZone "${timeZone}" must be written with the capitals of a time zone name, such as "${resolved}" for this zone: the browser reads a time zone name only with its capitals.`);
|
|
26
|
+
return { ok: true, value: { timeZone } };
|
|
27
|
+
}
|
|
28
|
+
/** How Intl spells the time zone `name`, or undefined when Intl does not know it. */
|
|
29
|
+
function intlSpelling(name) {
|
|
30
|
+
try {
|
|
31
|
+
return new Intl.DateTimeFormat("en-GB", { timeZone: name }).resolvedOptions().timeZone;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { type ParticipantSchedule } from "./arrivals.js";
|
|
1
2
|
import type { DwellWindow, StopWhen } from "../actors/stop-conditions.js";
|
|
2
3
|
import type { ReasoningEffort } from "../actors/reasoning-effort.js";
|
|
3
4
|
import { type DevicePreset } from "./device-presets.js";
|
|
4
5
|
import type { StudyTask } from "./tasks.js";
|
|
5
|
-
import type { StudyConfig } from "./types.js";
|
|
6
|
+
import type { StudyConfig, StudyParticipantFile } from "./types.js";
|
|
6
7
|
/** Who one participant is. Every route with participants carries this record. */
|
|
7
8
|
export interface Participant {
|
|
8
9
|
/** Declared roster id, else `lane-NN` (independent participants) or `role-NN` (shared-world participants). */
|
|
@@ -24,7 +25,7 @@ export interface Participant {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
/** A participant at a desktop: computer use and shared world. */
|
|
27
|
-
interface DesktopParticipant extends Participant {
|
|
28
|
+
export interface DesktopParticipant extends Participant {
|
|
28
29
|
readonly device: {
|
|
29
30
|
readonly name: string;
|
|
30
31
|
readonly preset: DevicePreset;
|
|
@@ -43,21 +44,25 @@ interface DesktopParticipant extends Participant {
|
|
|
43
44
|
* while the participant only waits.
|
|
44
45
|
*/
|
|
45
46
|
readonly idleWaitMs?: number;
|
|
46
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The participant's clock.timeZone, else actor.clock.timeZone: each turn tells it the time in
|
|
49
|
+
* this zone, and a hosted desktop runs its browser in it.
|
|
50
|
+
*/
|
|
47
51
|
readonly timeZone?: string;
|
|
48
52
|
};
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
readonly windDownMs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* When it starts and, on computer use, how its session ends: its startAfterMs, and its endAfterMs
|
|
55
|
+
* or maxSessionMs with its wind-down.
|
|
56
|
+
*/
|
|
57
|
+
readonly schedule: ParticipantSchedule;
|
|
55
58
|
}
|
|
56
59
|
/** Only independent computer-use participants consume a task protocol and their own target. */
|
|
57
60
|
export interface ComputerUseParticipant extends DesktopParticipant {
|
|
58
61
|
readonly tasks?: readonly StudyTask[];
|
|
59
62
|
/** app-url fan-out only: this participant's own entry URL; absent means the subject URL. */
|
|
60
63
|
readonly targetUrl?: string;
|
|
64
|
+
/** Hosted desktop only: the files copied onto it before the participant starts. */
|
|
65
|
+
readonly files?: readonly StudyParticipantFile[];
|
|
61
66
|
}
|
|
62
67
|
/** A participant on a provisioned plane: an optional same-origin path under `serve.url`. */
|
|
63
68
|
export interface ProvisionedParticipant extends DesktopParticipant {
|
|
@@ -93,4 +98,8 @@ export declare function sharedWorldParticipants(config: StudyConfig): SharedWorl
|
|
|
93
98
|
export declare function declaredParticipantIds(config: StudyConfig): string[];
|
|
94
99
|
/** The entry URLs roster entries declare in place of the subject URL (computer use, app-url). */
|
|
95
100
|
export declare function declaredTargets(config: StudyConfig): string[];
|
|
96
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Each participant's time zone, as `study check` and `study show` print it, in roster order with
|
|
103
|
+
* `none` for a participant without one. Undefined when no participant has a zone.
|
|
104
|
+
*/
|
|
105
|
+
export declare function describeTimeZones(participants: readonly DesktopParticipant[]): string | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Who takes part in a study: one record per participant, built from the declared roster or a count.
|
|
2
2
|
// Computer-use and shared-world participants share the desktop fields; each kind adds its own.
|
|
3
|
-
import {
|
|
3
|
+
import { participantSchedule } from "./arrivals.js";
|
|
4
4
|
import { resolveParticipantDevice } from "./device-presets.js";
|
|
5
5
|
import { computerUseParticipantCount, isSharedWorldComposition, participantIdAt, } from "./routing.js";
|
|
6
6
|
import { participantList, declaredParticipantCount, participantInstruction, } from "./study-fields.js";
|
|
@@ -15,7 +15,7 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
15
15
|
const maxOutputTokens = actor?.maxOutputTokens;
|
|
16
16
|
const maxWaitMs = actor?.maxWaitMs;
|
|
17
17
|
const idleWaitMs = entry?.idleWaitMs ?? actor?.idleWaitMs;
|
|
18
|
-
const timeZone = actor?.clock?.timeZone;
|
|
18
|
+
const timeZone = entry?.clock?.timeZone ?? actor?.clock?.timeZone;
|
|
19
19
|
return {
|
|
20
20
|
id: participantIdAt(index, entry?.id, kind),
|
|
21
21
|
index,
|
|
@@ -39,13 +39,7 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
39
39
|
...(idleWaitMs === undefined ? {} : { idleWaitMs }),
|
|
40
40
|
...(timeZone === undefined ? {} : { timeZone }),
|
|
41
41
|
},
|
|
42
|
-
|
|
43
|
-
...(entry?.endAfterMs === undefined
|
|
44
|
-
? {}
|
|
45
|
-
: {
|
|
46
|
-
endAfterMs: entry.endAfterMs,
|
|
47
|
-
windDownMs: entry.windDownMs ?? DEFAULT_WIND_DOWN_MS,
|
|
48
|
-
}),
|
|
42
|
+
schedule: participantSchedule(entry ?? {}),
|
|
49
43
|
};
|
|
50
44
|
}
|
|
51
45
|
/**
|
|
@@ -62,6 +56,7 @@ export function computerUseParticipants(config, countOverride) {
|
|
|
62
56
|
...desktopParticipant(config, entry, index, "lane", focus),
|
|
63
57
|
...(actor?.tasks === undefined ? {} : { tasks: actor.tasks }),
|
|
64
58
|
...(entry?.target === undefined ? {} : { targetUrl: entry.target }),
|
|
59
|
+
...(entry?.files === undefined ? {} : { files: entry.files }),
|
|
65
60
|
};
|
|
66
61
|
});
|
|
67
62
|
}
|
|
@@ -102,3 +97,12 @@ export function declaredTargets(config) {
|
|
|
102
97
|
.map((entry) => entry.target)
|
|
103
98
|
.filter((target) => target !== undefined);
|
|
104
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Each participant's time zone, as `study check` and `study show` print it, in roster order with
|
|
102
|
+
* `none` for a participant without one. Undefined when no participant has a zone.
|
|
103
|
+
*/
|
|
104
|
+
export function describeTimeZones(participants) {
|
|
105
|
+
if (participants.every((participant) => participant.limits.timeZone === undefined))
|
|
106
|
+
return undefined;
|
|
107
|
+
return `${participants.map((participant) => `${participant.id} ${participant.limits.timeZone ?? "none"}`).join(", ")}.`;
|
|
108
|
+
}
|
|
@@ -163,8 +163,13 @@ export interface ComputerUsePlan extends PlanBase {
|
|
|
163
163
|
/** The registered computer-use actor id. */
|
|
164
164
|
readonly actor: string;
|
|
165
165
|
readonly runner: ComputerUseRunner;
|
|
166
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* How many participants run at once: the declared cap clamped to [1, participants], else all of
|
|
168
|
+
* them, lowered to what the E2B plan and HUMANISH_CUA_MAX_CONCURRENCY allow (participantsAtOnce).
|
|
169
|
+
*/
|
|
167
170
|
readonly concurrency: number;
|
|
171
|
+
/** The count before HUMANISH_CUA_MAX_CONCURRENCY lowered it, when it did. */
|
|
172
|
+
readonly envLoweredConcurrencyFrom?: number;
|
|
168
173
|
/** When the participants start and how many run at once, with every session at its budget. */
|
|
169
174
|
readonly arrivals: ArrivalPlan;
|
|
170
175
|
readonly sessionBudgetMs: number;
|
package/dist/study/plan.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ export declare function resolveStudyDryRun(config: StudyConfig, override: boolea
|
|
|
8
8
|
/**
|
|
9
9
|
* The plan a study runs under, built without reading files or the network. From the environment
|
|
10
10
|
* (options.env, else process.env, as the routes read it) it reads the operator's E2B plan limits,
|
|
11
|
-
* the sandbox ceiling and the concurrent sandboxes, which no
|
|
11
|
+
* the sandbox ceiling and the concurrent sandboxes, and HUMANISH_CUA_MAX_CONCURRENCY, which no
|
|
12
|
+
* study file can know. Each route's
|
|
12
13
|
* planner makes every refusal that route makes, in the route's order and with its codes and
|
|
13
14
|
* messages; the route's exported runner calls the same planner.
|
|
14
15
|
*/
|
package/dist/study/plan.js
CHANGED
|
@@ -6,7 +6,7 @@ import { callerDrivingOf, planComputerUseStudy } from "../routes/computer-use/pl
|
|
|
6
6
|
import { injectedBrowser, planScriptedStudy } from "../routes/scripted/plan.js";
|
|
7
7
|
import { planSharedWorldStudy } from "../routes/shared-world/plan.js";
|
|
8
8
|
import { planTerminalStudy } from "../routes/terminal/plan.js";
|
|
9
|
-
import { concurrentSandboxes, sandboxCeiling, } from "../substrates/e2b/lifetime.js";
|
|
9
|
+
import { concurrencyCap, concurrentSandboxes, sandboxCeiling, } from "../substrates/e2b/lifetime.js";
|
|
10
10
|
import { localBrowserDefaults } from "../substrates/local/runtime-config.js";
|
|
11
11
|
import { THIS_REPO_DRY_RUN_ONLY } from "./composition-rules.js";
|
|
12
12
|
import { bundleLimitReason, estimateBundle } from "./bundle-estimate.js";
|
|
@@ -68,7 +68,8 @@ function planPreview(config, options, input) {
|
|
|
68
68
|
/**
|
|
69
69
|
* The plan a study runs under, built without reading files or the network. From the environment
|
|
70
70
|
* (options.env, else process.env, as the routes read it) it reads the operator's E2B plan limits,
|
|
71
|
-
* the sandbox ceiling and the concurrent sandboxes, which no
|
|
71
|
+
* the sandbox ceiling and the concurrent sandboxes, and HUMANISH_CUA_MAX_CONCURRENCY, which no
|
|
72
|
+
* study file can know. Each route's
|
|
72
73
|
* planner makes every refusal that route makes, in the route's order and with its codes and
|
|
73
74
|
* messages; the route's exported runner calls the same planner.
|
|
74
75
|
*/
|
|
@@ -79,6 +80,7 @@ export function planStudy(config, options, deps = {}) {
|
|
|
79
80
|
const e2b = {
|
|
80
81
|
sandboxCeiling: sandboxCeiling(env),
|
|
81
82
|
concurrentSandboxes: concurrentSandboxes(env),
|
|
83
|
+
concurrencyCap: concurrencyCap(env),
|
|
82
84
|
};
|
|
83
85
|
const result = planRoute(routeOf(config), study, options, input, deps, e2b);
|
|
84
86
|
if (!result.ok)
|
|
@@ -86,12 +88,11 @@ export function planStudy(config, options, deps = {}) {
|
|
|
86
88
|
const bundle = plannedBundle(result.plan);
|
|
87
89
|
// The manifest the CLI resolved enters the plan here and nowhere else; the routes read
|
|
88
90
|
// plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
|
|
89
|
-
const closing = closingReportWarning(result.plan);
|
|
90
91
|
const warnings = [
|
|
91
92
|
...recordedStudyWarnings(study),
|
|
92
93
|
...(result.plan.warnings ?? []),
|
|
93
94
|
...(bundle.warning === undefined ? [] : [bundle.warning]),
|
|
94
|
-
...(
|
|
95
|
+
...closingReportWarnings(result.plan),
|
|
95
96
|
];
|
|
96
97
|
const plan = {
|
|
97
98
|
...result.plan,
|
|
@@ -115,21 +116,33 @@ function plannedBundle(plan) {
|
|
|
115
116
|
return warning === undefined ? { estimate } : { estimate, warning };
|
|
116
117
|
}
|
|
117
118
|
/**
|
|
118
|
-
* The
|
|
119
|
-
* Its session has no read-only closing request, so each such participant ends
|
|
120
|
-
* end as its reason and no account of its session.
|
|
119
|
+
* The warnings for participants with a scheduled end or a session cap on Claude as a local-agent
|
|
120
|
+
* participant model. Its session has no read-only closing request, so each such participant ends
|
|
121
|
+
* at its end or cap with that as its reason and no account of its session.
|
|
121
122
|
*/
|
|
122
|
-
function
|
|
123
|
+
function closingReportWarnings(plan) {
|
|
123
124
|
if (plan.route !== "computer-use")
|
|
124
|
-
return
|
|
125
|
+
return [];
|
|
125
126
|
const { brain } = plan.runner;
|
|
126
127
|
if (brain.kind !== "local-agent" || brain.agent !== "claude")
|
|
127
|
-
return
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
128
|
+
return [];
|
|
129
|
+
const { participants } = plan.runner;
|
|
130
|
+
const scheduled = participants.filter(({ schedule }) => schedule.end?.kind === "scheduled_end");
|
|
131
|
+
const capped = participants.filter(({ schedule }) => schedule.end?.kind === "session_cap");
|
|
132
|
+
const claude = "Claude as a local-agent participant model cannot be asked for a closing report";
|
|
133
|
+
const writers = "A computer-use model (actor.type: openai-computer-use) or Codex (actor.localAgent: codex) writes one.";
|
|
134
|
+
return [
|
|
135
|
+
...(scheduled.length === 0
|
|
136
|
+
? []
|
|
137
|
+
: [
|
|
138
|
+
`${plural(scheduled.length, "participant")} ${scheduled.length === 1 ? "has" : "have"} a scheduled end (participants[].endAfterMs), and ${claude}, so ${scheduled.length === 1 ? "it ends" : "each ends"} with the scheduled end as its reason and no closing account. ${writers}`,
|
|
139
|
+
]),
|
|
140
|
+
...(capped.length === 0
|
|
141
|
+
? []
|
|
142
|
+
: [
|
|
143
|
+
`${plural(capped.length, "participant")} ${capped.length === 1 ? "has" : "have"} a session cap (participants[].maxSessionMs), and ${claude}, so ${capped.length === 1 ? "if it reaches its cap it ends" : "each that reaches its cap ends"} with the cap as its reason and no closing account. ${writers}`,
|
|
144
|
+
]),
|
|
145
|
+
];
|
|
133
146
|
}
|
|
134
147
|
function planRoute(route, study, options, input, deps, e2b) {
|
|
135
148
|
const { sandboxCeiling } = e2b;
|
package/dist/study/preflight.js
CHANGED
|
@@ -8,6 +8,7 @@ import { digest, fail, finalize, STUDY_CHECK_SCHEMA } from "./preflight-result.j
|
|
|
8
8
|
import { planStudy, routeOf } from "./plan.js";
|
|
9
9
|
import { describeArrivals, describeWindows } from "./arrivals.js";
|
|
10
10
|
import { describeBundle } from "./bundle-estimate.js";
|
|
11
|
+
import { describeTimeZones } from "./plan-participants.js";
|
|
11
12
|
import { resolveStudyManifest } from "./discover.js";
|
|
12
13
|
import { participantList } from "./study-fields.js";
|
|
13
14
|
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
@@ -82,6 +83,9 @@ export async function runStudyPreflight(options) {
|
|
|
82
83
|
const windows = windowsCheck(planned.planned.plan);
|
|
83
84
|
if (windows !== undefined)
|
|
84
85
|
ctx.checks.push(windows);
|
|
86
|
+
const timeZones = describeTimeZones(participants);
|
|
87
|
+
if (timeZones !== undefined)
|
|
88
|
+
ctx.checks.push({ name: "time zones", ok: true, message: timeZones });
|
|
85
89
|
const bundle = planned.planned.bundle;
|
|
86
90
|
if (bundle !== undefined)
|
|
87
91
|
ctx.checks.push({ name: "bundle size", ok: true, message: describeBundle(bundle) });
|
package/dist/study/types.d.ts
CHANGED
|
@@ -257,6 +257,13 @@ export interface StudySubject {
|
|
|
257
257
|
*/
|
|
258
258
|
localTree?: StudySubjectLocalTree;
|
|
259
259
|
}
|
|
260
|
+
/** One file a participant's desktop receives (`participants[].files`). */
|
|
261
|
+
export interface StudyParticipantFile {
|
|
262
|
+
/** Path of the file, relative to the folder that holds the study file. */
|
|
263
|
+
from: string;
|
|
264
|
+
/** Where it lands, relative to the desktop user's home folder. */
|
|
265
|
+
as: string;
|
|
266
|
+
}
|
|
260
267
|
/**
|
|
261
268
|
* One participant in a differentiated fan-out on the computer-use E2B route (`per-lane-worlds`).
|
|
262
269
|
* Each entry becomes an independent E2B desktop sandbox with its own persona/device/starting-steer. All
|
|
@@ -304,6 +311,11 @@ export interface StudyParticipantEntry {
|
|
|
304
311
|
* shared-world, scripted-browser, and terminal routes.
|
|
305
312
|
*/
|
|
306
313
|
target?: string;
|
|
314
|
+
/**
|
|
315
|
+
* Hosted computer use only: files copied onto this participant's desktop before it starts, so a
|
|
316
|
+
* browser upload dialog can find them. src/study/desktop-files.ts holds the rules.
|
|
317
|
+
*/
|
|
318
|
+
files?: StudyParticipantFile[];
|
|
307
319
|
/**
|
|
308
320
|
* Shared-world only: this participant's loopback entry route, resolved against
|
|
309
321
|
* `subject.serve.url`. It must be same-origin (loopback) with it, and the participant opens
|
|
@@ -334,8 +346,17 @@ export interface StudyParticipantEntry {
|
|
|
334
346
|
*/
|
|
335
347
|
endAfterMs?: number;
|
|
336
348
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
349
|
+
* Computer use: the longest this participant's session lasts, in milliseconds from the start of
|
|
350
|
+
* its session (1 to 86,400,000). It may end its session before then and counts as passed when it
|
|
351
|
+
* does so normally; at the cap it is asked for its closing report and the session ends as passed,
|
|
352
|
+
* with stop cause `session_cap`. Its session lasts the cap plus 5 minutes in place of
|
|
353
|
+
* execution.timeoutMs, and its sandbox is sized from that. Not with endAfterMs.
|
|
354
|
+
*/
|
|
355
|
+
maxSessionMs?: number;
|
|
356
|
+
/**
|
|
357
|
+
* With endAfterMs or maxSessionMs: how long before the end each turn tells the participant the
|
|
358
|
+
* end time and the minutes left. Default: a quarter of the participant's window or cap, at most
|
|
359
|
+
* 600000 (10 minutes).
|
|
339
360
|
*/
|
|
340
361
|
windDownMs?: number;
|
|
341
362
|
/**
|
|
@@ -343,6 +364,14 @@ export interface StudyParticipantEntry {
|
|
|
343
364
|
* no duration lasts while it only waits. Same range, and no longer than actor.maxWaitMs.
|
|
344
365
|
*/
|
|
345
366
|
idleWaitMs?: number;
|
|
367
|
+
/**
|
|
368
|
+
* Computer use and shared world: this participant's clock, in place of actor.clock. Its
|
|
369
|
+
* `timeZone` is the zone each turn gives the time in and, on a hosted desktop, the zone its
|
|
370
|
+
* browser runs in.
|
|
371
|
+
*/
|
|
372
|
+
clock?: {
|
|
373
|
+
timeZone: string;
|
|
374
|
+
};
|
|
346
375
|
}
|
|
347
376
|
export interface StudyActor {
|
|
348
377
|
/**
|
|
@@ -388,9 +417,10 @@ export interface StudyActor {
|
|
|
388
417
|
*/
|
|
389
418
|
idleWaitMs?: number;
|
|
390
419
|
/**
|
|
391
|
-
* Computer use: the
|
|
392
|
-
* the local time to each turn, for missions that
|
|
393
|
-
* scheduled end
|
|
420
|
+
* Computer use: the participants' clock, unless a participant declares its own. `timeZone`, a
|
|
421
|
+
* time zone name such as "Europe/London", adds the local time to each turn, for missions that
|
|
422
|
+
* refer to clock times, gives a scheduled end or session cap in that zone, and sets the zone
|
|
423
|
+
* of a hosted desktop's browser and of the clock in its panel, which shows UTC without one.
|
|
394
424
|
*/
|
|
395
425
|
clock?: {
|
|
396
426
|
timeZone: string;
|
package/dist/study/validation.js
CHANGED
|
@@ -114,6 +114,9 @@ export function sharedWorldValidationReason(config) {
|
|
|
114
114
|
const ending = participantList(config)?.findIndex((entry) => entry?.endAfterMs !== undefined);
|
|
115
115
|
if (ending !== undefined && ending >= 0)
|
|
116
116
|
return `participants[${ending}].endAfterMs ends a participant at a set time on route: computer-use only. A shared-world participant runs for execution.timeoutMs; remove endAfterMs and windDownMs.`;
|
|
117
|
+
const capped = participantList(config)?.findIndex((entry) => entry?.maxSessionMs !== undefined);
|
|
118
|
+
if (capped !== undefined && capped >= 0)
|
|
119
|
+
return `participants[${capped}].maxSessionMs caps a participant's session on route: computer-use only. A shared-world participant runs for execution.timeoutMs; remove maxSessionMs and windDownMs.`;
|
|
117
120
|
return config.subject.source === "app-url"
|
|
118
121
|
? externalPublicSharedWorldValidationReason(config)
|
|
119
122
|
: concurrentSharedWorldValidationReason(config);
|
package/dist/study/warnings.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResolvedPersona } from "./persona.js";
|
|
2
2
|
import type { StudyConfig } from "./types.js";
|
|
3
|
-
import { type Participant } from "./plan-participants.js";
|
|
3
|
+
import { type DesktopParticipant, type Participant } from "./plan-participants.js";
|
|
4
4
|
/**
|
|
5
5
|
* The fields a config sets that its route does not read, each as `field` or `field (reason)`.
|
|
6
6
|
* parseStudy refuses a study for any of them.
|
|
@@ -30,4 +30,4 @@ export declare function personaBackgroundWarnings(studyId: string, participants:
|
|
|
30
30
|
* and `study show`, which run no plan. Only computer-use and shared-world studies have them; a run's
|
|
31
31
|
* `--count` is not applied.
|
|
32
32
|
*/
|
|
33
|
-
export declare function plannedParticipants(config: StudyConfig): readonly
|
|
33
|
+
export declare function plannedParticipants(config: StudyConfig): readonly DesktopParticipant[];
|
package/dist/study/warnings.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cli } from "../cli/invocation.js";
|
|
2
2
|
import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
|
|
3
|
+
import { DESKTOP_FILES_ROUTE_REASON } from "./desktop-files.js";
|
|
3
4
|
import { computerUseParticipants, declaredParticipantIds, sharedWorldParticipants, } from "./plan-participants.js";
|
|
4
5
|
import { addressedRecipients } from "./parse/comms.js";
|
|
5
6
|
import { participantList, participantInstruction, declaredParticipantCount, } from "./study-fields.js";
|
|
@@ -27,6 +28,12 @@ const ACTOR_ROWS = [
|
|
|
27
28
|
applies: (config, routes) => Boolean(participantList(config)?.some((entry) => entry.host === true)) &&
|
|
28
29
|
!routes.externalPublic,
|
|
29
30
|
},
|
|
31
|
+
{
|
|
32
|
+
field: "participants[].files",
|
|
33
|
+
reason: DESKTOP_FILES_ROUTE_REASON,
|
|
34
|
+
applies: (config, routes) => Boolean(participantList(config)?.some((entry) => entry.files !== undefined)) &&
|
|
35
|
+
!(routes.hostedCuaBrowser && !routes.shared),
|
|
36
|
+
},
|
|
30
37
|
{
|
|
31
38
|
field: "actor.mission",
|
|
32
39
|
reason: "the scripted-browser actor runs no model",
|
|
@@ -71,6 +71,13 @@ export declare function enumerateLocalTree(root: string, options?: {
|
|
|
71
71
|
* produces zero entries; or total packed bytes exceed maxArchiveBytes.
|
|
72
72
|
*/
|
|
73
73
|
export declare function createLocalTreeArchive(root: string, options?: CreateLocalTreeArchiveOptions): LocalTreeArchive;
|
|
74
|
+
/**
|
|
75
|
+
* Whether a relative path an author wrote names a file the always-on denylist keeps on this
|
|
76
|
+
* machine: a secret-shaped name, or one under .git, node_modules or .humanish. It compares without
|
|
77
|
+
* case, since on a case-insensitive filesystem `.ENV` opens `.env`. participants[].files refuses
|
|
78
|
+
* the same paths.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isLocalTreeDenylisted(relPath: string): boolean;
|
|
74
81
|
/**
|
|
75
82
|
* Normalize an author-supplied exclude entry to the relPath shape enumeration
|
|
76
83
|
* produces (no leading "./", no trailing "/"). Absolute paths and glob syntax
|
|
@@ -283,6 +283,18 @@ function matchesBasenamePattern(basename, pattern) {
|
|
|
283
283
|
function isDenylistedBasename(basename) {
|
|
284
284
|
return LOCAL_TREE_DENYLIST_BASENAME_PATTERNS.some((pattern) => matchesBasenamePattern(basename, pattern));
|
|
285
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Whether a relative path an author wrote names a file the always-on denylist keeps on this
|
|
288
|
+
* machine: a secret-shaped name, or one under .git, node_modules or .humanish. It compares without
|
|
289
|
+
* case, since on a case-insensitive filesystem `.ENV` opens `.env`. participants[].files refuses
|
|
290
|
+
* the same paths.
|
|
291
|
+
*/
|
|
292
|
+
export function isLocalTreeDenylisted(relPath) {
|
|
293
|
+
const segments = relPath.toLowerCase().split("/");
|
|
294
|
+
const basename = segments.at(-1) ?? "";
|
|
295
|
+
return (segments.some((segment) => LOCAL_TREE_DENYLIST_PATH_SEGMENTS.some((denied) => denied.toLowerCase() === segment)) ||
|
|
296
|
+
LOCAL_TREE_DENYLIST_BASENAME_PATTERNS.some((pattern) => matchesBasenamePattern(basename, pattern.toLowerCase())));
|
|
297
|
+
}
|
|
286
298
|
function isDenylistedSegment(relPath) {
|
|
287
299
|
const segments = relPath.split("/");
|
|
288
300
|
return segments.some((segment) => LOCAL_TREE_DENYLIST_PATH_SEGMENTS.includes(segment));
|
|
@@ -76,6 +76,9 @@ extraChromiumFlags?: readonly string[], environment?: Readonly<Record<string, st
|
|
|
76
76
|
* `x-terminal-emulator` is tried first so a template that swaps
|
|
77
77
|
* the emulator still works; a desktop with neither is a template problem and fails closed rather
|
|
78
78
|
* than handing a participant an empty screen and calling it a study.
|
|
79
|
+
*
|
|
80
|
+
* A `timeZone` is TZ on the terminal, so its shells, the product under test and any browser started
|
|
81
|
+
* from it run in that zone.
|
|
79
82
|
*/
|
|
80
|
-
export declare function openDesktopTerminal(desktop: E2BDesktopSandbox, requestTimeoutMs: number, workdir: string | undefined): Promise<void>;
|
|
83
|
+
export declare function openDesktopTerminal(desktop: E2BDesktopSandbox, requestTimeoutMs: number, workdir: string | undefined, timeZone?: string): Promise<void>;
|
|
81
84
|
export declare function startDesktopStream(desktop: E2BDesktopSandbox, browserWindowId: string | undefined): Promise<void>;
|
|
@@ -367,9 +367,13 @@ function desktopBrowserFamily(value) {
|
|
|
367
367
|
* `x-terminal-emulator` is tried first so a template that swaps
|
|
368
368
|
* the emulator still works; a desktop with neither is a template problem and fails closed rather
|
|
369
369
|
* than handing a participant an empty screen and calling it a study.
|
|
370
|
+
*
|
|
371
|
+
* A `timeZone` is TZ on the terminal, so its shells, the product under test and any browser started
|
|
372
|
+
* from it run in that zone.
|
|
370
373
|
*/
|
|
371
|
-
export async function openDesktopTerminal(desktop, requestTimeoutMs, workdir) {
|
|
374
|
+
export async function openDesktopTerminal(desktop, requestTimeoutMs, workdir, timeZone) {
|
|
372
375
|
const dir = workdir ?? "/home/user";
|
|
376
|
+
const zone = timeZone === undefined ? "" : `TZ=${shellQuote(timeZone)} `;
|
|
373
377
|
const result = await runDetachedStep(e2bShell(desktop), {
|
|
374
378
|
name: "desktop-cli-terminal",
|
|
375
379
|
command: [
|
|
@@ -379,7 +383,7 @@ export async function openDesktopTerminal(desktop, requestTimeoutMs, workdir) {
|
|
|
379
383
|
// a study that measures our own mojibake against an unconfigured template would be measuring
|
|
380
384
|
// the template. The product-side fix (an ASCII fallback when the locale is not UTF-8) is in
|
|
381
385
|
// src/routes/terminal/encoding.ts, and it is the one that matters for real users.
|
|
382
|
-
` (cd ${shellQuote(dir)} 2>/dev/null || cd /home/user; DISPLAY=:0 LANG=C.UTF-8 LC_ALL=C.UTF-8 HUMANISH_STUDY_PARTICIPANT=1 nohup "$candidate" >/dev/null 2>&1 &)`,
|
|
386
|
+
` (cd ${shellQuote(dir)} 2>/dev/null || cd /home/user; DISPLAY=:0 LANG=C.UTF-8 LC_ALL=C.UTF-8 ${zone}HUMANISH_STUDY_PARTICIPANT=1 nohup "$candidate" >/dev/null 2>&1 &)`,
|
|
383
387
|
" sleep 3",
|
|
384
388
|
' echo "humanish: opened $candidate"',
|
|
385
389
|
" exit 0",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { E2BDesktopSandbox } from "./sdk.js";
|
|
2
|
+
/**
|
|
3
|
+
* Set every clock in the desktop's XFCE panel to `timeZone`, a tz database name, by writing the
|
|
4
|
+
* clock plugin's `timezone` property through xfconf-query. The property lives in the session's
|
|
5
|
+
* settings daemon, reached over the session bus whose address xfce4-session holds, and the panel
|
|
6
|
+
* redraws its clock when the property changes (live: 02:12 UTC became 11:12 for Asia/Tokyo with no
|
|
7
|
+
* restart). A desktop without xfconf-query or without a clock plugin is left as it is. Throws with
|
|
8
|
+
* xfconf-query's error output when a clock is found and cannot be set.
|
|
9
|
+
*/
|
|
10
|
+
export declare function setPanelClockZone(desktop: E2BDesktopSandbox, timeZone: string, requestTimeoutMs: number): Promise<void>;
|