humanish 0.114.0 → 0.116.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 (204) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CONTEXT.md +1 -1
  3. package/README.md +7 -6
  4. package/TELEMETRY.md +6 -0
  5. package/dist/actors/closing-report-limits.d.ts +11 -0
  6. package/dist/actors/closing-report-limits.js +13 -0
  7. package/dist/actors/closing-report.d.ts +68 -0
  8. package/dist/actors/closing-report.js +55 -0
  9. package/dist/actors/codex/app-server-ui.js +11 -4
  10. package/dist/actors/codex/restricted-participant-policy.d.ts +4 -47
  11. package/dist/actors/codex/restricted-participant-policy.js +12 -40
  12. package/dist/actors/computer-use/loop/debrief.d.ts +1 -10
  13. package/dist/actors/computer-use/loop/debrief.js +68 -70
  14. package/dist/actors/computer-use/loop/impressions.d.ts +2 -2
  15. package/dist/actors/computer-use/loop/impressions.js +6 -1
  16. package/dist/actors/computer-use/loop.d.ts +0 -1
  17. package/dist/actors/computer-use/loop.js +3 -3
  18. package/dist/actors/computer-use/openai-conversation.d.ts +1 -1
  19. package/dist/actors/computer-use/openai-conversation.js +3 -29
  20. package/dist/actors/computer-use/openai-provider.js +11 -5
  21. package/dist/actors/contract.d.ts +2 -20
  22. package/dist/actors/contract.js +0 -13
  23. package/dist/actors/scripted-browser/steps.js +6 -5
  24. package/dist/analysis/admission.d.ts +14 -4
  25. package/dist/analysis/admission.js +14 -9
  26. package/dist/analysis/analysis-limits.d.ts +35 -0
  27. package/dist/analysis/analysis-limits.js +41 -0
  28. package/dist/analysis/execute.d.ts +5 -0
  29. package/dist/analysis/execute.js +3 -0
  30. package/dist/analysis/finding-lead.d.ts +27 -0
  31. package/dist/analysis/finding-lead.js +23 -0
  32. package/dist/analysis/service.js +5 -3
  33. package/dist/analysis/validation.js +14 -21
  34. package/dist/cli/commands/analyze.js +2 -1
  35. package/dist/cli/commands/notes.js +5 -3
  36. package/dist/cli/commands/run-signals.js +5 -1
  37. package/dist/cli/commands/runs.js +2 -1
  38. package/dist/cli/commands/setup.js +3 -3
  39. package/dist/cli/commands/study-format.d.ts +5 -2
  40. package/dist/cli/commands/study-format.js +29 -9
  41. package/dist/cli/commands/study-route-computer-use.js +2 -2
  42. package/dist/cli/commands/study-route-scripted.js +1 -1
  43. package/dist/cli/commands/tui.js +59 -17
  44. package/dist/cli/doctor.js +9 -2
  45. package/dist/cli/findings-text.d.ts +5 -0
  46. package/dist/cli/findings-text.js +169 -0
  47. package/dist/cli/findings.d.ts +0 -4
  48. package/dist/cli/findings.js +11 -195
  49. package/dist/cli/io.js +10 -8
  50. package/dist/cli/telemetry.js +10 -22
  51. package/dist/cli/update-check.js +10 -19
  52. package/dist/cli/user-config.d.ts +11 -0
  53. package/dist/cli/user-config.js +25 -0
  54. package/dist/comms/connections.js +4 -3
  55. package/dist/comms/email-catch.js +7 -31
  56. package/dist/comms/setup.js +18 -12
  57. package/dist/evidence/encoded-text.d.ts +12 -11
  58. package/dist/evidence/encoded-text.js +81 -70
  59. package/dist/evidence/redaction.d.ts +11 -0
  60. package/dist/evidence/redaction.js +30 -11
  61. package/dist/evidence/secret-scrub.d.ts +26 -3
  62. package/dist/evidence/secret-scrub.js +172 -38
  63. package/dist/feedback/draft.d.ts +1 -1
  64. package/dist/feedback/draft.js +13 -16
  65. package/dist/feedback/export-bundle.js +21 -17
  66. package/dist/feedback/export.js +32 -46
  67. package/dist/feedback/feedback.js +31 -26
  68. package/dist/guest/desktop-keys.js +59 -5
  69. package/dist/keys/key-resolution.d.ts +9 -0
  70. package/dist/keys/key-resolution.js +62 -55
  71. package/dist/keys/key-status.d.ts +2 -0
  72. package/dist/keys/key-status.js +9 -16
  73. package/dist/observer/artifact.d.ts +1 -1
  74. package/dist/observer/http.d.ts +7 -0
  75. package/dist/observer/http.js +19 -1
  76. package/dist/observer/notes-route.d.ts +0 -1
  77. package/dist/observer/notes-route.js +24 -44
  78. package/dist/observer/pinned-files.js +4 -19
  79. package/dist/observer/render.js +6 -4
  80. package/dist/observer/run-routes.js +13 -28
  81. package/dist/observer/serve.js +2 -1
  82. package/dist/observer-app.html +7 -7
  83. package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
  84. package/dist/routes/computer-use/bundle-parts.js +2 -2
  85. package/dist/routes/computer-use/bundle.d.ts +4 -5
  86. package/dist/routes/computer-use/bundle.js +1 -3
  87. package/dist/routes/computer-use/fanout-records.js +10 -11
  88. package/dist/routes/computer-use/live-phase.d.ts +1 -1
  89. package/dist/routes/computer-use/live-phase.js +0 -1
  90. package/dist/routes/computer-use/participant-execution.d.ts +3 -2
  91. package/dist/routes/computer-use/participant-runs.d.ts +6 -2
  92. package/dist/routes/computer-use/participant-runs.js +9 -5
  93. package/dist/routes/computer-use/plan.d.ts +3 -0
  94. package/dist/routes/computer-use/plan.js +14 -11
  95. package/dist/routes/computer-use/rerun-selection.js +2 -2
  96. package/dist/routes/computer-use/result.js +3 -6
  97. package/dist/routes/computer-use/setup.d.ts +4 -6
  98. package/dist/routes/computer-use/setup.js +2 -6
  99. package/dist/routes/computer-use/single-bundle.d.ts +4 -5
  100. package/dist/routes/computer-use/single-bundle.js +22 -25
  101. package/dist/routes/computer-use/subject-projection.d.ts +4 -8
  102. package/dist/routes/computer-use/subject-projection.js +9 -31
  103. package/dist/routes/computer-use/types.d.ts +4 -41
  104. package/dist/routes/scripted/scenario.js +28 -12
  105. package/dist/routes/scripted/session-result.js +6 -3
  106. package/dist/routes/shared-world/bundle-records.js +2 -8
  107. package/dist/routes/shared-world/participant-specs.d.ts +0 -2
  108. package/dist/routes/shared-world/participant-specs.js +1 -20
  109. package/dist/routes/shared-world/plan.d.ts +3 -0
  110. package/dist/routes/shared-world/plan.js +23 -3
  111. package/dist/routes/shared-world/setup.js +2 -2
  112. package/dist/routes/terminal/ledger.d.ts +9 -9
  113. package/dist/routes/terminal/ledger.js +63 -40
  114. package/dist/routes/terminal/live-finish.js +7 -10
  115. package/dist/routes/terminal/plan.d.ts +3 -0
  116. package/dist/routes/terminal/plan.js +8 -5
  117. package/dist/routes/terminal/token-usage.d.ts +1 -2
  118. package/dist/routes/terminal/token-usage.js +4 -6
  119. package/dist/routes/terminal/types.d.ts +9 -1
  120. package/dist/run/bundle-shape.js +6 -1
  121. package/dist/run/bundle.d.ts +5 -3
  122. package/dist/run/contained-output.d.ts +59 -10
  123. package/dist/run/contained-output.js +160 -68
  124. package/dist/run/detail.d.ts +2 -1
  125. package/dist/run/detail.js +12 -7
  126. package/dist/run/escape-sequences.d.ts +11 -0
  127. package/dist/run/escape-sequences.js +93 -0
  128. package/dist/run/evidence-files.d.ts +8 -1
  129. package/dist/run/evidence-files.js +24 -18
  130. package/dist/run/locate.d.ts +32 -3
  131. package/dist/run/locate.js +45 -40
  132. package/dist/run/note-files.d.ts +26 -45
  133. package/dist/run/note-files.js +86 -58
  134. package/dist/run/note-shape.d.ts +35 -0
  135. package/dist/run/note-shape.js +20 -0
  136. package/dist/run/notes.d.ts +3 -14
  137. package/dist/run/notes.js +23 -75
  138. package/dist/run/outcomes.d.ts +24 -0
  139. package/dist/run/outcomes.js +19 -0
  140. package/dist/run/participant-records.d.ts +2 -0
  141. package/dist/run/participant-records.js +17 -2
  142. package/dist/run/paths.d.ts +2 -0
  143. package/dist/run/paths.js +5 -1
  144. package/dist/run/preflight-receipts.js +13 -8
  145. package/dist/run/project.js +7 -4
  146. package/dist/run/reclaim-outcomes.d.ts +8 -6
  147. package/dist/run/reclaim-outcomes.js +10 -6
  148. package/dist/run/reclaim.d.ts +5 -0
  149. package/dist/run/reclaim.js +43 -16
  150. package/dist/run/run-clock.d.ts +50 -0
  151. package/dist/run/run-clock.js +66 -0
  152. package/dist/run/run.js +5 -2
  153. package/dist/run/sandbox-ids.d.ts +7 -2
  154. package/dist/run/sandbox-ids.js +27 -11
  155. package/dist/run/secrets.d.ts +2 -1
  156. package/dist/run/secrets.js +18 -22
  157. package/dist/run/stored-runs.js +14 -7
  158. package/dist/run/transient-comms-secrets.d.ts +6 -5
  159. package/dist/run/transient-comms-secrets.js +23 -39
  160. package/dist/run-study.js +22 -13
  161. package/dist/study/adapter-scorer-loader.js +6 -4
  162. package/dist/study/discover.js +9 -6
  163. package/dist/study/init-paths.js +7 -5
  164. package/dist/study/persona-resolve.d.ts +2 -2
  165. package/dist/study/persona-resolve.js +9 -5
  166. package/dist/study/plan-types.d.ts +2 -2
  167. package/dist/study/plan.d.ts +5 -3
  168. package/dist/study/plan.js +18 -9
  169. package/dist/study/preflight-probes.js +11 -3
  170. package/dist/study/preflight.js +9 -5
  171. package/dist/study/summary.d.ts +12 -7
  172. package/dist/study/summary.js +24 -24
  173. package/dist/study/warnings.d.ts +19 -12
  174. package/dist/study/warnings.js +39 -17
  175. package/dist/substrates/e2b/connection.d.ts +12 -0
  176. package/dist/substrates/e2b/connection.js +35 -0
  177. package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
  178. package/dist/substrates/e2b/desktop-executor.js +42 -9
  179. package/dist/substrates/e2b/lifetime.d.ts +24 -4
  180. package/dist/substrates/e2b/lifetime.js +41 -4
  181. package/dist/substrates/e2b/sandbox.d.ts +4 -3
  182. package/dist/substrates/e2b/sandbox.js +13 -18
  183. package/dist/substrates/e2b/sdk.d.ts +20 -9
  184. package/dist/substrates/local/capacity.d.ts +18 -2
  185. package/dist/substrates/local/capacity.js +21 -2
  186. package/dist/substrates/local/runtime-host.js +21 -14
  187. package/dist/substrates/local/runtime.js +3 -11
  188. package/dist/tui/contract.d.ts +30 -5
  189. package/dist/tui/contract.js +4 -1
  190. package/dist/tui-app.js +149 -147
  191. package/dist/verify/artifacts.d.ts +9 -2
  192. package/dist/verify/artifacts.js +37 -13
  193. package/dist/verify/liveness.js +19 -5
  194. package/dist/verify/verify.js +54 -27
  195. package/docs/architecture/actor-contract.md +3 -3
  196. package/docs/architecture/guest-desktop.md +7 -2
  197. package/docs/architecture/restricted-codex-analysis.md +1 -1
  198. package/docs/architecture/state-driven-executor.md +5 -3
  199. package/docs/contracts/run-bundle.md +25 -13
  200. package/docs/contracts/schemas.md +38 -29
  201. package/docs/contracts/study-analysis.md +22 -5
  202. package/docs/product/automatic-analysis.md +7 -1
  203. package/package.json +8 -7
  204. package/skills/humanish/SKILL.md +1 -2
