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.
- package/CHANGELOG.md +41 -0
- package/CONTEXT.md +1 -1
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/closing-report-limits.d.ts +11 -0
- package/dist/actors/closing-report-limits.js +13 -0
- package/dist/actors/closing-report.d.ts +68 -0
- package/dist/actors/closing-report.js +55 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/codex/restricted-participant-policy.d.ts +4 -47
- package/dist/actors/codex/restricted-participant-policy.js +12 -40
- package/dist/actors/computer-use/loop/debrief.d.ts +1 -10
- package/dist/actors/computer-use/loop/debrief.js +68 -70
- package/dist/actors/computer-use/loop/impressions.d.ts +2 -2
- package/dist/actors/computer-use/loop/impressions.js +6 -1
- package/dist/actors/computer-use/loop.d.ts +0 -1
- package/dist/actors/computer-use/loop.js +3 -3
- package/dist/actors/computer-use/openai-conversation.d.ts +1 -1
- package/dist/actors/computer-use/openai-conversation.js +3 -29
- package/dist/actors/computer-use/openai-provider.js +11 -5
- package/dist/actors/contract.d.ts +2 -20
- package/dist/actors/contract.js +0 -13
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/admission.d.ts +14 -4
- package/dist/analysis/admission.js +14 -9
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/execute.d.ts +5 -0
- package/dist/analysis/execute.js +3 -0
- package/dist/analysis/finding-lead.d.ts +27 -0
- package/dist/analysis/finding-lead.js +23 -0
- package/dist/analysis/service.js +5 -3
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +5 -3
- 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 +11 -195
- package/dist/cli/io.js +10 -8
- package/dist/cli/telemetry.js +10 -22
- package/dist/cli/update-check.js +10 -19
- package/dist/cli/user-config.d.ts +11 -0
- package/dist/cli/user-config.js +25 -0
- package/dist/comms/connections.js +4 -3
- package/dist/comms/email-catch.js +7 -31
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/encoded-text.d.ts +12 -11
- package/dist/evidence/encoded-text.js +81 -70
- package/dist/evidence/redaction.d.ts +11 -0
- package/dist/evidence/redaction.js +30 -11
- package/dist/evidence/secret-scrub.d.ts +26 -3
- package/dist/evidence/secret-scrub.js +172 -38
- package/dist/feedback/draft.d.ts +1 -1
- package/dist/feedback/draft.js +13 -16
- package/dist/feedback/export-bundle.js +21 -17
- package/dist/feedback/export.js +32 -46
- package/dist/feedback/feedback.js +31 -26
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +62 -55
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/artifact.d.ts +1 -1
- package/dist/observer/http.d.ts +7 -0
- package/dist/observer/http.js +19 -1
- package/dist/observer/notes-route.d.ts +0 -1
- package/dist/observer/notes-route.js +24 -44
- package/dist/observer/pinned-files.js +4 -19
- package/dist/observer/render.js +6 -4
- package/dist/observer/run-routes.js +13 -28
- package/dist/observer/serve.js +2 -1
- 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 +59 -10
- package/dist/run/contained-output.js +160 -68
- 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 -1
- package/dist/run/evidence-files.js +24 -18
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/note-files.d.ts +26 -45
- package/dist/run/note-files.js +86 -58
- package/dist/run/note-shape.d.ts +35 -0
- package/dist/run/note-shape.js +20 -0
- package/dist/run/notes.d.ts +3 -14
- package/dist/run/notes.js +23 -75
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/participant-records.d.ts +2 -0
- package/dist/run/participant-records.js +17 -2
- 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.d.ts +6 -5
- package/dist/run/transient-comms-secrets.js +23 -39
- 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 +12 -7
- package/dist/study/summary.js +24 -24
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +39 -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/actor-contract.md +3 -3
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/architecture/state-driven-executor.md +5 -3
- package/docs/contracts/run-bundle.md +25 -13
- package/docs/contracts/schemas.md +38 -29
- package/docs/contracts/study-analysis.md +22 -5
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +8 -7
- 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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
92
|
+
/** readUnchangedFile for an analysis input, whose path must also be an evidence path. */
|
|
92
93
|
export async function readBoundedFileResult(root, relativePath, maxBytes) {
|
|
93
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
161
|
+
return { state: "read", bytes };
|
|
156
162
|
}
|
|
157
163
|
finally {
|
|
158
164
|
await handle.close();
|
package/dist/run/locate.d.ts
CHANGED
|
@@ -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
|
-
|
|
11
|
-
export
|
|
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<{
|
package/dist/run/locate.js
CHANGED
|
@@ -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 {
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
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
|
|
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
|
|
77
|
-
return bytes
|
|
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
|
|
81
|
-
|
|
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
|
|
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
|
}
|
package/dist/run/note-files.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
58
|
-
*
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
53
|
+
problem: NotesSharingProblem | null;
|
|
72
54
|
}>;
|
|
73
|
-
export {};
|