humanish 0.110.1 → 0.110.3
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/ARCHITECTURE.md +11 -16
- package/CHANGELOG.md +26 -0
- package/CONTEXT.md +1 -1
- package/README.md +8 -7
- package/dist/actors/computer-use/actor.js +2 -2
- package/dist/actors/computer-use/loop/ending.js +1 -1
- package/dist/actors/local-agent/cli.d.ts +6 -0
- package/dist/actors/local-agent/cli.js +8 -0
- package/dist/actors/local-agent/readiness.js +2 -1
- package/dist/actors/registry.js +1 -1
- package/dist/analysis/automatic-completion.js +8 -0
- package/dist/cli/commands/observe.js +18 -4
- package/dist/cli/commands/run-signals.js +13 -4
- package/dist/cli/commands/runs.js +36 -13
- package/dist/cli/commands/setup.js +23 -14
- package/dist/cli/commands/study-route-computer-use.js +7 -3
- package/dist/cli/commands/study-route-open.js +1 -0
- package/dist/cli/commands/study-route-preview.js +1 -0
- package/dist/cli/commands/study-route-shared-world.js +4 -2
- package/dist/cli/commands/study-run.js +9 -19
- package/dist/cli/commands/watch.js +14 -4
- package/dist/cli/doctor.js +9 -6
- package/dist/cli/findings.d.ts +5 -0
- package/dist/cli/findings.js +28 -6
- package/dist/cli/first-run-path.d.ts +27 -7
- package/dist/cli/first-run-path.js +112 -75
- package/dist/cli/invocation.d.ts +7 -0
- package/dist/cli/invocation.js +32 -0
- package/dist/cli/io.d.ts +5 -2
- package/dist/cli/io.js +9 -16
- package/dist/cli/observer-follow.d.ts +24 -0
- package/dist/cli/observer-follow.js +48 -3
- package/dist/cli/orientation.js +16 -14
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +11 -0
- package/dist/evidence/encoded-text.d.ts +9 -6
- package/dist/evidence/encoded-text.js +84 -19
- package/dist/evidence/redaction.d.ts +2 -0
- package/dist/evidence/redaction.js +134 -21
- package/dist/feedback/export-bundle.js +2 -2
- package/dist/keys/key-resolution.js +6 -5
- package/dist/observer/exposure.js +1 -1
- package/dist/observer/library.js +3 -2
- package/dist/observer/render.js +2 -1
- package/dist/observer/run-routes.js +8 -1
- package/dist/observer/serve.js +2 -1
- package/dist/routes/computer-use/participant-runs.d.ts +6 -9
- package/dist/routes/computer-use/participant-runs.js +16 -36
- package/dist/routes/computer-use/plan.d.ts +2 -2
- package/dist/routes/computer-use/plan.js +6 -1
- package/dist/routes/computer-use/route.d.ts +5 -18
- package/dist/routes/computer-use/route.js +17 -78
- package/dist/routes/computer-use/setup.d.ts +3 -2
- package/dist/routes/computer-use/types.d.ts +6 -10
- package/dist/routes/scripted/route.d.ts +4 -17
- package/dist/routes/scripted/route.js +10 -42
- package/dist/routes/scripted/types.d.ts +1 -6
- package/dist/routes/shared-world/plan.js +5 -1
- package/dist/routes/shared-world/route.d.ts +5 -11
- package/dist/routes/shared-world/route.js +24 -63
- package/dist/routes/shared-world/types.d.ts +2 -6
- package/dist/routes/terminal/bundle.js +2 -1
- package/dist/routes/terminal/credentials.d.ts +5 -5
- package/dist/routes/terminal/credentials.js +14 -6
- package/dist/routes/terminal/plan.js +8 -0
- package/dist/routes/terminal/route.d.ts +3 -13
- package/dist/routes/terminal/route.js +24 -63
- package/dist/routes/terminal/runtime-auth.d.ts +6 -0
- package/dist/routes/terminal/runtime-auth.js +5 -0
- package/dist/routes/terminal/session.d.ts +2 -3
- package/dist/routes/terminal/session.js +2 -3
- package/dist/routes/terminal/types.d.ts +2 -7
- package/dist/run/dry-run.js +2 -1
- package/dist/run/judge.js +2 -1
- package/dist/run/local-only-files.d.ts +13 -0
- package/dist/run/local-only-files.js +35 -0
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +34 -2
- package/dist/run/route-shell.d.ts +60 -0
- package/dist/run/route-shell.js +51 -0
- package/dist/run/run-not-found.js +3 -2
- package/dist/run/run.d.ts +6 -1
- package/dist/run/run.js +11 -2
- package/dist/run/sandbox-creates.js +5 -3
- package/dist/run/sandbox-receipts.d.ts +6 -5
- package/dist/run/sandbox-receipts.js +12 -7
- package/dist/run/status.d.ts +2 -0
- package/dist/run/status.js +5 -0
- package/dist/study/agents-section.d.ts +21 -0
- package/dist/study/agents-section.js +86 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/init-templates.d.ts +1 -1
- package/dist/study/init-templates.js +43 -129
- package/dist/study/init.js +32 -35
- package/dist/study/local-browser-host.d.ts +11 -0
- package/dist/study/local-browser-host.js +42 -0
- package/dist/study/parse/actors.js +9 -1
- package/dist/study/parse/subject.js +13 -1
- package/dist/study/parse/url-credentials.d.ts +9 -0
- package/dist/study/parse/url-credentials.js +50 -0
- package/dist/study/preflight-probes.js +2 -1
- package/dist/study/types.d.ts +8 -6
- package/dist/study/url-credentials.d.ts +6 -0
- package/dist/study/url-credentials.js +34 -0
- package/dist/substrates/e2b/sandbox.js +33 -15
- package/dist/substrates/local/firecracker-desktop.js +3 -2
- package/dist/substrates/local/runtime-host.js +5 -4
- package/dist/substrates/local/runtime.js +5 -4
- package/dist/tui/contract.js +5 -4
- package/dist/verify/liveness.js +2 -1
- package/dist/verify/verify.js +2 -1
- package/docs/architecture/actor-contract.md +1 -1
- package/docs/architecture/external-public-shared-world.md +1 -1
- package/docs/architecture/terminal-product-route.md +26 -24
- package/docs/contracts/run-bundle.md +20 -4
- package/docs/contracts/schemas.md +6 -5
- package/docs/contracts/study-analysis.md +5 -2
- package/docs/product/automatic-analysis.md +1 -1
- package/package.json +4 -2
- package/skills/humanish/SKILL.md +22 -18
|
@@ -2,6 +2,7 @@ import { normalizeExtraExcludeEntry } from "../../subject/local-tree-archive.js"
|
|
|
2
2
|
import { ENV_NAME_PATTERN, invalid, posInt, str, strList } from "./values.js";
|
|
3
3
|
import { parseEnvValues, parseState, subjectStateInvalidReason } from "./subject-state.js";
|
|
4
4
|
import { isRecord } from "../../run/type-guards.js";
|
|
5
|
+
import { urlCredentialReason } from "./url-credentials.js";
|
|
5
6
|
export function parseSubject(raw) {
|
|
6
7
|
if (!isRecord(raw)) {
|
|
7
8
|
return invalid("`subject` is required and must be an object.");
|
|
@@ -175,7 +176,7 @@ function parseServedApp(raw, subject) {
|
|
|
175
176
|
return stateResult;
|
|
176
177
|
}
|
|
177
178
|
if (stateResult.value) {
|
|
178
|
-
// Semantic validation is shared with the engine (
|
|
179
|
+
// Semantic validation is shared with the engine (planComputerUseStudy re-enforces it for
|
|
179
180
|
// configs that arrive through the library API without the parser).
|
|
180
181
|
const reason = subjectStateInvalidReason(stateResult.value, subject.env);
|
|
181
182
|
if (reason) {
|
|
@@ -202,6 +203,9 @@ function parseAppSubject(raw, source, subject) {
|
|
|
202
203
|
else if (!isHttpUrl(appUrl)) {
|
|
203
204
|
return invalid("`subject.appUrl` must be an http(s) URL.");
|
|
204
205
|
}
|
|
206
|
+
const credential = urlCredentialReason("subject.appUrl", appUrl);
|
|
207
|
+
if (credential)
|
|
208
|
+
return invalid(credential);
|
|
205
209
|
subject.appUrl = appUrl;
|
|
206
210
|
// publicTarget (external-public shared-world ownership attestation) is app-url-only. Shape it
|
|
207
211
|
// here; its required-on-that-route semantics live in externalPublicSharedWorldValidationReason.
|
|
@@ -278,6 +282,11 @@ function parseProduct(raw) {
|
|
|
278
282
|
if (badSurface) {
|
|
279
283
|
return invalid(`subject.product.publicSurfaces entries must be http(s) URLs (got "${badSurface}").`);
|
|
280
284
|
}
|
|
285
|
+
const surfaceCredential = publicSurfaces
|
|
286
|
+
.map((surface) => urlCredentialReason("subject.product.publicSurfaces", surface))
|
|
287
|
+
.find((reason) => reason !== undefined);
|
|
288
|
+
if (surfaceCredential)
|
|
289
|
+
return invalid(surfaceCredential);
|
|
281
290
|
return {
|
|
282
291
|
ok: true,
|
|
283
292
|
value: {
|
|
@@ -332,6 +341,9 @@ function parseServe(raw) {
|
|
|
332
341
|
if (!url || !isLoopbackUrl(url)) {
|
|
333
342
|
return invalid("`subject.serve.url` must be a loopback http(s) URL (127.0.0.1 or localhost), because the app runs inside the sandbox.");
|
|
334
343
|
}
|
|
344
|
+
const credential = urlCredentialReason("subject.serve.url", url);
|
|
345
|
+
if (credential)
|
|
346
|
+
return invalid(credential);
|
|
335
347
|
const serve = { start, url };
|
|
336
348
|
const install = str(raw.install);
|
|
337
349
|
if (install)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the URL declared at `field` cannot be used: it has userinfo, or its path, query or fragment
|
|
3
|
+
* holds a value verify flags as a secret, as written or encoded. Undefined for a URL with neither,
|
|
4
|
+
* and for text that is not a URL, which the field's own shape check refuses. `base` resolves a
|
|
5
|
+
* relative entry path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function urlCredentialReason(field: string, value: string, base?: string): string | undefined;
|
|
8
|
+
/** Why the participant entry at `field` cannot be used, resolved as a path on the subject. */
|
|
9
|
+
export declare function entryCredentialReason(field: string, entry: string): string | undefined;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// A URL a study declares is recorded in the run, shown in the participant's address bar and so in
|
|
2
|
+
// every screenshot and model request. It may not carry a credential. Ordinary query parameters,
|
|
3
|
+
// such as a filter or a page number, stay as they are.
|
|
4
|
+
import { scanEncodedText } from "../../evidence/encoded-text.js";
|
|
5
|
+
// A relative participant entry is resolved against this before it is read.
|
|
6
|
+
const ENTRY_BASE = "http://127.0.0.1/";
|
|
7
|
+
/**
|
|
8
|
+
* Why the URL declared at `field` cannot be used: it has userinfo, or its path, query or fragment
|
|
9
|
+
* holds a value verify flags as a secret, as written or encoded. Undefined for a URL with neither,
|
|
10
|
+
* and for text that is not a URL, which the field's own shape check refuses. `base` resolves a
|
|
11
|
+
* relative entry path.
|
|
12
|
+
*/
|
|
13
|
+
export function urlCredentialReason(field, value, base) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(value, base);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
// The URL parser drops tabs and line breaks, which can split a key the scan would otherwise read.
|
|
22
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
23
|
+
return `\`${field}\` has a tab, line break or other control character in it, which the URL parser drops. Write the URL without it.`;
|
|
24
|
+
if (url.username !== "" || url.password !== "")
|
|
25
|
+
return `\`${field}\` has a user name or password in it. humanish records the URL in the run, and the participant's browser shows it in every screenshot, so the credential would reach the bundle and the model. Remove it from the URL and give the participant a test account to sign in with.`;
|
|
26
|
+
if (urlPartsHoldSecret(value, url))
|
|
27
|
+
return `\`${field}\` carries a credential in its path, query or fragment, such as a token, a signature or a preview bypass parameter. humanish records the URL in the run, and the participant's browser shows it in every screenshot. Remove it from the URL. For a protected preview, make the deployment reachable without a token for the study, or build the app in the desktop with a clone subject.`;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Why the participant entry at `field` cannot be used, resolved as a path on the subject. */
|
|
31
|
+
export function entryCredentialReason(field, entry) {
|
|
32
|
+
return urlCredentialReason(field, entry, ENTRY_BASE);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether the URL's path, query or fragment holds a secret: read whole, as the parser normalized
|
|
36
|
+
* it and as written (`..` segments can drop a segment from the normalized path), and one segment,
|
|
37
|
+
* value or fragment piece at a time, so the text around an encoded key cannot shift its decoding.
|
|
38
|
+
*/
|
|
39
|
+
function urlPartsHoldSecret(value, url) {
|
|
40
|
+
const written = value.replace(/^[a-z][a-z0-9+.-]{0,31}:\/\/[^/?#]*/i, "");
|
|
41
|
+
const normalized = `${url.pathname}${url.search}${url.hash}`;
|
|
42
|
+
const pieces = [
|
|
43
|
+
written,
|
|
44
|
+
normalized,
|
|
45
|
+
...written.split(/[/?#&=;]/),
|
|
46
|
+
...normalized.split(/[/?#&=;]/),
|
|
47
|
+
];
|
|
48
|
+
return pieces.some((piece) => piece.length > 0 &&
|
|
49
|
+
scanEncodedText(piece, { secretsOnly: true, allowOpaqueBase64: true }).sensitive);
|
|
50
|
+
}
|
|
@@ -14,6 +14,7 @@ import { redactText } from "../evidence/redaction.js";
|
|
|
14
14
|
import { digest, fail, finalize } from "./preflight-result.js";
|
|
15
15
|
import { rosterOf } from "./parse/actors.js";
|
|
16
16
|
import { plural } from "../run/text.js";
|
|
17
|
+
import { cli } from "../cli/invocation.js";
|
|
17
18
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
18
19
|
// Room on the probe's lease for desktop boot and teardown around the work it does.
|
|
19
20
|
const PREFLIGHT_LEASE_BUFFER_MS = 5 * 60_000;
|
|
@@ -240,7 +241,7 @@ async function settlePreflightJournal(ctx, journal, gone) {
|
|
|
240
241
|
return;
|
|
241
242
|
if (!gone) {
|
|
242
243
|
await abandonPreflightJournal(journal).catch(() => undefined);
|
|
243
|
-
ctx.warnings.push(`The preflight sandbox's receipt stays in .humanish/preflight/${journal.id}; run
|
|
244
|
+
ctx.warnings.push(`The preflight sandbox's receipt stays in .humanish/preflight/${journal.id}; run \`${cli("reclaim --preflight")}\` to kill it by id.`);
|
|
244
245
|
return;
|
|
245
246
|
}
|
|
246
247
|
await discardPreflightJournal(journal).catch((error) => {
|
package/dist/study/types.d.ts
CHANGED
|
@@ -448,10 +448,11 @@ export interface StudyExecutionTerminal {
|
|
|
448
448
|
stdin?: StudyTerminalStdin;
|
|
449
449
|
}
|
|
450
450
|
/**
|
|
451
|
-
* The terminal agent's runtime-auth channel. "openai-
|
|
452
|
-
* key
|
|
453
|
-
*
|
|
454
|
-
*
|
|
451
|
+
* The terminal agent's runtime-auth channel. "openai-egress" (the default) keeps the raw runtime
|
|
452
|
+
* key in an E2B outbound header transform for the default OpenAI endpoint and passes an inert
|
|
453
|
+
* placeholder to Codex. It still gives every sandbox process a spendable OpenAI proxy capability;
|
|
454
|
+
* it is not a spend cap or an egress restriction. "openai-env" passes the raw key command-scoped,
|
|
455
|
+
* where the agent and its child processes can read it.
|
|
455
456
|
*/
|
|
456
457
|
export type StudyRuntimeAuth = "openai-env" | "openai-egress";
|
|
457
458
|
export type StudyDesktopBrowser = "default" | "chrome" | "chromium" | "firefox";
|
|
@@ -559,8 +560,9 @@ export interface StudyExecution {
|
|
|
559
560
|
caps?: StudyScenarioCaps;
|
|
560
561
|
/** `terminal-product` route: the terminal transport + stdin posture. Consumed on that route. */
|
|
561
562
|
terminal?: StudyExecutionTerminal;
|
|
562
|
-
/** `terminal-product` route: runtime key placement, defaulting to openai-
|
|
563
|
-
*
|
|
563
|
+
/** `terminal-product` route: runtime key placement, defaulting to openai-egress, an external
|
|
564
|
+
* header transform. openai-env passes the key to the agent command. Dry-runs record declarations
|
|
565
|
+
* only. Inert on other routes. */
|
|
564
566
|
runtimeAuth?: StudyRuntimeAuth;
|
|
565
567
|
/** Terminal Codex package pin. Omit to resolve latest once, observe it, then execute that version. */
|
|
566
568
|
runtime?: {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StudyConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The first URL in `config` that carries a credential, with why: the subject's app, serve and
|
|
4
|
+
* public surface URLs, and each participant's target and entry.
|
|
5
|
+
*/
|
|
6
|
+
export declare function studyUrlCredentialReason(config: StudyConfig): string | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Every URL a study config declares, checked for a credential. Route planners call it, since a
|
|
2
|
+
// library caller's config skips the parser that refuses these first.
|
|
3
|
+
import { rosterOf } from "./parse/actors.js";
|
|
4
|
+
import { entryCredentialReason, urlCredentialReason } from "./parse/url-credentials.js";
|
|
5
|
+
/**
|
|
6
|
+
* The first URL in `config` that carries a credential, with why: the subject's app, serve and
|
|
7
|
+
* public surface URLs, and each participant's target and entry.
|
|
8
|
+
*/
|
|
9
|
+
export function studyUrlCredentialReason(config) {
|
|
10
|
+
const { subject } = config;
|
|
11
|
+
const reasons = [
|
|
12
|
+
() => subject.appUrl === undefined
|
|
13
|
+
? undefined
|
|
14
|
+
: urlCredentialReason("subject.appUrl", subject.appUrl),
|
|
15
|
+
() => subject.serve === undefined
|
|
16
|
+
? undefined
|
|
17
|
+
: urlCredentialReason("subject.serve.url", subject.serve.url),
|
|
18
|
+
...(subject.product?.publicSurfaces ?? []).map((surface) => () => urlCredentialReason("subject.product.publicSurfaces", surface)),
|
|
19
|
+
...config.actors.flatMap((actor) => (rosterOf(actor) ?? []).flatMap((participant, index) => [
|
|
20
|
+
() => participant.target === undefined
|
|
21
|
+
? undefined
|
|
22
|
+
: urlCredentialReason(`participants[${index}].target`, participant.target),
|
|
23
|
+
() => participant.entry === undefined
|
|
24
|
+
? undefined
|
|
25
|
+
: entryCredentialReason(`participants[${index}].entry`, participant.entry),
|
|
26
|
+
])),
|
|
27
|
+
];
|
|
28
|
+
for (const reason of reasons) {
|
|
29
|
+
const found = reason();
|
|
30
|
+
if (found !== undefined)
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// provider error, capture of the exact id and kill authority, and a provider-qualified receipt in
|
|
5
5
|
// the run directory before the caller gets the handle. The desktop startup guard
|
|
6
6
|
// (guardDesktopSandboxCreate) is installed by loadE2BDesktopModule, so a create whose desktop
|
|
7
|
-
// startup fails has already reclaimed its handle when the error reaches the retry here
|
|
8
|
-
// reports the id to the registry
|
|
7
|
+
// startup fails has already reclaimed its handle when the error reaches the retry here. The guard
|
|
8
|
+
// reports the id before desktop startup begins, and the id goes to the registry and the receipt
|
|
9
|
+
// journal at that point, so a startup error that quotes it is scrubbed from every run file.
|
|
9
10
|
import { redactText, toErrorMessage } from "../../evidence/redaction.js";
|
|
10
11
|
import { beginSandboxCreate, recordSandboxOwnerOnce, SandboxCreateRefusedError, sandboxOwnerTags, } from "../../run/sandbox-creates.js";
|
|
11
12
|
import { appendSandboxOwner, appendSandboxReceipt } from "../../run/sandbox-receipts.js";
|
|
@@ -24,6 +25,9 @@ async function acquire(request, template) {
|
|
|
24
25
|
const target = request.receipt;
|
|
25
26
|
// Refused before any provider call once the run is stopping.
|
|
26
27
|
const ticket = target === null ? undefined : beginSandboxCreate(target.root, target.participantId);
|
|
28
|
+
// The receipts written when the guard reported an id, before desktop startup. A startup error
|
|
29
|
+
// can quote the id and the create may never return, so the journal holds it first.
|
|
30
|
+
const reported = new Map();
|
|
27
31
|
try {
|
|
28
32
|
// The owner tags let reclaim find this sandbox on E2B even if its id never reaches this
|
|
29
33
|
// process: a create that throws after E2B allocated, or a process that dies mid-create. The
|
|
@@ -34,8 +38,26 @@ async function acquire(request, template) {
|
|
|
34
38
|
const options = owner === undefined
|
|
35
39
|
? request.options
|
|
36
40
|
: { ...request.options, metadata: { ...request.options.metadata, ...owner } };
|
|
41
|
+
const receipt = (sandboxId) => {
|
|
42
|
+
if (target === null)
|
|
43
|
+
return Promise.resolve(false);
|
|
44
|
+
const { root, participantId, now = Date.now } = target;
|
|
45
|
+
return appendSandboxReceipt(root, {
|
|
46
|
+
at: new Date(now()).toISOString(),
|
|
47
|
+
laneId: participantId,
|
|
48
|
+
provider: "e2b",
|
|
49
|
+
sandboxId,
|
|
50
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
51
|
+
});
|
|
52
|
+
};
|
|
37
53
|
if (ticket !== undefined)
|
|
38
|
-
observeDesktopAllocation(options, (sandboxId) =>
|
|
54
|
+
observeDesktopAllocation(options, (sandboxId) => {
|
|
55
|
+
ticket.created(sandboxId);
|
|
56
|
+
// Started synchronously, so the run's writers know the id at once. A clock that throws
|
|
57
|
+
// fails only this write; the receipt after create throws it again.
|
|
58
|
+
if (!reported.has(sandboxId))
|
|
59
|
+
reported.set(sandboxId, new Promise((resolve) => resolve(receipt(sandboxId))).catch(() => false));
|
|
60
|
+
});
|
|
39
61
|
const sandbox = await withOneRetryOnTransientE2BError(() => {
|
|
40
62
|
if (ticket?.stopping())
|
|
41
63
|
throw new SandboxCreateRefusedError();
|
|
@@ -47,18 +69,11 @@ async function acquire(request, template) {
|
|
|
47
69
|
ticket?.created(sandbox.sandboxId);
|
|
48
70
|
const allocation = ownE2BSandbox(module, sandbox.sandboxId, ticket);
|
|
49
71
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await
|
|
55
|
-
at: new Date(now()).toISOString(),
|
|
56
|
-
laneId: participantId,
|
|
57
|
-
provider: "e2b",
|
|
58
|
-
sandboxId: allocation.resourceId,
|
|
59
|
-
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
72
|
+
// Best effort by contract: a failed write leaves the owner tags and the TTL as the
|
|
73
|
+
// backstops and never fails the participant. One line per sandbox: a receipt the guard's
|
|
74
|
+
// report wrote is not written again, so reclaim counts the sandbox once.
|
|
75
|
+
if (!(await reported.get(allocation.resourceId)))
|
|
76
|
+
await receipt(allocation.resourceId);
|
|
62
77
|
}
|
|
63
78
|
catch (error) {
|
|
64
79
|
// The caller never receives this sandbox, so release it here. A throwing or invalid injected
|
|
@@ -69,6 +84,9 @@ async function acquire(request, template) {
|
|
|
69
84
|
return { sandbox, allocation };
|
|
70
85
|
}
|
|
71
86
|
finally {
|
|
87
|
+
// A create that threw settles once its reported receipts are on disk, as one that returned
|
|
88
|
+
// does. These writes never reject.
|
|
89
|
+
await Promise.all(reported.values());
|
|
72
90
|
ticket?.settled();
|
|
73
91
|
}
|
|
74
92
|
}
|
|
@@ -9,6 +9,7 @@ import { ownDesktopAllocation, } from "../desktop-session.js";
|
|
|
9
9
|
import { runtimeDocker, runtimeExec, usesLima } from "./runtime-host.js";
|
|
10
10
|
import { openLimaTunnel } from "./runtime-ssh.js";
|
|
11
11
|
import { guestMediaConfigSchema } from "../../guest/media-config.js";
|
|
12
|
+
import { cli } from "../../cli/invocation.js";
|
|
12
13
|
const docker = async (args) => (await runtimeDocker(args, {}, 60_000)).stdout.trim();
|
|
13
14
|
const readLogs = async (id) => {
|
|
14
15
|
const result = await runtimeDocker(["logs", "--tail", "100", id], {}, 10_000);
|
|
@@ -18,12 +19,12 @@ function checkDesktopOptions(options) {
|
|
|
18
19
|
if (options.media !== undefined) {
|
|
19
20
|
guestMediaConfigSchema.parse(options.media);
|
|
20
21
|
if (options.assets.media !== true)
|
|
21
|
-
throw new Error(
|
|
22
|
+
throw new Error(`This local runtime does not include media. Run ${cli("runtime setup --media")}.`);
|
|
22
23
|
}
|
|
23
24
|
if (options.recording !== undefined) {
|
|
24
25
|
desktopRecordingConfigSchema.parse(options.recording);
|
|
25
26
|
if (options.assets.media !== true)
|
|
26
|
-
throw new Error(
|
|
27
|
+
throw new Error(`This runtime does not include the recorder. Run ${cli("runtime setup --media")}.`);
|
|
27
28
|
}
|
|
28
29
|
let url;
|
|
29
30
|
try {
|
|
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { cli } from "../../cli/invocation.js";
|
|
6
7
|
const exec = promisify(execFile);
|
|
7
8
|
export const LIMA_INSTANCE = "humanish-runtime";
|
|
8
9
|
export const usesLima = (options = {}) => (options.platform ?? process.platform) === "darwin";
|
|
@@ -86,11 +87,11 @@ export async function limaStatus(options = {}) {
|
|
|
86
87
|
version = (await hostExec("limactl", ["--version"], options)).stdout;
|
|
87
88
|
}
|
|
88
89
|
catch {
|
|
89
|
-
throw new Error(
|
|
90
|
+
throw new Error(`Install Lima 2.2 or newer (brew install lima), then run ${cli("runtime setup")}. Docker Desktop is not required.`);
|
|
90
91
|
}
|
|
91
92
|
const match = /\b(\d+)\.(\d+)\.\d+/.exec(version);
|
|
92
93
|
if (!match || Number(match[1]) < 2 || (Number(match[1]) === 2 && Number(match[2]) < 2))
|
|
93
|
-
throw new Error(
|
|
94
|
+
throw new Error(`Update Lima to 2.2 or newer (brew upgrade lima), then run ${cli("runtime setup")}.`);
|
|
94
95
|
const output = (await hostExec("limactl", ["list", "--json"], options)).stdout.trim();
|
|
95
96
|
const instance = output
|
|
96
97
|
.split("\n")
|
|
@@ -104,8 +105,8 @@ export async function limaStatus(options = {}) {
|
|
|
104
105
|
ready: instance?.status === "Running",
|
|
105
106
|
exists: !!instance,
|
|
106
107
|
message: instance
|
|
107
|
-
?
|
|
108
|
-
:
|
|
108
|
+
? `The humanish Lima host is stopped. Run ${cli("runtime setup")} to start it.`
|
|
109
|
+
: `Run ${cli("runtime setup")} to create the humanish Lima host and install the browser runtime.`,
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
export async function prepareLima(options = {}, progress) {
|
|
@@ -7,6 +7,7 @@ import { Readable, Transform } from "node:stream";
|
|
|
7
7
|
import { pipeline } from "node:stream/promises";
|
|
8
8
|
import { LOCAL_MEDIA_RUNTIME_RELEASES, LOCAL_RUNTIME_RELEASES } from "./runtime-release.js";
|
|
9
9
|
import { limaStatus, loadRuntimeArchive, prepareLima, runtimeArchitecture, runtimeDocker, runtimeExec, usesLima, } from "./runtime-host.js";
|
|
10
|
+
import { cli } from "../../cli/invocation.js";
|
|
10
11
|
const docker = async (args, options) => (await runtimeDocker(args, options)).stdout.trim();
|
|
11
12
|
const runtimeRelease = (options) => options.release ??
|
|
12
13
|
(options.media ? LOCAL_MEDIA_RUNTIME_RELEASES : LOCAL_RUNTIME_RELEASES)[runtimeArchitecture(options) ?? "amd64"];
|
|
@@ -80,7 +81,7 @@ export async function localRuntimeStatus(options = {}) {
|
|
|
80
81
|
installed: false,
|
|
81
82
|
message: lima
|
|
82
83
|
? "Docker is unavailable inside the humanish Lima host. Check limactl shell humanish-runtime -- sudo systemctl status docker; Docker Desktop is not used."
|
|
83
|
-
:
|
|
84
|
+
: `Docker is unavailable. Install/start Docker Engine and give your account access, then run ${cli("runtime setup")}.`,
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
const release = runtimeRelease(options);
|
|
@@ -108,7 +109,7 @@ export async function localRuntimeStatus(options = {}) {
|
|
|
108
109
|
: {
|
|
109
110
|
ok: true,
|
|
110
111
|
installed: false,
|
|
111
|
-
message:
|
|
112
|
+
message: `Local runtime will download before the first live run. Run ${cli("runtime setup")} to prepare it now.`,
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
115
|
const image = images[0];
|
|
@@ -161,7 +162,7 @@ export async function prepareLocalRuntime(options = {}) {
|
|
|
161
162
|
try {
|
|
162
163
|
const response = await fetch(release.url, { signal });
|
|
163
164
|
if (!response.ok || !response.body)
|
|
164
|
-
throw new Error(`Runtime download failed (HTTP ${response.status}). Run
|
|
165
|
+
throw new Error(`Runtime download failed (HTTP ${response.status}). Run ${cli("runtime setup")} to retry.`);
|
|
165
166
|
let bytes = 0;
|
|
166
167
|
const hash = createHash("sha256");
|
|
167
168
|
await pipeline(Readable.fromWeb(response.body), new Transform({
|
|
@@ -181,7 +182,7 @@ export async function prepareLocalRuntime(options = {}) {
|
|
|
181
182
|
await loadRuntimeArchive(archive, { ...options, signal });
|
|
182
183
|
const after = await localRuntimeStatus(options);
|
|
183
184
|
if (!after.assets)
|
|
184
|
-
throw new Error(
|
|
185
|
+
throw new Error(`Downloaded runtime did not provide the expected image. Run ${cli("runtime setup")} to retry.`);
|
|
185
186
|
return after.assets;
|
|
186
187
|
}
|
|
187
188
|
finally {
|
package/dist/tui/contract.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// It also makes the UI testable without a terminal: a test hands `startTui` a fake index and reads
|
|
14
14
|
// the frames back.
|
|
15
|
+
import { cli } from "../cli/invocation.js";
|
|
15
16
|
/** Node version the Ink runtime requires (ink@7 declares `engines.node >= 22`). */
|
|
16
17
|
export const TUI_MIN_NODE_MAJOR = 22;
|
|
17
18
|
/**
|
|
@@ -41,12 +42,12 @@ export const TUI_BUNDLE_URL = new URL("../tui-app.js", import.meta.url);
|
|
|
41
42
|
*/
|
|
42
43
|
export function terminalSurfaceMessage(state) {
|
|
43
44
|
if (!state.supported) {
|
|
44
|
-
return
|
|
45
|
+
return `\`${cli("tui")}\` needs Node ${TUI_MIN_NODE_MAJOR}+ (this is ${state.nodeVersion}); every other command works here`;
|
|
45
46
|
}
|
|
46
47
|
if (!state.bundlePresent) {
|
|
47
|
-
return "
|
|
48
|
+
return `\`${cli("tui")}\` is not built in this checkout; run \`pnpm build\` (installed packages ship it prebuilt)`;
|
|
48
49
|
}
|
|
49
50
|
return state.interactive
|
|
50
|
-
? "
|
|
51
|
-
: "
|
|
51
|
+
? `\`${cli("tui")}\` opens the interactive surface for browsing studies and runs`
|
|
52
|
+
: `\`${cli("tui")}\` is the interactive screen a person uses to watch and start runs here. This shell is not a terminal, so pass it on to a person at one`;
|
|
52
53
|
}
|
package/dist/verify/liveness.js
CHANGED
|
@@ -11,6 +11,7 @@ import { runLiveness } from "../run/run-index.js";
|
|
|
11
11
|
import { parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT } from "../run/sandbox-receipts.js";
|
|
12
12
|
import { RUN_STATUS_FILE } from "../run/status.js";
|
|
13
13
|
import { plural } from "../run/text.js";
|
|
14
|
+
import { cli } from "../cli/invocation.js";
|
|
14
15
|
/** The stable code every RUN_NOT_FINISHED warning starts with. */
|
|
15
16
|
const RUN_NOT_FINISHED = "RUN_NOT_FINISHED";
|
|
16
17
|
const GONE_STATES = new Set(["killed", "already-gone"]);
|
|
@@ -55,7 +56,7 @@ async function reclaimState(runPaths, runId) {
|
|
|
55
56
|
// Matched by digest: a reclaim receipt names each sandbox by digest, and one written before it
|
|
56
57
|
// did holds the raw id, which is digested here.
|
|
57
58
|
const journaled = new Set((journal === null ? [] : parseSandboxReceipts(journal)).map((receipt) => sandboxIdDigest(receipt.sandboxId)));
|
|
58
|
-
const command =
|
|
59
|
+
const command = `\`${cli(`reclaim --run ${runId}`)}\``;
|
|
59
60
|
const receipt = reclaimReceipt(await readRunJsonIfExists(runPaths, RECLAIM_RECEIPT_ARTIFACT), runId);
|
|
60
61
|
if (receipt === undefined)
|
|
61
62
|
return {
|
package/dist/verify/verify.js
CHANGED
|
@@ -19,6 +19,7 @@ import { rerunLineageFindings } from "./rerun.js";
|
|
|
19
19
|
import { sharedWorldEvidenceFindings } from "./shared-world.js";
|
|
20
20
|
import { subjectStateFindings, undeclaredSubjectStateWarnings } from "./subject.js";
|
|
21
21
|
import { plural } from "../run/text.js";
|
|
22
|
+
import { cli } from "../cli/invocation.js";
|
|
22
23
|
export const VERIFY_SCHEMA = "humanish.verify-result.v1";
|
|
23
24
|
// The UNSCANNED_ARTIFACT reason names this many paths and counts the rest.
|
|
24
25
|
const MAX_LISTED_UNSCANNED = 10;
|
|
@@ -387,7 +388,7 @@ function buildShareSafety(args) {
|
|
|
387
388
|
];
|
|
388
389
|
reasons.push({
|
|
389
390
|
code: "RAW_SANDBOX_ID",
|
|
390
|
-
message: `Raw sandbox ids appear in ${files.sort().join(", ")}. Runs from 0.110 keep them only in sandbox-receipts.ndjson;
|
|
391
|
+
message: `Raw sandbox ids appear in ${files.sort().join(", ")}. Runs from 0.110 keep them only in sandbox-receipts.ndjson; \`${cli("export --format bundle --redact-screenshots")}\` writes a copy without them.`,
|
|
391
392
|
});
|
|
392
393
|
}
|
|
393
394
|
const rawStreamIds = rawScreenshotStreamIds(args.bundle, args.redactedShapeFrames);
|
|
@@ -17,7 +17,7 @@ capture-vs-publish rule in
|
|
|
17
17
|
|
|
18
18
|
`codex-exec` is a real dispatch key for terminal-product studies, but the exported
|
|
19
19
|
descriptor `runSession` is a fail-closed compatibility entry. Live execution is
|
|
20
|
-
owned by `
|
|
20
|
+
owned by `runLiveTerminalSession` (`src/routes/terminal/session.ts`), which coordinates sandbox creation,
|
|
21
21
|
command-scoped runtime auth, evidence, caps, and by-id cleanup.
|
|
22
22
|
|
|
23
23
|
The `pi-agent-core` and `claude-agent-sdk` descriptors, the `app` run kind and the
|
|
@@ -64,7 +64,7 @@ public `runComputerUseLoop` refuses them. The actor passes them through
|
|
|
64
64
|
`runComputerUseLoopWithTaps`. A library caller wraps the executor's `observe` (for `url` and
|
|
65
65
|
`screenshot`) or the provider's `nextTurn` (for `reasoning` and `message`).
|
|
66
66
|
|
|
67
|
-
Flow, a host-first barrier inside
|
|
67
|
+
Flow, a host-first barrier inside the shared-world route's fan-out:
|
|
68
68
|
|
|
69
69
|
1. **Designated host.** Exactly one roster entry carries `host: true` (validated). Its mission = create
|
|
70
70
|
the shared lobby; its browser opens `subject.appUrl`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Terminal-product route
|
|
2
2
|
|
|
3
3
|
The live terminal-product route has shipped since `0.8.0`, with the in-sandbox runtime,
|
|
4
|
-
|
|
4
|
+
explicit credential placement (an E2B egress proxy by default, or command-scoped), exact-id cleanup proof, an interventions ledger, a cost and
|
|
5
5
|
no-spend ledger, caps, and product scoring and feedback hooks. The
|
|
6
6
|
[original plan](https://github.com/danielgwilson/humanish/blob/main/docs/history/goals/terminal-product-lane/goal.md)
|
|
7
7
|
holds the build order and the safety contract.
|
|
@@ -24,19 +24,19 @@ fail-closed cross-validation, and forward-declared warnings.
|
|
|
24
24
|
|
|
25
25
|
## The composition
|
|
26
26
|
|
|
27
|
-
| Axis | Value
|
|
28
|
-
| --------------------------------- |
|
|
29
|
-
| `subject.source` | `terminal-product`
|
|
30
|
-
| `subject.product` | `{ name, publicSurfaces[], install?, workdir?, upload? }`: the only world the agent sees
|
|
31
|
-
| `execution.target` | `e2b-terminal` (or absent → implied)
|
|
32
|
-
| `execution.terminal` | `{ transport: exec-stream, stdin: disabled }`
|
|
33
|
-
| `execution.runtimeAuth` | `openai-
|
|
34
|
-
| `execution.runtime.version` | Optional exact `@openai/codex` version; observed before keyed execution
|
|
35
|
-
| `actor.model` / `reasoningEffort` | Passed to Codex as `--model` (default `gpt-5.6-sol`) and `-c model_reasoning_effort`; declarations, not observed provider identity
|
|
36
|
-
| `caps` | `{ maxUsd, maxJobs, maxMinutes }`: the blast-radius budget; `maxUsd > 0` is refused: no adopter cost source exists until issue 347 lands
|
|
37
|
-
| `policies` | `allowPrivateRepoAccess` / `allowProviderCredentials` / `allowPaymentCredentials` / `allowGitHubMutation`, all default false
|
|
38
|
-
| `actor.type` | `codex-exec`: a registered terminal actor (`keyPlacement: in-sandbox-command-scoped`)
|
|
39
|
-
| route | `terminal` → `
|
|
27
|
+
| Axis | Value |
|
|
28
|
+
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `subject.source` | `terminal-product` |
|
|
30
|
+
| `subject.product` | `{ name, publicSurfaces[], install?, workdir?, upload? }`: the only world the agent sees |
|
|
31
|
+
| `execution.target` | `e2b-terminal` (or absent → implied) |
|
|
32
|
+
| `execution.terminal` | `{ transport: exec-stream, stdin: disabled }` |
|
|
33
|
+
| `execution.runtimeAuth` | `openai-egress` (default) or opt-in `openai-env`; names-only durable evidence |
|
|
34
|
+
| `execution.runtime.version` | Optional exact `@openai/codex` version; observed before keyed execution |
|
|
35
|
+
| `actor.model` / `reasoningEffort` | Passed to Codex as `--model` (default `gpt-5.6-sol`) and `-c model_reasoning_effort`; declarations, not observed provider identity |
|
|
36
|
+
| `caps` | `{ maxUsd, maxJobs, maxMinutes }`: the blast-radius budget; `maxUsd > 0` is refused: no adopter cost source exists until issue 347 lands |
|
|
37
|
+
| `policies` | `allowPrivateRepoAccess` / `allowProviderCredentials` / `allowPaymentCredentials` / `allowGitHubMutation`, all default false |
|
|
38
|
+
| `actor.type` | `codex-exec`: a registered terminal actor (`keyPlacement: in-sandbox-command-scoped`) |
|
|
39
|
+
| route | `terminal` → `admitTerminalPlan` ([`src/routes/terminal/route.ts`](https://github.com/danielgwilson/humanish/blob/main/src/routes/terminal/route.ts)) |
|
|
40
40
|
|
|
41
41
|
Routing is `routeOf` (`src/study/plan.ts`). It sends every `terminal-product` subject to
|
|
42
42
|
this route, even with an unregistered actor, so this route refuses the actor.
|
|
@@ -86,11 +86,8 @@ product's no-spend boundary still needs separate interpretation.
|
|
|
86
86
|
|
|
87
87
|
## Runtime auth: raw-key placement and remaining provider access
|
|
88
88
|
|
|
89
|
-
`execution.runtimeAuth: openai-
|
|
90
|
-
`
|
|
91
|
-
that was the host source. Child processes can read and use the raw key.
|
|
92
|
-
|
|
93
|
-
Opt in to keeping the raw key outside the sandbox:
|
|
89
|
+
`execution.runtimeAuth: openai-egress` is the default: a study that declares no
|
|
90
|
+
`runtimeAuth` keeps the raw key outside the sandbox, as if it declared:
|
|
94
91
|
|
|
95
92
|
```yaml
|
|
96
93
|
execution:
|
|
@@ -115,7 +112,7 @@ output and errors, including errors during sandbox creation.
|
|
|
115
112
|
This mode supports the default OpenAI endpoint only. humanish explicitly sets Codex's
|
|
116
113
|
built-in `openai` provider and `openai_base_url` to `https://api.openai.com/v1` for
|
|
117
114
|
that invocation. It does not support a custom provider, proxy base URL, or regional
|
|
118
|
-
endpoint under this mode
|
|
115
|
+
endpoint under this mode; a study that needs one declares `openai-env`. E2B
|
|
119
116
|
header rules are a public-beta capability;
|
|
120
117
|
`tests/routes/terminal/runtime-auth.test.ts` checks the local contract against the
|
|
121
118
|
installed Desktop SDK (the lockfile has `@e2b/desktop` 2.4.0, resolving `e2b` 2.49.0).
|
|
@@ -136,6 +133,11 @@ requests can fail. E2B domain allowlists are routing controls rather than strict
|
|
|
136
133
|
destination isolation on shared infrastructure. An existing exact OpenAI host
|
|
137
134
|
rule is rejected instead of silently overwritten.
|
|
138
135
|
|
|
136
|
+
`execution.runtimeAuth: openai-env` is the opt-in alternative. It supplies
|
|
137
|
+
`CODEX_API_KEY` command-scoped to Codex, with `OPENAI_API_KEY` also supplied when
|
|
138
|
+
that was the host source. Child processes can read and use the raw key, and so can
|
|
139
|
+
anything the agent runs or a page it reads asks it to run.
|
|
140
|
+
|
|
139
141
|
Evidence records the selected auth mode and the residual proxy capability.
|
|
140
142
|
Resolved live actor traces use `keyPlacement: external` in `openai-egress`; the
|
|
141
143
|
actor registry continues to describe the default `in-sandbox-command-scoped`
|
|
@@ -197,10 +199,10 @@ deadline.
|
|
|
197
199
|
|
|
198
200
|
## The original command-scoped safety contract
|
|
199
201
|
|
|
200
|
-
The
|
|
201
|
-
On the computer-use route the model's key stays _outside_ the sandbox;
|
|
202
|
-
agent-under-test runs _inside_ with a real `OPENAI_API_KEY`/`CODEX_API_KEY` and
|
|
203
|
-
|
|
202
|
+
The `openai-env` mode **inverts** the credential-placement default of every other E2B route.
|
|
203
|
+
On the computer-use route the model's key stays _outside_ the sandbox; under `openai-env` the
|
|
204
|
+
agent-under-test runs _inside_ with a real `OPENAI_API_KEY`/`CODEX_API_KEY` and is **presumed
|
|
205
|
+
exfiltratable**. The placement rule in invariants-and-defaults.md
|
|
204
206
|
applies: _keys live where the keyed process runs, and nowhere else;
|
|
205
207
|
blast radius is bounded by key scoping and budgets, not by hoping._
|
|
206
208
|
|
|
@@ -585,7 +585,9 @@ shareSafety:
|
|
|
585
585
|
bundle is safe to promote into a public issue. Public promotion should branch on
|
|
586
586
|
`shareSafety.status`:
|
|
587
587
|
|
|
588
|
-
- `share_ready`: feedback draft commands may render public issue payloads
|
|
588
|
+
- `share_ready`: feedback draft commands may render public issue payloads, and `observe --all --safe`
|
|
589
|
+
serves the run. The folder still holds `sandbox-receipts.ndjson` (raw sandbox ids) and `status.json`
|
|
590
|
+
(the recording pid); no Observer server serves them and bundle export omits them;
|
|
589
591
|
- `local_only`: keep the run local; only supported redaction-only cases can produce a shareable derivative;
|
|
590
592
|
`RAW_SANDBOX_ID` is one: a file other than `sandbox-receipts.ndjson` names one of the run's raw
|
|
591
593
|
sandbox ids, which verify reads from the receipts and from any raw `providerResources[].id` in
|
|
@@ -658,6 +660,16 @@ trace references outside `screenshots/`. An unregistered
|
|
|
658
660
|
`.mp4`, and a file or directory whose name contains `\`, are public-safety
|
|
659
661
|
findings and block the run.
|
|
660
662
|
|
|
663
|
+
The patterns live in `src/evidence/redaction.ts`: provider key formats (OpenAI,
|
|
664
|
+
Anthropic, E2B, GitHub, GitLab, npm, Vercel, AWS, Google, Stripe, Slack,
|
|
665
|
+
SendGrid, Hugging Face), JWTs, bearer and basic `Authorization` values, private
|
|
666
|
+
key headers, database URLs with a password, the password in any URL's user info,
|
|
667
|
+
credential query and fragment parameters with a value of 16 characters or more,
|
|
668
|
+
credential-named upper-case variables set to a value of 16 characters or more
|
|
669
|
+
with a digit and a letter, an AWS secret access key next to its name, E2B URLs, and Linux,
|
|
670
|
+
macOS and Windows local paths. `tests/verify/secret-formats.test.ts` lists the
|
|
671
|
+
formats and the ordinary values that must not match.
|
|
672
|
+
|
|
661
673
|
Before matching, the scan undoes JSON and JS escapes, percent-encoding and HTML
|
|
662
674
|
character references, the decoding bundle export applies (`decodeEscapes`), and
|
|
663
675
|
then JSON whitespace escapes and quoted-printable. It reads inside base64 runs of
|
|
@@ -673,9 +685,13 @@ wrapped across lines, and inside hex runs of 32 characters or more:
|
|
|
673
685
|
|
|
674
686
|
`observer/index.html` is exempt from that base64 rule: serve renders it from
|
|
675
687
|
`run.json`, export regenerates it, and it embeds the Observer's own base64 fonts.
|
|
676
|
-
Its text is still scanned.
|
|
677
|
-
|
|
678
|
-
|
|
688
|
+
Its text is still scanned. A base64 run of up to 4096 characters is also read one
|
|
689
|
+
slash-separated piece at a time, and from just after each slash in its first 64
|
|
690
|
+
characters, so a URL path before an encoded key cannot shift its decoding. Known
|
|
691
|
+
limits: base64 split across separate strings, nesting deeper than three levels,
|
|
692
|
+
base64 that holds a slash itself and starts more than 64 characters into a longer
|
|
693
|
+
run, and encodings a pattern scan cannot undo (such as encryption) still grade
|
|
694
|
+
`share_ready`. `tests/verify/encoding-coverage.test.ts`
|
|
679
695
|
records each case.
|
|
680
696
|
|
|
681
697
|
Real email receiving adds `publication.restrictions: [real-communications]` and
|