@@ -0,0 +1,93 @@
1
+ // What a terminal draws with and what a URL encodes with, found in one pass. RunSecrets looks for a
2
+ // value in a view of the text with the first dropped and the second decoded, so that a color code
3
+ // or a percent escape inside a value does not hide it.
4
+ const BEL = "\x07";
5
+ const STRING_TERMINATOR = "\x1b\\";
6
+ const between = (code, low, high) => code >= low && code <= high;
7
+ const isHexDigit = (code) => between(code, 0x30, 0x39) || between(code, 0x41, 0x46) || between(code, 0x61, 0x66);
8
+ /**
9
+ * The end of a control sequence whose parameter bytes start at `from`: parameter bytes, then
10
+ * intermediate bytes, then one final byte. -1 when no final byte follows.
11
+ */
12
+ function controlSequenceEnd(text, from) {
13
+ let at = from;
14
+ while (at < text.length && between(text.charCodeAt(at), 0x30, 0x3f))
15
+ at += 1;
16
+ while (at < text.length && between(text.charCodeAt(at), 0x20, 0x2f))
17
+ at += 1;
18
+ return at < text.length && between(text.charCodeAt(at), 0x40, 0x7e) ? at + 1 : -1;
19
+ }
20
+ /** The end of the run of percent escapes that starts at `from`, or `from` when none does. */
21
+ function percentRunEnd(text, from) {
22
+ let end = from;
23
+ while (end + 2 < text.length &&
24
+ text[end] === "%" &&
25
+ isHexDigit(text.charCodeAt(end + 1)) &&
26
+ isHexDigit(text.charCodeAt(end + 2)))
27
+ end += 3;
28
+ return end;
29
+ }
30
+ /**
31
+ * The stretches of the text a value's view drops or decodes, in order and without overlap, each
32
+ * as [start, end): operating-system commands (`ESC ]`), control sequences (`ESC [`) and the
33
+ * two-byte escapes `ESC 7`, `ESC 8`, `ESC =` and `ESC >`, raw or JSON-escaped (`\u001b`), and runs
34
+ * of percent escapes. An operating-system command ends at the first bell character after it or,
35
+ * with none after it, at the last `ESC \` in the text. A JSON-escaped one ends at the first
36
+ * backslash after it, which must start `\u0007` or `\u001b\\`. A start with no end is left as
37
+ * written. One regex of the six shapes finds the same stretches, in time quadratic in a run of
38
+ * unterminated commands; this takes time linear in the text.
39
+ */
40
+ export function escapeSequences(text) {
41
+ const lastTerminator = text.lastIndexOf(STRING_TERMINATOR);
42
+ // The first bell character at or after the last place searched from. Starts only move forward,
43
+ // so each stretch of text is searched once.
44
+ let bel = text.indexOf(BEL);
45
+ const commandEnd = (start) => {
46
+ if (bel !== -1 && bel < start + 2)
47
+ bel = text.indexOf(BEL, start + 2);
48
+ if (bel !== -1)
49
+ return bel + 1;
50
+ return lastTerminator >= start + 2 ? lastTerminator + STRING_TERMINATOR.length : -1;
51
+ };
52
+ const jsonCommandEnd = (start) => {
53
+ const backslash = text.indexOf("\\", start + 7);
54
+ if (backslash === -1)
55
+ return -1;
56
+ if (text.startsWith("\\u0007", backslash))
57
+ return backslash + 6;
58
+ return text.startsWith("\\u001b\\\\", backslash) ? backslash + 8 : -1;
59
+ };
60
+ const endAt = (at) => {
61
+ switch (text[at]) {
62
+ case "\x1b": {
63
+ const next = text[at + 1];
64
+ if (next === "]")
65
+ return commandEnd(at);
66
+ if (next === "[")
67
+ return controlSequenceEnd(text, at + 2);
68
+ return next !== undefined && "78=>".includes(next) ? at + 2 : -1;
69
+ }
70
+ case "\\":
71
+ if (text.startsWith("\\u001b]", at))
72
+ return jsonCommandEnd(at);
73
+ return text.startsWith("\\u001b[", at) ? controlSequenceEnd(text, at + 7) : -1;
74
+ case "%": {
75
+ const end = percentRunEnd(text, at);
76
+ return end === at ? -1 : end;
77
+ }
78
+ default:
79
+ return -1;
80
+ }
81
+ };
82
+ const sequences = [];
83
+ for (let at = 0; at < text.length;) {
84
+ const end = endAt(at);
85
+ if (end === -1)
86
+ at += 1;
87
+ else {
88
+ sequences.push([at, end]);
89
+ at = end;
90
+ }
91
+ }
92
+ return sequences;
93
+ }
@@ -31,5 +31,12 @@ export type BoundedFileResult = {
31
31
  } | {
32
32
  state: "unavailable";
33
33
  };
