humanish 0.136.0 → 0.137.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 (63) hide show
  1. package/CHANGELOG.md +22 -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/observation.js +2 -0
  5. package/dist/actors/computer-use/loop/session.d.ts +3 -2
  6. package/dist/actors/computer-use/loop/session.js +8 -12
  7. package/dist/actors/computer-use/loop/types.d.ts +14 -1
  8. package/dist/evidence/browser-network.d.ts +108 -0
  9. package/dist/evidence/browser-network.js +516 -0
  10. package/dist/evidence/trace-page.d.ts +31 -1
  11. package/dist/evidence/trace-page.js +68 -2
  12. package/dist/observer-app.html +4 -4
  13. package/dist/routes/computer-use/bundle.js +1 -0
  14. package/dist/routes/computer-use/e2b-desktop/desktop.js +6 -0
  15. package/dist/routes/computer-use/e2b-desktop/fidelity.d.ts +12 -1
  16. package/dist/routes/computer-use/e2b-desktop/fidelity.js +7 -2
  17. package/dist/routes/computer-use/e2b-desktop/network.d.ts +52 -0
  18. package/dist/routes/computer-use/e2b-desktop/network.js +153 -0
  19. package/dist/routes/computer-use/e2b-desktop/start.js +18 -1
  20. package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -0
  21. package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
  22. package/dist/routes/computer-use/e2b-desktop/teardown.js +6 -3
  23. package/dist/routes/computer-use/fanout-records.js +10 -0
  24. package/dist/routes/computer-use/participant-desktop.d.ts +7 -2
  25. package/dist/routes/computer-use/participant-model.js +2 -1
  26. package/dist/routes/computer-use/plan.js +2 -1
  27. package/dist/routes/computer-use/single-bundle.d.ts +3 -0
  28. package/dist/routes/computer-use/single-bundle.js +4 -0
  29. package/dist/routes/computer-use/types.d.ts +7 -0
  30. package/dist/routes/scripted/plan.js +2 -2
  31. package/dist/routes/shared-world/plan.js +3 -2
  32. package/dist/routes/terminal/plan.js +2 -2
  33. package/dist/run/live-items.d.ts +20 -2
  34. package/dist/run/live-items.js +23 -14
  35. package/dist/run/streams.d.ts +7 -0
  36. package/dist/study/config.js +4 -1
  37. package/dist/study/keys.d.ts +3 -0
  38. package/dist/study/keys.js +2 -0
  39. package/dist/study/parse/comms.js +1 -20
  40. package/dist/study/parse/execution.js +30 -1
  41. package/dist/study/parse/values.d.ts +5 -0
  42. package/dist/study/parse/values.js +22 -0
  43. package/dist/study/plan-base.js +6 -1
  44. package/dist/study/plan-types.d.ts +1 -1
  45. package/dist/study/study-fields.d.ts +7 -1
  46. package/dist/study/study-fields.js +10 -0
  47. package/dist/study/types.d.ts +11 -0
  48. package/dist/study/validation.d.ts +8 -0
  49. package/dist/study/validation.js +31 -2
  50. package/dist/substrates/detached.d.ts +5 -0
  51. package/dist/substrates/detached.js +9 -0
  52. package/dist/substrates/e2b/cdp-network-capture.d.ts +23 -0
  53. package/dist/substrates/e2b/cdp-network-capture.js +194 -0
  54. package/dist/substrates/e2b/cdp-probe.d.ts +20 -5
  55. package/dist/substrates/e2b/cdp-probe.js +100 -36
  56. package/dist/substrates/e2b/desktop-cdp.d.ts +32 -2
  57. package/dist/substrates/e2b/desktop-cdp.js +87 -5
  58. package/dist/verify/network.d.ts +10 -0
  59. package/dist/verify/network.js +162 -0
  60. package/dist/verify/pages.js +1 -16
  61. package/dist/verify/verify.js +8 -2
  62. package/docs/contracts/run-bundle.md +86 -1
  63. package/package.json +6 -6
