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
package/dist/run/reclaim.js
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
// reclaim-receipt.json is written before the first kill and after each one, so an exit at any
|
|
9
9
|
// point leaves a record of every sandbox not yet confirmed gone. `--check` asks E2B the same
|
|
10
10
|
// questions and kills nothing.
|
|
11
|
+
import { e2bAccount, e2bConnection } from "../substrates/e2b/connection.js";
|
|
11
12
|
import { E2B_DEBUG_ENV, e2bDebugMode, loadE2BDesktopModule, } from "../substrates/e2b/sdk.js";
|
|
12
|
-
import {
|
|
13
|
+
import { readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
13
14
|
import { discardPreflightJournal, listPreflightJournals, preflightReclaimDecision, } from "./preflight-receipts.js";
|
|
14
|
-
import { readRunJsonIfExists, resolveRunPath } from "./locate.js";
|
|
15
|
+
import { readRunJsonIfExists, resolveRunPath, runJsonValue } from "./locate.js";
|
|
15
16
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
16
17
|
import { RUN_STATUS_FILE } from "./status.js";
|
|
17
18
|
import { isRecord } from "./type-guards.js";
|
|
@@ -80,7 +81,7 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
|
80
81
|
return {
|
|
81
82
|
...base,
|
|
82
83
|
runId,
|
|
83
|
-
error: { code: "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE", message:
|
|
84
|
+
error: { code: "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE", message: reclaimed.message },
|
|
84
85
|
};
|
|
85
86
|
const { state, receiptCount, outcomes, tagSearch, createsInFlight } = reclaimed;
|
|
86
87
|
return {
|
|
@@ -105,8 +106,14 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
|
105
106
|
* whatever its records say.
|
|
106
107
|
*/
|
|
107
108
|
async function recordedNoSandbox(runPaths) {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
109
|
+
const bundleRead = await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE);
|
|
110
|
+
const statusRead = await readRunJsonIfExists(runPaths, RUN_STATUS_FILE);
|
|
111
|
+
// A record that is there and refused could say anything, so neither shortcut applies and
|
|
112
|
+
// reclaim runs in full. A missing one says nothing, as before.
|
|
113
|
+
if (bundleRead.status === "refused" || statusRead.status === "refused")
|
|
114
|
+
return undefined;
|
|
115
|
+
const bundle = runJsonValue(bundleRead);
|
|
116
|
+
const status = runJsonValue(statusRead);
|
|
110
117
|
const records = [bundle, status].filter(isRecord);
|
|
111
118
|
const reason = records.length > 0 && records.every((record) => record.mode === "dry-run")
|
|
112
119
|
? "dry-run"
|
|
@@ -118,15 +125,18 @@ async function recordedNoSandbox(runPaths) {
|
|
|
118
125
|
: undefined;
|
|
119
126
|
if (reason === undefined)
|
|
120
127
|
return undefined;
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
const earlierRead = await readRunJsonIfExists(runPaths, RECLAIM_RECEIPT_ARTIFACT);
|
|
129
|
+
if (earlierRead.status !== "missing") {
|
|
130
|
+
const earlier = runJsonValue(earlierRead);
|
|
123
131
|
if (!isRecord(earlier) || !Array.isArray(earlier.outcomes) || earlier.outcomes.length > 0)
|
|
124
132
|
return undefined;
|
|
125
133
|
}
|
|
126
|
-
|
|
134
|
+
const journal = await readContainedRegularFile(runPaths, SANDBOX_RECEIPTS_ARTIFACT, RUN_ARTIFACT_MAX_BYTES);
|
|
135
|
+
if (journal.status === "missing")
|
|
127
136
|
return reason;
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
return journal.status === "read" && journal.bytes.toString("utf8").trim() === ""
|
|
138
|
+
? reason
|
|
139
|
+
: undefined;
|
|
130
140
|
}
|
|
131
141
|
/** The execution failure kinds that leave a sandbox or a provider's resources unconfirmed. */
|
|
132
142
|
const CLEANUP_KINDS = new Set(["sandbox-cleanup", "provider-cleanup"]);
|
|
@@ -190,7 +200,7 @@ export async function reclaimPreflightSandboxes(cwd, hooks = {}) {
|
|
|
190
200
|
const reclaimed = await reclaimRoot(journal.root, journal.id, hooks, warnings);
|
|
191
201
|
if (reclaimed.kind === "unreadable") {
|
|
192
202
|
// The receipt may name a live sandbox; the journal stays for a later reclaim.
|
|
193
|
-
warnings.push(`Preflight ${journal.id} left alone: ${
|
|
203
|
+
warnings.push(`Preflight ${journal.id} left alone: ${reclaimed.message}`);
|
|
194
204
|
states.push("unknown");
|
|
195
205
|
continue;
|
|
196
206
|
}
|
|
@@ -241,15 +251,28 @@ const E2B_DEBUG_REFUSAL = {
|
|
|
241
251
|
message: `${E2B_DEBUG_ENV}=true puts the E2B SDK in debug mode, where Sandbox.kill reports success without contacting E2B. Nothing was killed and the receipts were kept; unset ${E2B_DEBUG_ENV} and run reclaim again.`,
|
|
242
252
|
};
|
|
243
253
|
const UNREADABLE_MESSAGE = "sandbox-receipts.ndjson is present but could not be read safely (not a single regular file, or a read error). Nothing was killed and the receipts were kept; check the file, then run reclaim again.";
|
|
254
|
+
/**
|
|
255
|
+
* Why reclaim stopped before it killed or wrote anything: the receipts journal, or the receipt an
|
|
256
|
+
* earlier reclaim wrote, is there and cannot be read.
|
|
257
|
+
*/
|
|
258
|
+
function unreadableMessage(file, refusal) {
|
|
259
|
+
if (file === SANDBOX_RECEIPTS_ARTIFACT && refusal.reason !== "too-large")
|
|
260
|
+
return UNREADABLE_MESSAGE;
|
|
261
|
+
const kept = file === SANDBOX_RECEIPTS_ARTIFACT ? "the receipts were kept" : "the receipt was kept";
|
|
262
|
+
return `${refusalText(file, refusal)}. Nothing was killed and ${kept}; check the file, then run reclaim again.`;
|
|
263
|
+
}
|
|
244
264
|
/**
|
|
245
265
|
* Find and stop (or, for `--check`, look up) every sandbox one directory owns: its receipts, the
|
|
246
266
|
* creates this process reported, and E2B's tagged list, each once by exact id.
|
|
247
267
|
*/
|
|
248
268
|
async function reclaimRoot(root, label, hooks, warnings) {
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
251
|
-
return {
|
|
252
|
-
|
|
269
|
+
const journalRead = await readContainedRegularFile(root, SANDBOX_RECEIPTS_ARTIFACT, RUN_ARTIFACT_MAX_BYTES);
|
|
270
|
+
if (journalRead.status === "refused")
|
|
271
|
+
return {
|
|
272
|
+
kind: "unreadable",
|
|
273
|
+
message: unreadableMessage(SANDBOX_RECEIPTS_ARTIFACT, journalRead),
|
|
274
|
+
};
|
|
275
|
+
const journal = journalRead.status === "read" ? journalRead.bytes.toString("utf8") : "";
|
|
253
276
|
const receipts = parseSandboxReceipts(journal);
|
|
254
277
|
const owners = parseSandboxOwners(journal, label);
|
|
255
278
|
const check = hooks.check === true;
|
|
@@ -259,7 +282,8 @@ async function reclaimRoot(root, label, hooks, warnings) {
|
|
|
259
282
|
let e2b;
|
|
260
283
|
let moduleError;
|
|
261
284
|
try {
|
|
262
|
-
|
|
285
|
+
const module = await (hooks.loadModule ?? loadE2BDesktopModule)();
|
|
286
|
+
e2b = e2bAccount(module, e2bConnection(hooks.env ?? process.env));
|
|
263
287
|
}
|
|
264
288
|
catch (error) {
|
|
265
289
|
moduleError = `Cannot load @e2b/desktop to reach E2B: ${redactText(toErrorMessage(error))}`;
|
|
@@ -269,6 +293,9 @@ async function reclaimRoot(root, label, hooks, warnings) {
|
|
|
269
293
|
// An earlier reclaim may have killed a sandbox no receipt names (the signal handler's creates
|
|
270
294
|
// and tag finds). Its outcome is carried forward, so a later reclaim never erases that record.
|
|
271
295
|
const earlier = check ? [] : await earlierOutcomes(root, label);
|
|
296
|
+
// The new receipt replaces the old one, so an old one that cannot be read stops reclaim here.
|
|
297
|
+
if (!Array.isArray(earlier))
|
|
298
|
+
return { kind: "unreadable", message: unreadableMessage(RECLAIM_RECEIPT_ARTIFACT, earlier) };
|
|
272
299
|
const set = targetSet({
|
|
273
300
|
e2b,
|
|
274
301
|
check,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ActorTraceItem } from "../actors/contract.js";
|
|
2
|
+
import type { RunDesktopRecording } from "../evidence/desktop-recording-types.js";
|
|
3
|
+
/** What the clock reads from a stream. A run.json stream and an Observer stream both have it. */
|
|
4
|
+
export interface ClockStream {
|
|
5
|
+
actor?: {
|
|
6
|
+
items: readonly ActorTraceItem[];
|
|
7
|
+
};
|
|
8
|
+
liveActor?: {
|
|
9
|
+
items: readonly ActorTraceItem[];
|
|
10
|
+
};
|
|
11
|
+
recording?: RunDesktopRecording;
|
|
12
|
+
}
|
|
13
|
+
/** Where the run clock starts and ends, in epoch ms. */
|
|
14
|
+
export interface RunClock {
|
|
15
|
+
startMs: number;
|
|
16
|
+
endMs: number;
|
|
17
|
+
/** Every moment on the clock, ascending without repeats, for keyboard seeking. */
|
|
18
|
+
boundariesMs: number[];
|
|
19
|
+
}
|
|
20
|
+
/** A desktop video's place on the run clock, in epoch ms. */
|
|
21
|
+
export interface RecordingInterval {
|
|
22
|
+
startMs: number;
|
|
23
|
+
endMs: number;
|
|
24
|
+
recording: RunDesktopRecording;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A stream's recorded trace items: the finished actor's, else the flush of a participant still
|
|
28
|
+
* running, so every reader of a live run sees the items grow.
|
|
29
|
+
*/
|
|
30
|
+
export declare function traceItems(stream: ClockStream): readonly ActorTraceItem[];
|
|
31
|
+
/**
|
|
32
|
+
* Whether a trace item is a capture, a frame of the participant's replay: a screenshot, or a
|
|
33
|
+
* scripted action that carries its own screenshot. A notice can cite an earlier screenshot for
|
|
34
|
+
* context, and does not capture it again.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isCapture<Item extends Pick<ActorTraceItem, "kind" | "screenshotRef">>(item: Item): item is Item & {
|
|
37
|
+
screenshotRef: NonNullable<ActorTraceItem["screenshotRef"]>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* A participant's capture times in epoch ms, in recorded order. Null when a capture has no stamp
|
|
41
|
+
* or a stamp goes back: such a participant adds nothing to the run clock, and its replay runs at
|
|
42
|
+
* an average pace. Times are never made up from the actor's duration.
|
|
43
|
+
*/
|
|
44
|
+
export declare function captureTimes(stamps: readonly (number | undefined)[]): number[] | null;
|
|
45
|
+
/** Where a desktop video sits on the run clock; null without a readable start and a length. */
|
|
46
|
+
export declare function recordingInterval(recording: RunDesktopRecording | undefined): RecordingInterval | null;
|
|
47
|
+
/** The run clock of every stream of a run, or null when none has a timed capture or a video. */
|
|
48
|
+
export declare function runClock(streams: readonly ClockStream[]): RunClock | null;
|
|
49
|
+
/** A run clock time or span in whole minutes and seconds: `02:31`. Minutes go past 59. */
|
|
50
|
+
export declare function formatRunTime(ms: number): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The run clock: the Observer's study timeline, and the clock a reviewer note's time counts from.
|
|
2
|
+
// It runs from the first to the last moment any participant recorded: a timed capture, or the start
|
|
3
|
+
// or end of a desktop video. The Observer bundles this module, so it imports types only
|
|
4
|
+
// (docs/decisions/0002-observer-is-one-self-contained-file.md).
|
|
5
|
+
/**
|
|
6
|
+
* A stream's recorded trace items: the finished actor's, else the flush of a participant still
|
|
7
|
+
* running, so every reader of a live run sees the items grow.
|
|
8
|
+
*/
|
|
9
|
+
export function traceItems(stream) {
|
|
10
|
+
return stream.actor?.items ?? stream.liveActor?.items ?? [];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether a trace item is a capture, a frame of the participant's replay: a screenshot, or a
|
|
14
|
+
* scripted action that carries its own screenshot. A notice can cite an earlier screenshot for
|
|
15
|
+
* context, and does not capture it again.
|
|
16
|
+
*/
|
|
17
|
+
export function isCapture(item) {
|
|
18
|
+
return (item.kind === "screenshot" || item.kind === "ui_action") && Boolean(item.screenshotRef);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A participant's capture times in epoch ms, in recorded order. Null when a capture has no stamp
|
|
22
|
+
* or a stamp goes back: such a participant adds nothing to the run clock, and its replay runs at
|
|
23
|
+
* an average pace. Times are never made up from the actor's duration.
|
|
24
|
+
*/
|
|
25
|
+
export function captureTimes(stamps) {
|
|
26
|
+
const times = [];
|
|
27
|
+
for (const time of stamps) {
|
|
28
|
+
if (time === undefined || !Number.isFinite(time) || time < (times.at(-1) ?? -Infinity))
|
|
29
|
+
return null;
|
|
30
|
+
times.push(time);
|
|
31
|
+
}
|
|
32
|
+
return times;
|
|
33
|
+
}
|
|
34
|
+
/** Where a desktop video sits on the run clock; null without a readable start and a length. */
|
|
35
|
+
export function recordingInterval(recording) {
|
|
36
|
+
if (!recording)
|
|
37
|
+
return null;
|
|
38
|
+
const startMs = Date.parse(recording.startedAt);
|
|
39
|
+
const endMs = startMs + recording.durationMs;
|
|
40
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || recording.durationMs <= 0)
|
|
41
|
+
return null;
|
|
42
|
+
return { startMs, endMs, recording };
|
|
43
|
+
}
|
|
44
|
+
/** The run clock of every stream of a run, or null when none has a timed capture or a video. */
|
|
45
|
+
export function runClock(streams) {
|
|
46
|
+
const moments = new Set();
|
|
47
|
+
for (const stream of streams) {
|
|
48
|
+
const stamps = traceItems(stream)
|
|
49
|
+
.filter(isCapture)
|
|
50
|
+
.map((item) => (item.at === undefined ? Number.NaN : Date.parse(item.at)));
|
|
51
|
+
for (const time of captureTimes(stamps) ?? [])
|
|
52
|
+
moments.add(time);
|
|
53
|
+
const video = recordingInterval(stream.recording);
|
|
54
|
+
if (video)
|
|
55
|
+
moments.add(video.startMs).add(video.endMs);
|
|
56
|
+
}
|
|
57
|
+
const boundariesMs = [...moments].sort((a, b) => a - b);
|
|
58
|
+
if (boundariesMs.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
return { startMs: boundariesMs[0], endMs: boundariesMs.at(-1), boundariesMs };
|
|
61
|
+
}
|
|
62
|
+
/** A run clock time or span in whole minutes and seconds: `02:31`. Minutes go past 59. */
|
|
63
|
+
export function formatRunTime(ms) {
|
|
64
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
65
|
+
return `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`;
|
|
66
|
+
}
|
package/dist/run/run.js
CHANGED
|
@@ -15,7 +15,7 @@ import { writeContainedOutputFile, writePreparedRunLatestPointer } from "./conta
|
|
|
15
15
|
import { scrubRunSandboxIds, withPublicSandboxIds } from "./sandbox-ids.js";
|
|
16
16
|
import { RunSecrets } from "./secrets.js";
|
|
17
17
|
import { RUN_STATUS_FILE, beginRunStatus, runStatusOutcome, } from "./status.js";
|
|
18
|
-
import { readRunJsonIfExists } from "./locate.js";
|
|
18
|
+
import { readRunJsonIfExists, runJsonValue } from "./locate.js";
|
|
19
19
|
/** An entry point called in a state that does not admit it. */
|
|
20
20
|
class RunLifecycleError extends Error {
|
|
21
21
|
name = "RunLifecycleError";
|
|
@@ -166,7 +166,10 @@ function runPublisher(args) {
|
|
|
166
166
|
});
|
|
167
167
|
await writeContainedOutputFile(paths, RUN_BUNDLE_FILE, json(publicBundle), "utf8");
|
|
168
168
|
await afterBundle(publicBundle);
|
|
169
|
-
|
|
169
|
+
// The run writes its own status.json; one it refuses renders as a missing one does.
|
|
170
|
+
const status = recorded === undefined
|
|
171
|
+
? runJsonValue(await readRunJsonIfExists(paths, RUN_STATUS_FILE))
|
|
172
|
+
: undefined;
|
|
170
173
|
await writeContainedOutputFile(paths, "review.json", json(publicBundle.review), "utf8");
|
|
171
174
|
await writeContainedOutputFile(paths, "review.md", options.renderReview(publicBundle, status), "utf8");
|
|
172
175
|
const events = publicBundle.events.map((event) => JSON.stringify(event)).join("\n");
|
|
@@ -24,7 +24,10 @@ export declare function inventorySandboxIds(files: readonly {
|
|
|
24
24
|
export declare function holdsKeyedSandboxId(file: string, text: string): boolean;
|
|
25
25
|
/**
|
|
26
26
|
* The raw sandbox ids a run's receipts journal, with any this process receipted whose append
|
|
27
|
-
* failed;
|
|
27
|
+
* failed; none from the journal when it is missing or malformed, or when a folder is where it goes
|
|
28
|
+
* (a failed append can leave one, and the process still holds those ids). A journal refused for
|
|
29
|
+
* any other reason, a link or one too large to read say, throws ContainedReadRefusedError: every
|
|
30
|
+
* caller would otherwise write or grade a run's files without the ids it may hold.
|
|
28
31
|
*/
|
|
29
32
|
export declare function readRunSandboxIds(root: PreparedOutputRoot): Promise<string[]>;
|
|
30
33
|
/**
|
|
@@ -41,7 +44,9 @@ export declare function scrubSandboxIdBytes(file: string, bytes: Buffer, ids: re
|
|
|
41
44
|
* Rewrite each file of a finished run that names one of its raw sandbox ids, apart from the
|
|
42
45
|
* receipts, with the label in place of the id. Writers such as a participant's actor.json can
|
|
43
46
|
* quote an SDK error that names the sandbox. Run.finish calls it before analysis reads the run.
|
|
44
|
-
* Best effort: verify grades a file this misses.
|
|
47
|
+
* Best effort for writes and listings: verify grades a file this misses. A text file it refuses
|
|
48
|
+
* to read, or a journal it refuses, throws once the sweep ends, naming each, since such a file
|
|
49
|
+
* may still hold an id.
|
|
45
50
|
*/
|
|
46
51
|
export declare function scrubRunSandboxIds(paths: PreparedRunArtifactPaths): Promise<void>;
|
|
47
52
|
/**
|
package/dist/run/sandbox-ids.js
CHANGED
|
@@ -6,7 +6,7 @@ import { opendir, realpath } from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { readPlainText } from "../evidence/plain-text.js";
|
|
8
8
|
import { collectSandboxIds, redactSandboxIds, sandboxIdDigest, REDACTED_SANDBOX_ID, } from "../evidence/redaction.js";
|
|
9
|
-
import { readContainedRegularFile, writeContainedOutputFile, } from "./contained-output.js";
|
|
9
|
+
import { ContainedReadRefusedError, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, } from "./contained-output.js";
|
|
10
10
|
import { bindExistingRunArtifactPaths, isSafeRunIdSegment, validatePreparedRunRootIdentity, } from "./paths.js";
|
|
11
11
|
import { appendedSandboxIds, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
|
|
12
12
|
/** How text names a sandbox: the marker with the id's digest. */
|
|
@@ -96,14 +96,19 @@ export function holdsKeyedSandboxId(file, text) {
|
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
98
|
* The raw sandbox ids a run's receipts journal, with any this process receipted whose append
|
|
99
|
-
* failed;
|
|
99
|
+
* failed; none from the journal when it is missing or malformed, or when a folder is where it goes
|
|
100
|
+
* (a failed append can leave one, and the process still holds those ids). A journal refused for
|
|
101
|
+
* any other reason, a link or one too large to read say, throws ContainedReadRefusedError: every
|
|
102
|
+
* caller would otherwise write or grade a run's files without the ids it may hold.
|
|
100
103
|
*/
|
|
101
104
|
export async function readRunSandboxIds(root) {
|
|
105
|
+
const read = await readContainedRegularFile(root, SANDBOX_RECEIPTS_ARTIFACT, RUN_ARTIFACT_MAX_BYTES);
|
|
106
|
+
if (read.status === "refused" && read.reason !== "directory")
|
|
107
|
+
throw new ContainedReadRefusedError(SANDBOX_RECEIPTS_ARTIFACT, read);
|
|
102
108
|
let journaled = [];
|
|
103
109
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
journaled = receiptSandboxIds(bytes);
|
|
110
|
+
if (read.status === "read")
|
|
111
|
+
journaled = receiptSandboxIds(read.bytes);
|
|
107
112
|
}
|
|
108
113
|
catch {
|
|
109
114
|
// The ids this process receipted still apply.
|
|
@@ -138,7 +143,9 @@ const MAX_SWEPT_ENTRIES = 10_000;
|
|
|
138
143
|
* Rewrite each file of a finished run that names one of its raw sandbox ids, apart from the
|
|
139
144
|
* receipts, with the label in place of the id. Writers such as a participant's actor.json can
|
|
140
145
|
* quote an SDK error that names the sandbox. Run.finish calls it before analysis reads the run.
|
|
141
|
-
* Best effort: verify grades a file this misses.
|
|
146
|
+
* Best effort for writes and listings: verify grades a file this misses. A text file it refuses
|
|
147
|
+
* to read, or a journal it refuses, throws once the sweep ends, naming each, since such a file
|
|
148
|
+
* may still hold an id.
|
|
142
149
|
*/
|
|
143
150
|
export async function scrubRunSandboxIds(paths) {
|
|
144
151
|
const ids = await readRunSandboxIds(paths);
|
|
@@ -147,12 +154,15 @@ export async function scrubRunSandboxIds(paths) {
|
|
|
147
154
|
const valid = await validatePreparedRunRootIdentity(paths).then(() => true, () => false);
|
|
148
155
|
if (!valid)
|
|
149
156
|
return;
|
|
157
|
+
const refused = [];
|
|
150
158
|
const scrub = async (file) => {
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
159
|
+
const read = await readContainedRegularFile(paths, file, RUN_ARTIFACT_MAX_BYTES);
|
|
160
|
+
if (read.status === "refused")
|
|
161
|
+
refused.push(refusalText(file, read));
|
|
162
|
+
if (read.status !== "read")
|
|
153
163
|
return;
|
|
154
|
-
const scrubbed = scrubSandboxIdBytes(file, bytes, ids);
|
|
155
|
-
if (scrubbed !== bytes)
|
|
164
|
+
const scrubbed = scrubSandboxIdBytes(file, read.bytes, ids);
|
|
165
|
+
if (scrubbed !== read.bytes)
|
|
156
166
|
await writeContainedOutputFile(paths, file, scrubbed);
|
|
157
167
|
};
|
|
158
168
|
let seen = 0;
|
|
@@ -179,6 +189,8 @@ export async function scrubRunSandboxIds(paths) {
|
|
|
179
189
|
await walk("").catch(() => {
|
|
180
190
|
// A directory that changed while it was listed ends the sweep; verify names what it missed.
|
|
181
191
|
});
|
|
192
|
+
if (refused.length > 0)
|
|
193
|
+
throw new Error(`The sandbox id sweep could not read every run file, so these may still name a sandbox: ${refused.join("; ")}.`);
|
|
182
194
|
}
|
|
183
195
|
/**
|
|
184
196
|
* A run's result as it may be returned or printed: the same object when it names no sandbox,
|
|
@@ -187,12 +199,16 @@ export async function scrubRunSandboxIds(paths) {
|
|
|
187
199
|
export async function publicRunResult(result, cwd) {
|
|
188
200
|
let ids = [];
|
|
189
201
|
if (typeof result.runId === "string" && isSafeRunIdSegment(result.runId)) {
|
|
202
|
+
let paths;
|
|
190
203
|
try {
|
|
191
|
-
|
|
204
|
+
paths = await bindExistingRunArtifactPaths(cwd, result.runId);
|
|
192
205
|
}
|
|
193
206
|
catch {
|
|
194
207
|
// A run that never created its directory has no receipts; the keys are still redacted.
|
|
195
208
|
}
|
|
209
|
+
// A journal that is there and refused throws: the result cannot be printed without its ids.
|
|
210
|
+
if (paths !== undefined)
|
|
211
|
+
ids = await readRunSandboxIds(paths);
|
|
196
212
|
}
|
|
197
213
|
return publicSandboxView(result, ids);
|
|
198
214
|
}
|
package/dist/run/secrets.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export declare class RunSecrets {
|
|
|
11
11
|
* Replaces every held value with the marker, as written, in each encoded form, and where a URL
|
|
12
12
|
* carries it partly percent-encoded or a terminal escape sequence splits it. It reads the values
|
|
13
13
|
* on each call, so a value added after the scrub was handed to a participant or a subject is
|
|
14
|
-
* scrubbed from then on.
|
|
14
|
+
* scrubbed from then on. If the result still holds a value in any readingsOf, as when a marker
|
|
15
|
+
* written before `x` spells the value `SECRET]x`, the whole text becomes the marker.
|
|
15
16
|
*/
|
|
16
17
|
readonly scrub: (text: string) => string;
|
|
17
18
|
/** `seed` keeps its order; values shorter than the floor are dropped. */
|
package/dist/run/secrets.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// replaced as written and in the encoded forms a URL, a JSON body or a log line gives it;
|
|
5
5
|
// redactText runs after the scrub and finds secrets by shape.
|
|
6
6
|
import { scrubLiterals } from "../evidence/redaction.js";
|
|
7
|
-
import { encodedForms } from "../evidence/secret-scrub.js";
|
|
7
|
+
import { encodedForms, holdsSecretValue } from "../evidence/secret-scrub.js";
|
|
8
|
+
import { escapeSequences } from "./escape-sequences.js";
|
|
8
9
|
export class RunSecrets {
|
|
9
10
|
// Private fields stay out of JSON.stringify and object spread, so a Run that reaches a writer
|
|
10
11
|
// whole cannot carry the values with it.
|
|
@@ -12,11 +13,14 @@ export class RunSecrets {
|
|
|
12
13
|
/** Every held value as written and encoded, longest first. */
|
|
13
14
|
#forms = [];
|
|
14
15
|
#minLength;
|
|
16
|
+
/** holdsSecretValue of the held values, built at its first use after `add` holds a new one. */
|
|
17
|
+
#holds;
|
|
15
18
|
/**
|
|
16
19
|
* Replaces every held value with the marker, as written, in each encoded form, and where a URL
|
|
17
20
|
* carries it partly percent-encoded or a terminal escape sequence splits it. It reads the values
|
|
18
21
|
* on each call, so a value added after the scrub was handed to a participant or a subject is
|
|
19
|
-
* scrubbed from then on.
|
|
22
|
+
* scrubbed from then on. If the result still holds a value in any readingsOf, as when a marker
|
|
23
|
+
* written before `x` spells the value `SECRET]x`, the whole text becomes the marker.
|
|
20
24
|
*/
|
|
21
25
|
scrub;
|
|
22
26
|
/** `seed` keeps its order; values shorter than the floor are dropped. */
|
|
@@ -30,7 +34,9 @@ export class RunSecrets {
|
|
|
30
34
|
const literal = scrubLiterals(this.#forms, marker);
|
|
31
35
|
this.scrub = (text) => {
|
|
32
36
|
const scrubbed = literal(text);
|
|
33
|
-
|
|
37
|
+
const result = replaceSpans(scrubbed, viewSpans(scrubbed, this.#forms), marker);
|
|
38
|
+
this.#holds ??= holdsSecretValue(this.#held);
|
|
39
|
+
return this.#holds(result) ? marker : result;
|
|
34
40
|
};
|
|
35
41
|
}
|
|
36
42
|
/** Holds each value the run learns while it runs, unless it is under the floor or already held. */
|
|
@@ -39,6 +45,7 @@ export class RunSecrets {
|
|
|
39
45
|
if (value.length < this.#minLength || this.#held.includes(value))
|
|
40
46
|
continue;
|
|
41
47
|
this.#held.push(value);
|
|
48
|
+
this.#holds = undefined;
|
|
42
49
|
for (const form of encodedForms(value))
|
|
43
50
|
if (!this.#forms.includes(form))
|
|
44
51
|
this.#forms.push(form);
|
|
@@ -69,20 +76,9 @@ export class RunSecrets {
|
|
|
69
76
|
return mergeSpans(found);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
|
-
// What a terminal draws with (operating-system commands, control sequences and two-byte escapes,
|
|
73
|
-
// as written or JSON-escaped, as a JSON event carries a command's colored output) and what a URL
|
|
74
|
-
// encodes with (runs of percent escapes). The view a value is looked for in drops the first and
|
|
75
|
-
// decodes the second.
|
|
76
|
-
const VIEW_SEQUENCE = new RegExp([
|
|
77
|
-
"\\x1b\\][^\\x07]*(?:\\x07|\\x1b\\\\)",
|
|
78
|
-
"\\x1b\\[[0-?]*[ -/]*[@-~]",
|
|
79
|
-
"\\x1b[78=>]",
|
|
80
|
-
"\\\\u001b\\][^\\\\]*(?:\\\\u0007|\\\\u001b\\\\\\\\)",
|
|
81
|
-
"\\\\u001b\\[[0-?]*[ -/]*[@-~]",
|
|
82
|
-
"(?:%[0-9A-Fa-f]{2})+",
|
|
83
|
-
].join("|"), "g");
|
|
84
79
|
/**
|
|
85
|
-
* Where a value is found in the text's view
|
|
80
|
+
* Where a value is found in the text's view, which drops terminal escape sequences and decodes
|
|
81
|
+
* percent escapes (escapeSequences). A browser encodes a space or a quote in a URL path
|
|
86
82
|
* and leaves a `/` or a `:` as written, so a value with both matches no single encoded form, and a
|
|
87
83
|
* color code inside a value splits it. Each found value maps back to the span of `text` it came
|
|
88
84
|
* from, so the text keeps every other character as written.
|
|
@@ -104,21 +100,21 @@ function viewSpans(text, values) {
|
|
|
104
100
|
};
|
|
105
101
|
let cursor = 0;
|
|
106
102
|
let changed = false;
|
|
107
|
-
for (const
|
|
103
|
+
for (const [start, end] of escapeSequences(text)) {
|
|
108
104
|
let plain = "";
|
|
109
|
-
if (
|
|
105
|
+
if (text[start] === "%") {
|
|
110
106
|
try {
|
|
111
|
-
plain = decodeURIComponent(
|
|
107
|
+
plain = decodeURIComponent(text.slice(start, end));
|
|
112
108
|
}
|
|
113
109
|
catch {
|
|
114
110
|
// Not UTF-8: the run stays as written and is copied with the text after it.
|
|
115
111
|
continue;
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
|
-
copy(cursor,
|
|
119
|
-
cursor =
|
|
114
|
+
copy(cursor, start);
|
|
115
|
+
cursor = end;
|
|
120
116
|
for (let unit = 0; unit < plain.length; unit += 1) {
|
|
121
|
-
starts.push(
|
|
117
|
+
starts.push(start);
|
|
122
118
|
ends.push(cursor);
|
|
123
119
|
}
|
|
124
120
|
view += plain;
|
package/dist/run/stored-runs.js
CHANGED
|
@@ -2,12 +2,12 @@ import { lstat, readdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { redactText } from "../evidence/redaction.js";
|
|
4
4
|
import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, resolveRunsRoot, } from "./paths.js";
|
|
5
|
-
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, } from "./contained-output.js";
|
|
5
|
+
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, refusalText, } from "./contained-output.js";
|
|
6
6
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
7
7
|
import { isReviewSummary, isRunOutcome } from "./bundle-shape.js";
|
|
8
8
|
import { bundleDisplayFacts, displayedReview, runDisplay, } from "./display.js";
|
|
9
9
|
import { RUN_STATUS_FILE } from "./status.js";
|
|
10
|
-
import { readLatest, readRunJsonIfExists, resolveRunPath } from "./locate.js";
|
|
10
|
+
import { readLatest, readRunJsonIfExists, resolveRunPath, runJsonValue } from "./locate.js";
|
|
11
11
|
import { withCuaReviewProvenance } from "./outcomes.js";
|
|
12
12
|
import { isNodeError, isRecord } from "./type-guards.js";
|
|
13
13
|
import { invalidRunStorageVerifyResult, verifyResolvedRun, } from "../verify/verify.js";
|
|
@@ -70,7 +70,8 @@ export async function listRuns(cwdInput) {
|
|
|
70
70
|
if (runsRoot && entryRunPaths.physicalRunsRoot !== runsRoot.physicalPath) {
|
|
71
71
|
return runsUnavailableResult(cwd, new Error("humanish runs root changed physical destination."));
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
// A listing shows what it can read: a run.json it refuses lists like a missing one.
|
|
74
|
+
const bundle = runJsonValue(await readRunJsonIfExists(entryRunPaths, RUN_BUNDLE_FILE));
|
|
74
75
|
runs.push({
|
|
75
76
|
runId: entryName,
|
|
76
77
|
createdAt: isRecord(bundle) && typeof bundle.createdAt === "string" ? bundle.createdAt : null,
|
|
@@ -105,7 +106,7 @@ async function listedRunDisplay(runPaths, bundle) {
|
|
|
105
106
|
if (displayed === undefined)
|
|
106
107
|
return {};
|
|
107
108
|
const status = displayed.outcome === undefined
|
|
108
|
-
? await readRunJsonIfExists(runPaths, RUN_STATUS_FILE)
|
|
109
|
+
? runJsonValue(await readRunJsonIfExists(runPaths, RUN_STATUS_FILE))
|
|
109
110
|
: undefined;
|
|
110
111
|
return { display: runDisplay(bundleDisplayFacts(displayed, status)) };
|
|
111
112
|
}
|
|
@@ -155,18 +156,24 @@ export async function readReview(cwdInput, runInput) {
|
|
|
155
156
|
if (!verified.ok || !verified.bundlePath) {
|
|
156
157
|
return verified;
|
|
157
158
|
}
|
|
158
|
-
const
|
|
159
|
+
const reviewRead = runPaths ? await readRunJsonIfExists(runPaths, "review.json") : undefined;
|
|
160
|
+
const review = reviewRead === undefined ? null : runJsonValue(reviewRead);
|
|
159
161
|
if (!isReviewSummary(review)) {
|
|
160
162
|
return {
|
|
161
163
|
...verified,
|
|
162
164
|
ok: false,
|
|
163
165
|
error: {
|
|
164
166
|
code: "HUMANISH_INVALID_RUN_BUNDLE",
|
|
165
|
-
message:
|
|
167
|
+
message: reviewRead?.status === "refused"
|
|
168
|
+
? `${refusalText("review.json", reviewRead)}.`
|
|
169
|
+
: "review.json is missing or invalid.",
|
|
166
170
|
},
|
|
167
171
|
};
|
|
168
172
|
}
|
|
169
|
-
|
|
173
|
+
// The display reads what it can: a run.json refused here shows as a missing one does.
|
|
174
|
+
const bundle = runPaths
|
|
175
|
+
? runJsonValue(await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE))
|
|
176
|
+
: null;
|
|
170
177
|
const projected = isRecord(bundle) && Array.isArray(bundle.streams)
|
|
171
178
|
? withCuaReviewProvenance(review, bundle.streams.filter(isRecord))
|
|
172
179
|
: review;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { REDACTION_MARKERS } from "../evidence/redaction.js";
|
|
3
|
-
import { holdsSecretValue, scrubSecretValues } from "../evidence/secret-scrub.js";
|
|
4
|
-
import { escapeRegExp } from "./text.js";
|
|
3
|
+
import { holdsSecretValue, scrubSecretValues, scrubValuesAsWritten, } from "../evidence/secret-scrub.js";
|
|
5
4
|
const scopes = new AsyncLocalStorage();
|
|
6
5
|
const MAX_VALUES = 8192;
|
|
7
6
|
const MAX_BYTES = 1024 * 1024;
|
|
@@ -15,7 +14,7 @@ function usable(scope) {
|
|
|
15
14
|
function fail(scope) {
|
|
16
15
|
scope.failed = true;
|
|
17
16
|
scope.values.clear();
|
|
18
|
-
delete scope.
|
|
17
|
+
delete scope.literal;
|
|
19
18
|
throw new Error("TRANSIENT_NARRATION_SECRET_LIMIT");
|
|
20
19
|
}
|
|
21
20
|
/** Enclose both participant execution and its automatic analysis in one scope. Nested runs isolate too. */
|
|
@@ -30,7 +29,7 @@ export async function withTransientCommsSecrets(work) {
|
|
|
30
29
|
scope.closed = true;
|
|
31
30
|
scope.values.clear();
|
|
32
31
|
scope.bytes = 0;
|
|
33
|
-
delete scope.
|
|
32
|
+
delete scope.literal;
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
35
|
export function registerTransientCommsSecrets(values) {
|
|
@@ -50,7 +49,7 @@ export function registerTransientCommsSecrets(values) {
|
|
|
50
49
|
fail(scope);
|
|
51
50
|
scope.values.add(value);
|
|
52
51
|
scope.bytes += bytes;
|
|
53
|
-
delete scope.
|
|
52
|
+
delete scope.literal;
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
/** Literal, longest-first replacement in one pass; replacement text is never matched recursively. */
|
|
@@ -59,18 +58,8 @@ export function scrubTransientCommsText(text) {
|
|
|
59
58
|
if (!scope)
|
|
60
59
|
return text;
|
|
61
60
|
usable(scope);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
scope.pattern ??= new RegExp([...scope.values]
|
|
66
|
-
.sort((a, b) => b.length - a.length)
|
|
67
|
-
.map(escapeRegExp)
|
|
68
|
-
.join("|"), "g");
|
|
69
|
-
return text.replace(scope.pattern, REDACTION_MARKERS.secret);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return fail(scope);
|
|
73
|
-
}
|
|
61
|
+
scope.literal ??= scrubValuesAsWritten([...scope.values]);
|
|
62
|
+
return scope.literal(text);
|
|
74
63
|
}
|
|
75
64
|
const REDACTED = REDACTION_MARKERS.secret;
|
|
76
65
|
/**
|