34
- /** Size refusals are distinguished only after the same contained regular-file checks. */
34
+ /** readUnchangedFile for an analysis input, whose path must also be an evidence path. */
35
35
  export declare function readBoundedFileResult(root: PreparedOutputRoot, relativePath: string, maxBytes: number): Promise<BoundedFileResult>;
36
+ /**
37
+ * At most `maxBytes` of a contained single-link regular file that stays the same file, with the
38
+ * same size and times, from the first check to the last. Size refusals are distinguished only after
39
+ * the same checks. `relativePath` uses `/`, and any name a contained read accepts
40
+ * (isSafeRelativeFilePath) is read: verify and export read the names they always did.
41
+ */
42
+ export declare function readUnchangedFile(root: PreparedOutputRoot, relativePath: string, maxBytes: number): Promise<BoundedFileResult>;
@@ -2,11 +2,12 @@ import { constants } from "node:fs";
2
2
  import { lstat, open, realpath } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { isPathInside, validatePreparedRunRootIdentity } from "./paths.js";
5
- import { assertPreparedSelectedOutputDirectory, } from "./contained-output.js";
5
+ import { assertPreparedSelectedOutputDirectory, isSafeRelativeFilePath, readOpenedAtMost, } from "./contained-output.js";
6
6
  import { isNodeError } from "./type-guards.js";
