humanish 0.115.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.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/computer-use/loop/debrief.js +61 -27
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +2 -1
- package/dist/cli/commands/run-signals.js +5 -1
- package/dist/cli/commands/runs.js +2 -1
- package/dist/cli/commands/setup.js +3 -3
- package/dist/cli/commands/study-format.d.ts +5 -2
- package/dist/cli/commands/study-format.js +29 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +1 -1
- package/dist/cli/commands/tui.js +59 -17
- package/dist/cli/doctor.js +9 -2
- package/dist/cli/findings-text.d.ts +5 -0
- package/dist/cli/findings-text.js +169 -0
- package/dist/cli/findings.d.ts +0 -4
- package/dist/cli/findings.js +5 -167
- package/dist/cli/io.js +6 -1
- package/dist/cli/update-check.js +4 -1
- package/dist/comms/connections.js +4 -3
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/redaction.js +9 -1
- package/dist/evidence/secret-scrub.d.ts +15 -5
- package/dist/evidence/secret-scrub.js +67 -9
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/export-bundle.js +18 -14
- package/dist/feedback/feedback.js +25 -7
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +60 -48
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/pinned-files.js +1 -1
- package/dist/observer/render.js +3 -2
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
- package/dist/routes/computer-use/bundle-parts.js +2 -2
- package/dist/routes/computer-use/bundle.d.ts +4 -5
- package/dist/routes/computer-use/bundle.js +1 -3
- package/dist/routes/computer-use/fanout-records.js +10 -11
- package/dist/routes/computer-use/live-phase.d.ts +1 -1
- package/dist/routes/computer-use/live-phase.js +0 -1
- package/dist/routes/computer-use/participant-execution.d.ts +3 -2
- package/dist/routes/computer-use/participant-runs.d.ts +6 -2
- package/dist/routes/computer-use/participant-runs.js +9 -5
- package/dist/routes/computer-use/plan.d.ts +3 -0
- package/dist/routes/computer-use/plan.js +14 -11
- package/dist/routes/computer-use/rerun-selection.js +2 -2
- package/dist/routes/computer-use/result.js +3 -6
- package/dist/routes/computer-use/setup.d.ts +4 -6
- package/dist/routes/computer-use/setup.js +2 -6
- package/dist/routes/computer-use/single-bundle.d.ts +4 -5
- package/dist/routes/computer-use/single-bundle.js +22 -25
- package/dist/routes/computer-use/subject-projection.d.ts +4 -8
- package/dist/routes/computer-use/subject-projection.js +9 -31
- package/dist/routes/computer-use/types.d.ts +4 -41
- package/dist/routes/scripted/scenario.js +28 -12
- package/dist/routes/scripted/session-result.js +6 -3
- package/dist/routes/shared-world/bundle-records.js +2 -8
- package/dist/routes/shared-world/participant-specs.d.ts +0 -2
- package/dist/routes/shared-world/participant-specs.js +1 -20
- package/dist/routes/shared-world/plan.d.ts +3 -0
- package/dist/routes/shared-world/plan.js +23 -3
- package/dist/routes/shared-world/setup.js +2 -2
- package/dist/routes/terminal/ledger.d.ts +9 -9
- package/dist/routes/terminal/ledger.js +63 -40
- package/dist/routes/terminal/live-finish.js +7 -10
- package/dist/routes/terminal/plan.d.ts +3 -0
- package/dist/routes/terminal/plan.js +8 -5
- package/dist/routes/terminal/token-usage.d.ts +1 -2
- package/dist/routes/terminal/token-usage.js +4 -6
- package/dist/routes/terminal/types.d.ts +9 -1
- package/dist/run/bundle-shape.js +6 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/contained-output.d.ts +53 -5
- package/dist/run/contained-output.js +141 -42
- package/dist/run/detail.d.ts +2 -1
- package/dist/run/detail.js +12 -7
- package/dist/run/escape-sequences.d.ts +11 -0
- package/dist/run/escape-sequences.js +93 -0
- package/dist/run/evidence-files.d.ts +8 -8
- package/dist/run/evidence-files.js +19 -23
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/notes.d.ts +0 -8
- package/dist/run/notes.js +5 -51
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/paths.d.ts +2 -0
- package/dist/run/paths.js +5 -1
- package/dist/run/preflight-receipts.js +13 -8
- package/dist/run/project.js +7 -4
- package/dist/run/reclaim-outcomes.d.ts +8 -6
- package/dist/run/reclaim-outcomes.js +10 -6
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +43 -16
- package/dist/run/run-clock.d.ts +50 -0
- package/dist/run/run-clock.js +66 -0
- package/dist/run/run.js +5 -2
- package/dist/run/sandbox-ids.d.ts +7 -2
- package/dist/run/sandbox-ids.js +27 -11
- package/dist/run/secrets.d.ts +2 -1
- package/dist/run/secrets.js +18 -22
- package/dist/run/stored-runs.js +14 -7
- package/dist/run/transient-comms-secrets.js +6 -17
- package/dist/run-study.js +22 -13
- package/dist/study/adapter-scorer-loader.js +6 -4
- package/dist/study/discover.js +9 -6
- package/dist/study/init-paths.js +7 -5
- package/dist/study/persona-resolve.d.ts +2 -2
- package/dist/study/persona-resolve.js +9 -5
- package/dist/study/plan-types.d.ts +2 -2
- package/dist/study/plan.d.ts +5 -3
- package/dist/study/plan.js +18 -9
- package/dist/study/preflight-probes.js +11 -3
- package/dist/study/preflight.js +9 -5
- package/dist/study/summary.d.ts +8 -2
- package/dist/study/summary.js +20 -22
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +38 -17
- package/dist/substrates/e2b/connection.d.ts +12 -0
- package/dist/substrates/e2b/connection.js +35 -0
- package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
- package/dist/substrates/e2b/desktop-executor.js +42 -9
- package/dist/substrates/e2b/lifetime.d.ts +24 -4
- package/dist/substrates/e2b/lifetime.js +41 -4
- package/dist/substrates/e2b/sandbox.d.ts +4 -3
- package/dist/substrates/e2b/sandbox.js +13 -18
- package/dist/substrates/e2b/sdk.d.ts +20 -9
- package/dist/substrates/local/capacity.d.ts +18 -2
- package/dist/substrates/local/capacity.js +21 -2
- package/dist/substrates/local/runtime-host.js +21 -14
- package/dist/substrates/local/runtime.js +3 -11
- package/dist/tui/contract.d.ts +30 -5
- package/dist/tui/contract.js +4 -1
- package/dist/tui-app.js +149 -147
- package/dist/verify/artifacts.d.ts +9 -2
- package/dist/verify/artifacts.js +37 -13
- package/dist/verify/liveness.js +19 -5
- package/dist/verify/verify.js +54 -27
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/state-driven-executor.md +4 -2
- package/docs/contracts/run-bundle.md +18 -10
- package/docs/contracts/schemas.md +27 -18
- package/docs/contracts/study-analysis.md +10 -1
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +2 -2
|
@@ -8,7 +8,7 @@ import { actorRegistry, isTerminalActorDescriptor } from "../../actors/registry.
|
|
|
8
8
|
import { isNonEmpty, planBase } from "../../study/plan-base.js";
|
|
9
9
|
import { studyUrlCredentialReason } from "../../study/url-credentials.js";
|
|
10
10
|
import { desktopMediaValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
11
|
-
import {
|
|
11
|
+
import { ceilingAdvice } from "../../substrates/e2b/lifetime.js";
|
|
12
12
|
import { NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
|
|
13
13
|
import { terminalSandboxTimeoutMs } from "./lifetime.js";
|
|
14
14
|
import { isExactRuntimeVersion, TERMINAL_RUNTIME_VERSION_TIMEOUT_MS } from "./runtime.js";
|
|
@@ -105,13 +105,16 @@ export function planTerminalStudy(config, input) {
|
|
|
105
105
|
if (maxUsd > 0 && input.hasCostProbe !== true)
|
|
106
106
|
return refuse("HUMANISH_TERMINAL_UNPRICED_CAP", `caps.maxUsd=${maxUsd} cannot be enforced: Codex reports no provider cost for the participant (its token cost is only estimated after the run) and no product, media or payment spend is measured, so a positive dollar cap can never trip. Set caps.maxUsd to 0 and bound the run with caps.maxMinutes, the codex command's wall-clock kill. No sandbox was created and the runtime key was not used.`, descriptor.id);
|
|
107
107
|
// The sandbox's timeout covers the steps before the codex command, maxMinutes and the teardown
|
|
108
|
-
// buffer, and E2B refuses a sandbox
|
|
109
|
-
// after the plan prints, from a provider 400 that names neither knob.
|
|
108
|
+
// buffer, and E2B refuses a sandbox past the plan's ceiling. Refuse here, with the arithmetic,
|
|
109
|
+
// rather than after the plan prints, from a provider 400 that names neither knob.
|
|
110
|
+
const ceiling = input.sandboxCeiling;
|
|
111
|
+
if (!ceiling.ok)
|
|
112
|
+
return refuse("HUMANISH_TERMINAL_CAPS_INVALID", ceiling.message, descriptor.id);
|
|
110
113
|
const productInstall = shared.product.install !== undefined;
|
|
111
114
|
const sandboxTimeoutMs = terminalSandboxTimeoutMs({ maxMinutes, productInstall });
|
|
112
|
-
if (sandboxTimeoutMs >
|
|
115
|
+
if (sandboxTimeoutMs > ceiling.ms) {
|
|
113
116
|
const headroomMinutes = (sandboxTimeoutMs - maxMinutes * 60_000) / 60_000;
|
|
114
|
-
return refuse("HUMANISH_TERMINAL_CAPS_INVALID", `caps.maxMinutes ${maxMinutes} derives a ${sandboxTimeoutMs / 60_000}m sandbox deadline, and a sandbox may not live longer than ${
|
|
117
|
+
return refuse("HUMANISH_TERMINAL_CAPS_INVALID", `caps.maxMinutes ${maxMinutes} derives a ${sandboxTimeoutMs / 60_000}m sandbox deadline, and a sandbox may not live longer than ${ceiling.ms / 60_000}m. The deadline is maxMinutes plus ${headroomMinutes}m: the Node bootstrap (${NODE_BOOTSTRAP_TIMEOUT_MS / 60_000}m), the runtime version check (${TERMINAL_RUNTIME_VERSION_TIMEOUT_MS / 60_000}m)${productInstall ? `, the product setup (${PRODUCT_SETUP_TIMEOUT_MS / 60_000}m)` : ""} and the teardown buffer (${TERMINAL_SANDBOX_TIMEOUT_BUFFER_MS / 60_000}m). Lower caps.maxMinutes to at most ${ceiling.ms / 60_000 - headroomMinutes}.${ceilingAdvice(sandboxTimeoutMs)} No sandbox was created and the runtime key was not used.`, descriptor.id);
|
|
115
118
|
}
|
|
116
119
|
return {
|
|
117
120
|
ok: true,
|
|
@@ -13,6 +13,5 @@ export declare function terminalUsageRecords(lines: string): string[];
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function parseTerminalTokenUsage(transcript: string): ActorTokenUsage | undefined;
|
|
15
15
|
/** Human-readable token statement for the cost ledger and the no-spend proof. Reports what was
|
|
16
|
-
* counted
|
|
17
|
-
* "nothing was consumed". */
|
|
16
|
+
* counted, so a reader never reads "no charge recorded" as "nothing was consumed". */
|
|
18
17
|
export declare function describeTokenUsage(usage: ActorTokenUsage): string;
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
// {"type":"turn.completed","usage":{"input_tokens":201536,"cached_input_tokens":170558,
|
|
10
10
|
// "cache_write_input_tokens":30951,"output_tokens":2283,"reasoning_output_tokens":902}}
|
|
11
11
|
//
|
|
12
|
-
// So the route
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// tokens-known-rate-unknown rather than a guessed dollar figure.
|
|
12
|
+
// So the route records a measured token fact whether or not a rate prices it. Rates stay out of this
|
|
13
|
+
// module: the route prices these counts once, from the model it passed to Codex, with
|
|
14
|
+
// src/run/pricing.ts (live-finish.ts), and the ledger and run.json both read that estimate.
|
|
16
15
|
// Bounded to a single line on purpose. `[^}]*` would run past a truncated record's missing brace
|
|
17
16
|
// and swallow the next, valid record's body, silently dropping a real turn from the count.
|
|
18
17
|
const USAGE_RE = /"type"\s*:\s*"turn\.completed"\s*,\s*"usage"\s*:\s*(\{[^}\n]*\})/g;
|
|
@@ -81,8 +80,7 @@ export function parseTerminalTokenUsage(transcript) {
|
|
|
81
80
|
};
|
|
82
81
|
}
|
|
83
82
|
/** Human-readable token statement for the cost ledger and the no-spend proof. Reports what was
|
|
84
|
-
* counted
|
|
85
|
-
* "nothing was consumed". */
|
|
83
|
+
* counted, so a reader never reads "no charge recorded" as "nothing was consumed". */
|
|
86
84
|
export function describeTokenUsage(usage) {
|
|
87
85
|
const parts = [];
|
|
88
86
|
if (usage.input !== undefined)
|
|
@@ -232,6 +232,11 @@ export interface InterventionRecord {
|
|
|
232
232
|
export interface CostLine {
|
|
233
233
|
/** known zero (0) | not measured (null). The key is always present when the line is applicable. */
|
|
234
234
|
usd: number | null;
|
|
235
|
+
/** The provider line's price for its counted tokens, from the trace's `estimatedCost` (the
|
|
236
|
+
* figure run.json's model-tokens line gives), when no charge was measured. An estimate from a
|
|
237
|
+
* rate table: `usd` stays null, and neither `knownTotalUsd` nor caps.maxUsd counts it. Present
|
|
238
|
+
* only with source `estimated-token-usage`. */
|
|
239
|
+
estimatedUsd?: number;
|
|
235
240
|
/** Optional billable-unit count, same discipline: a known count, or null = not measured. */
|
|
236
241
|
count?: number | null;
|
|
237
242
|
/** How this line's value was established (provenance for the verifier + the human reviewer). */
|
|
@@ -239,7 +244,10 @@ export interface CostLine {
|
|
|
239
244
|
/** Tokens were counted but no rate could price them, so `usd` stays null while the note
|
|
240
245
|
* carries the measured token totals. Distinct from "unmeasured", which means no
|
|
241
246
|
* signal at all. */
|
|
242
|
-
| "unpriced-token-usage"
|
|
247
|
+
| "unpriced-token-usage"
|
|
248
|
+
/** Tokens were counted and priced from a rate table: `estimatedUsd` holds the estimate and
|
|
249
|
+
* `usd` stays null, since no charge was measured. */
|
|
250
|
+
| "estimated-token-usage";
|
|
243
251
|
/** A short, public-safe note (never a secret value). */
|
|
244
252
|
note: string;
|
|
245
253
|
}
|
package/dist/run/bundle-shape.js
CHANGED
|
@@ -213,10 +213,15 @@ export const ARCHIVE_SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
|
213
213
|
function isRunSubjectProvenance(value) {
|
|
214
214
|
if (!isRecord(value))
|
|
215
215
|
return false;
|
|
216
|
-
if (value.source !== "clone" &&
|
|
216
|
+
if (value.source !== "clone" &&
|
|
217
|
+
value.source !== "app-url" &&
|
|
218
|
+
value.source !== "local-tree" &&
|
|
219
|
+
value.source !== "desktop-cli")
|
|
217
220
|
return false;
|
|
218
221
|
if (value.repo !== undefined && typeof value.repo !== "string")
|
|
219
222
|
return false;
|
|
223
|
+
if (value.product !== undefined && typeof value.product !== "string")
|
|
224
|
+
return false;
|
|
220
225
|
if (value.commit !== undefined && typeof value.commit !== "string")
|
|
221
226
|
return false;
|
|
222
227
|
if (value.archiveSha256 !== undefined &&
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -158,9 +158,11 @@ export interface RunSubjectStateStepRecord {
|
|
|
158
158
|
* pre-existing bundles).
|
|
159
159
|
*/
|
|
160
160
|
export interface RunSubjectProvenance {
|
|
161
|
-
source: "clone" | "app-url" | "local-tree";
|
|
161
|
+
source: "clone" | "app-url" | "local-tree" | "desktop-cli";
|
|
162
162
|
/** Clone-route only. Honors policies.redactRepos exactly as the provenance event does. */
|
|
163
163
|
repo?: string;
|
|
164
|
+
/** Desktop-cli only: the product the study declares in `subject.product.name`. */
|
|
165
|
+
product?: string;
|
|
164
166
|
/** Clone-route: the cloned commit SHA. Local-tree route: the host-side HEAD at pack time,
|
|
165
167
|
* when the packed root was a git work tree. */
|
|
166
168
|
commit?: string;
|
|
@@ -184,8 +186,8 @@ export interface RunSubjectProvenance {
|
|
|
184
186
|
* an external DB is still unpinned overall).
|
|
185
187
|
* declared-not-run: steps declared but not (all) executed ok: dry-run contract bundles
|
|
186
188
|
* and failed live provisioning.
|
|
187
|
-
* undeclared: no subject.state block (stateless apps, app-url subjects),
|
|
188
|
-
* "absence declared" marker that provenance requires when there is none.
|
|
189
|
+
* undeclared: no subject.state block (stateless apps, app-url and desktop-cli subjects),
|
|
190
|
+
* the explicit "absence declared" marker that provenance requires when there is none.
|
|
189
191
|
* external-public: an operator-declared, operator-owned public deployment used
|
|
190
192
|
* directly as the shared plane; humanish neither provisioned nor seeded it (no getHost, no
|
|
191
193
|
* clone, no in-sandbox filesystem). It is not "seeded" (nothing was seeded) and not "unpinned" (this is
|
|
@@ -46,13 +46,61 @@ export declare function writeContainedOutputFile(rootInput: PreparedOutputRoot,
|
|
|
46
46
|
* writeContainedOutputFile renames a finished file into place instead, replacing what is there.
|
|
47
47
|
*/
|
|
48
48
|
export declare function createContainedOutputFile(rootInput: PreparedOutputRoot, relativePath: string, data: string): Promise<void>;
|
|
49
|
-
/** Read one regular file only when both lexical and physical paths stay in root. */
|
|
50
|
-
export declare function readContainedRegularFile(rootInput: PreparedOutputRoot, relativePath: string): Promise<Buffer | null>;
|
|
51
49
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
* A file in a run directory or a preflight journal. verify reads every run file whole to scan it,
|
|
51
|
+
* and a frame whole to check it, so this covers the largest screenshot src/evidence/image.ts
|
|
52
|
+
* accepts (32 MiB). Every other reader of a run file uses the same limit, so no command reads a
|
|
53
|
+
* run file that verify could not scan.
|
|
54
54
|
*/
|
|
55
|
-
export declare
|
|
55
|
+
export declare const RUN_ARTIFACT_MAX_BYTES: number;
|
|
56
|
+
/** `.humanish/runs/latest.json`: a run id and a path. */
|
|
57
|
+
export declare const LATEST_POINTER_MAX_BYTES: number;
|
|
58
|
+
/** A file a person writes in the project: study, persona and scenario YAML, package.json, agent instructions. */
|
|
59
|
+
export declare const PROJECT_FILE_MAX_BYTES: number;
|
|
60
|
+
/**
|
|
61
|
+
* Why a contained read did not read a file that is there. `too-large`: it holds more than the
|
|
62
|
+
* limit. `changed`: it, its folder or the root changed while it was opened or read, which
|
|
63
|
+
* includes growing past the limit. `directory`: a folder is at the path. `not-regular`: a link, a
|
|
64
|
+
* hard link or a special file. `unsafe-path`: the path leaves the root or passes through a link.
|
|
65
|
+
* `unreadable`: the system refused the open or the read.
|
|
66
|
+
*/
|
|
67
|
+
export type ContainedRefusalReason = "too-large" | "changed" | "directory" | "not-regular" | "unsafe-path" | "unreadable";
|
|
68
|
+
/** A file that is there and was not read: why, and the most the read would have taken. */
|
|
69
|
+
export interface ContainedRefusal {
|
|
70
|
+
readonly status: "refused";
|
|
71
|
+
readonly reason: ContainedRefusalReason;
|
|
72
|
+
readonly limit: number;
|
|
73
|
+
}
|
|
74
|
+
/** What a contained read found: the bytes, nothing at the path, or a file it refused. */
|
|
75
|
+
export type ContainedRead = {
|
|
76
|
+
readonly status: "read";
|
|
77
|
+
readonly bytes: Buffer;
|
|
78
|
+
} | {
|
|
79
|
+
readonly status: "missing";
|
|
80
|
+
} | ContainedRefusal;
|
|
81
|
+
/** `relativePath` and why it was refused, as a clause: "x.yaml is larger than 4194304 bytes, ...". */
|
|
82
|
+
export declare function refusalText(relativePath: string, refusal: ContainedRefusal): string;
|
|
83
|
+
/** A refused contained read, for a caller that stops on one. The message names the file. */
|
|
84
|
+
export declare class ContainedReadRefusedError extends Error {
|
|
85
|
+
readonly relativePath: string;
|
|
86
|
+
readonly refusal: ContainedRefusal;
|
|
87
|
+
constructor(relativePath: string, refusal: ContainedRefusal);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read one regular file only when both lexical and physical paths stay in root and it holds at
|
|
91
|
+
* most `maxBytes`. A larger file is refused without being read, and one that grows past
|
|
92
|
+
* `maxBytes` while it is read is refused after at most `maxBytes + 1` bytes. `missing` means
|
|
93
|
+
* nothing is at the path, or a folder on it is missing.
|
|
94
|
+
*/
|
|
95
|
+
export declare function readContainedRegularFile(rootInput: PreparedOutputRoot, relativePath: string, maxBytes: number): Promise<ContainedRead>;
|
|
96
|
+
/**
|
|
97
|
+
* At most `maxBytes` from the start of an opened file, or null when it holds more. It reads by
|
|
98
|
+
* position, in chunks of up to 64 KiB, and stops one byte past the limit, so a file that grew
|
|
99
|
+
* after it was opened is never read whole.
|
|
100
|
+
*/
|
|
101
|
+
export declare function readOpenedAtMost(handle: FileHandle, maxBytes: number): Promise<Buffer | null>;
|
|
56
102
|
/** The caller owns this checked descriptor and must close it after reading/streaming. */
|
|
57
103
|
export declare function openContainedRegularFile(rootInput: PreparedOutputRoot, relativePath: string): Promise<FileHandle | null>;
|
|
58
104
|
export declare function assertSafeOutputPathSegment(value: string, label?: string): void;
|
|
105
|
+
/** A non-empty relative path with no empty, `.` or `..` segment: the names a contained read accepts. */
|
|
106
|
+
export declare function isSafeRelativeFilePath(value: string): boolean;
|
|
@@ -150,73 +150,162 @@ export async function createContainedOutputFile(rootInput, relativePath, data) {
|
|
|
150
150
|
}
|
|
151
151
|
await handle.close();
|
|
152
152
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
// The most humanish reads of one file of each kind. A larger file is refused unread.
|
|
154
|
+
/**
|
|
155
|
+
* A file in a run directory or a preflight journal. verify reads every run file whole to scan it,
|
|
156
|
+
* and a frame whole to check it, so this covers the largest screenshot src/evidence/image.ts
|
|
157
|
+
* accepts (32 MiB). Every other reader of a run file uses the same limit, so no command reads a
|
|
158
|
+
* run file that verify could not scan.
|
|
159
|
+
*/
|
|
160
|
+
export const RUN_ARTIFACT_MAX_BYTES = 32 * 1024 * 1024;
|
|
161
|
+
/** `.humanish/runs/latest.json`: a run id and a path. */
|
|
162
|
+
export const LATEST_POINTER_MAX_BYTES = 64 * 1024;
|
|
163
|
+
/** A file a person writes in the project: study, persona and scenario YAML, package.json, agent instructions. */
|
|
164
|
+
export const PROJECT_FILE_MAX_BYTES = 4 * 1024 * 1024;
|
|
165
|
+
/** `relativePath` and why it was refused, as a clause: "x.yaml is larger than 4194304 bytes, ...". */
|
|
166
|
+
export function refusalText(relativePath, refusal) {
|
|
167
|
+
switch (refusal.reason) {
|
|
168
|
+
case "too-large":
|
|
169
|
+
return `${relativePath} is larger than ${refusal.limit} bytes, the most humanish reads of it`;
|
|
170
|
+
case "changed":
|
|
171
|
+
return `${relativePath} changed while humanish read it`;
|
|
172
|
+
case "directory":
|
|
173
|
+
return `${relativePath} is a folder`;
|
|
174
|
+
case "not-regular":
|
|
175
|
+
return `${relativePath} is not a single-link regular file`;
|
|
176
|
+
case "unsafe-path":
|
|
177
|
+
return `${relativePath} leaves its folder or passes through a link`;
|
|
178
|
+
case "unreadable":
|
|
179
|
+
return `${relativePath} could not be read`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** A refused contained read, for a caller that stops on one. The message names the file. */
|
|
183
|
+
export class ContainedReadRefusedError extends Error {
|
|
184
|
+
relativePath;
|
|
185
|
+
refusal;
|
|
186
|
+
constructor(relativePath, refusal) {
|
|
187
|
+
super(`${refusalText(relativePath, refusal)}.`);
|
|
188
|
+
this.relativePath = relativePath;
|
|
189
|
+
this.refusal = refusal;
|
|
190
|
+
this.name = "ContainedReadRefusedError";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Read one regular file only when both lexical and physical paths stay in root and it holds at
|
|
195
|
+
* most `maxBytes`. A larger file is refused without being read, and one that grows past
|
|
196
|
+
* `maxBytes` while it is read is refused after at most `maxBytes + 1` bytes. `missing` means
|
|
197
|
+
* nothing is at the path, or a folder on it is missing.
|
|
198
|
+
*/
|
|
199
|
+
export async function readContainedRegularFile(rootInput, relativePath, maxBytes) {
|
|
200
|
+
const opened = await openContained(rootInput, relativePath);
|
|
201
|
+
if (opened.status === "missing")
|
|
202
|
+
return opened;
|
|
203
|
+
const refused = (reason) => ({
|
|
204
|
+
status: "refused",
|
|
205
|
+
reason,
|
|
206
|
+
limit: maxBytes,
|
|
207
|
+
});
|
|
208
|
+
if (opened.status === "refused")
|
|
209
|
+
return refused(opened.reason);
|
|
210
|
+
const { handle } = opened;
|
|
158
211
|
try {
|
|
159
|
-
|
|
212
|
+
if ((await handle.stat()).size > maxBytes)
|
|
213
|
+
return refused("too-large");
|
|
214
|
+
const bytes = await readOpenedAtMost(handle, maxBytes);
|
|
215
|
+
return bytes === null ? refused("changed") : { status: "read", bytes };
|
|
160
216
|
}
|
|
161
217
|
catch {
|
|
162
|
-
return
|
|
218
|
+
return refused("unreadable");
|
|
163
219
|
}
|
|
164
220
|
finally {
|
|
165
221
|
await handle.close().catch(() => { });
|
|
166
222
|
}
|
|
167
223
|
}
|
|
168
224
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
225
|
+
* At most `maxBytes` from the start of an opened file, or null when it holds more. It reads by
|
|
226
|
+
* position, in chunks of up to 64 KiB, and stops one byte past the limit, so a file that grew
|
|
227
|
+
* after it was opened is never read whole.
|
|
171
228
|
*/
|
|
172
|
-
export async function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
229
|
+
export async function readOpenedAtMost(handle, maxBytes) {
|
|
230
|
+
const chunks = [];
|
|
231
|
+
let total = 0;
|
|
232
|
+
for (;;) {
|
|
233
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
234
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, total);
|
|
235
|
+
if (bytesRead === 0)
|
|
236
|
+
return Buffer.concat(chunks, total);
|
|
237
|
+
total += bytesRead;
|
|
238
|
+
if (total > maxBytes)
|
|
239
|
+
return null;
|
|
240
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
181
241
|
}
|
|
182
242
|
}
|
|
183
243
|
/** The caller owns this checked descriptor and must close it after reading/streaming. */
|
|
184
244
|
export async function openContainedRegularFile(rootInput, relativePath) {
|
|
185
|
-
|
|
245
|
+
const opened = await openContained(rootInput, relativePath);
|
|
246
|
+
return opened.status === "open" ? opened.handle : null;
|
|
247
|
+
}
|
|
248
|
+
/** The open behind both readers: every check, and what stopped it when one failed. */
|
|
249
|
+
async function openContained(rootInput, relativePath) {
|
|
250
|
+
const refused = (reason) => ({ status: "refused", reason });
|
|
251
|
+
const missingOr = (error, reason) => isNodeError(error) && error.code === "ENOENT"
|
|
252
|
+
? { status: "missing" }
|
|
253
|
+
: isNodeError(error) && (error.code === "EACCES" || error.code === "EPERM")
|
|
254
|
+
? refused("unreadable")
|
|
255
|
+
: refused(reason);
|
|
186
256
|
try {
|
|
187
257
|
assertSafeRelativeOutputPath(relativePath, false);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return refused("unsafe-path");
|
|
261
|
+
}
|
|
262
|
+
let root;
|
|
263
|
+
try {
|
|
264
|
+
root = await resolveOutputRoot(rootInput);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return refused("changed");
|
|
268
|
+
}
|
|
269
|
+
const candidate = path.resolve(root, normalizeRelativeOutputPath(relativePath));
|
|
270
|
+
if (!isPathInside(root, candidate) || candidate === root)
|
|
271
|
+
return refused("unsafe-path");
|
|
272
|
+
let before;
|
|
273
|
+
try {
|
|
193
274
|
await assertContainedDirectoryChain(root, path.dirname(candidate));
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
275
|
+
before = await lstat(candidate, { bigint: true });
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
return missingOr(error, "unsafe-path");
|
|
279
|
+
}
|
|
280
|
+
if (before.isDirectory())
|
|
281
|
+
return refused("directory");
|
|
282
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink > 1n)
|
|
283
|
+
return refused("not-regular");
|
|
284
|
+
let handle;
|
|
285
|
+
try {
|
|
286
|
+
if (!isPathInside(root, await realpath(candidate)))
|
|
287
|
+
return refused("unsafe-path");
|
|
288
|
+
// O_NONBLOCK: a file swapped for a FIFO after the lstat opens at once and fails the fstat
|
|
289
|
+
// below, where a blocking open would wait for a writer.
|
|
290
|
+
handle = await open(candidate, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return missingOr(error, "changed");
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
203
296
|
const after = await handle.stat({ bigint: true });
|
|
204
297
|
if (!after.isFile() ||
|
|
205
298
|
after.nlink > 1n ||
|
|
206
299
|
after.dev !== before.dev ||
|
|
207
|
-
after.ino !== before.ino
|
|
300
|
+
after.ino !== before.ino ||
|
|
301
|
+
(await resolveOutputRoot(rootInput)) !== root)
|
|
208
302
|
throw new Error("Artifact identity changed.");
|
|
209
|
-
}
|
|
210
|
-
const revalidatedRoot = await resolveOutputRoot(rootInput);
|
|
211
|
-
if (revalidatedRoot !== root) {
|
|
212
|
-
throw new Error("Artifact root changed.");
|
|
213
|
-
}
|
|
214
303
|
await assertContainedDirectoryChain(root, path.dirname(candidate));
|
|
215
|
-
return handle;
|
|
304
|
+
return { status: "open", handle };
|
|
216
305
|
}
|
|
217
306
|
catch {
|
|
218
|
-
await handle
|
|
219
|
-
return
|
|
307
|
+
await handle.close().catch(() => { });
|
|
308
|
+
return refused("changed");
|
|
220
309
|
}
|
|
221
310
|
}
|
|
222
311
|
export function assertSafeOutputPathSegment(value, label = "Output path segment") {
|
|
@@ -229,6 +318,16 @@ export function assertSafeOutputPathSegment(value, label = "Output path segment"
|
|
|
229
318
|
throw new Error(`${label} must be one non-empty path segment.`);
|
|
230
319
|
}
|
|
231
320
|
}
|
|
321
|
+
/** A non-empty relative path with no empty, `.` or `..` segment: the names a contained read accepts. */
|
|
322
|
+
export function isSafeRelativeFilePath(value) {
|
|
323
|
+
try {
|
|
324
|
+
assertSafeRelativeOutputPath(value, false);
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
232
331
|
function assertSafeRelativeOutputPath(value, allowEmpty) {
|
|
233
332
|
if ((!allowEmpty && value.length === 0) || value.includes("\0")) {
|
|
234
333
|
throw new Error("Output path must be a non-empty relative path.");
|
package/dist/run/detail.d.ts
CHANGED
|
@@ -50,7 +50,8 @@ export interface RunDetail {
|
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Read one run's participants. Returns null when the run has no readable bundle yet, an ordinary
|
|
53
|
-
* state for a run that has just started, not a failure.
|
|
53
|
+
* state for a run that has just started, not a failure. Throws when run.json is there and cannot
|
|
54
|
+
* be read: larger than RUN_ARTIFACT_MAX_BYTES, a link, or refused by the system.
|
|
54
55
|
*
|
|
55
56
|
* Reads the bundle narrowly rather than through `loadRunBundle`, which applies the strict
|
|
56
57
|
* evidence-of-record guard. That guard is right for verification and wrong here: a mid-run flush is
|
package/dist/run/detail.js
CHANGED
|
@@ -14,7 +14,7 @@ import { resolveRunPath } from "./locate.js";
|
|
|
14
14
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
15
15
|
import { savedCaption } from "./participant-caption.js";
|
|
16
16
|
import { recordedPersonaId, streamParticipantIdOf } from "./participant-records.js";
|
|
17
|
-
import { readContainedRegularFile } from "./contained-output.js";
|
|
17
|
+
import { ContainedReadRefusedError, readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
18
18
|
import { isPathInside, resolvePhysicalCwd } from "./paths.js";
|
|
19
19
|
const RUN_DETAIL_SCHEMA = "humanish.run-detail.v1";
|
|
20
20
|
/**
|
|
@@ -105,7 +105,8 @@ function costOf(trace) {
|
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
107
|
* Read one run's participants. Returns null when the run has no readable bundle yet, an ordinary
|
|
108
|
-
* state for a run that has just started, not a failure.
|
|
108
|
+
* state for a run that has just started, not a failure. Throws when run.json is there and cannot
|
|
109
|
+
* be read: larger than RUN_ARTIFACT_MAX_BYTES, a link, or refused by the system.
|
|
109
110
|
*
|
|
110
111
|
* Reads the bundle narrowly rather than through `loadRunBundle`, which applies the strict
|
|
111
112
|
* evidence-of-record guard. That guard is right for verification and wrong here: a mid-run flush is
|
|
@@ -118,13 +119,17 @@ export async function readRunDetail(cwdInput, runId) {
|
|
|
118
119
|
const runPaths = await resolveRunPath(cwd, runId).catch(() => null);
|
|
119
120
|
if (runPaths === null)
|
|
120
121
|
return null;
|
|
122
|
+
// A contained read: a run.json swapped for a symlink or hardlink after resolve is refused.
|
|
123
|
+
const raw = await readContainedRegularFile(runPaths, RUN_BUNDLE_FILE, RUN_ARTIFACT_MAX_BYTES);
|
|
124
|
+
// One that changed while it was read is being written now, as a torn one is below. One that is
|
|
125
|
+
// there and refused for another reason is not a run that has yet to write it.
|
|
126
|
+
if (raw.status === "refused" && raw.reason !== "changed")
|
|
127
|
+
throw new ContainedReadRefusedError(RUN_BUNDLE_FILE, raw);
|
|
128
|
+
if (raw.status !== "read")
|
|
129
|
+
return null;
|
|
121
130
|
let bundle;
|
|
122
131
|
try {
|
|
123
|
-
|
|
124
|
-
const raw = await readContainedRegularFile(runPaths, RUN_BUNDLE_FILE);
|
|
125
|
-
if (raw === null)
|
|
126
|
-
return null;
|
|
127
|
-
const parsed = JSON.parse(raw.toString("utf8"));
|
|
132
|
+
const parsed = JSON.parse(raw.bytes.toString("utf8"));
|
|
128
133
|
if (parsed === null || typeof parsed !== "object")
|
|
129
134
|
return null;
|
|
130
135
|
bundle = parsed;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stretches of the text a value's view drops or decodes, in order and without overlap, each
|
|
3
|
+
* as [start, end): operating-system commands (`ESC ]`), control sequences (`ESC [`) and the
|
|
4
|
+
* two-byte escapes `ESC 7`, `ESC 8`, `ESC =` and `ESC >`, raw or JSON-escaped (`\u001b`), and runs
|
|
5
|
+
* of percent escapes. An operating-system command ends at the first bell character after it or,
|
|
6
|
+
* with none after it, at the last `ESC \` in the text. A JSON-escaped one ends at the first
|
|
7
|
+
* backslash after it, which must start `\u0007` or `\u001b\\`. A start with no end is left as
|
|
8
|
+
* written. One regex of the six shapes finds the same stretches, in time quadratic in a run of
|
|
9
|
+
* unterminated commands; this takes time linear in the text.
|
|
10
|
+
*/
|
|
11
|
+
export declare function escapeSequences(text: string): [number, number][];
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type FileHandle } from "node:fs/promises";
|
|
2
1
|
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
3
2
|
/**
|
|
4
3
|
* Analysis inputs are retained local artifacts, never URLs or caller-selected outputs. The path is
|
|
@@ -23,12 +22,6 @@ export declare function readBoundedFile(root: PreparedOutputRoot, relativePath:
|
|
|
23
22
|
* there that could not be read. Errors other than ENOENT propagate.
|
|
24
23
|
*/
|
|
25
24
|
export declare function pathMissing(filePath: string): Promise<boolean>;
|
|
26
|
-
/**
|
|
27
|
-
* At most `maxBytes` from the start of an opened file, or null when it holds more. It reads by
|
|
28
|
-
* position, in chunks of up to 64 KiB, and stops one byte past the limit, so a file that grew
|
|
29
|
-
* after it was opened is never read whole.
|
|
30
|
-
*/
|
|
31
|
-
export declare function readOpenedAtMost(handle: FileHandle, maxBytes: number): Promise<Buffer | null>;
|
|
32
25
|
export type BoundedFileResult = {
|
|
33
26
|
state: "read";
|
|
34
27
|
bytes: Buffer;
|
|
@@ -38,5 +31,12 @@ export type BoundedFileResult = {
|
|
|
38
31
|
} | {
|
|
39
32
|
state: "unavailable";
|
|
40
33
|
};
|
|
41
|
-
/**
|
|
34
|
+
/** readUnchangedFile for an analysis input, whose path must also be an evidence path. */
|
|
42
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>;
|