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
@@ -0,0 +1,50 @@
1
+ import { REDACTION_MARKERS } from "../evidence/redaction.js";
2
+ import { isRecord } from "../run/type-guards.js";
3
+ // A recorded page (`items[].page`, `finalPage`) keeps a URL's origin and path only. The text scan
4
+ // finds secret shapes in it as in any trace text, but a query string, a fragment or user info can
5
+ // carry a short token no pattern matches, so a page holding one is a public-safety finding.
6
+ // The markers that can stand for a whole origin, such as a withheld sandbox host.
7
+ const MARKERS = new Set(Object.values(REDACTION_MARKERS));
8
+ // `scheme:` alone (a non-web URL) or `scheme://host[:port]`, with nothing after the host. The URL
9
+ // parser reads a backslash there as a slash, so a backslash counts as a path too.
10
+ const ORIGIN = /^[a-z][a-z0-9+.-]*:(?:\/\/[^/\\?#@]*)?$/i;
11
+ /** What is wrong with a recorded page, or undefined when it holds an origin and a path only. */
12
+ function pageProblem(page) {
13
+ if (!isRecord(page) || typeof page.origin !== "string" || typeof page.path !== "string")
14
+ return "is not an origin and a path";
15
+ if (/[?#]/.test(page.path))
16
+ return "has a query string or fragment in its path";
17
+ if (!MARKERS.has(page.origin) && !ORIGIN.test(page.origin))
18
+ return "has an origin with user info, a path, a query string or a fragment";
19
+ return undefined;
20
+ }
21
+ /**
22
+ * One finding per recorded page that holds more than an origin and a path, in each finished
23
+ * trace's items and summary and each running participant's items. A finding names where the page
24
+ * is, never what it says.
25
+ */
26
+ export function pageFindings(bundle) {
27
+ const findings = [];
28
+ const check = (where, page) => {
29
+ if (page === undefined)
30
+ return;
31
+ const problem = pageProblem(page);
32
+ if (problem !== undefined)
33
+ findings.push(`${where} ${problem}`);
34
+ };
35
+ // The shape guard admits items that are not records, so each is read as unknown.
36
+ const checkItems = (where, items) => {
37
+ if (!Array.isArray(items))
38
+ return;
39
+ items.forEach((item, index) => {
40
+ if (isRecord(item))
41
+ check(`${where}[${index}].page`, item.page);
42
+ });
43
+ };
44
+ for (const stream of bundle.streams) {
45
+ checkItems(`${stream.id} items`, stream.actor?.items);
46
+ check(`${stream.id} finalPage`, stream.actor?.finalPage);
47
+ checkItems(`${stream.id} liveActor.items`, stream.liveActor?.items);
48
+ }
49
+ return findings;
50
+ }
@@ -16,6 +16,7 @@ import { runNotFoundMessage } from "../run/run-not-found.js";
16
16
  import { actorVerdictConsistencyFindings, noEngagementActorFindings, validateCodexAppServerEvidence, validateTerminalProductEvidence, } from "./actor.js";
17
17
  import { invalidRunEvidenceReferences, MAX_REPORTED_FINDINGS, missingLocalEvidenceArtifacts, rawScreenshotPostureWarnings, rawScreenshotStreamIds, redactedShapeFramePaths, scanRunPublicSafetyArtifacts, scanSourceEvidence, streamScreenshotPaths, unscannedMessage, } from "./artifacts.js";
18
18
  import { flatContextWarnings } from "./context-growth.js";
19
+ import { pageFindings } from "./pages.js";
19
20
  import { runNotFinished } from "./liveness.js";
20
21
  import { costAndReceiptFindings } from "./costs.js";
21
22
  import { rerunLineageFindings } from "./rerun.js";
@@ -229,7 +230,7 @@ function evidenceArtifactsCheck(shaped, problems) {
229
230
  async function loadTraces(runPaths, stored) {
230
231
  const scanned = { sensitive: [], opaque: [] };
231
232
  if (!isRunBundle(stored))
232
- return { bundle: stored, refusedTraces: [], ...scanned };
233
+ return { bundle: stored, refusedTraces: [], ...scanned, pages: [] };
233
234
  // Each trace is source evidence: scanned by the bytes it was joined from, apart from the
234
235
  // directory walk and any path exemption the walk makes.
235
236
  const { bundle, refused } = await loadStreamTraces(runPaths, stored, {
@@ -239,9 +240,14 @@ async function loadTraces(runPaths, stored) {
239
240
  scanned[scan === "sensitive" ? "sensitive" : "opaque"].push(file);
240
241
  },
241
242
  });
242
- return { bundle, refusedTraces: refused.map((error) => error.message), ...scanned };
243
+ return {
244
+ bundle,
245
+ refusedTraces: refused.map((error) => error.message),
246
+ ...scanned,
247
+ pages: pageFindings(bundle),
248
+ };
243
249
  }
244
- /** The trace scan's results, beside the walk's: each file named once. */
250
+ /** The trace scan's results, beside the walk's: each file named once, then each page finding. */
245
251
  function addTraceScan(traces, findings, unscanned) {
246
252
  for (const file of traces.sensitive)
247
253
  if (!findings.includes(`sensitive text ${file}`))
@@ -249,6 +255,7 @@ function addTraceScan(traces, findings, unscanned) {
249
255
  for (const file of traces.opaque)
250
256
  if (!unscanned.files.includes(file))
251
257
  unscanned.files.push(file);
258
+ findings.push(...traces.pages);
252
259
  }
253
260
  /** The top-level run.json sections the shape guard requires. */
254
261
  const REQUIRED_SECTIONS = [
@@ -167,6 +167,7 @@ export interface ActorTraceItem {
167
167
  command?: { text?: string; cwd?: string; exitCode?: number; outputTail?: string };
168
168
  screenshotRef?: { path: string; redaction: "blurred" | "ocr_scrubbed" | "none" };
169
169
  text?: string; // redacted
170
+ page?: { origin: string; path: string }; // computer-use screenshot items; redacted, no query
170
171
  }
171
172
 
172
173
  export interface ActorCapabilities {
@@ -204,6 +205,7 @@ export interface ActorTrace {
204
205
  ids: { sessionId?: string; threadId?: string; turnId?: string; model?: string };
205
206
  counts: Record<string, number>;
206
207
  items: ActorTraceItem[];
208
+ finalPage?: { origin: string; path: string }; // computer-use: the last observation's page
207
209
  tokenUsage?: { input?: number; output?: number; total?: number; costUsd?: number };
208
210
  estimatedCost?: ActorEstimatedCost; // humanish.actor-estimated-cost.v1 (additive)
209
211
  capabilities: ActorCapabilities;
@@ -120,7 +120,10 @@ The runtime `location.href` (and the 6-char code inside it) is never persisted r
120
120
  runtime-only; the threaded code flows only into the follower's composed prompt (never a raw bundle
121
121
  field) and is scrubbed from all narration once latched; the shared origin and lobby path persist only
122
122
  as sha256-16 digests (`publicOriginDigest`, `lobbyConvergenceDigest`). This matches the existing
123
- e2b-URL / host-digest redaction discipline.
123
+ e2b-URL / host-digest redaction discipline. The computer-use loop records each observation's page
124
+ origin and path by default; the shared-world runner (`participantRunDeps` in
125
+ `src/routes/shared-world/participant-specs.ts`) runs every session with `recordPages: false`, on
126
+ both planes, so no page and no `finalPage` reaches a shared-world trace.
124
127
 
125
128
  ## Mobile fidelity caveat
126
129
 
@@ -74,7 +74,16 @@ public-safe metadata.
74
74
 
75
75
  The browser polls `observer-feed.json` with `no-store` caching: the server's
76
76
  projection of the run as `{ data, items }`, observer-data.json's content and the
77
- items, from one read of the run per poll. Static
77
+ items, from one read of the run per poll. While a participant runs, its entry in
78
+ `items` carries `live`, a cursor `<position>.<generation>.<items>.<bytes>` naming its
79
+ live file (`live/<position>.<generation>.ndjson`) and how far the list goes. The page
80
+ sends the cursors of its last body back as `?after=<cursor>,<cursor>`, and the server
81
+ then reads and sends only the items past each one, marking that entry with
82
+ `liveAfter`, the cursor they follow (`src/observer/feed-cursors.ts`). A cursor for an
83
+ earlier generation, past the index or off a line end gets the participant's full list,
84
+ and so does every participant of a finished run. A tail is checked for line ends and
85
+ its line count; the sha256 run.json records is checked whenever a list is read whole.
86
+ A page that cannot join a body sends no cursors with its next poll. Static
78
87
  `file://` opening still works for immutable review, but follow mode is the
79
88
  operator path. Agents and CI should use `humanish watch --json --no-open` for
80
89
  the same fresh evidence without browser open or a long-running process.
@@ -146,6 +146,13 @@ certification that the artifact is free of every class in the policy below. A
146
146
  first-class PII/PHI detector is [planned](https://github.com/danielgwilson/humanish/issues/108) and would move the listed
147
147
  PII classes from author-responsibility to enforced.
148
148
 
149
+ The same scan reads the page each computer-use observation recorded (`items[].page`,
150
+ `finalPage`). verify also reports a page whose path holds a query string or fragment, or whose
151
+ origin holds user info or anything after the host, since such a part can carry a short token no
152
+ pattern matches. The recorder withholds email- and token-shaped path segments
153
+ ([schemas.md](schemas.md#actor-trace)); a name or other personal data in a readable path is not
154
+ detected.
155
+
149
156
  Required redaction gates:
150
157
 
151
158
  - run bundle verification;
@@ -332,7 +332,7 @@ An actor trace may include `stopCause` alongside its unchanged `status`,
332
332
  `completionReason` and verbatim `reason`. Computer-use sessions distinguish
333
333
  `provider_output_limit`, `provider_token_limit`, `time_limit`, `spend_limit`,
334
334
  `study_spend_limit`, `adapter_limit`, `provider_incomplete`, `provider_status`,
335
- `provider_refused_prompt`, `harness_aborted`, `usage_unreported`, and `scheduled_end`. Absence means the route or
335
+ `provider_refused_prompt`, `harness_aborted`, `usage_unreported`, `scheduled_end`, and `session_cap`. Absence means the route or
336
336
  recording did not retain this precise field; it does not mean the participant
337
337
  finished.
338
338
 
@@ -454,6 +454,36 @@ contain the older requested-screen `stream.viewport` shape without `desktopGeome
454
454
  Deterministic Playwright-style adapters remain free to declare a viewport that they also render
455
455
  exactly; this hosted-desktop rule does not change that contract.
456
456
 
457
+ ## Desktop Files
458
+
459
+ A hosted computer-use participant's stream carries `desktopFiles` when its study gave it
460
+ `participants[].files`: one entry per file confirmed on the desktop before the participant
461
+ started, in the order they landed.
462
+
463
+ ```yaml
464
+ desktopFiles:
465
+ - as: Downloads/insurance-card.png
466
+ size: 251
467
+ sha256: <64 hex characters>
468
+ ```
469
+
470
+ `as` is relative to the desktop user's home folder. `size` is the byte count humanish copied,
471
+ which `files.getInfo` on the desktop matched. `sha256` is the digest of those bytes, read on the
472
+ host before any sandbox existed. The contents and the `from` path are not recorded. A participant
473
+ whose copy failed records the files confirmed before the failure. No `desktopFiles` means no file
474
+ was confirmed on that desktop: the study gave the participant none, the run was a dry run, or the
475
+ copy failed before the first file was confirmed. A copy that failed records a `warn` event of type
476
+ `cua-lab.desktop.files.completed`.
477
+
478
+ ## Participant Time Zone
479
+
480
+ A computer-use or shared-world participant's stream carries `timeZone`, a tz database name such as
481
+ `Europe/London`, when its study gave it one: `participants[].clock.timeZone`, else
482
+ `actor.clock.timeZone`. Its turns gave the local time in that zone, and on a hosted desktop its
483
+ browser launched with `TZ` set to it. The field records the declared zone, also on a dry run; it is
484
+ not read back from the browser. No `timeZone` means the participant had none, and its hosted
485
+ browser ran in the desktop's UTC.
486
+
457
487
  ## Subject Provenance
458
488
 
459
489
  `subject` is an optional, additive top-level field: structured provenance for
@@ -210,6 +210,24 @@ The rest of this section names keys by their v3 spelling:
210
210
  Public/non-loopback targets still require `policies.allowPublicTargets: true`.
211
211
  `target` is mutually exclusive with `entry`: `target` is an absolute app-url
212
212
  browser target; `entry` is a shared-world participant's same-origin path;
213
+ - `participants[].files` (computer-use on `execution.target: e2b-desktop` only):
214
+ 1 to 20 `{ from, as }` entries, each a file copied onto that participant's desktop
215
+ before its browser opens. `from` is relative to the folder that holds the study file
216
+ (`cwd` for a library config); `as` is relative to the desktop user's home folder.
217
+ The parser refuses an absolute path, a `.`, `..` or empty part, a backslash, an `as`
218
+ that starts with `~`, lands in `subject/` or on a name starting with `.humanish`, looks
219
+ like a secret, a local path or encoded text, repeats another `as` or is its folder,
220
+ and a `from` the local-tree denylist names in any case (`isLocalTreeDenylisted` in
221
+ `src/subject/local-tree-archive.ts`). The route also refuses an `as` that holds a
222
+ value from the run's environment, such as a key. The route reads each distinct `from` once, before
223
+ any sandbox and on dry runs too, with `readContainedRegularFile`: a missing file, a
224
+ folder, a symbolic or hard link, a path out of the folder, a file above 10 MiB, more
225
+ than 25 MiB for one participant or 100 MiB of distinct files for the study is
226
+ refused with `HUMANISH_COMPUTER_USE_DESKTOP_FILES_INVALID` (`readDesktopFiles` in
227
+ `src/study/desktop-files.ts`). Each file
228
+ is written with the E2B SDK's `files.write` and its `files.getInfo` size must match, or
229
+ the participant fails before its first turn. The stream records `desktopFiles`
230
+ ([run-bundle.md](run-bundle.md#desktop-files)). Other routes refuse the field;
213
231
  - a `participants` entry with `count: n` (computer-use E2B route): a group of
214
232
  repeated participants. It needs an `id` and the parser expands it to
215
233
  `<id>-01` … `<id>-NN`, even when n is 1, before the engine runs (`viewer-01`,
@@ -235,17 +253,42 @@ The rest of this section names keys by their v3 spelling:
235
253
  participant's session budget is its window plus 5 minutes (`participantSessionMs` in
236
254
  `src/study/arrivals.ts`) in place of `execution.timeoutMs`, and its sandbox deadline adds the
237
255
  route's headroom to that. At the end the loop requests the closing report (`debrief.trigger:
238
- scheduled_end`) and the session ends `goal_satisfied` with `stopCause: scheduled_end`. Before
256
+ scheduled_end`) and the session ends `goal_satisfied` with `stopCause: scheduled_end`. The
257
+ outcome an accepted report declares, its `outcome` field or the first line of its summary, is
258
+ the trace's `declaredOutcome`. The blocker rule reads a declared `reached` or `blocked` before
259
+ the summary, and after a declared `not_reached` it reads the summary. Before
239
260
  the end, a turn that requests no action does not end the session: the trace records a
240
261
  `participant ended before its scheduled end` notice, the next request says the time left, and
241
262
  the turn counts toward the idle backstop. Before the end, a turn that only waits or takes
242
263
  screenshots counts toward neither backstop streak (`advanceBackstop`'s `waitingIsTheTask`). The
243
264
  plan entry carries `endAfterMs`, and `study check` prints a `windows` row;
265
+ - `participants[].maxSessionMs` (computer-use only, also on `count:` groups): the longest the
266
+ participant's session lasts, in whole milliseconds from 1 to 86,400,000, counted from the start
267
+ of its own session. `windDownMs` applies to it as to `endAfterMs`. Refused: a value outside that
268
+ range, `maxSessionMs` beside `endAfterMs` on one participant, `maxSessionMs` on `shared-world`,
269
+ and a cap whose sandbox deadline passes the sandbox ceiling. The session budget is the cap plus
270
+ 5 minutes (`participantSessionMs`) in place of `execution.timeoutMs`, and the schedule holds the
271
+ participant's slot that long. The route reads the cap's end when the session starts
272
+ (`participantEnd` in `src/study/arrivals.ts`, `participantClock` in
273
+ `src/routes/computer-use/participant-model.ts`) and passes it to the loop as
274
+ `clock.end` with `kind: session_cap`. The participant may end its session before the cap, and
275
+ its waiting turns count toward the backstops. At the cap the loop requests the closing report
276
+ (`debrief.trigger: session_cap`) and the session ends `goal_satisfied` with
277
+ `stopCause: session_cap`, its report's declared outcome read as at a scheduled end. The plan
278
+ entry carries `maxSessionMs`, and the plan line shows
279
+ `cap=<duration>`;
244
280
  - `participants[].idleWaitMs` (computer-use and shared-world, also on `count:` groups): this
245
281
  participant's `actor.idleWaitMs`, in the same range and refused above an explicit
246
282
  `actor.maxWaitMs` (`waitLimitValidationReason`). Its trace records it in `waitSettings`;
247
- - `actor.clock.timeZone` (computer-use): a tz database time zone name, such as `Europe/London`. Each turn's context hint carries
248
- the local time in it, and a scheduled end is given in it (UTC without it);
283
+ - `actor.clock.timeZone` (computer-use and shared-world): a tz database time zone name, such as `Europe/London`. Each turn's context hint carries
284
+ the local time in it, and a scheduled end or session cap is given in it (UTC without it). On a hosted desktop the
285
+ participant's browser launches with `TZ` set to it (`openParticipantSurface` in
286
+ `src/routes/computer-use/e2b-desktop/start.ts`). `parseClock` in `src/study/parse/clock.ts` refuses a name Intl does not know, a
287
+ UTC offset and a case variant of Intl's spelling, which Chrome's ICU reads as UTC or `Etc/Unknown`. The computer-use planner
288
+ refuses a zone on the local runtime and the in-process route with `HUMANISH_COMPUTER_USE_TIME_ZONE_UNSUPPORTED`;
289
+ - `participants[].clock.timeZone` (computer-use and shared-world, also on `count:` groups): this participant's `actor.clock`,
290
+ with the same rule. The plan's `limits.timeZone` is the participant's zone, else the actor's. `study check` prints a
291
+ `time zones` row, and each participant's stream in `run.json` records `timeZone`;
249
292
  - `execution.concurrency` (computer-use E2B routes, including shared-world): a
250
293
  cap on participants in flight at once. When omitted, every participant runs
251
294
  simultaneously: both routes resolve it when they plan, from the final participant count
@@ -1011,6 +1054,32 @@ Core-owned fields:
1011
1054
  click-like `ui_action` items, structured `coord` (`x`/`y`). Both are additive:
1012
1055
  absent on older bundles and non-stamping producers, and absence means
1013
1056
  "timing/position unrecorded", never zero
1057
+ - optional `items[].page` and `finalPage` (`{ origin, path }`, `TracePage` in
1058
+ `src/actors/contract.ts`): where a computer-use participant's browser was. The loop puts `page`
1059
+ on the `screenshot` item of each observation that read a URL: `turn-00-start` is the page before
1060
+ any action, `turn-01` the page after turn 1's actions, and dwell frames have their own.
1061
+ `finalPage` is the page of the session's last observation, which is a closing observation with
1062
+ no item when tasks are declared; it is absent when that observation read no URL. The screenshot
1063
+ items with a `page`, in item order, then `finalPage`, list the participant's path through the
1064
+ app. `origin` is the URL's scheme, host and port; `path` is its percent-encoded path. A query
1065
+ string, fragment and user info are never kept, and a non-web URL keeps its scheme only
1066
+ (`{ origin: "about:", path: "" }`). Redaction (`tracePage` in `src/evidence/trace-page.ts`):
1067
+ a path segment becomes `[REDACTED_URL_PART]` when it percent-encodes a letter, digit, `-`, `.`,
1068
+ `_`, `~` or `%`, or when any decoded reading of it is email-shaped, holds a run of 16 or more letters,
1069
+ digits, `-` and `_` with a piece of 8 or more characters mixing letters and digits, or changes
1070
+ under the trace text redaction (the study's secrets, then `redactText`). The whole path then
1071
+ goes through that redaction, is cut to 1024 characters ending `[TRUNCATED]`, and is withheld
1072
+ whole when verify's trace scan would still read it as sensitive or opaque. A host label
1073
+ shaped like that token run becomes `[REDACTED_URL_PART]`, then the origin goes through the same
1074
+ redaction (an E2B sandbox host becomes `[REDACTED_SECRET]`). When origin and path together
1075
+ still change under that redaction (a study secret that is a whole link), both become
1076
+ `[REDACTED_URL_PART]`. A token shorter than 16 characters, an
1077
+ all-digit id and personal data in readable words stay, so the sharing rules for synthetic data
1078
+ still apply. Pages come from the browser-state probe, which hosted runs have on Chrome and
1079
+ Chromium only; a Firefox participant, or one whose probe is unavailable, has screenshots without
1080
+ pages. The local browser route and the shared-world route record none. verify reports a page
1081
+ whose path holds `?` or `#`, or whose origin is neither a redaction marker nor a scheme with an
1082
+ optional host and port, as a public-safety finding. Both fields are additive; absence means "page not recorded"
1014
1083
  - optional `modelSettings` (`humanish.model-settings.v1`): how the model was
1015
1084
  asked to run, alongside `ids.model` which says which model it was.
1016
1085
  `reasoningEffort` records the value the request actually carried,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.125.0",
3
+ "version": "0.126.0",
4
4
  "description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
5
5
  "keywords": [
6
6
  "cli",
@@ -96,7 +96,7 @@
96
96
  "comms:agentmail:contract": "node --import tsx scripts/agentmail-contract-check.mjs",
97
97
  "format": "oxfmt",
98
98
  "format:check": "oxfmt --check",
99
- "lint": "node scripts/check-lint-cap.mjs --max-warnings=424",
99
+ "lint": "node scripts/check-lint-cap.mjs --max-warnings=411",
100
100
  "knip": "knip",
101
101
  "prose:check": "node scripts/check-code-prose.mjs",
102
102
  "vocabulary:check": "tsx scripts/check-retired-words.ts",