7
7
  // Reading a file from a retained run directory for analysis: the path must be a plain relative path
8
8
  // inside the run, the file a single-link regular file that stays inside it, and the read bounded.
9
- // Evidence capture, analysis jobs, the analysis store, cost reading and export all read through here.
9
+ // Evidence capture, analysis jobs, the analysis store, cost reading, verify and export all read
10
+ // through here.
10
11
  /**
11
12
  * Analysis inputs are retained local artifacts, never URLs or caller-selected outputs. The path is
12
13
  * percent-decoded up to five times, and each round must still be a plain relative path, so an
@@ -88,9 +89,23 @@ export async function pathMissing(filePath) {
88
89
  }
89
90
  }
90
91
  const unavailable = { state: "unavailable" };
91
- /** Size refusals are distinguished only after the same contained regular-file checks. */
92
+ /** readUnchangedFile for an analysis input, whose path must also be an evidence path. */
92
93
  export async function readBoundedFileResult(root, relativePath, maxBytes) {
93
- if (!isEvidencePath(relativePath) || !Number.isSafeInteger(maxBytes) || maxBytes < 1)
94
+ return isEvidencePath(relativePath)
95
+ ? readUnchangedFile(root, relativePath, maxBytes)
96
+ : unavailable;
97
+ }
98
+ /**
99
+ * At most `maxBytes` of a contained single-link regular file that stays the same file, with the
100
+ * same size and times, from the first check to the last. Size refusals are distinguished only after
101
+ * the same checks. `relativePath` uses `/`, and any name a contained read accepts
102
+ * (isSafeRelativeFilePath) is read: verify and export read the names they always did.
103
+ */
104
+ export async function readUnchangedFile(root, relativePath, maxBytes) {
105
+ if (!isSafeRelativeFilePath(relativePath) ||
106
+ relativePath.includes("\\") ||
107
+ !Number.isSafeInteger(maxBytes) ||
108
+ maxBytes < 1)
94
109
  return unavailable;
95
110
  const validateRoot = async () => {
96
111
  if ("physicalRunRoot" in root) {
@@ -133,26 +148,17 @@ export async function readBoundedFileResult(root, relativePath, maxBytes) {
133
148
  try {
134
149
  if (!sameFile(before, await handle.stat({ bigint: true })))
135
150
  return unavailable;
136
- const chunks = [];
137
- let total = 0;
138
- while (total <= maxBytes) {
139
- const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
140
- const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
141
- if (bytesRead === 0)
142
- break;
143
- total += bytesRead;
144
- if (total > maxBytes)
145
- return unavailable;
146
- chunks.push(chunk.subarray(0, bytesRead));
147
- }
148
- if (!sameFile(before, await handle.stat({ bigint: true })) || total !== Number(before.size))
151
+ const bytes = await readOpenedAtMost(handle, maxBytes);
152
+ if (bytes === null ||
153
+ !sameFile(before, await handle.stat({ bigint: true })) ||
154
+ bytes.length !== Number(before.size))
149
155
  return unavailable;
150
156
  if ((await validateRoot()) !== physicalRoot)
151
157
  return unavailable;
152
158
  await validateParents();
153
159
  if (!sameFile(before, await lstat(candidate, { bigint: true })))
154
160
  return unavailable;
155
- return { state: "read", bytes: Buffer.concat(chunks, total) };
161
+ return { state: "read", bytes };
156
162
  }
157
163
  finally {
158
164
  await handle.close();
@@ -1,14 +1,43 @@
1
1
  import { type PreparedRunArtifactPaths } from "./paths.js";
2
- import { type PreparedSelectedOutputDirectory } from "./contained-output.js";
2
+ import { type ContainedRefusal, type PreparedSelectedOutputDirectory } from "./contained-output.js";
3
3
  import type { RunPointer } from "./results.js";
4
4
  import { type RunBundle } from "./bundle.js";
5
+ import { type BoundedFileResult } from "./evidence-files.js";
5
6
  /** The run input that resolves through latest.json rather than naming a run directory. */
6
7
  export declare const LATEST_RUN_ALIAS = "latest";
7
8
  /** Resolve "latest" or an explicit run id to its prepared artifact paths. */
8
9
  export declare function resolveRunPath(cwd: string, runInput: string): Promise<PreparedRunArtifactPaths | null>;
9
10
  export declare function readLatest(runsRoot: PreparedSelectedOutputDirectory): Promise<RunPointer | null>;
10
- export declare function readRunJsonIfExists(runPaths: PreparedRunArtifactPaths, ...segments: string[]): Promise<unknown>;
11
- export declare function readRunTextIfExists(runPaths: PreparedRunArtifactPaths, ...segments: string[]): Promise<string | null>;
11
+ /** A run file as text: its text, nothing at the path, or why it was refused. */
12
+ export type RunTextRead = {
13
+ status: "read";
14
+ text: string;
15
+ } | {
16
+ status: "missing";
17
+ } | ContainedRefusal;
18
+ /** A run file as JSON. `value` is undefined when the file is there but is not valid JSON. */
19
+ export type RunJsonRead = {
20
+ status: "read";
21
+ value: unknown;
22
+ } | {
23
+ status: "missing";
24
+ } | ContainedRefusal;
25
+ /**
26
+ * The JSON, or null for a file that is missing, refused or not valid JSON: for a caller that
27
+ * treats the three alike, as every reader did before a refusal had its own state.
28
+ */
29
+ export declare function runJsonValue(read: RunJsonRead): unknown;
30
+ /** A run file as JSON, within RUN_ARTIFACT_MAX_BYTES. */
31
+ export declare function readRunJsonIfExists(runPaths: PreparedRunArtifactPaths, ...segments: string[]): Promise<RunJsonRead>;
32
+ /** A run file as text, within RUN_ARTIFACT_MAX_BYTES. */
33
+ export declare function readRunTextIfExists(runPaths: PreparedRunArtifactPaths, ...segments: string[]): Promise<RunTextRead>;
34
+ /**
35
+ * A run file, read whole through readUnchangedFile: `limit` when it holds more than
36
+ * RUN_ARTIFACT_MAX_BYTES, `unavailable` when it is missing, unsafe, or changes while it is read.
37
+ * A `\` in the path reads as `/`.
38
+ */
39
+ export declare function readSafeRunArtifact(runPaths: PreparedRunArtifactPaths, relativePath: string): Promise<BoundedFileResult>;
40
+ /** readSafeRunArtifact's bytes, or null for any file it does not read. */
12
41
  export declare function readSafeRunArtifactBytes(runPaths: PreparedRunArtifactPaths, relativePath: string): Promise<Buffer | null>;
13
42
  export declare function readSafeRunArtifactJson(runPaths: PreparedRunArtifactPaths, relativePath: string): Promise<unknown>;
14
43
  export declare function loadRunBundle(cwdInput: string, runInput: string): Promise<{
@@ -1,10 +1,9 @@
1
- import { lstat } from "node:fs/promises";
2
1
  import path from "node:path";
3
- import { bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveExistingRunDirectory, resolveLatestRunDirectory, validatePreparedRunArtifactPaths, } from "./paths.js";
4
- import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, readContainedRegularFile, } from "./contained-output.js";
2
+ import { bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveExistingRunDirectory, resolveLatestRunDirectory, RUNS_RELATIVE_ROOT, validatePreparedRunArtifactPaths, } from "./paths.js";
3
+ import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, ContainedReadRefusedError, LATEST_POINTER_MAX_BYTES, readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
5
4
  import { RUN_BUNDLE_FILE } from "./bundle.js";
6
5
  import { isRunBundle, isRunPointer } from "./bundle-shape.js";
7
- import { isNodeError } from "./type-guards.js";
6
+ import { readUnchangedFile } from "./evidence-files.js";
8
7
  /** The run input that resolves through latest.json rather than naming a run directory. */
9
8
  export const LATEST_RUN_ALIAS = "latest";
10
9
  /** Resolve "latest" or an explicit run id to its prepared artifact paths. */
@@ -33,59 +32,61 @@ export async function resolveRunPath(cwd, runInput) {
33
32
  return bindExistingRunArtifactPaths(cwd, runInput);
34
33
  }
35
34
  export async function readLatest(runsRoot) {
36
- const latestPath = path.join(runsRoot.physicalPath, "latest.json");
37
- let latestStats;
38
- try {
39
- latestStats = await lstat(latestPath, { bigint: true });
40
- }
41
- catch (error) {
42
- if (isNodeError(error) && error.code === "ENOENT") {
43
- return null;
44
- }
45
- throw error;
46
- }
47
- if (latestStats.isSymbolicLink() || !latestStats.isFile() || latestStats.nlink !== 1n) {
48
- throw new Error("Latest run pointer must be a single-link regular file.");
49
- }
50
- const bytes = await readContainedRegularFile(runsRoot, "latest.json");
51
- if (!bytes) {
52
- throw new Error("Latest run pointer changed while it was being read.");
35
+ const read = await readContainedRegularFile(runsRoot, "latest.json", LATEST_POINTER_MAX_BYTES);
36
+ if (read.status === "missing")
37
+ return null;
38
+ if (read.status === "refused") {
39
+ if (read.reason === "too-large")
40
+ throw new ContainedReadRefusedError(path.join(RUNS_RELATIVE_ROOT, "latest.json"), read);
41
+ throw new Error(read.reason === "not-regular" || read.reason === "directory"
42
+ ? "Latest run pointer must be a single-link regular file."
43
+ : "Latest run pointer changed while it was being read.");
53
44
  }
54
45
  let latest;
55
46
  try {
56
- latest = JSON.parse(bytes.toString("utf8"));
47
+ latest = JSON.parse(read.bytes.toString("utf8"));
57
48
  }
58
49
  catch {
59
50
  return null;
60
51
  }
61
52
  return isRunPointer(latest) ? latest : null;
62
53
  }
54
+ /**
55
+ * The JSON, or null for a file that is missing, refused or not valid JSON: for a caller that
56
+ * treats the three alike, as every reader did before a refusal had its own state.
57
+ */
58
+ export function runJsonValue(read) {
59
+ return read.status === "read" ? (read.value ?? null) : null;
60
+ }
61
+ /** A run file as JSON, within RUN_ARTIFACT_MAX_BYTES. */
63
62
  export async function readRunJsonIfExists(runPaths, ...segments) {
64
- const text = await readRunTextIfExists(runPaths, ...segments);
65
- if (text === null) {
66
- return null;
67
- }
63
+ const read = await readRunTextIfExists(runPaths, ...segments);
64
+ if (read.status !== "read")
65
+ return read;
68
66
  try {
69
- return JSON.parse(text);
67
+ return { status: "read", value: JSON.parse(read.text) };
70
68
  }
71
69
  catch {
72
- return null;
70
+ return { status: "read", value: undefined };
73
71
  }
74
72
  }
73
+ /** A run file as text, within RUN_ARTIFACT_MAX_BYTES. */
75
74
  export async function readRunTextIfExists(runPaths, ...segments) {
76
- const bytes = await readContainedRegularFile(runPaths, segments.join("/"));
77
- return bytes?.toString("utf8") ?? null;
75
+ const read = await readContainedRegularFile(runPaths, segments.join("/"), RUN_ARTIFACT_MAX_BYTES);
76
+ return read.status === "read" ? { status: "read", text: read.bytes.toString("utf8") } : read;
78
77
  }
78
+ /**
79
+ * A run file, read whole through readUnchangedFile: `limit` when it holds more than
80
+ * RUN_ARTIFACT_MAX_BYTES, `unavailable` when it is missing, unsafe, or changes while it is read.
81
+ * A `\` in the path reads as `/`.
82
+ */
83
+ export async function readSafeRunArtifact(runPaths, relativePath) {
84
+ return readUnchangedFile(runPaths, relativePath.replace(/\\/g, "/"), RUN_ARTIFACT_MAX_BYTES);
85
+ }
86
+ /** readSafeRunArtifact's bytes, or null for any file it does not read. */
79
87
  export async function readSafeRunArtifactBytes(runPaths, relativePath) {
80
- const normalized = relativePath.replace(/\\/g, "/");
81
- const segments = normalized.split("/");
82
- if (path.isAbsolute(relativePath) ||
83
- path.win32.isAbsolute(relativePath) ||
84
- segments.length === 0 ||
85
- segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
86
- return null;
87
- }
88
- return readContainedRegularFile(runPaths, normalized);
88
+ const read = await readSafeRunArtifact(runPaths, relativePath);
89
+ return read.state === "read" ? read.bytes : null;
89
90
  }
90
91
  export async function readSafeRunArtifactJson(runPaths, relativePath) {
91
92
  const bytes = await readSafeRunArtifactBytes(runPaths, relativePath);
@@ -112,7 +113,11 @@ export async function loadRunBundlePrepared(cwdInput, runPaths) {
112
113
  const cwd = path.resolve(cwdInput);
113
114
  await validatePreparedRunArtifactPaths(runPaths);
114
115
  const bundlePath = path.join(runPaths.absoluteRunRoot, RUN_BUNDLE_FILE);
115
- const bundle = await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE);
116
+ const read = await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE);
117
+ // A run.json it refuses is no reason to call the run absent: the caller reports why.
118
+ if (read.status === "refused")
119
+ throw new ContainedReadRefusedError(RUN_BUNDLE_FILE, read);
120
+ const bundle = read.status === "read" ? read.value : null;
116
121
  if (!isRunBundle(bundle)) {
117
122
  return null;
118
123
  }
@@ -1,26 +1,8 @@
1
+ import { type RunNote } from "./note-shape.js";
1
2
  import { type PreparedRunArtifactPaths } from "./paths.js";
2
3
  export declare const RUN_NOTES_DIR = "notes";
3
4
  /** The bytes one note file may hold: 2000 characters of text with JSON escapes, and its fields. */
4
5
  export declare const MAX_NOTE_FILE_BYTES: number;
5
- /** The notes one run keeps. A reader reads this many and names the rest. */
6
- export declare const MAX_RUN_NOTES = 500;
7
- export declare const MAX_NOTE_TEXT = 2000;
8
- export interface RunNote {
9
- id: string;
10
- /** Milliseconds from the run clock's start. */
11
- atMs: number;
12
- /** The participant's stream id, or null for a note on the whole run. */
13
- participant: string | null;
14
- /** The latest timed capture or event at or before the moment, with its participant. */
15
- nearest: {
16
- participant: string;
17
- itemId: string;
18
- } | null;
19
- text: string;
20
- author: string;
21
- createdAt: string;
22
- editedAt: string | null;
23
- }
24
6
  /** A run's notes as read, in the order they were added, and why any file was skipped. */
25
7
  export interface RunNotes {
26
8
  runId: string;
@@ -33,34 +15,34 @@ export declare function newRunNoteId(createdAt: Date): string;
33
15
  export declare function runNoteFile(id: string): string;
34
16
  /** The note as its file holds it. */
35
17
  export declare function encodeRunNote(note: RunNote, runId: string): string;
36
- /** The note in a note file's bytes, when it is a note of `runId` stored under its own `id`. */
37
- export declare function decodeRunNote(bytes: Buffer, runId: string, id: string): RunNote | null;
38
- /**
39
- * Up to MAX_LISTED_ENTRIES names in the notes directory at `dir`, or null when there is none.
40
- * Throws when `dir` is there and is not a plain directory.
41
- */
42
- export declare function listNoteEntries(dir: string): Promise<{
43
- names: string[];
44
- truncated: boolean;
45
- } | null>;
46
- /** The note ids to read from a listing of notes/, in creation order, and what was left out. */
47
- export declare function noteListing(listing: {
48
- names: readonly string[];
49
- truncated: boolean;
50
- }): {
51
- ids: string[];
52
- skipped: string[];
53
- };
54
18
  /** How many note files notes/ lists, without reading any. Throws when notes/ is unusable. */
55
19
  export declare function countRunNotes(prepared: PreparedRunArtifactPaths): Promise<number>;
56
20
  /**
57
- * The run's notes. Each file is read through the bounded reader, which refuses a link, a special
58
- * file or anything over MAX_NOTE_FILE_BYTES before reading past the limit; a file it refuses, or
59
- * that is not a note of this run, is skipped and named in `skipped`.
21
+ * Where a run's notes are read from: the run's id, its directory, and a reader of the files under
22
+ * it that refuses a link or a special file and stops at `maxBytes`.
60
23
  */
61
- export declare function readRunNotes(prepared: PreparedRunArtifactPaths): Promise<RunNotes>;
62
- /** What verify's text scan finds in shared notes: a secret-shaped value, or text it cannot read. */
63
- type NotesFinding = "sensitive" | "opaque";
24
+ export interface RunNoteFiles {
25
+ readonly runId: string;
26
+ /** The run directory's physical path, once it is checked to be the directory bound. Throws otherwise. */
27
+ directory(): Promise<string>;
28
+ /** A file under the run directory: its bytes, "limit" when it holds more than `maxBytes`, or null. */
29
+ read(relativePath: string, maxBytes: number): Promise<Buffer | "limit" | null>;
30
+ }
31
+ /**
32
+ * The run's notes, read from its prepared paths through the bounded reader or through `from`, the
33
+ * files of a run the caller already holds (the served Observer passes its pinned root's). Each file
34
+ * is read within MAX_NOTE_FILE_BYTES; a file the reader refuses, or that is not a note of this run,
35
+ * is skipped and named in `skipped`.
36
+ */
37
+ export declare function readRunNotes(from: PreparedRunArtifactPaths | RunNoteFiles): Promise<RunNotes>;
38
+ /** What verify's text scan finds in notes about to be shared, and the share safety that gives them. */
39
+ export interface NotesSharingProblem {
40
+ status: "local_only" | "blocked";
41
+ reason: {
42
+ code: "PUBLIC_SAFETY_FINDINGS" | "UNSCANNED_ARTIFACT";
43
+ message: string;
44
+ };
45
+ }
64
46
  /**
65
47
  * The run's notes as a caller is about to share them, read once, with what verify's text scan
66
48
  * finds in that set as it would be shared. The caller shares this set only, so a note added or
@@ -68,6 +50,5 @@ type NotesFinding = "sensitive" | "opaque";
68
50
  */
69
51
  export declare function readNotesForSharing(prepared: PreparedRunArtifactPaths): Promise<{
70
52
  notes: RunNotes;
71
- finding: NotesFinding | null;
53
+ problem: NotesSharingProblem | null;
72
54
  }>;
73
- export {};