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
|
@@ -54,12 +54,13 @@ export async function readCommsConnections(cwd) {
|
|
|
54
54
|
return empty();
|
|
55
55
|
if ((await lstat(file)).size > 65_536)
|
|
56
56
|
throw new Error("Connection configuration is too large.");
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
const read = await readContainedRegularFile(root, "comms.yaml", 65_536);
|
|
58
|
+
// Gone since the check above, or refused: either way it cannot be read safely.
|
|
59
|
+
if (read.status !== "read")
|
|
59
60
|
throw new Error("Cannot safely read connection configuration.");
|
|
60
61
|
// YAML diagnostics may echo the offending value; do not let them reach a status or frame.
|
|
61
62
|
try {
|
|
62
|
-
return parseConnections(parse(bytes.toString("utf8"), { maxAliasCount: 0 }));
|
|
63
|
+
return parseConnections(parse(read.bytes.toString("utf8"), { maxAliasCount: 0 }));
|
|
63
64
|
}
|
|
64
65
|
catch {
|
|
65
66
|
throw new Error("Invalid connection configuration.");
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// 127.0.0.1 only; request bodies are never logged; request size is capped.
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
|
+
import { readBodyAtMost } from "../observer/http.js";
|
|
12
13
|
import { capturedInlineImages } from "./images.js";
|
|
13
14
|
// ---------------------------------------------------------------- shared parse helpers
|
|
14
15
|
function str(value) {
|
|
@@ -116,26 +117,6 @@ export const DEFAULT_EMAIL_PROFILES = [
|
|
|
116
117
|
sendgridEmailProfile,
|
|
117
118
|
];
|
|
118
119
|
// ---------------------------------------------------------------- server
|
|
119
|
-
class BodyTooLargeError extends Error {
|
|
120
|
-
}
|
|
121
|
-
function readBody(req, limit) {
|
|
122
|
-
return new Promise((resolve, reject) => {
|
|
123
|
-
let size = 0;
|
|
124
|
-
const chunks = [];
|
|
125
|
-
req.on("data", (chunk) => {
|
|
126
|
-
size += chunk.length;
|
|
127
|
-
if (size > limit) {
|
|
128
|
-
// Reject without destroying the socket here: the handler responds 413 cleanly first (a
|
|
129
|
-
// write-after-destroy would otherwise reset the connection), then tears the request down.
|
|
130
|
-
reject(new BodyTooLargeError("request body too large"));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
chunks.push(chunk);
|
|
134
|
-
});
|
|
135
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
136
|
-
req.on("error", reject);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
120
|
const MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
140
121
|
/**
|
|
141
122
|
* Start the vendor-neutral email catch, routing accepted sends into `channel`. Returns the loopback
|
|
@@ -170,21 +151,16 @@ export async function startEmailCatchServer(channel, options = {}) {
|
|
|
170
151
|
respondJson(res, 404, { error: "not found" });
|
|
171
152
|
return;
|
|
172
153
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (!res.headersSent)
|
|
179
|
-
respondJson(res, error instanceof BodyTooLargeError ? 413 : 400, {
|
|
180
|
-
error: "request body could not be read",
|
|
181
|
-
});
|
|
182
|
-
req.destroy();
|
|
154
|
+
const body = await readBodyAtMost(req, MAX_BODY_BYTES).catch(() => undefined);
|
|
155
|
+
if (body === null || body === undefined) {
|
|
156
|
+
// The rest of the body is never read, so the connection closes after this answer.
|
|
157
|
+
res.setHeader("connection", "close");
|
|
158
|
+
respondJson(res, body === null ? 413 : 400, { error: "request body could not be read" });
|
|
183
159
|
return;
|
|
184
160
|
}
|
|
185
161
|
let parsed;
|
|
186
162
|
try {
|
|
187
|
-
parsed = JSON.parse(
|
|
163
|
+
parsed = JSON.parse(body.length > 0 ? body.toString("utf8") : "{}");
|
|
188
164
|
}
|
|
189
165
|
catch {
|
|
190
166
|
respondJson(res, 422, { error: "invalid JSON body" });
|
package/dist/comms/setup.js
CHANGED
|
@@ -13,6 +13,14 @@ import { resolveStudyManifest } from "../study/discover.js";
|
|
|
13
13
|
import { assertPreparedSelectedOutputDirectory, prepareManagedHumanishOutputDirectory, prepareSelectedOutputDirectory, readContainedRegularFile, writeContainedOutputFile, } from "../run/contained-output.js";
|
|
14
14
|
import { otherStudyFiles } from "../study/files.js";
|
|
15
15
|
import { isNodeError } from "../run/type-guards.js";
|
|
16
|
+
/** The most of a study file the receiving copy reads, for its source and its destination. */
|
|
17
|
+
const MAX_STUDY_COPY_BYTES = 1024 * 1024;
|
|
18
|
+
/** The read found exactly `expected`, or found nothing where `expected` is null. */
|
|
19
|
+
function readsAs(read, expected) {
|
|
20
|
+
return expected === null
|
|
21
|
+
? read.status === "missing"
|
|
22
|
+
: read.status === "read" && read.bytes.equals(expected);
|
|
23
|
+
}
|
|
16
24
|
export async function checkCommsConnection(args) {
|
|
17
25
|
const base = {
|
|
18
26
|
schema: "humanish.comms-check.v1",
|
|
@@ -128,9 +136,10 @@ export async function configureCommsStudy(args) {
|
|
|
128
136
|
const rel = path
|
|
129
137
|
.relative(root.requestedPath, path.resolve(args.cwd, source.path))
|
|
130
138
|
.replace(/\\/g, "/");
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
139
|
+
const sourceRead = await readContainedRegularFile(root, rel, MAX_STUDY_COPY_BYTES);
|
|
140
|
+
if (sourceRead.status !== "read")
|
|
133
141
|
return { ...base, message: "The study file is missing or too large." };
|
|
142
|
+
const bytes = sourceRead.bytes;
|
|
134
143
|
const text = bytes.toString("utf8");
|
|
135
144
|
const raw = parse(text);
|
|
136
145
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -182,13 +191,12 @@ export async function configureCommsStudy(args) {
|
|
|
182
191
|
...base,
|
|
183
192
|
message: `${others.join(", ")} already uses the name ${stem}. Rename or remove it before configuring email, so \`run ${stem}\` reads one file.`,
|
|
184
193
|
};
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
catch {
|
|
194
|
+
// An existing destination is replaced only when its bytes are in the plan token, so one that
|
|
195
|
+
// cannot be read whole is refused instead of being taken as absent.
|
|
196
|
+
const destinationRead = await readContainedRegularFile(root, destination, MAX_STUDY_COPY_BYTES);
|
|
197
|
+
if (destinationRead.status === "refused")
|
|
190
198
|
return { ...base, message: "The local destination could not be read safely." };
|
|
191
|
-
|
|
199
|
+
const prior = destinationRead.status === "read" ? destinationRead.bytes : null;
|
|
192
200
|
const content = String(study);
|
|
193
201
|
const token = createHash("sha256")
|
|
194
202
|
.update(bytes)
|
|
@@ -219,10 +227,8 @@ export async function configureCommsStudy(args) {
|
|
|
219
227
|
const lock = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
220
228
|
const identity = await lock.stat();
|
|
221
229
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
if (!currentSource?.equals(bytes) ||
|
|
225
|
-
(prior === null ? currentDestination !== null : !currentDestination?.equals(prior)))
|
|
230
|
+
const reread = (file) => readContainedRegularFile(root, file, MAX_STUDY_COPY_BYTES);
|
|
231
|
+
if (!readsAs(await reread(rel), bytes) || !readsAs(await reread(destination), prior))
|
|
226
232
|
return { ...base, message: "Files changed while saving. Preview again." };
|
|
227
233
|
await writeContainedOutputFile(directory, filename, content, "utf8");
|
|
228
234
|
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
/** Each run of characters U+0080 to U+00FF read as the UTF-8 bytes they may be: é as é. */
|
|
2
|
+
export declare const latin1RunsAsUtf8: (text: string) => string;
|
|
1
3
|
/**
|
|
2
4
|
* Undoes JSON and JS escapes, percent-encoding and HTML character references, one pass each. Each
|
|
3
|
-
* percent escape is one character, the byte's code, so `caf%C3%A9` reads as `café`;
|
|
4
|
-
*
|
|
5
|
-
* the other keeps whole, as `%E2%80%80` (U+2000, a space, in UTF-8) does.
|
|
5
|
+
* percent escape is one character, the byte's code, so `caf%C3%A9` reads as `café`; readingsOf
|
|
6
|
+
* also gives the reading as UTF-8, `café`.
|
|
6
7
|
*/
|
|
7
8
|
export declare function decodeEscapes(text: string): string;
|
|
8
|
-
/** decodeEscapes with each run of percent escapes read as UTF-8. */
|
|
9
|
-
export declare function decodeEscapesUtf8(text: string): string;
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Every reading of the text a scan matches, each once: as written, decodeEscapes, with percent
|
|
11
|
+
* escapes read as UTF-8, decodeEscapes with transfer escapes expanded, and the text as written and
|
|
12
|
+
* decoded with each run of U+0080 to U+00FF characters read as UTF-8 bytes, which undoes text that
|
|
13
|
+
* a reader already decoded one byte per character. Either percent reading can split a value that
|
|
14
|
+
* the other keeps whole, as `%E2%80%80` (U+2000, a space, in UTF-8) does.
|
|
13
15
|
*/
|
|
14
|
-
export declare function
|
|
16
|
+
export declare function readingsOf(text: string): string[];
|
|
15
17
|
/** How scanEncodedText reads text. */
|
|
16
18
|
export interface EncodedTextScanOptions {
|
|
17
19
|
/** Do not count an encoded binary run as opaque. */
|
|
@@ -26,9 +28,8 @@ export interface EncodedTextScan {
|
|
|
26
28
|
readonly opaque: boolean;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
* Scans text for secrets
|
|
30
|
-
* and
|
|
31
|
-
* base64 (standard, URL-safe or line-wrapped) and hex run. A run that decodes to text is scanned in
|
|
31
|
+
* Scans text for secrets in each of its readingsOf, and inside each base64 (standard, URL-safe or
|
|
32
|
+
* line-wrapped) and hex run. A run that decodes to text is scanned in
|
|
32
33
|
* turn. A run that decodes to an archive, or to other binary past MIN_OPAQUE_BASE64_RUN characters,
|
|
33
34
|
* is opaque unless the caller allows opaque runs.
|
|
34
35
|
*/
|
|
@@ -2,43 +2,44 @@
|
|
|
2
2
|
// undoes the encodings a reader undoes for free (JSON and JS escapes, percent-encoding, HTML
|
|
3
3
|
// entities) and looks inside base64 runs. verify and bundle export share decodeEscapes, so they
|
|
4
4
|
// judge the same decoded text.
|
|
5
|
+
import { isUtf8 } from "node:buffer";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
7
|
import { readPlainText } from "./plain-text.js";
|
|
7
8
|
import { containsSensitive } from "./redaction.js";
|
|
8
9
|
// HTML5 named references that stand for printable ASCII. Letters and digits have only numeric
|
|
9
10
|
// references, which decodeEscapes handles.
|
|
10
|
-
const ASCII_ENTITIES =
|
|
11
|
-
amp
|
|
12
|
-
apos
|
|
13
|
-
ast
|
|
14
|
-
bsol
|
|
15
|
-
colon
|
|
16
|
-
comma
|
|
17
|
-
commat
|
|
18
|
-
dollar
|
|
19
|
-
equals
|
|
20
|
-
excl
|
|
21
|
-
grave
|
|
22
|
-
gt
|
|
23
|
-
hat
|
|
24
|
-
lcub
|
|
25
|
-
lowbar
|
|
26
|
-
lpar
|
|
27
|
-
lsqb
|
|
28
|
-
lt
|
|
29
|
-
num
|
|
30
|
-
percnt
|
|
31
|
-
period
|
|
32
|
-
plus
|
|
33
|
-
quest
|
|
34
|
-
quot
|
|
35
|
-
rcub
|
|
36
|
-
rpar
|
|
37
|
-
rsqb
|
|
38
|
-
semi
|
|
39
|
-
sol
|
|
40
|
-
verbar
|
|
41
|
-
|
|
11
|
+
const ASCII_ENTITIES = new Map([
|
|
12
|
+
["amp", "&"],
|
|
13
|
+
["apos", "'"],
|
|
14
|
+
["ast", "*"],
|
|
15
|
+
["bsol", "\\"],
|
|
16
|
+
["colon", ":"],
|
|
17
|
+
["comma", ","],
|
|
18
|
+
["commat", "@"],
|
|
19
|
+
["dollar", "$"],
|
|
20
|
+
["equals", "="],
|
|
21
|
+
["excl", "!"],
|
|
22
|
+
["grave", "`"],
|
|
23
|
+
["gt", ">"],
|
|
24
|
+
["hat", "^"],
|
|
25
|
+
["lcub", "{"],
|
|
26
|
+
["lowbar", "_"],
|
|
27
|
+
["lpar", "("],
|
|
28
|
+
["lsqb", "["],
|
|
29
|
+
["lt", "<"],
|
|
30
|
+
["num", "#"],
|
|
31
|
+
["percnt", "%"],
|
|
32
|
+
["period", "."],
|
|
33
|
+
["plus", "+"],
|
|
34
|
+
["quest", "?"],
|
|
35
|
+
["quot", '"'],
|
|
36
|
+
["rcub", "}"],
|
|
37
|
+
["rpar", ")"],
|
|
38
|
+
["rsqb", "]"],
|
|
39
|
+
["semi", ";"],
|
|
40
|
+
["sol", "/"],
|
|
41
|
+
["verbar", "|"],
|
|
42
|
+
]);
|
|
42
43
|
function codePoint(value, original) {
|
|
43
44
|
return Number.isInteger(value) && value >= 0 && value <= 0x10ffff
|
|
44
45
|
? String.fromCodePoint(value)
|
|
@@ -57,20 +58,19 @@ function sequenceLength(lead) {
|
|
|
57
58
|
return 0;
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
61
|
+
* Bytes read as UTF-8, as a browser reads a URL. A byte that starts no valid sequence stands for
|
|
62
|
+
* itself, the character with that code, as decodeEscapes reads every byte.
|
|
62
63
|
*/
|
|
63
|
-
function
|
|
64
|
-
|
|
64
|
+
function utf8Characters(bytes) {
|
|
65
|
+
if (isUtf8(bytes))
|
|
66
|
+
return bytes.toString("utf8");
|
|
65
67
|
let text = "";
|
|
66
68
|
for (let at = 0; at < bytes.length;) {
|
|
67
69
|
const length = sequenceLength(bytes[at]);
|
|
68
|
-
const sequence =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (decoded !== undefined && Buffer.from(decoded, "utf8").equals(sequence)) {
|
|
73
|
-
text += decoded;
|
|
70
|
+
const sequence = bytes.subarray(at, at + length);
|
|
71
|
+
// isUtf8 refuses an overlong form, a surrogate half and a bad continuation byte.
|
|
72
|
+
if (length > 0 && sequence.length === length && isUtf8(sequence)) {
|
|
73
|
+
text += sequence.toString("utf8");
|
|
74
74
|
at += length;
|
|
75
75
|
}
|
|
76
76
|
else {
|
|
@@ -80,31 +80,47 @@ function decodePercentRun(run) {
|
|
|
80
80
|
}
|
|
81
81
|
return text;
|
|
82
82
|
}
|
|
83
|
+
const decodePercentRun = (run) => utf8Characters(Buffer.from(run.replace(/%/g, ""), "hex"));
|
|
84
|
+
// A character from U+0080 to U+00FF, which a byte of 0x80 or more becomes when read one per
|
|
85
|
+
// character.
|
|
86
|
+
const HIGH_BYTE = /[\x80-\xff]/;
|
|
87
|
+
/** Each run of characters U+0080 to U+00FF read as the UTF-8 bytes they may be: é as é. */
|
|
88
|
+
export const latin1RunsAsUtf8 = (text) => text.replace(/[\x80-\xff]+/g, (run) => utf8Characters(Buffer.from(run, "latin1")));
|
|
83
89
|
function decodePercentBytes(run) {
|
|
84
90
|
return Buffer.from(run.replace(/%/g, ""), "hex").toString("latin1");
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
87
93
|
* Undoes JSON and JS escapes, percent-encoding and HTML character references, one pass each. Each
|
|
88
|
-
* percent escape is one character, the byte's code, so `caf%C3%A9` reads as `café`;
|
|
89
|
-
*
|
|
90
|
-
* the other keeps whole, as `%E2%80%80` (U+2000, a space, in UTF-8) does.
|
|
94
|
+
* percent escape is one character, the byte's code, so `caf%C3%A9` reads as `café`; readingsOf
|
|
95
|
+
* also gives the reading as UTF-8, `café`.
|
|
91
96
|
*/
|
|
92
97
|
export function decodeEscapes(text) {
|
|
93
98
|
return decodeWith(text, decodePercentBytes);
|
|
94
99
|
}
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
/** The readings a scan matches, with the two that scanEncodedText also searches for runs. */
|
|
101
|
+
function readings(text) {
|
|
102
|
+
const decoded = decodeEscapes(text);
|
|
103
|
+
// The UTF-8 reading differs only where a percent escape holds a byte of 0x80 or more, which
|
|
104
|
+
// decodeEscapes writes as a character from U+0080 to U+00FF. Testing the decoded text also
|
|
105
|
+
// catches a `%` that a `\u0025` wrote.
|
|
106
|
+
const utf8 = HIGH_BYTE.test(decoded) ? decodeWith(text, decodePercentRun) : decoded;
|
|
107
|
+
const expanded = decodeTransferEscapes(decoded);
|
|
108
|
+
const recovered = [text, decoded].filter((reading) => HIGH_BYTE.test(reading));
|
|
109
|
+
return {
|
|
110
|
+
decoded,
|
|
111
|
+
expanded,
|
|
112
|
+
all: [...new Set([text, decoded, utf8, expanded, ...recovered.map(latin1RunsAsUtf8)])],
|
|
113
|
+
};
|
|
98
114
|
}
|
|
99
115
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
116
|
+
* Every reading of the text a scan matches, each once: as written, decodeEscapes, with percent
|
|
117
|
+
* escapes read as UTF-8, decodeEscapes with transfer escapes expanded, and the text as written and
|
|
118
|
+
* decoded with each run of U+0080 to U+00FF characters read as UTF-8 bytes, which undoes text that
|
|
119
|
+
* a reader already decoded one byte per character. Either percent reading can split a value that
|
|
120
|
+
* the other keeps whole, as `%E2%80%80` (U+2000, a space, in UTF-8) does.
|
|
102
121
|
*/
|
|
103
|
-
export function
|
|
104
|
-
|
|
105
|
-
return undefined;
|
|
106
|
-
const utf8 = decodeEscapesUtf8(text);
|
|
107
|
-
return utf8 === decoded ? undefined : utf8;
|
|
122
|
+
export function readingsOf(text) {
|
|
123
|
+
return readings(text).all;
|
|
108
124
|
}
|
|
109
125
|
function decodeWith(text, percent) {
|
|
110
126
|
return text
|
|
@@ -114,7 +130,10 @@ function decodeWith(text, percent) {
|
|
|
114
130
|
.replace(/(?:%[0-9a-f]{2})+/gi, percent)
|
|
115
131
|
.replace(/&#x([0-9a-f]{1,6});?/gi, (match, hex) => codePoint(Number.parseInt(hex, 16), match))
|
|
116
132
|
.replace(/&#(\d{1,7});?/g, (match, digits) => codePoint(Number.parseInt(digits, 10), match))
|
|
117
|
-
.replace(/&([a-z]+);/gi,
|
|
133
|
+
.replace(/&([a-z]+);/gi, namedReference);
|
|
134
|
+
}
|
|
135
|
+
function namedReference(match, name) {
|
|
136
|
+
return ASCII_ENTITIES.get(name.toLowerCase()) ?? match;
|
|
118
137
|
}
|
|
119
138
|
// Sixteen characters hold twelve bytes, enough for the start of a key. Shorter runs are mostly
|
|
120
139
|
// words and identifiers.
|
|
@@ -262,8 +281,7 @@ function inspectDecoded(bytes, run, options, depth) {
|
|
|
262
281
|
if (inner !== undefined) {
|
|
263
282
|
if (depth < MAX_DEPTH)
|
|
264
283
|
return scanEncodedText(inner, options, depth + 1);
|
|
265
|
-
|
|
266
|
-
return matches(inner) || (innerDecoded !== inner && matches(innerDecoded)) ? SENSITIVE : CLEAN;
|
|
284
|
+
return readingsOf(inner).some(matches) ? SENSITIVE : CLEAN;
|
|
267
285
|
}
|
|
268
286
|
// A key next to a few binary bytes is still a printable stretch.
|
|
269
287
|
if (matches(printableStretches(bytes)))
|
|
@@ -276,22 +294,15 @@ function inspectDecoded(bytes, run, options, depth) {
|
|
|
276
294
|
};
|
|
277
295
|
}
|
|
278
296
|
/**
|
|
279
|
-
* Scans text for secrets
|
|
280
|
-
* and
|
|
281
|
-
* base64 (standard, URL-safe or line-wrapped) and hex run. A run that decodes to text is scanned in
|
|
297
|
+
* Scans text for secrets in each of its readingsOf, and inside each base64 (standard, URL-safe or
|
|
298
|
+
* line-wrapped) and hex run. A run that decodes to text is scanned in
|
|
282
299
|
* turn. A run that decodes to an archive, or to other binary past MIN_OPAQUE_BASE64_RUN characters,
|
|
283
300
|
* is opaque unless the caller allows opaque runs.
|
|
284
301
|
*/
|
|
285
302
|
export function scanEncodedText(text, options = {}, depth = 0) {
|
|
286
303
|
const matches = matcherOf(options);
|
|
287
|
-
const decoded =
|
|
288
|
-
|
|
289
|
-
const expanded = decodeTransferEscapes(decoded);
|
|
290
|
-
// A decoding that returns the same string would match the same way, so it is not matched again.
|
|
291
|
-
if (matches(text) ||
|
|
292
|
-
(decoded !== text && matches(decoded)) ||
|
|
293
|
-
(utf8 !== undefined && matches(utf8)) ||
|
|
294
|
-
(expanded !== decoded && matches(expanded)))
|
|
304
|
+
const { decoded, expanded, all } = readings(text);
|
|
305
|
+
if (all.some(matches))
|
|
295
306
|
return SENSITIVE;
|
|
296
307
|
const runs = [];
|
|
297
308
|
// A piece or a run decoded from a slash only adds findings; its bytes are not judged opaque.
|
|
@@ -341,7 +352,7 @@ export function scanEncodedText(text, options = {}, depth = 0) {
|
|
|
341
352
|
// Bump when scanEncodedText, decodeEscapes or the sensitive patterns change what they return. The
|
|
342
353
|
// cache lives in one process, so the version guards results across a hot reload or a test that
|
|
343
354
|
// swaps the scanner.
|
|
344
|
-
const ENCODED_SCAN_VERSION =
|
|
355
|
+
const ENCODED_SCAN_VERSION = 5;
|
|
345
356
|
// Distinct files one process verifies in a burst (a run's files, a serve library's runs).
|
|
346
357
|
const SCAN_CACHE_LIMIT = 256;
|
|
347
358
|
const scanCache = new Map();
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every marker humanish writes in place of what it withholds. The known-value scrub leaves text
|
|
3
|
+
* wholly inside one of these as it is, so a writer of a new marker adds it here.
|
|
4
|
+
*/
|
|
5
|
+
export declare const REDACTION_MARKERS: {
|
|
6
|
+
readonly secret: "[REDACTED_SECRET]";
|
|
7
|
+
readonly localPath: "[REDACTED_LOCAL_PATH]";
|
|
8
|
+
readonly runtimePath: "[REDACTED_RUNTIME_PATH]";
|
|
9
|
+
readonly promptText: "[REDACTED_PROMPT_TEXT]";
|
|
10
|
+
readonly lobbyCode: "[REDACTED_LOBBY_CODE]";
|
|
11
|
+
};
|
|
1
12
|
/** Every pattern containsSensitive tests. A test pins each to ASCII, which scanEncodedText relies on. */
|
|
2
13
|
export declare function sensitivePatterns(): readonly RegExp[];
|
|
3
14
|
/** True if the text contains a credential. Local paths and a URL on an E2B host alone do not count. */
|
|
@@ -126,18 +126,29 @@ const SECRET_PATTERNS = [
|
|
|
126
126
|
SANDBOX_URL,
|
|
127
127
|
/BEGIN (RSA|OPENSSH|PRIVATE) KEY/gi,
|
|
128
128
|
];
|
|
129
|
+
/**
|
|
130
|
+
* Every marker humanish writes in place of what it withholds. The known-value scrub leaves text
|
|
131
|
+
* wholly inside one of these as it is, so a writer of a new marker adds it here.
|
|
132
|
+
*/
|
|
133
|
+
export const REDACTION_MARKERS = {
|
|
134
|
+
secret: "[REDACTED_SECRET]",
|
|
135
|
+
localPath: "[REDACTED_LOCAL_PATH]",
|
|
136
|
+
runtimePath: "[REDACTED_RUNTIME_PATH]",
|
|
137
|
+
promptText: "[REDACTED_PROMPT_TEXT]",
|
|
138
|
+
lobbyCode: "[REDACTED_LOBBY_CODE]",
|
|
139
|
+
};
|
|
129
140
|
const LOCAL_PATH_PATTERNS = [
|
|
130
|
-
[/\/private\/var\/folders\/[^\s"'`<>)]*/g,
|
|
131
|
-
[/\/var\/folders\/[^\s"'`<>)]*/g,
|
|
132
|
-
[/\/private\/tmp\/[^\s"'`<>)]*/g,
|
|
133
|
-
[/\/tmp\/[^\s"'`<>)]*/g,
|
|
134
|
-
[/\/Users\/[A-Za-z0-9._-]+(?:\/[^\s"'`<>)]*)?/g,
|
|
135
|
-
[/\/home\/[A-Za-z0-9._-]+(?:\/[^\s"'`<>)]*)?/g,
|
|
141
|
+
[/\/private\/var\/folders\/[^\s"'`<>)]*/g, REDACTION_MARKERS.localPath],
|
|
142
|
+
[/\/var\/folders\/[^\s"'`<>)]*/g, REDACTION_MARKERS.localPath],
|
|
143
|
+
[/\/private\/tmp\/[^\s"'`<>)]*/g, REDACTION_MARKERS.localPath],
|
|
144
|
+
[/\/tmp\/[^\s"'`<>)]*/g, REDACTION_MARKERS.localPath],
|
|
145
|
+
[/\/Users\/[A-Za-z0-9._-]+(?:\/[^\s"'`<>)]*)?/g, REDACTION_MARKERS.localPath],
|
|
146
|
+
[/\/home\/[A-Za-z0-9._-]+(?:\/[^\s"'`<>)]*)?/g, REDACTION_MARKERS.runtimePath],
|
|
136
147
|
// A Windows profile path, with `\`, `\\` (JSON-escaped) or `/` between segments. A match never
|
|
137
148
|
// ends in a backslash, so it cannot swallow the escape of a closing quote.
|
|
138
149
|
[
|
|
139
150
|
/\b[A-Za-z]:(?:\\\\|\\|\/)Users(?:\\\\|\\|\/)[^\\/\s"'`<>)]+[^\s"'`<>)]*/g,
|
|
140
|
-
|
|
151
|
+
REDACTION_MARKERS.localPath,
|
|
141
152
|
],
|
|
142
153
|
];
|
|
143
154
|
/** Every pattern containsSensitive tests. A test pins each to ASCII, which scanEncodedText relies on. */
|
|
@@ -171,7 +182,15 @@ export function containsSensitive(text) {
|
|
|
171
182
|
// suffix so redaction cannot turn a string's contents into JSON delimiters.
|
|
172
183
|
// This also works when the closing quote arrives in a later stream callback.
|
|
173
184
|
function redactLocalPaths(text, label) {
|
|
174
|
-
return LOCAL_PATH_PATTERNS.reduce((current, [pattern, replacement]) => current.replace(pattern, (match) => (label ?? replacement) + (match
|
|
185
|
+
return LOCAL_PATH_PATTERNS.reduce((current, [pattern, replacement]) => current.replace(pattern, (match) => (label ?? replacement) + trailingBackslashes(match)), text);
|
|
186
|
+
}
|
|
187
|
+
// Read from the end: `/\\+$/` retries a run of backslashes from each of its characters when
|
|
188
|
+
// something follows the run, which took 1.4 s on a path holding 40,000 of them.
|
|
189
|
+
function trailingBackslashes(text) {
|
|
190
|
+
let start = text.length;
|
|
191
|
+
while (start > 0 && text.charCodeAt(start - 1) === 0x5c)
|
|
192
|
+
start -= 1;
|
|
193
|
+
return text.slice(start);
|
|
175
194
|
}
|
|
176
195
|
/**
|
|
177
196
|
* What a shared copy of a run says in place of a provider sandbox id. scripts/public-surface-scan.mjs
|
|
@@ -265,7 +284,7 @@ function redactSecrets(text) {
|
|
|
265
284
|
return SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, (...args) => {
|
|
266
285
|
const groups = args.at(-1);
|
|
267
286
|
const keep = isRecord(groups) && typeof groups.keep === "string" ? groups.keep : "";
|
|
268
|
-
return `${keep}
|
|
287
|
+
return `${keep}${REDACTION_MARKERS.secret}`;
|
|
269
288
|
}), text);
|
|
270
289
|
}
|
|
271
290
|
/** Redact secrets to [REDACTED_SECRET] and local paths to their path labels. */
|
|
@@ -274,7 +293,7 @@ export function redactText(text) {
|
|
|
274
293
|
}
|
|
275
294
|
/** Redact every sensitive match (secrets and paths) to a single [REDACTED_SECRET] label. */
|
|
276
295
|
export function redactToSecretLabel(text) {
|
|
277
|
-
return redactLocalPaths(redactSecrets(text),
|
|
296
|
+
return redactLocalPaths(redactSecrets(text), REDACTION_MARKERS.secret);
|
|
278
297
|
}
|
|
279
298
|
function canonicalizePath(value) {
|
|
280
299
|
const resolved = path.resolve(value);
|
|
@@ -526,7 +545,7 @@ function blurPass(src, width, height, radius, horizontal) {
|
|
|
526
545
|
* pattern-match, such as provisioned keys and subject env values. `values` is read on each call,
|
|
527
546
|
* so a value registered later is scrubbed too.
|
|
528
547
|
*/
|
|
529
|
-
export function scrubLiterals(values, marker =
|
|
548
|
+
export function scrubLiterals(values, marker = REDACTION_MARKERS.secret) {
|
|
530
549
|
return (text) => values.reduce((current, value) => current.split(value).join(marker), text);
|
|
531
550
|
}
|
|
532
551
|
/**
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A value as written, and percent-encoded, JSON-escaped once (also with non-ASCII characters as
|
|
3
|
-
* `\u` escapes) and twice, its UTF-8 bytes read one per character
|
|
4
|
-
*
|
|
5
|
-
* is
|
|
3
|
+
* `\u` escapes) and twice, its UTF-8 bytes read one per character and the reverse, base64 padded,
|
|
4
|
+
* unpadded and at each byte offset, base64url and hex. Twice, because a JSON event can carry a
|
|
5
|
+
* command's JSON output as a string. An escaped form is searched for at any length, as the value
|
|
6
|
+
* is: it holds a backslash or a `%`, so it is not ordinary text. A base64 or hex form is searched
|
|
7
|
+
* for from MIN_ENCODED_FORM characters, so a four-byte value is found in base64 written whole and
|
|
8
|
+
* not inside a longer run, where four or five of its characters do not depend on its neighbours.
|
|
6
9
|
*/
|
|
7
10
|
export declare function encodedForms(value: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* A scrub that replaces each non-empty value as written with `[REDACTED_SECRET]`, markers
|
|
13
|
+
* included. From the start of the text, the longest value that starts at a position is replaced
|
|
14
|
+
* and the search goes on after it, so the marker it writes is never searched. One global regex of
|
|
15
|
+
* the values, longest first, does that fastest. V8 compiles it at its first use and refuses it
|
|
16
|
+
* once a value has 32,768 characters; the scrub then finds each value itself, with the same
|
|
17
|
+
* output, in time linear in the text for each value.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scrubValuesAsWritten(values: readonly string[]): (text: string) => string;
|
|
20
|
+
/**
|
|
21
|
+
* A check that the text holds one of the values' forms outside the markers in any of its
|
|
22
|
+
* readingsOf. A scrub's output must pass it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function holdsSecretValue(values: readonly string[]): (text: string) => boolean;
|
|
8
25
|
/**
|
|
9
26
|
* A scrub that replaces each non-empty value and its encoded forms with `[REDACTED_SECRET]`. Every
|
|
10
27
|
* occurrence of every form is found on its own, and overlapping finds are merged into one span, so
|
|
@@ -12,5 +29,11 @@ export declare function encodedForms(value: string): string[];
|
|
|
12
29
|
* through decodeEscapes, the decoder verify uses, and returns the decoded text: a warning with
|
|
13
30
|
* percent-encoding, JSON escapes or HTML references shows them decoded, with the values removed.
|
|
14
31
|
* That finds a value written partly encoded, which no single encoded form matches.
|
|
32
|
+
*
|
|
33
|
+
* The result's other readingsOf only detect. If one still holds a form, the scrub returns
|
|
34
|
+
* `[REDACTED_SECRET]` in place of the whole text. That happens for a value encoded twice, which
|
|
35
|
+
* the decoded text shows encoded once; for a value the UTF-8 or transfer reading holds, such as
|
|
36
|
+
* one with one of two non-ASCII characters percent-encoded; and for a value that holds part of a
|
|
37
|
+
* marker, such as `ET]x`, which the marker that replaced it can spell again.
|
|
15
38
|
*/
|
|
16
39
|
export declare function scrubSecretValues(values: readonly string[]): (text: string) => string;
|