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
|
@@ -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
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { DwellWindow, StopWhen } from "../actors/stop-conditions.js";
|
|
|
2
2
|
import type { ReasoningEffort } from "../actors/reasoning-effort.js";
|
|
3
3
|
import { type DevicePreset } from "./device-presets.js";
|
|
4
4
|
import type { StudyTask } from "./tasks.js";
|
|
5
|
-
import type { StudyConfig } from "./types.js";
|
|
5
|
+
import type { StudyConfig, StudyParticipantFile } from "./types.js";
|
|
6
6
|
/** Who one participant is. Every route with participants carries this record. */
|
|
7
7
|
export interface Participant {
|
|
8
8
|
/** Declared roster id, else `lane-NN` (independent participants) or `role-NN` (shared-world participants). */
|
|
@@ -24,7 +24,7 @@ export interface Participant {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
/** A participant at a desktop: computer use and shared world. */
|
|
27
|
-
interface DesktopParticipant extends Participant {
|
|
27
|
+
export interface DesktopParticipant extends Participant {
|
|
28
28
|
readonly device: {
|
|
29
29
|
readonly name: string;
|
|
30
30
|
readonly preset: DevicePreset;
|
|
@@ -43,14 +43,22 @@ interface DesktopParticipant extends Participant {
|
|
|
43
43
|
* while the participant only waits.
|
|
44
44
|
*/
|
|
45
45
|
readonly idleWaitMs?: number;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* The participant's clock.timeZone, else actor.clock.timeZone: each turn tells it the time in
|
|
48
|
+
* this zone, and a hosted desktop runs its browser in it.
|
|
49
|
+
*/
|
|
47
50
|
readonly timeZone?: string;
|
|
48
51
|
};
|
|
49
52
|
/** When it starts, in ms after the run starts its participants; absent, with the run. */
|
|
50
53
|
readonly startAfterMs?: number;
|
|
51
54
|
/** Computer use: when it ends, on the same clock; absent, it runs for the study's session budget. */
|
|
52
55
|
readonly endAfterMs?: number;
|
|
53
|
-
/**
|
|
56
|
+
/** Computer use: the longest its session lasts, from its start; it may end the session sooner. */
|
|
57
|
+
readonly maxSessionMs?: number;
|
|
58
|
+
/**
|
|
59
|
+
* With endAfterMs or maxSessionMs: how long before the end each turn tells it the time left
|
|
60
|
+
* (default applied).
|
|
61
|
+
*/
|
|
54
62
|
readonly windDownMs?: number;
|
|
55
63
|
}
|
|
56
64
|
/** Only independent computer-use participants consume a task protocol and their own target. */
|
|
@@ -58,6 +66,8 @@ export interface ComputerUseParticipant extends DesktopParticipant {
|
|
|
58
66
|
readonly tasks?: readonly StudyTask[];
|
|
59
67
|
/** app-url fan-out only: this participant's own entry URL; absent means the subject URL. */
|
|
60
68
|
readonly targetUrl?: string;
|
|
69
|
+
/** Hosted desktop only: the files copied onto it before the participant starts. */
|
|
70
|
+
readonly files?: readonly StudyParticipantFile[];
|
|
61
71
|
}
|
|
62
72
|
/** A participant on a provisioned plane: an optional same-origin path under `serve.url`. */
|
|
63
73
|
export interface ProvisionedParticipant extends DesktopParticipant {
|
|
@@ -93,4 +103,8 @@ export declare function sharedWorldParticipants(config: StudyConfig): SharedWorl
|
|
|
93
103
|
export declare function declaredParticipantIds(config: StudyConfig): string[];
|
|
94
104
|
/** The entry URLs roster entries declare in place of the subject URL (computer use, app-url). */
|
|
95
105
|
export declare function declaredTargets(config: StudyConfig): string[];
|
|
96
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Each participant's time zone, as `study check` and `study show` print it, in roster order with
|
|
108
|
+
* `none` for a participant without one. Undefined when no participant has a zone.
|
|
109
|
+
*/
|
|
110
|
+
export declare function describeTimeZones(participants: readonly DesktopParticipant[]): string | undefined;
|
|
@@ -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,
|
|
@@ -46,6 +46,12 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
46
46
|
endAfterMs: entry.endAfterMs,
|
|
47
47
|
windDownMs: entry.windDownMs ?? DEFAULT_WIND_DOWN_MS,
|
|
48
48
|
}),
|
|
49
|
+
...(entry?.maxSessionMs === undefined
|
|
50
|
+
? {}
|
|
51
|
+
: {
|
|
52
|
+
maxSessionMs: entry.maxSessionMs,
|
|
53
|
+
windDownMs: entry.windDownMs ?? DEFAULT_WIND_DOWN_MS,
|
|
54
|
+
}),
|
|
49
55
|
};
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
@@ -62,6 +68,7 @@ export function computerUseParticipants(config, countOverride) {
|
|
|
62
68
|
...desktopParticipant(config, entry, index, "lane", focus),
|
|
63
69
|
...(actor?.tasks === undefined ? {} : { tasks: actor.tasks }),
|
|
64
70
|
...(entry?.target === undefined ? {} : { targetUrl: entry.target }),
|
|
71
|
+
...(entry?.files === undefined ? {} : { files: entry.files }),
|
|
65
72
|
};
|
|
66
73
|
});
|
|
67
74
|
}
|
|
@@ -102,3 +109,12 @@ export function declaredTargets(config) {
|
|
|
102
109
|
.map((entry) => entry.target)
|
|
103
110
|
.filter((target) => target !== undefined);
|
|
104
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Each participant's time zone, as `study check` and `study show` print it, in roster order with
|
|
114
|
+
* `none` for a participant without one. Undefined when no participant has a zone.
|
|
115
|
+
*/
|
|
116
|
+
export function describeTimeZones(participants) {
|
|
117
|
+
if (participants.every((participant) => participant.limits.timeZone === undefined))
|
|
118
|
+
return undefined;
|
|
119
|
+
return `${participants.map((participant) => `${participant.id} ${participant.limits.timeZone ?? "none"}`).join(", ")}.`;
|
|
120
|
+
}
|
package/dist/study/plan.js
CHANGED
|
@@ -86,12 +86,11 @@ export function planStudy(config, options, deps = {}) {
|
|
|
86
86
|
const bundle = plannedBundle(result.plan);
|
|
87
87
|
// The manifest the CLI resolved enters the plan here and nowhere else; the routes read
|
|
88
88
|
// plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
|
|
89
|
-
const closing = closingReportWarning(result.plan);
|
|
90
89
|
const warnings = [
|
|
91
90
|
...recordedStudyWarnings(study),
|
|
92
91
|
...(result.plan.warnings ?? []),
|
|
93
92
|
...(bundle.warning === undefined ? [] : [bundle.warning]),
|
|
94
|
-
...(
|
|
93
|
+
...closingReportWarnings(result.plan),
|
|
95
94
|
];
|
|
96
95
|
const plan = {
|
|
97
96
|
...result.plan,
|
|
@@ -115,21 +114,33 @@ function plannedBundle(plan) {
|
|
|
115
114
|
return warning === undefined ? { estimate } : { estimate, warning };
|
|
116
115
|
}
|
|
117
116
|
/**
|
|
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.
|
|
117
|
+
* The warnings for participants with a scheduled end or a session cap on Claude as a local-agent
|
|
118
|
+
* participant model. Its session has no read-only closing request, so each such participant ends
|
|
119
|
+
* at its end or cap with that as its reason and no account of its session.
|
|
121
120
|
*/
|
|
122
|
-
function
|
|
121
|
+
function closingReportWarnings(plan) {
|
|
123
122
|
if (plan.route !== "computer-use")
|
|
124
|
-
return
|
|
123
|
+
return [];
|
|
125
124
|
const { brain } = plan.runner;
|
|
126
125
|
if (brain.kind !== "local-agent" || brain.agent !== "claude")
|
|
127
|
-
return
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
126
|
+
return [];
|
|
127
|
+
const { participants } = plan.runner;
|
|
128
|
+
const scheduled = participants.filter((participant) => participant.endAfterMs !== undefined);
|
|
129
|
+
const capped = participants.filter((participant) => participant.maxSessionMs !== undefined);
|
|
130
|
+
const claude = "Claude as a local-agent participant model cannot be asked for a closing report";
|
|
131
|
+
const writers = "A computer-use model (actor.type: openai-computer-use) or Codex (actor.localAgent: codex) writes one.";
|
|
132
|
+
return [
|
|
133
|
+
...(scheduled.length === 0
|
|
134
|
+
? []
|
|
135
|
+
: [
|
|
136
|
+
`${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}`,
|
|
137
|
+
]),
|
|
138
|
+
...(capped.length === 0
|
|
139
|
+
? []
|
|
140
|
+
: [
|
|
141
|
+
`${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}`,
|
|
142
|
+
]),
|
|
143
|
+
];
|
|
133
144
|
}
|
|
134
145
|
function planRoute(route, study, options, input, deps, e2b) {
|
|
135
146
|
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,16 @@ 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 600000 (10 minutes).
|
|
339
359
|
*/
|
|
340
360
|
windDownMs?: number;
|
|
341
361
|
/**
|
|
@@ -343,6 +363,14 @@ export interface StudyParticipantEntry {
|
|
|
343
363
|
* no duration lasts while it only waits. Same range, and no longer than actor.maxWaitMs.
|
|
344
364
|
*/
|
|
345
365
|
idleWaitMs?: number;
|
|
366
|
+
/**
|
|
367
|
+
* Computer use and shared world: this participant's clock, in place of actor.clock. Its
|
|
368
|
+
* `timeZone` is the zone each turn gives the time in and, on a hosted desktop, the zone its
|
|
369
|
+
* browser runs in.
|
|
370
|
+
*/
|
|
371
|
+
clock?: {
|
|
372
|
+
timeZone: string;
|
|
373
|
+
};
|
|
346
374
|
}
|
|
347
375
|
export interface StudyActor {
|
|
348
376
|
/**
|
|
@@ -388,9 +416,10 @@ export interface StudyActor {
|
|
|
388
416
|
*/
|
|
389
417
|
idleWaitMs?: number;
|
|
390
418
|
/**
|
|
391
|
-
* Computer use: the
|
|
392
|
-
* the local time to each turn, for missions that
|
|
393
|
-
* scheduled end
|
|
419
|
+
* Computer use: the participants' clock, unless a participant declares its own. `timeZone`, a
|
|
420
|
+
* time zone name such as "Europe/London", adds the local time to each turn, for missions that
|
|
421
|
+
* refer to clock times, gives a scheduled end or session cap in that zone, and sets the zone
|
|
422
|
+
* of a hosted desktop's browser and of the clock in its panel, which shows UTC without one.
|
|
394
423
|
*/
|
|
395
424
|
clock?: {
|
|
396
425
|
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>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The clock in the hosted desktop's panel. The stock template's XFCE panel sits above every window,
|
|
2
|
+
// so a participant sees its clock in each screenshot, and it shows the desktop's zone, UTC, unless
|
|
3
|
+
// its clock plugin names another.
|
|
4
|
+
import { shellQuote } from "../shell.js";
|
|
5
|
+
import { e2bShell } from "./shell.js";
|
|
6
|
+
/**
|
|
7
|
+
* Set every clock in the desktop's XFCE panel to `timeZone`, a tz database name, by writing the
|
|
8
|
+
* clock plugin's `timezone` property through xfconf-query. The property lives in the session's
|
|
9
|
+
* settings daemon, reached over the session bus whose address xfce4-session holds, and the panel
|
|
10
|
+
* redraws its clock when the property changes (live: 02:12 UTC became 11:12 for Asia/Tokyo with no
|
|
11
|
+
* restart). A desktop without xfconf-query or without a clock plugin is left as it is. Throws with
|
|
12
|
+
* xfconf-query's error output when a clock is found and cannot be set.
|
|
13
|
+
*/
|
|
14
|
+
export async function setPanelClockZone(desktop, timeZone, requestTimeoutMs) {
|
|
15
|
+
const result = await e2bShell(desktop).run([
|
|
16
|
+
`zone=${shellQuote(timeZone)}`,
|
|
17
|
+
"command -v xfconf-query >/dev/null 2>&1 || exit 0",
|
|
18
|
+
'session_pid="$(pgrep -x xfce4-session | head -n 1)"',
|
|
19
|
+
'if [ -n "$session_pid" ]; then',
|
|
20
|
+
` bus="$(tr '\\0' '\\n' < "/proc/$session_pid/environ" | sed -n 's/^DBUS_SESSION_BUS_ADDRESS=//p')"`,
|
|
21
|
+
' if [ -n "$bus" ]; then export DBUS_SESSION_BUS_ADDRESS="$bus"; fi',
|
|
22
|
+
"fi",
|
|
23
|
+
`for plugin in $(xfconf-query -c xfce4-panel -p /plugins -l -v | awk '$2 == "clock" { print $1 }'); do`,
|
|
24
|
+
' xfconf-query -c xfce4-panel -p "$plugin/timezone" -n -t string -s "$zone" || exit 1',
|
|
25
|
+
"done",
|
|
26
|
+
].join("\n"), { requestTimeoutMs, timeoutMs: 15_000 });
|
|
27
|
+
if (result.exitCode !== 0)
|
|
28
|
+
throw new Error(result.stderr.trim() || `xfconf-query exited ${result.exitCode}`);
|
|
29
|
+
}
|
|
@@ -154,6 +154,12 @@ export interface E2BDesktopSandbox {
|
|
|
154
154
|
requestTimeoutMs?: number;
|
|
155
155
|
useOctetStream?: boolean;
|
|
156
156
|
}): Promise<unknown>;
|
|
157
|
+
/** A file's size as the sandbox sees it. A relative path is under the sandbox user's home. */
|
|
158
|
+
getInfo?(path: string, options?: {
|
|
159
|
+
requestTimeoutMs?: number;
|
|
160
|
+
}): Promise<{
|
|
161
|
+
size: number;
|
|
162
|
+
}>;
|
|
157
163
|
};
|
|
158
164
|
launch(application: string, uri?: string): Promise<void>;
|
|
159
165
|
/** Open a file or URL with the desktop's default application (present on @e2b/desktop >= 1.x). */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RunBundle } from "../run/bundle.js";
|
|
2
|
+
/**
|
|
3
|
+
* One finding per recorded page that holds more than an origin and a path, in each finished
|
|
4
|
+
* trace's items and summary and each running participant's items. A finding names where the page
|
|
5
|
+
* is, never what it says.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pageFindings(bundle: RunBundle): string[];
|