@@ -13,11 +13,29 @@ export interface LiveItemsCursor {
13
13
  items: number;
14
14
  bytes: number;
15
15
  }
16
+ /** How run.json binds a file of JSON lines: its byte count, its sha256 and its number of lines. */
17
+ export interface JsonLinesIndex {
18
+ path: string;
19
+ bytes: number;
20
+ sha256: string;
21
+ lines: number;
22
+ }
23
+ /** What a refusal calls one line of a file and several: `an item` and `items` for a live file. */
24
+ export interface LineNoun {
25
+ one: string;
26
+ many: string;
27
+ }
16
28
  /**
17
- * The items the indexed prefix of a live file holds, or why the bytes do not match the index:
29
+ * The JSON object on each line of a file run.json binds, or why the bytes do not match the index:
18
30
  * `bytes` must be exactly the indexed byte count, hash to the indexed sha256, and hold the indexed
19
- * number of lines, each a JSON object.
31
+ * number of lines, each a JSON object. A refusal names the file and counts, never what it holds.
20
32
  */
33
+ export declare function boundJsonLines(bytes: Buffer, index: JsonLinesIndex, noun: LineNoun): {
34
+ lines: Record<string, unknown>[];
35
+ } | {
36
+ refused: string;
37
+ };
38
+ /** The items the indexed prefix of a live file holds, or why the bytes do not match the index. */
21
39
  export declare function liveItemsFromBytes(bytes: Buffer, index: LiveItemsIndex): {
22
40
  items: ActorTraceItem[];
23
41
  } | {
@@ -29,19 +29,24 @@ export function isLiveItemsIndex(value) {
29
29
  typeof value.sha256 === "string" &&
30
30
  /^[0-9a-f]{64}$/.test(value.sha256));
31
31
  }
32
+ const ITEM = { one: "an item", many: "items" };
32
33
  /**
33
- * The items the indexed prefix of a live file holds, or why the bytes do not match the index:
34
+ * The JSON object on each line of a file run.json binds, or why the bytes do not match the index:
34
35
  * `bytes` must be exactly the indexed byte count, hash to the indexed sha256, and hold the indexed
35
- * number of lines, each a JSON object.
36
+ * number of lines, each a JSON object. A refusal names the file and counts, never what it holds.
36
37
  */
37
- export function liveItemsFromBytes(bytes, index) {
38
+ export function boundJsonLines(bytes, index, noun) {
38
39
  if (bytes.length !== index.bytes)
39
40
  return {
40
41
  refused: `${index.path} holds ${bytes.length} bytes and run.json records ${index.bytes}`,
41
42
  };
42
43
  if (createHash("sha256").update(bytes).digest("hex") !== index.sha256)
43
44
  return { refused: `${index.path} does not match the sha256 run.json records for it` };
44
- return itemLinesOf(bytes, index.items, index.path);
45
+ return objectLinesOf(bytes, index.lines, index.path, noun);
46
+ }
47
+ /** The items the indexed prefix of a live file holds, or why the bytes do not match the index. */
48
+ export function liveItemsFromBytes(bytes, index) {
49
+ return asItems(boundJsonLines(bytes, { ...index, lines: index.items }, ITEM));
45
50
  }
46
51
  /**
47
52
  * The items past `after` in the indexed prefix of a live file, from the file's bytes between
@@ -59,10 +64,14 @@ export function liveItemsAfter(bytes, index, after) {
59
64
  };
60
65
  if (bytes[0] !== 0x0a)
61
66
  return { refused: `${index.path} has no line end before byte ${after.bytes}` };
62
- return itemLinesOf(bytes.subarray(1), index.items - after.items, index.path);
67
+ return asItems(objectLinesOf(bytes.subarray(1), index.items - after.items, index.path, ITEM));
68
+ }
69
+ /** A live file's lines as trace items. */
70
+ function asItems(read) {
71
+ return "refused" in read ? read : { items: read.lines };
63
72
  }
64
73
  /** The JSON object on each of `count` lines in `bytes`, which must be UTF-8 and end a line. */
65
- function itemLinesOf(bytes, count, file) {
74
+ function objectLinesOf(bytes, count, file, noun) {
66
75
  let text;
67
76
  try {
68
77
  text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
@@ -74,21 +83,21 @@ function itemLinesOf(bytes, count, file) {
74
83
  if (lines.length > 0 && lines.pop() !== "")
75
84
  return { refused: `${file} does not end its indexed bytes at a line end` };
76
85
  if (lines.length !== count)
77
- return { refused: `${file} holds ${lines.length} items and run.json records ${count}` };
78
- const items = [];
86
+ return { refused: `${file} holds ${lines.length} ${noun.many} and run.json records ${count}` };
87
+ const objects = [];
79
88
  for (const line of lines) {
80
- let item;
89
+ let value;
81
90
  try {
82
- item = JSON.parse(line);
91
+ value = JSON.parse(line);
83
92
  }
84
93
  catch {
85
94
  return { refused: `${file} holds a line that is not JSON` };
86
95
  }
87
- if (!isRecord(item))
88
- return { refused: `${file} holds a line that is not an item` };
89
- items.push(item);
96
+ if (!isRecord(value))
97
+ return { refused: `${file} holds a line that is not ${noun.one}` };
98
+ objects.push(value);
90
99
  }
91
- return { items };
100
+ return { lines: objects };
92
101
  }
93
102
  /** The warning for a participant whose live items were written in full on two writes in a row. */
94
103
  function rewrittenEvent(stream, position) {
@@ -1,5 +1,6 @@
1
1
  import type { CodexAppServerTrace } from "../actors/codex/app-server-trace.js";
2
2
  import type { ActorStatus, ActorTrace, ActorTraceItem } from "../actors/contract.js";
3
+ import type { BrowserNetworkIndex } from "../evidence/browser-network.js";
3
4
  import type { RunDesktopRecording } from "../evidence/desktop-recording-types.js";
4
5
  export type RunStreamKind = "ui" | "browser" | "terminal" | "tui" | "codex-ui" | "artifact" | "summary";
5
6
  export type RunSimulationStatus = "queued" | "preparing" | "running" | "passed" | "abandoned" | "incomplete" | "complete" | "blocked" | "timed_out" | "failed" | "contract_proof_only";
@@ -310,6 +311,12 @@ export interface RunStream {
310
311
  };
311
312
  completion?: RunStreamCompletion;
312
313
  recording?: RunDesktopRecording;
314
+ /**
315
+ * The participant's browser network log, when its study set `execution.network`: where the
316
+ * file is, what binds it, and why records are missing. The shape guard does not read it; verify
317
+ * checks it with the file (src/verify/network.ts).
318
+ */
319
+ network?: BrowserNetworkIndex;
313
320
  artifacts: Array<{
314
321
  label: string;
315
322
  path: string;
@@ -39,7 +39,7 @@ import { invalid, optionalStr, str } from "./parse/values.js";
39
39
  import { isComputerUseComposition, routeOf } from "./routing.js";
40
40
  import { declaredParticipantIds } from "./plan-participants.js";
41
41
  import { ID_PATTERN, STUDY_SCHEMA, } from "./types.js";
42
- import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "./validation.js";
42
+ import { automaticAnalysisRouteReason, desktopMediaValidationReason, networkCaptureValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "./validation.js";
43
43
  import { forwardDeclaredWarnings, inertFieldLabels, inertFieldPaths } from "./warnings.js";
44
44
  import { plural } from "../run/text.js";
45
45
  /**
@@ -228,6 +228,9 @@ function checkStudyConfig(config) {
228
228
  const mediaReason = desktopMediaValidationReason(config);
229
229
  if (mediaReason)
230
230
  return invalid(mediaReason);
231
+ const networkReason = networkCaptureValidationReason(config);
232
+ if (networkReason)
233
+ return invalid(networkReason);
231
234
  const outputLimitReason = outputTokenLimitValidationReason(config);
232
235
  if (outputLimitReason)
233
236
  return invalid(outputLimitReason);
@@ -187,6 +187,9 @@ export declare const EXECUTION: {
187
187
  transport: true;
188
188
  doNotTrack: true;
189
189
  };
190
+ network: {
191
+ origins: true;
192
+ };
190
193
  desktop: {
191
194
  template: true;
192
195
  browser: true;
@@ -119,6 +119,8 @@ export const EXECUTION = {
119
119
  completionTimeoutMs: true,
120
120
  egressAllow: true,
121
121
  terminal: { stdin: true, transport: true, doNotTrack: true },
122
+ // true or false, or a mapping whose keys are checked here.
123
+ network: { origins: true },
122
124
  desktop: {
123
125
  template: true,
124
126
  browser: true,
@@ -1,4 +1,4 @@
1
- import { invalid, posInt, str } from "./values.js";
1
+ import { exactOrigin, invalid, posInt, str } from "./values.js";
2
2
  import { isRecord } from "../../run/type-guards.js";
3
3
  // Fail-loud (never silently swallow a comms setting): a malformed `comms` block returns a parse
4
4
  // failure rather than being dropped.
@@ -104,25 +104,6 @@ function parseEmailConnection(raw) {
104
104
  }
105
105
  return { ok: true, value };
106
106
  }
107
- /** The origin of an http(s) URL with no credentials, path, query or fragment, else undefined. */
108
- function exactOrigin(input) {
109
- if (typeof input !== "string")
110
- return undefined;
111
- try {
112
- const url = new URL(input);
113
- return ["http:", "https:"].includes(url.protocol) &&
114
- !url.username &&
115
- !url.password &&
116
- url.pathname === "/" &&
117
- !url.search &&
118
- !url.hash
119
- ? url.origin
120
- : undefined;
121
- }
122
- catch {
123
- return undefined;
124
- }
125
- }
126
107
  /** `comms.email.external`: the catch the adopter runs, its inbox and the env var holding its token. */
127
108
  function parseExternalCatch(raw) {
128
109
  if (raw === undefined)
@@ -1,7 +1,7 @@
1
1
  import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
2
2
  import { DEVICE_PRESET_NAMES, isDevicePresetName } from "../device-presets.js";
3
3
  import { isExactRuntimeVersion } from "../../routes/terminal/runtime.js";
4
- import { invalid, nonNegNumber, posInt, str } from "./values.js";
4
+ import { exactOrigin, invalid, nonNegNumber, posInt, str } from "./values.js";
5
5
  import { isRecord } from "../../run/type-guards.js";
6
6
  import { scanEncodedText } from "../../evidence/encoded-text.js";
7
7
  export function parseExecution(raw) {
@@ -34,6 +34,11 @@ export function parseExecution(raw) {
34
34
  }
35
35
  if (desktopResult.value)
36
36
  execution.desktop = desktopResult.value;
37
+ const network = parseNetwork(raw.network);
38
+ if (!network.ok)
39
+ return network;
40
+ if (network.value !== undefined)
41
+ execution.network = network.value;
37
42
  const terminalResult = parseTerminal(raw.terminal);
38
43
  if (!terminalResult.ok) {
39
44
  return terminalResult;
@@ -71,6 +76,30 @@ export function parseExecution(raw) {
71
76
  }
72
77
  return { ok: true, value: Object.keys(execution).length > 0 ? execution : undefined };
73
78
  }
79
+ /** `execution.network`: true, false, or `{ origins }`. False reads as absent. */
80
+ function parseNetwork(raw) {
81
+ if (raw === undefined || raw === false)
82
+ return { ok: true, value: undefined };
83
+ if (raw === true)
84
+ return { ok: true, value: true };
85
+ if (!isRecord(raw))
86
+ return invalid("`execution.network` must be true, false, or a mapping with `origins`: the origins whose requests are kept beside the target's.");
87
+ if (raw.origins === undefined)
88
+ return { ok: true, value: {} };
89
+ if (!Array.isArray(raw.origins) || raw.origins.length > 16)
90
+ return invalid("`execution.network.origins` must list at most 16 exact http(s) origins, such as https://api.example.com.");
91
+ const origins = [];
92
+ for (const entry of raw.origins) {
93
+ const origin = exactOrigin(entry);
94
+ if (origin === undefined)
95
+ return invalid("`execution.network.origins` accepts exact http(s) origins without credentials, path, query or fragment, such as https://api.example.com.");
96
+ // The network log's index records each origin, so one verify would flag is refused here.
97
+ if (scanEncodedText(origin).sensitive)
98
+ return invalid("`execution.network.origins` has an origin that looks like a secret or a local path, and the run records each origin in its network log index. List the origin of the API or asset host only.");
99
+ origins.push(origin);
100
+ }
101
+ return { ok: true, value: { origins: [...new Set(origins)] } };
102
+ }
74
103
  function parseTerminal(raw) {
75
104
  if (raw === undefined) {
76
105
  return { ok: true, value: undefined };
@@ -14,3 +14,8 @@ export declare function posInt(value: unknown): number | undefined;
14
14
  /** A non-negative finite number (0 allowed; caps default to 0 = no-spend). Accepts a numeric
15
15
  * string too, since YAML scalars can arrive as strings. */
16
16
  export declare function nonNegNumber(value: unknown): number | undefined;
17
+ /**
18
+ * The origin of an http(s) URL with no credentials, path, query or fragment, else undefined. The
19
+ * origin lists `comms.email.allowedOrigins` and `execution.network.origins` take this rule.
20
+ */
21
+ export declare function exactOrigin(input: unknown): string | undefined;
@@ -60,3 +60,25 @@ export function nonNegNumber(value) {
60
60
  }
61
61
  return undefined;
62
62
  }
63
+ /**
64
+ * The origin of an http(s) URL with no credentials, path, query or fragment, else undefined. The
65
+ * origin lists `comms.email.allowedOrigins` and `execution.network.origins` take this rule.
66
+ */
67
+ export function exactOrigin(input) {
68
+ if (typeof input !== "string")
69
+ return undefined;
70
+ try {
71
+ const url = new URL(input);
72
+ return ["http:", "https:"].includes(url.protocol) &&
73
+ !url.username &&
74
+ !url.password &&
75
+ url.pathname === "/" &&
76
+ !url.search &&
77
+ !url.hash
78
+ ? url.origin
79
+ : undefined;
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ }
@@ -20,7 +20,9 @@ function residualOf(config) {
20
20
  ...(policies === undefined ? {} : { policies }),
21
21
  ...(defaults === undefined ? {} : { defaults }),
22
22
  ...(review === undefined ? {} : { review }),
23
- ...(config.execution?.desktop === undefined && config.execution?.target === undefined
23
+ ...(config.execution?.desktop === undefined &&
24
+ config.execution?.target === undefined &&
25
+ config.execution?.network === undefined
24
26
  ? {}
25
27
  : {
26
28
  execution: {
@@ -30,6 +32,9 @@ function residualOf(config) {
30
32
  ...(config.execution?.target === undefined
31
33
  ? {}
32
34
  : { target: config.execution.target }),
35
+ ...(config.execution?.network === undefined
36
+ ? {}
37
+ : { network: config.execution.network }),
33
38
  },
34
39
  }),
35
40
  subject: {
@@ -51,7 +51,7 @@ export type Requirement = {
51
51
  * fields at run time.
52
52
  */
53
53
  export type ResidualConfig = Pick<StudyConfig, "comms" | "policies" | "defaults" | "review"> & {
54
- readonly execution?: Pick<NonNullable<StudyConfig["execution"]>, "desktop" | "target">;
54
+ readonly execution?: Pick<NonNullable<StudyConfig["execution"]>, "desktop" | "target" | "network">;
55
55
  readonly subject: Pick<StudyConfig["subject"], "clone" | "localTree" | "repos" | "envValues">;
56
56
  };
57
57
  export interface PlannedAnalysis {
@@ -1,7 +1,13 @@
1
- import type { StudyConfig, StudyParticipantEntry } from "./types.js";
1
+ import type { StudyConfig, StudyNetworkCapture, StudyParticipantEntry } from "./types.js";
2
2
  /** The participants listed one by one: a `participants` list. Undefined when none is listed. */
3
3
  export declare function participantList(config: StudyConfig): readonly StudyParticipantEntry[] | undefined;
4
4
  /** The declared number of identical participants: `participants: <n>` or `participants.count`. */
5
5
  export declare function declaredParticipantCount(config: StudyConfig): number | undefined;
6
6
  /** The steer each of a counted group of participants gets: `participants.instruction`. */
7
7
  export declare function participantInstruction(config: StudyConfig): string | undefined;
8
+ /**
9
+ * The origins each participant's network log keeps beside its target's
10
+ * (`execution.network.origins`), or undefined when capture is off: `execution.network` absent or
11
+ * false.
12
+ */
13
+ export declare function networkCaptureOrigins(network: StudyNetworkCapture | undefined): readonly string[] | undefined;
@@ -17,6 +17,16 @@ export function declaredParticipantCount(config) {
17
17
  export function participantInstruction(config) {
18
18
  return isGroup(config.participants) ? config.participants.instruction : undefined;
19
19
  }
20
+ /**
21
+ * The origins each participant's network log keeps beside its target's
22
+ * (`execution.network.origins`), or undefined when capture is off: `execution.network` absent or
23
+ * false.
24
+ */
25
+ export function networkCaptureOrigins(network) {
26
+ if (network === undefined || network === false)
27
+ return undefined;
28
+ return network === true ? [] : (network.origins ?? []);
29
+ }
20
30
  function isGroup(participants) {
21
31
  return typeof participants === "object" && participants !== null && !Array.isArray(participants);
22
32
  }
@@ -570,6 +570,15 @@ export interface StudyDesktopFidelity {
570
570
  /** Full user-agent string to present; default: a mobile Safari string. */
571
571
  userAgent?: string;
572
572
  }
573
+ /**
574
+ * `execution.network`: record each browser participant's requests to `network/<id>.ndjson`.
575
+ * `true` keeps the requests to the participant's target origin; `origins` adds up to 16 more exact
576
+ * http(s) origins. Only computer-use participants in hosted Chrome or Chromium on
577
+ * `execution.target: e2b-desktop` read it; every other route refuses it.
578
+ */
579
+ export type StudyNetworkCapture = boolean | {
580
+ origins?: string[];
581
+ };
573
582
  export interface StudyExecution {
574
583
  target?: StudyExecutionTarget;
575
584
  /** Actor session wall-clock budget. Consumed on the app-url route. */
@@ -582,6 +591,8 @@ export interface StudyExecution {
582
591
  */
583
592
  concurrency?: number;
584
593
  desktop?: StudyExecutionDesktop;
594
+ /** Each browser participant's network log; see StudyNetworkCapture. Absent or false: off. */
595
+ network?: StudyNetworkCapture;
585
596
  /** `terminal-product` route: the terminal transport + stdin posture. Consumed on that route. */
586
597
  terminal?: StudyExecutionTerminal;
587
598
  /** `terminal-product` route: runtime key placement, defaulting to openai-egress, an external
@@ -1,3 +1,4 @@
1
+ import { type StudyRoute } from "./routing.js";
1
2
  import type { StudyConfig } from "./types.js";
2
3
  /**
3
4
  * Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
@@ -21,6 +22,13 @@ export declare function smtpValidationReason(config: StudyConfig): string | unde
21
22
  * topology may not identify the backend that is actually executing.
22
23
  */
23
24
  export declare function desktopMediaValidationReason(config: StudyConfig, supportsMedia?: boolean): string | undefined;
25
+ /**
26
+ * Why a study's `execution.network` cannot run where it is going, or undefined. Only a
27
+ * computer-use participant in hosted Chrome or Chromium has the in-sandbox DevTools client that
28
+ * records requests. Enforced at parse and again by each route's planner, which passes its own
29
+ * route, for a library caller's config.
30
+ */
31
+ export declare function networkCaptureValidationReason(config: StudyConfig, route?: StudyRoute): string | undefined;
24
32
  /** Reused by direct library runners so unsupported receiving never becomes inert configuration. */
25
33
  export declare function receivingEmailValidationReason(config: StudyConfig): string | undefined;
26
34
  /** Refuse task declarations that the selected execution path would discard.
@@ -4,8 +4,8 @@ import { waitFieldsReason } from "../actors/computer-use/wait.js";
4
4
  import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./parse/actors.js";
5
5
  import { isHttpUrl, isLoopbackUrl } from "./parse/subject.js";
6
6
  import { declaredTargets } from "./plan-participants.js";
7
- import { actorResolvesToComputerUse, computerUseParticipantCount, participantCountReason, registeredComputerUseActors, resolveEntryUrl, isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
8
- import { participantList } from "./study-fields.js";
7
+ import { actorResolvesToComputerUse, computerUseParticipantCount, participantCountReason, registeredComputerUseActors, resolveEntryUrl, isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, routeOf, } from "./routing.js";
8
+ import { networkCaptureOrigins, participantList } from "./study-fields.js";
9
9
  import { isStartAfterMs, rosterStartsReason, startAfterMessage, windowReason } from "./arrivals.js";
10
10
  /**
11
11
  * Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
@@ -206,6 +206,35 @@ export function desktopMediaValidationReason(config, supportsMedia = isComputerU
206
206
  }
207
207
  return undefined;
208
208
  }
209
+ /**
210
+ * Why a study's `execution.network` cannot run where it is going, or undefined. Only a
211
+ * computer-use participant in hosted Chrome or Chromium has the in-sandbox DevTools client that
212
+ * records requests. Enforced at parse and again by each route's planner, which passes its own
213
+ * route, for a library caller's config.
214
+ */
215
+ export function networkCaptureValidationReason(config, route = routeOf(config)) {
216
+ if (networkCaptureOrigins(config.execution?.network) === undefined)
217
+ return undefined;
218
+ const hosted = "only computer-use participants in hosted Chrome or Chromium on `execution.target: e2b-desktop` record their requests";
219
+ if (route === "terminal")
220
+ return "`execution.network` records each browser participant's requests, and route: terminal launches no browser: its participant works in a shell. Remove `execution.network`.";
221
+ if (route === "shared-world")
222
+ return "`execution.network` is not supported on route: shared-world yet: a shared world's lobby code sits in a page address the network log would keep, and that route keeps observed addresses only as digests. Remove `execution.network`, or run independent participants on route: computer-use.";
223
+ if (route === "scripted")
224
+ return `\`execution.network\` is not supported on route: scripted yet: ${hosted}. Remove \`execution.network\`.`;
225
+ if (route !== "computer-use")
226
+ return `\`execution.network\` is not read on route: ${route}: ${hosted}. Remove \`execution.network\`.`;
227
+ if (config.subject.source === "desktop-cli")
228
+ return "`execution.network` records each browser participant's requests, and a desktop-cli subject opens a terminal window and no browser, so there are none to record. Remove `execution.network`.";
229
+ if (config.subject.source === "local-app")
230
+ return `\`execution.network\` is not supported for a local-app subject: humanish does not launch that participant's browser, and ${hosted}. Remove \`execution.network\`.`;
231
+ const target = config.execution?.target;
232
+ if (target !== "e2b-desktop")
233
+ return `\`execution.network\` is not supported with \`execution.target: ${target ?? "(absent)"}\` yet: ${hosted}. Set \`execution.target: e2b-desktop\`, or remove \`execution.network\`.`;
234
+ if (config.execution?.desktop?.browser === "firefox")
235
+ return "`execution.network` needs Chrome or Chromium, and `execution.desktop.browser: firefox` launches Firefox with no DevTools port to record requests through. Set `execution.desktop.browser` to chrome, chromium or default, or remove `execution.network`.";
236
+ return undefined;
237
+ }
209
238
  /** Reused by direct library runners so unsupported receiving never becomes inert configuration. */
210
239
  export function receivingEmailValidationReason(config) {
211
240
  if (config.comms?.email?.kind !== "real")
@@ -27,6 +27,11 @@ export interface DetachedStepResult {
27
27
  }
28
28
  /** Read the capped log tail for a step (raw; caller redacts). */
29
29
  export declare function readDetachedLog(shell: Shell, name: string, requestTimeoutMs?: number): Promise<string>;
30
+ /**
31
+ * Whether a process started by startDetachedProcess or runDetachedStep has exited. Its wrapper
32
+ * writes a status file however the command ended, a signal included.
33
+ */
34
+ export declare function detachedProcessExited(shell: Shell, name: string, requestTimeoutMs?: number): Promise<boolean>;
30
35
  /**
31
36
  * Run a bounded step (install/build) detached, polling its atomic status file until it
32
37
  * exits or the budget runs out. On timeout the process group is killed and the log tail is
@@ -75,6 +75,15 @@ export async function readDetachedLog(shell, name, requestTimeoutMs = DEFAULT_RE
75
75
  const result = await runOrThrow(shell, `tail -c ${LOG_TAIL_BYTES} ${shellQuote(`${stepDir(name)}/log.txt`)} 2>/dev/null || true`, { requestTimeoutMs });
76
76
  return result.stdout;
77
77
  }
78
+ /**
79
+ * Whether a process started by startDetachedProcess or runDetachedStep has exited. Its wrapper
80
+ * writes a status file however the command ended, a signal included.
81
+ */
82
+ export async function detachedProcessExited(shell, name, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
83
+ assertName(name);
84
+ const result = await runOrThrow(shell, `test -e ${shellQuote(`${stepDir(name)}/status`)} && echo exited || true`, { requestTimeoutMs });
85
+ return result.stdout.trim() === "exited";
86
+ }
78
87
  /**
79
88
  * Run a bounded step (install/build) detached, polling its atomic status file until it
80
89
  * exits or the budget runs out. On timeout the process group is killed and the log tail is
@@ -0,0 +1,23 @@
1
+ /** The `network` field of a hold request. */
2
+ export interface ChromeNetworkCaptureRequest {
3
+ /**
4
+ * Sandbox file the holder appends to; its folder is created. Defaults to
5
+ * /tmp/humanish-network/events.ndjson.
6
+ */
7
+ eventsPath?: string;
8
+ /** Origins, as `new URL(url).origin` writes them, whose request addresses the holder writes. */
9
+ origins: string[];
10
+ }
11
+ /**
12
+ * The capture half of the holder, spliced into CHROME_CDP_PROBE_PY so the shipped script stays one
13
+ * string. Line kinds: `start` (Network answered on the launch page), `request`, `response`,
14
+ * `servedFromCache`, `finished`, `failed`, `backForward` (a main-frame restore from the
15
+ * back-forward cache, which sends no request), `cap` (the file reached 32 MiB and writing stopped)
16
+ * and `exit` (the holder stopped; written in a finally). Ids are `<session>:<requestId>`, the
17
+ * session numbered in attach order from 0 for the launch page, because request ids repeat across
18
+ * renderers. `wallTime` is seconds since the epoch on the sandbox clock; `timestamp` is CDP's
19
+ * monotonic clock, for durations. A path longer than 4,096 characters ends at its last whole
20
+ * segment within them, then `/[TRUNCATED]`: the host redacts each segment, and a token cut in two
21
+ * can fall below the length its rules withhold.
22
+ */
23
+ export declare const NETWORK_CAPTURE_PY: string;