humanish 0.109.0 → 0.110.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 +20 -0
- package/README.md +1 -1
- package/dist/analysis/automatic-config.d.ts +1 -1
- package/dist/cli/commands/comms.js +25 -7
- package/dist/cli/commands/migrate.js +15 -4
- package/dist/cli/commands/observe.js +4 -2
- package/dist/cli/commands/run-command.js +4 -3
- package/dist/cli/commands/runs.js +13 -3
- package/dist/cli/commands/setup.js +19 -5
- package/dist/cli/commands/study-format.js +29 -2
- package/dist/cli/commands/study.js +4 -3
- package/dist/cli/commands/tui.js +4 -3
- package/dist/cli/commands/watch.js +2 -2
- package/dist/cli/doctor.d.ts +3 -1
- package/dist/cli/doctor.js +5 -4
- package/dist/cli/io.d.ts +17 -3
- package/dist/cli/io.js +20 -3
- package/dist/comms/receiving-runtime.js +1 -1
- package/dist/comms/setup.js +1 -1
- package/dist/evidence/redaction.d.ts +15 -0
- package/dist/evidence/redaction.js +87 -0
- package/dist/feedback/export-bundle.js +11 -5
- package/dist/feedback/export.js +3 -0
- package/dist/keys/key-resolution.d.ts +7 -0
- package/dist/keys/key-resolution.js +10 -8
- package/dist/routes/computer-use/in-process-desktop.js +1 -1
- package/dist/routes/computer-use/live-phase.d.ts +1 -0
- package/dist/routes/computer-use/plan.js +3 -3
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/setup.js +1 -1
- package/dist/routes/computer-use/types.d.ts +6 -0
- package/dist/routes/scripted/types.d.ts +1 -0
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/credentials.js +1 -1
- package/dist/routes/terminal/live-finish.js +8 -4
- package/dist/routes/terminal/types.d.ts +3 -0
- package/dist/run/bundle-shape.js +1 -0
- package/dist/run/bundle.d.ts +6 -0
- package/dist/run/reclaim.d.ts +3 -0
- package/dist/run/reclaim.js +10 -3
- package/dist/run/results.d.ts +3 -0
- package/dist/run/run.js +14 -8
- package/dist/run/sandbox-ids.d.ts +53 -0
- package/dist/run/sandbox-ids.js +198 -0
- package/dist/run/sandbox-receipts.d.ts +2 -0
- package/dist/run/sandbox-receipts.js +26 -0
- package/dist/run/status.js +5 -2
- package/dist/run/stored-runs.js +10 -1
- package/dist/run-study.js +6 -1
- package/dist/study/doctor.js +2 -2
- package/dist/study/init-templates.js +2 -2
- package/dist/study/migrate.d.ts +7 -2
- package/dist/study/migrate.js +60 -4
- package/dist/study/run-study-events.js +1 -1
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/types.d.ts +1 -1
- package/dist/verify/artifacts.d.ts +4 -0
- package/dist/verify/artifacts.js +8 -0
- package/dist/verify/liveness.js +8 -4
- package/dist/verify/verify.d.ts +1 -1
- package/dist/verify/verify.js +21 -1
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/contracts/run-bundle.md +20 -1
- package/package.json +4 -2
- package/skills/humanish/SKILL.md +2 -2
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
export declare function sensitivePatterns(): readonly RegExp[];
|
|
3
3
|
/** True if the text contains any secret-shaped token or known local path. */
|
|
4
4
|
export declare function containsSensitive(text: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* What a shared copy of a run says in place of a provider sandbox id. scripts/public-surface-scan.mjs
|
|
7
|
+
* passes only this value at a sandbox-id key.
|
|
8
|
+
*/
|
|
9
|
+
export declare const REDACTED_SANDBOX_ID = "[redacted-sandbox-id]";
|
|
10
|
+
/** A sandbox id's public stand-in: its digest matches a receipt without naming the sandbox. */
|
|
11
|
+
export declare function sandboxIdDigest(id: string): string;
|
|
12
|
+
/** The raw ids at the keys redactSandboxIds replaces, anywhere in `value`. */
|
|
13
|
+
export declare function collectSandboxIds(value: unknown, into?: Set<string>): Set<string>;
|
|
14
|
+
/**
|
|
15
|
+
* `value` with every sandbox id replaced by REDACTED_SANDBOX_ID plus its digest: strings at
|
|
16
|
+
* `sandboxId` and `subjectSandboxId`, and the `id` of each `providerResources` or `resources` entry. Raw ids live
|
|
17
|
+
* only in a run's sandbox-receipts.ndjson; every record and output names a sandbox by digest.
|
|
18
|
+
*/
|
|
19
|
+
export declare function redactSandboxIds(value: unknown): unknown;
|
|
5
20
|
/** Redact secrets to [REDACTED_SECRET] and local paths to their path labels. */
|
|
6
21
|
export declare function redactText(text: string): string;
|
|
7
22
|
/** Redact every sensitive match (secrets and paths) to a single [REDACTED_SECRET] label. */
|
|
@@ -55,6 +55,93 @@ export function containsSensitive(text) {
|
|
|
55
55
|
function redactLocalPaths(text, label) {
|
|
56
56
|
return LOCAL_PATH_PATTERNS.reduce((current, [pattern, replacement]) => current.replace(pattern, (match) => (label ?? replacement) + (match.match(/\\+$/)?.[0] ?? "")), text);
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* What a shared copy of a run says in place of a provider sandbox id. scripts/public-surface-scan.mjs
|
|
60
|
+
* passes only this value at a sandbox-id key.
|
|
61
|
+
*/
|
|
62
|
+
export const REDACTED_SANDBOX_ID = "[redacted-sandbox-id]";
|
|
63
|
+
/** The keys run writers put a sandbox id under; resource entries hold one as `id`. */
|
|
64
|
+
const SANDBOX_ID_KEYS = new Set(["sandboxId", "subjectSandboxId"]);
|
|
65
|
+
/** Arrays of sandbox records: run.json's `providerResources` and cleanup.json's `resources`. */
|
|
66
|
+
const RESOURCE_ARRAY_KEYS = new Set(["providerResources", "resources"]);
|
|
67
|
+
/** A sandbox id's public stand-in: its digest matches a receipt without naming the sandbox. */
|
|
68
|
+
export function sandboxIdDigest(id) {
|
|
69
|
+
return digestText(id, 16);
|
|
70
|
+
}
|
|
71
|
+
/** The digest field written beside a redacted id: `idDigest`, `sandboxIdDigest`, … */
|
|
72
|
+
const digestKey = (key) => `${key}Digest`;
|
|
73
|
+
/**
|
|
74
|
+
* The entries of `object` with each raw id at one of `keys` replaced by REDACTED_SANDBOX_ID and its
|
|
75
|
+
* digest written beside it, unless the object already records one.
|
|
76
|
+
*/
|
|
77
|
+
function redactIdEntries(object, keys) {
|
|
78
|
+
const entries = Object.entries(object);
|
|
79
|
+
const present = new Set(entries.map(([key]) => key));
|
|
80
|
+
return entries.flatMap(([key, child]) => {
|
|
81
|
+
if (!keys.has(key) || typeof child !== "string" || child === REDACTED_SANDBOX_ID)
|
|
82
|
+
return [[key, child]];
|
|
83
|
+
// A label that a text scrub left at the key already carries the id's digest.
|
|
84
|
+
const digest = SANDBOX_ID_LABEL.exec(child)?.[1] ?? sandboxIdDigest(child);
|
|
85
|
+
return [
|
|
86
|
+
[key, REDACTED_SANDBOX_ID],
|
|
87
|
+
...(present.has(digestKey(key)) ? [] : [[digestKey(key), digest]]),
|
|
88
|
+
];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** How free text names a sandbox: the marker with the id's digest inside it. */
|
|
92
|
+
const SANDBOX_ID_LABEL = /^\[redacted-sandbox-id ([0-9a-f]{16})\]$/;
|
|
93
|
+
const RESOURCE_ID_KEYS = new Set(["id"]);
|
|
94
|
+
/** A raw id at a sandbox-id key: neither the marker nor a label that names its digest. */
|
|
95
|
+
const isRawSandboxId = (value) => typeof value === "string" && value !== REDACTED_SANDBOX_ID && !SANDBOX_ID_LABEL.test(value);
|
|
96
|
+
/** The raw ids at the keys redactSandboxIds replaces, anywhere in `value`. */
|
|
97
|
+
export function collectSandboxIds(value, into = new Set()) {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
for (const item of value)
|
|
100
|
+
collectSandboxIds(item, into);
|
|
101
|
+
return into;
|
|
102
|
+
}
|
|
103
|
+
if (value === null || typeof value !== "object")
|
|
104
|
+
return into;
|
|
105
|
+
for (const [key, child] of Object.entries(value)) {
|
|
106
|
+
if (SANDBOX_ID_KEYS.has(key) && isRawSandboxId(child))
|
|
107
|
+
into.add(child);
|
|
108
|
+
if (RESOURCE_ARRAY_KEYS.has(key) && Array.isArray(child))
|
|
109
|
+
for (const resource of child) {
|
|
110
|
+
const id = resource !== null && typeof resource === "object" ? Reflect.get(resource, "id") : null;
|
|
111
|
+
if (isRawSandboxId(id))
|
|
112
|
+
into.add(id);
|
|
113
|
+
}
|
|
114
|
+
collectSandboxIds(child, into);
|
|
115
|
+
}
|
|
116
|
+
return into;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `value` with every sandbox id replaced by REDACTED_SANDBOX_ID plus its digest: strings at
|
|
120
|
+
* `sandboxId` and `subjectSandboxId`, and the `id` of each `providerResources` or `resources` entry. Raw ids live
|
|
121
|
+
* only in a run's sandbox-receipts.ndjson; every record and output names a sandbox by digest.
|
|
122
|
+
*/
|
|
123
|
+
export function redactSandboxIds(value) {
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
const items = value.map(redactSandboxIds);
|
|
126
|
+
return items.every((item, index) => item === value[index]) ? value : items;
|
|
127
|
+
}
|
|
128
|
+
if (value === null || typeof value !== "object")
|
|
129
|
+
return value;
|
|
130
|
+
// fromEntries keeps a key such as __proto__ an own property, as JSON.parse made it.
|
|
131
|
+
const entries = redactIdEntries(value, SANDBOX_ID_KEYS).map(([key, child]) => {
|
|
132
|
+
if (RESOURCE_ARRAY_KEYS.has(key) && Array.isArray(child))
|
|
133
|
+
return [
|
|
134
|
+
key,
|
|
135
|
+
child.map((resource) => resource !== null && typeof resource === "object" && !Array.isArray(resource)
|
|
136
|
+
? Object.fromEntries(redactIdEntries(redactSandboxIds(resource), RESOURCE_ID_KEYS))
|
|
137
|
+
: redactSandboxIds(resource)),
|
|
138
|
+
];
|
|
139
|
+
return [key, redactSandboxIds(child)];
|
|
140
|
+
});
|
|
141
|
+
const changed = entries.length !== Object.keys(value).length ||
|
|
142
|
+
entries.some(([key, child]) => child !== value[key]);
|
|
143
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
144
|
+
}
|
|
58
145
|
/** Redact secrets to [REDACTED_SECRET] and local paths to their path labels. */
|
|
59
146
|
export function redactText(text) {
|
|
60
147
|
const withoutSecrets = SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, "[REDACTED_SECRET]"), text);
|
|
@@ -8,7 +8,8 @@ import { parse as parseYaml } from "yaml";
|
|
|
8
8
|
import { ACTOR_TRACE_SCHEMA } from "../actors/contract.js";
|
|
9
9
|
import { renderObserver } from "../observer/render.js";
|
|
10
10
|
import { buildObserverData } from "../observer/data.js";
|
|
11
|
-
import {
|
|
11
|
+
import { inventorySandboxIds, scrubSandboxIdBytes } from "../run/sandbox-ids.js";
|
|
12
|
+
import { containsSensitive, redactSandboxIds, redactScreenshot, redactText, } from "../evidence/redaction.js";
|
|
12
13
|
import { decodeEscapes } from "../evidence/encoded-text.js";
|
|
13
14
|
import { readPlainText } from "../evidence/plain-text.js";
|
|
14
15
|
import { streamScreenshotPaths } from "../verify/artifacts.js";
|
|
@@ -224,7 +225,8 @@ function transformText(file, imagePaths) {
|
|
|
224
225
|
const parsed = JSON.parse(text);
|
|
225
226
|
// Parsing also resolves escape sequences in string values before inspection.
|
|
226
227
|
assertNoInlineRaster(JSON.stringify(parsed));
|
|
227
|
-
|
|
228
|
+
// The copy names no sandbox: an id is local operational evidence, and the copy is shared.
|
|
229
|
+
const transformed = redactSandboxIds(updateScreenshotDeclarations(parsed, imagePaths));
|
|
228
230
|
if (JSON.stringify(transformed) === JSON.stringify(parsed))
|
|
229
231
|
return file.bytes;
|
|
230
232
|
assertRewritableNumbers(text);
|
|
@@ -237,7 +239,7 @@ function transformText(file, imagePaths) {
|
|
|
237
239
|
return line;
|
|
238
240
|
const parsed = JSON.parse(line);
|
|
239
241
|
assertNoInlineRaster(JSON.stringify(parsed));
|
|
240
|
-
const updated = JSON.stringify(updateScreenshotDeclarations(parsed, imagePaths));
|
|
242
|
+
const updated = JSON.stringify(redactSandboxIds(updateScreenshotDeclarations(parsed, imagePaths)));
|
|
241
243
|
if (updated === JSON.stringify(parsed))
|
|
242
244
|
return line;
|
|
243
245
|
assertRewritableNumbers(line);
|
|
@@ -299,6 +301,7 @@ function keptPngPaths(source, bundle) {
|
|
|
299
301
|
async function writeDerivative(source, stagePaths, bundle) {
|
|
300
302
|
const entries = [];
|
|
301
303
|
const imagePaths = keptPngPaths(source, bundle);
|
|
304
|
+
const sandboxIds = inventorySandboxIds(source.files);
|
|
302
305
|
let images = 0;
|
|
303
306
|
for (const file of source.files) {
|
|
304
307
|
const isPng = path.extname(file.path).toLowerCase() === ".png";
|
|
@@ -329,7 +332,7 @@ async function writeDerivative(source, stagePaths, bundle) {
|
|
|
329
332
|
bytes = transformText(file, imagePaths);
|
|
330
333
|
if (file.path === "run.json") {
|
|
331
334
|
assertRewritableNumbers(file.bytes.toString("utf8"));
|
|
332
|
-
const transformed = updateScreenshotDeclarations(bundle, imagePaths);
|
|
335
|
+
const transformed = redactSandboxIds(updateScreenshotDeclarations(bundle, imagePaths));
|
|
333
336
|
transformed.redaction = {
|
|
334
337
|
status: "passed",
|
|
335
338
|
notes: `${bundle.redaction.notes} Exported copy: screenshots were blurred during export; the original capture is retained separately. See derivation.json.`,
|
|
@@ -342,6 +345,7 @@ async function writeDerivative(source, stagePaths, bundle) {
|
|
|
342
345
|
}
|
|
343
346
|
bytes = jsonBytes(transformed);
|
|
344
347
|
}
|
|
348
|
+
bytes = scrubSandboxIdBytes(file.path, bytes, sandboxIds);
|
|
345
349
|
action = bytes.equals(file.bytes) ? "copied" : "updated";
|
|
346
350
|
}
|
|
347
351
|
await writeContainedOutputFile(stagePaths, file.path, bytes);
|
|
@@ -495,7 +499,9 @@ async function verifyFrozenSource(workspace, source, runId) {
|
|
|
495
499
|
verified.shareSafety.reasons.every(
|
|
496
500
|
// Both are repaired in the copy: frames are blurred, and an unread PNG is dropped or
|
|
497
501
|
// refused by keptPngPaths. Any other unread file fails as an unsupported format.
|
|
498
|
-
(reason) => reason.code === "RAW_SCREENSHOTS" ||
|
|
502
|
+
(reason) => reason.code === "RAW_SCREENSHOTS" ||
|
|
503
|
+
reason.code === "RAW_SANDBOX_ID" ||
|
|
504
|
+
reason.code === "UNSCANNED_ARTIFACT")))) {
|
|
499
505
|
return { shareSafety: verified.shareSafety };
|
|
500
506
|
}
|
|
501
507
|
const loaded = await loadRunBundlePrepared(rawSnapshot.physicalPath, snapshotPaths);
|
package/dist/feedback/export.js
CHANGED
|
@@ -384,6 +384,9 @@ function shareSafetyRemedy(codes) {
|
|
|
384
384
|
if (codes.includes("RAW_SCREENSHOTS")) {
|
|
385
385
|
steps.push("Re-run with policies.redactScreenshots: true to blur screenshots at capture.");
|
|
386
386
|
}
|
|
387
|
+
if (codes.includes("RAW_SANDBOX_ID")) {
|
|
388
|
+
steps.push("Export with --format bundle --redact-screenshots, which writes a copy without raw sandbox ids, and share from that copy.");
|
|
389
|
+
}
|
|
387
390
|
if (codes.includes("UNSCANNED_ARTIFACT")) {
|
|
388
391
|
steps.push("verify cannot read the files that `humanish verify --json` lists under UNSCANNED_ARTIFACT as text: remove them from the run folder, or keep only images an actor trace references as stream screenshots.");
|
|
389
392
|
}
|
|
@@ -22,6 +22,11 @@ export declare function discoverProviderKeys(args: {
|
|
|
22
22
|
deps?: KeyResolutionDeps;
|
|
23
23
|
announced?: ReadonlySet<string>;
|
|
24
24
|
}): Promise<ResolvedKeyFill[]>;
|
|
25
|
+
/** What `--dotenv` loaded: the path as given, and the names it set because they were unset. */
|
|
26
|
+
export interface DotenvLoad {
|
|
27
|
+
path: string;
|
|
28
|
+
names: readonly string[];
|
|
29
|
+
}
|
|
25
30
|
export interface KeySourceProbe {
|
|
26
31
|
name: string;
|
|
27
32
|
/** The source that supplies the key right now, or null when missing everywhere. */
|
|
@@ -43,6 +48,8 @@ export declare function probeKeySources(names: readonly string[], args: {
|
|
|
43
48
|
cwd: string;
|
|
44
49
|
env: NodeJS.ProcessEnv;
|
|
45
50
|
deps?: KeyResolutionDeps;
|
|
51
|
+
/** The names `--dotenv` put into `env`, so their rows name the file, not the process env. */
|
|
52
|
+
dotenv?: DotenvLoad;
|
|
46
53
|
}): Promise<KeySourceProbe[]>;
|
|
47
54
|
/** Resolve a `humanish keys set` target: a vendor alias or a raw env name. Null = invalid. */
|
|
48
55
|
export declare function resolveKeyName(vendorOrName: string): string | null;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Provider-key discovery: resolve the keys a live run needs through each vendor's
|
|
2
|
-
// native chain instead of demanding a per-repo --
|
|
2
|
+
// native chain instead of demanding a per-repo --dotenv that operators hand-copy keys into.
|
|
3
3
|
//
|
|
4
4
|
// The chain, per key, fill-only (a rung never overrides anything already present):
|
|
5
|
-
// 1. process env, including whatever --
|
|
5
|
+
// 1. process env, including whatever --dotenv just loaded (explicit always wins);
|
|
6
6
|
// 2. `.humanish/local/provider.env`, the project-local overlay this CLI's own --help
|
|
7
7
|
// examples document;
|
|
8
8
|
// 3. the owning vendor's native store, where one exists:
|
|
@@ -24,7 +24,7 @@ import { loadEnvFile } from "./env-file.js";
|
|
|
24
24
|
/** The only names implicit discovery may fill (and `humanish keys set` may store). Everything
|
|
25
25
|
* else in an overlay/store file is ignored-and-named: a repo-planted NODE_OPTIONS/LD_PRELOAD
|
|
26
26
|
* must never enter process env off a file the operator did not explicitly pass (an explicit
|
|
27
|
-
* --
|
|
27
|
+
* --dotenv remains the operator's own full-file load). */
|
|
28
28
|
const KNOWN_PROVIDER_KEYS = [
|
|
29
29
|
"OPENAI_API_KEY",
|
|
30
30
|
"ANTHROPIC_API_KEY",
|
|
@@ -213,7 +213,7 @@ export async function discoverProviderKeys(args) {
|
|
|
213
213
|
if (announced === undefined || announced.has(fill_.name))
|
|
214
214
|
announce(`humanish keys: ${fill_.name} from ${fill_.source}`);
|
|
215
215
|
for (const name of ignored)
|
|
216
|
-
announce(`humanish keys: ignored ${name}, which is not a provider key. Discovery loads provider keys only; pass the file with --
|
|
216
|
+
announce(`humanish keys: ignored ${name}, which is not a provider key. Discovery loads provider keys only; pass the file with --dotenv to load every name in it.`);
|
|
217
217
|
return fills;
|
|
218
218
|
}
|
|
219
219
|
/** True when the file's parent directory is (or traverses) a symlink at its last component:
|
|
@@ -253,7 +253,7 @@ export function describeMissingKeys(names, env) {
|
|
|
253
253
|
const hints = names.map((name) => `${name}: ${missingKeyHint(name)}`).join("; ");
|
|
254
254
|
const strict = env.HUMANISH_STRICT_KEYS?.trim() === "1";
|
|
255
255
|
const chain = strict
|
|
256
|
-
? "key discovery is disabled (HUMANISH_STRICT_KEYS=1); only process env and --
|
|
256
|
+
? "key discovery is disabled (HUMANISH_STRICT_KEYS=1); only process env and --dotenv are read"
|
|
257
257
|
: `also checked ${PROJECT_OVERLAY_RELATIVE}, ~/.e2b/config.json, gh auth token, and ${path.join("~", ".config", "humanish", "keys.env")}`;
|
|
258
258
|
return `${chain}. Fill: ${hints}.`;
|
|
259
259
|
}
|
|
@@ -278,7 +278,9 @@ export async function probeKeySources(names, args) {
|
|
|
278
278
|
args.env.GITHUB_TOKEN !== undefined &&
|
|
279
279
|
args.env.GITHUB_TOKEN.trim() !== "";
|
|
280
280
|
const source = inEnv
|
|
281
|
-
?
|
|
281
|
+
? args.dotenv?.names.includes(name)
|
|
282
|
+
? `--dotenv ${args.dotenv.path}`
|
|
283
|
+
: "process env"
|
|
282
284
|
: aliasInEnv
|
|
283
285
|
? "process env (GITHUB_TOKEN)"
|
|
284
286
|
: (bySource.get(name) ?? null);
|
|
@@ -307,7 +309,7 @@ export function setUserKey(name, value, env, deps = {}) {
|
|
|
307
309
|
throw new Error(`Not a valid env name: ${name}`);
|
|
308
310
|
}
|
|
309
311
|
if (!PROVIDER_KEY_SET.has(name)) {
|
|
310
|
-
throw new Error(`The store holds provider keys only (${[...PROVIDER_KEY_SET].join(", ")}). For anything else, use an explicit --
|
|
312
|
+
throw new Error(`The store holds provider keys only (${[...PROVIDER_KEY_SET].join(", ")}). For anything else, use an explicit --dotenv.`);
|
|
311
313
|
}
|
|
312
314
|
const storePath = userKeyStorePath(env, deps);
|
|
313
315
|
const dir = path.dirname(storePath);
|
|
@@ -325,7 +327,7 @@ export function setUserKey(name, value, env, deps = {}) {
|
|
|
325
327
|
// would silently resolve to a different secret on the next run.
|
|
326
328
|
const roundTrip = parseStoreLine(`${name}=${trimmed}`);
|
|
327
329
|
if (roundTrip === null || roundTrip[0] !== name || roundTrip[1] !== trimmed) {
|
|
328
|
-
throw new Error("The value does not round-trip the store format (avoid leading quotes and '#'); pass it via --
|
|
330
|
+
throw new Error("The value does not round-trip the store format (avoid leading quotes and '#'); pass it via --dotenv instead.");
|
|
329
331
|
}
|
|
330
332
|
writeStore(storePath, entries);
|
|
331
333
|
return { path: storePath };
|
|
@@ -20,7 +20,7 @@ export function createInProcessDesktop(deps) {
|
|
|
20
20
|
throw new Error("The participant desktop must be prepared and may only be opened once, before finalization.");
|
|
21
21
|
opened = true;
|
|
22
22
|
if (deps.inProcessExecutor === undefined)
|
|
23
|
-
throw new Error("The in-process route needs
|
|
23
|
+
throw new Error("The in-process route needs RunStudyOptions.inProcess.");
|
|
24
24
|
return { executor: await deps.inProcessExecutor(deps.appUrl) };
|
|
25
25
|
},
|
|
26
26
|
finalize() {
|
|
@@ -159,7 +159,7 @@ function driverReason(driving, { localAppSubject, inProcessRoute }) {
|
|
|
159
159
|
if (driving.inProcess && !driving.createProvider)
|
|
160
160
|
return {
|
|
161
161
|
code: "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER",
|
|
162
|
-
message: "
|
|
162
|
+
message: "RunStudyOptions.inProcess needs RunStudyOptions.createProvider: an in-process executor returns no screenshot, so it needs a provider that does not read images. The default OpenAI computer-use provider reads screenshots and would stop the session.",
|
|
163
163
|
};
|
|
164
164
|
// There is no built-in in-process driver for a local app.
|
|
165
165
|
if (localAppSubject && !inProcessRoute)
|
|
@@ -233,7 +233,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
233
233
|
const source = config.subject.source;
|
|
234
234
|
if (driving.inProcess &&
|
|
235
235
|
(source === "clone" || source === "local-tree" || source === "desktop-cli"))
|
|
236
|
-
return refuse("in-scope", "HUMANISH_COMPUTER_USE_SUBJECT_INVALID", `
|
|
236
|
+
return refuse("in-scope", "HUMANISH_COMPUTER_USE_SUBJECT_INVALID", `RunStudyOptions.inProcess drives subject.appUrl in this process, and a ${source} subject needs the hosted desktop the in-process route never creates. Use an app-url or local-app subject with inProcess, or remove inProcess to run on a hosted desktop.`, actor);
|
|
237
237
|
// The parser's rule; without a product the desktop study fails later.
|
|
238
238
|
const productReason = desktopCliProductReason(config);
|
|
239
239
|
if (productReason)
|
|
@@ -243,7 +243,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
243
243
|
return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", `Computer-use fan-out is capped at ${MAX_COMPUTER_USE_PARTICIPANTS} participants (resolved ${participants.length}); N concurrent paid desktops is real spend.`, actor);
|
|
244
244
|
const [first, ...rest] = participants;
|
|
245
245
|
if (driving.inProcess && rest.length > 0)
|
|
246
|
-
return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", "Fan-out to more than one participant is not supported on the in-process route (
|
|
246
|
+
return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", "Fan-out to more than one participant is not supported on the in-process route (RunStudyOptions.inProcess): fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips. Run a single in-process participant, or fan out on the E2B route.", actor);
|
|
247
247
|
if (first === undefined)
|
|
248
248
|
throw new Error("computerUseParticipants returned no participant");
|
|
249
249
|
const appUrl = config.subject.appUrl ?? "";
|
|
@@ -55,7 +55,7 @@ export async function liveCuaRejection(args) {
|
|
|
55
55
|
if (unsetSubjectEnv.length > 0) {
|
|
56
56
|
return {
|
|
57
57
|
code: "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING",
|
|
58
|
-
message: `subject.env declares ${unsetSubjectEnv.join(", ")} but the environment does not provide ${unsetSubjectEnv.length === 1 ? "it" : "them"} (pass via --
|
|
58
|
+
message: `subject.env declares ${unsetSubjectEnv.join(", ")} but the environment does not provide ${unsetSubjectEnv.length === 1 ? "it" : "them"} (pass via --dotenv; values are never persisted).`,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
// Fail-closed cap: a maxUsd cap needs a measurable per-turn estimate.
|
|
@@ -89,7 +89,7 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
89
89
|
ok: false,
|
|
90
90
|
result: fail(...args),
|
|
91
91
|
});
|
|
92
|
-
// runStudyWith's
|
|
92
|
+
// runStudyWith's `localVm` supplies the desktop of a local-target app-url study; a direct route call or
|
|
93
93
|
// an in-process executor on the same study has none. It reads the declared source, as the planner
|
|
94
94
|
// did: a library config the parser never saw can plan to an app-url subject from another source.
|
|
95
95
|
if (config.subject.source === "app-url" &&
|
|
@@ -155,7 +155,10 @@ export interface CuaParticipantResult {
|
|
|
155
155
|
screenshots: number;
|
|
156
156
|
};
|
|
157
157
|
sandbox?: {
|
|
158
|
+
/** "[redacted-sandbox-id]"; the raw id is only in the run's sandbox-receipts.ndjson. */
|
|
158
159
|
sandboxId: string;
|
|
160
|
+
/** The id's digest, which matches its receipt. */
|
|
161
|
+
sandboxIdDigest?: string;
|
|
159
162
|
killed: boolean;
|
|
160
163
|
streamUrlPresent: boolean;
|
|
161
164
|
};
|
|
@@ -240,7 +243,10 @@ export interface CuaActorStudyResult extends AutomaticAnalysisResult, StudyResul
|
|
|
240
243
|
screenshots: number;
|
|
241
244
|
};
|
|
242
245
|
sandbox?: {
|
|
246
|
+
/** "[redacted-sandbox-id]"; the raw id is only in the run's sandbox-receipts.ndjson. */
|
|
243
247
|
sandboxId: string;
|
|
248
|
+
/** The id's digest, which matches its receipt. */
|
|
249
|
+
sandboxIdDigest?: string;
|
|
244
250
|
killed: boolean;
|
|
245
251
|
/** The stream URL itself (carries an auth key) is runtime-only and is deliberately not
|
|
246
252
|
* surfaced on the result; the sandbox is already dead by the time the result exists. */
|
|
@@ -141,7 +141,7 @@ async function admitSharedWorldRun(plan, input) {
|
|
|
141
141
|
}
|
|
142
142
|
const unsetSubjectEnv = missingSubjectEnv(plan.requirements, env);
|
|
143
143
|
if (unsetSubjectEnv.length > 0) {
|
|
144
|
-
return fail("HUMANISH_SHARED_WORLD_SUBJECT_ENV_MISSING", `subject.env declares ${unsetSubjectEnv.join(", ")} but the environment does not provide ${unsetSubjectEnv.length === 1 ? "it" : "them"} (pass via --
|
|
144
|
+
return fail("HUMANISH_SHARED_WORLD_SUBJECT_ENV_MISSING", `subject.env declares ${unsetSubjectEnv.join(", ")} but the environment does not provide ${unsetSubjectEnv.length === 1 ? "it" : "them"} (pass via --dotenv; values are never persisted).`);
|
|
145
145
|
}
|
|
146
146
|
return undefined;
|
|
147
147
|
}
|
|
@@ -95,6 +95,7 @@ export interface ConcurrentSharedWorldParticipantResult {
|
|
|
95
95
|
/** The actor sandbox lifecycle proof (the getHost/key value is never surfaced here). */
|
|
96
96
|
sandbox?: {
|
|
97
97
|
sandboxId: string;
|
|
98
|
+
sandboxIdDigest?: string;
|
|
98
99
|
killed: boolean;
|
|
99
100
|
};
|
|
100
101
|
error?: {
|
|
@@ -119,6 +120,7 @@ export interface ConcurrentSharedWorldStudyResult extends AutomaticAnalysisResul
|
|
|
119
120
|
/** The one subject sandbox lifecycle proof. */
|
|
120
121
|
subjectSandbox?: {
|
|
121
122
|
sandboxId: string;
|
|
123
|
+
sandboxIdDigest?: string;
|
|
122
124
|
killed: boolean;
|
|
123
125
|
};
|
|
124
126
|
/** Whether ≥2 actor windows overlapped in time (proven concurrency; live only). */
|
|
@@ -38,7 +38,7 @@ export function buildRuntimeAuth(args) {
|
|
|
38
38
|
return {
|
|
39
39
|
ok: false,
|
|
40
40
|
code: "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING",
|
|
41
|
-
message: `Live terminal-product studies declare runtimeAuth "${String(args.runtimeAuth)}" and need ${ALLOWED_RUNTIME_KEY_NAMES.join(" or ")} in the environment (pass via --
|
|
41
|
+
message: `Live terminal-product studies declare runtimeAuth "${String(args.runtimeAuth)}" and need ${ALLOWED_RUNTIME_KEY_NAMES.join(" or ")} in the environment (pass via --dotenv; the selected auth mode places the value in command-scoped env or an external E2B header transform; the value is never persisted).`,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
const keyValue = args.env[keyName];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Finishing a live terminal session: the actor trace from the captured stream, the spend ledger
|
|
2
2
|
// and its caps check, the evidence files, the bundle, and the study result.
|
|
3
|
+
import { publicSandboxView, readRunSandboxIds, scrubSandboxIds } from "../../run/sandbox-ids.js";
|
|
3
4
|
import { buildRunCostSummary } from "../../run/cost-summary.js";
|
|
4
5
|
import { writeContainedOutputFile } from "../../run/contained-output.js";
|
|
5
6
|
import { validatePreparedRunArtifactPaths, } from "../../run/paths.js";
|
|
@@ -209,8 +210,11 @@ export async function finishLiveTerminalSession(inputs) {
|
|
|
209
210
|
/** Persist the terminal evidence: redacted events, normalized transcript, ledgers, actor trace. */
|
|
210
211
|
async function writeTerminalEvidence(runPaths, evidence) {
|
|
211
212
|
const { terminalEvents, normalizedTranscript, ledgers, trace } = evidence;
|
|
212
|
-
|
|
213
|
-
await
|
|
214
|
-
|
|
215
|
-
await writeContainedOutputFile(runPaths,
|
|
213
|
+
// The sandbox's raw id stays in sandbox-receipts.ndjson; these files name it by digest.
|
|
214
|
+
const ids = await readRunSandboxIds(runPaths);
|
|
215
|
+
const scrub = (text) => scrubSandboxIds(text, ids);
|
|
216
|
+
await writeContainedOutputFile(runPaths, TERMINAL_EVENTS_ARTIFACT, scrub(`${terminalEvents.map((e) => JSON.stringify(e)).join("\n")}${terminalEvents.length > 0 ? "\n" : ""}`), "utf8");
|
|
217
|
+
await writeContainedOutputFile(runPaths, TERMINAL_TRANSCRIPT_ARTIFACT, scrub(`${normalizedTranscript}\n`), "utf8");
|
|
218
|
+
await writeContainedOutputFile(runPaths, TERMINAL_LEDGERS_ARTIFACT, `${JSON.stringify(publicSandboxView(ledgers, ids), null, 2)}\n`, "utf8");
|
|
219
|
+
await writeContainedOutputFile(runPaths, "actor.json", scrub(`${JSON.stringify(trace, null, 2)}\n`), "utf8");
|
|
216
220
|
}
|
|
@@ -147,7 +147,10 @@ export interface TerminalProductStudyResult extends AutomaticAnalysisResult, Stu
|
|
|
147
147
|
};
|
|
148
148
|
/** Live-only: the sandbox lifecycle proof (the key/auth value is never surfaced here). */
|
|
149
149
|
sandbox?: {
|
|
150
|
+
/** "[redacted-sandbox-id]"; the raw id is only in the run's sandbox-receipts.ndjson. */
|
|
150
151
|
sandboxId: string;
|
|
152
|
+
/** The id's digest, which matches its receipt. */
|
|
153
|
+
sandboxIdDigest?: string;
|
|
151
154
|
killed: boolean;
|
|
152
155
|
/** By-id proof (never a re-list): 0 = confirmed reclaimed, 1 = still present (unconfirmed),
|
|
153
156
|
* -1 = kill(id) itself failed or was unavailable. See TerminalLedgers["cleanup"]. */
|
package/dist/run/bundle-shape.js
CHANGED
|
@@ -73,6 +73,7 @@ function isRunProviderResource(value) {
|
|
|
73
73
|
value.kind === "sandbox" &&
|
|
74
74
|
typeof value.id === "string" &&
|
|
75
75
|
value.id.trim().length > 0 &&
|
|
76
|
+
(value.idDigest === undefined || typeof value.idDigest === "string") &&
|
|
76
77
|
value.owner === "humanish" &&
|
|
77
78
|
(value.status === "running" || value.status === "killed" || value.status === "unknown") &&
|
|
78
79
|
(value.simId === undefined || typeof value.simId === "string") &&
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -413,7 +413,13 @@ export interface RunProviderResource {
|
|
|
413
413
|
schema: "humanish.provider-resource.v1";
|
|
414
414
|
provider: "e2b-desktop";
|
|
415
415
|
kind: "sandbox";
|
|
416
|
+
/**
|
|
417
|
+
* "[redacted-sandbox-id]" in runs from 0.110 on; earlier runs recorded the raw id here. The raw id
|
|
418
|
+
* is in the run's sandbox-receipts.ndjson.
|
|
419
|
+
*/
|
|
416
420
|
id: string;
|
|
421
|
+
/** The id's digest, which matches its receipt. Absent on runs recorded before 0.110. */
|
|
422
|
+
idDigest?: string;
|
|
417
423
|
owner: "humanish";
|
|
418
424
|
status: "running" | "killed" | "unknown";
|
|
419
425
|
simId?: string;
|
package/dist/run/reclaim.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { type PreparedRunArtifactPaths } from "./paths.js";
|
|
|
3
3
|
declare const RECLAIM_RESULT_SCHEMA = "humanish.reclaim-result.v1";
|
|
4
4
|
export declare const RECLAIM_RECEIPT_ARTIFACT = "reclaim-receipt.json";
|
|
5
5
|
interface ReclaimOutcome {
|
|
6
|
+
/** "[redacted-sandbox-id]"; the raw id is only in the run's sandbox-receipts.ndjson. */
|
|
6
7
|
sandboxId: string;
|
|
8
|
+
/** The id's digest, which matches its receipt. */
|
|
9
|
+
sandboxIdDigest: string;
|
|
7
10
|
laneId: string;
|
|
8
11
|
/** killed = kill(id) confirmed; already-gone = the server no longer knows the id (TTL or a
|
|
9
12
|
* prior cleanup got it); kill-failed = the attempt errored (the TTL backstop still applies);
|
package/dist/run/reclaim.js
CHANGED
|
@@ -9,7 +9,8 @@ import { discardPreflightJournal, listPreflightJournals, preflightReclaimDecisio
|
|
|
9
9
|
import { resolveRunPath } from "./locate.js";
|
|
10
10
|
import { parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
|
|
11
11
|
import { destroyE2BSandbox } from "../substrates/e2b/sandbox.js";
|
|
12
|
-
import { redactText, toErrorMessage } from "../evidence/redaction.js";
|
|
12
|
+
import { REDACTED_SANDBOX_ID, redactText, sandboxIdDigest, toErrorMessage, } from "../evidence/redaction.js";
|
|
13
|
+
import { scrubSandboxIds } from "./sandbox-ids.js";
|
|
13
14
|
import { runIdOf } from "./paths.js";
|
|
14
15
|
const RECLAIM_RESULT_SCHEMA = "humanish.reclaim-result.v1";
|
|
15
16
|
export const RECLAIM_RECEIPT_ARTIFACT = "reclaim-receipt.json";
|
|
@@ -182,10 +183,16 @@ async function reclaimJournal(root, hooks) {
|
|
|
182
183
|
if (seen.has(key))
|
|
183
184
|
continue;
|
|
184
185
|
seen.add(key);
|
|
186
|
+
// The result and reclaim-receipt.json name the sandbox by digest, and a provider error that
|
|
187
|
+
// quotes the id is scrubbed; the raw id stays in sandbox-receipts.ndjson.
|
|
188
|
+
const attempt = await destroy(receipt);
|
|
185
189
|
outcomes.push({
|
|
186
|
-
sandboxId:
|
|
190
|
+
sandboxId: REDACTED_SANDBOX_ID,
|
|
191
|
+
sandboxIdDigest: sandboxIdDigest(receipt.sandboxId),
|
|
187
192
|
laneId: receipt.laneId,
|
|
188
|
-
...(
|
|
193
|
+
...("detail" in attempt
|
|
194
|
+
? { ...attempt, detail: scrubSandboxIds(attempt.detail, [receipt.sandboxId]) }
|
|
195
|
+
: attempt),
|
|
189
196
|
});
|
|
190
197
|
}
|
|
191
198
|
return { kind: "done", receiptCount: receipts.length, outcomes };
|
package/dist/run/results.d.ts
CHANGED
|
@@ -37,7 +37,10 @@ export interface RunResult {
|
|
|
37
37
|
export interface CleanupResourceResult {
|
|
38
38
|
provider: RunProviderResource["provider"];
|
|
39
39
|
kind: RunProviderResource["kind"];
|
|
40
|
+
/** "[redacted-sandbox-id]"; the raw id is only in the run's sandbox-receipts.ndjson. */
|
|
40
41
|
id: string;
|
|
42
|
+
/** The id's digest, which matches its receipt; absent when the record had neither. */
|
|
43
|
+
idDigest?: string;
|
|
41
44
|
/** Cleanup reads recorded evidence and never kills a sandbox, so it never writes `killed`. */
|
|
42
45
|
status: "already_clean" | "failed" | "skipped";
|
|
43
46
|
message: string;
|
package/dist/run/run.js
CHANGED
|
@@ -9,6 +9,7 @@ import {} from "./results.js";
|
|
|
9
9
|
import { createRunArtifactPaths, validatePreparedRunArtifactPaths, } from "./paths.js";
|
|
10
10
|
import { registerActiveRun } from "./active-runs.js";
|
|
11
11
|
import { writeContainedOutputFile, writePreparedRunLatestPointer } from "./contained-output.js";
|
|
12
|
+
import { scrubRunSandboxIds, withPublicSandboxIds } from "./sandbox-ids.js";
|
|
12
13
|
import { beginRunStatus, runStatusOutcome } from "./status.js";
|
|
13
14
|
/** An entry point called in a state that does not admit it. */
|
|
14
15
|
class RunLifecycleError extends Error {
|
|
@@ -55,6 +56,16 @@ export class FinishedRun {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
59
|
+
/** Where FinishedRun renders the Observer, when the run was started with one. */
|
|
60
|
+
function observerTarget(options) {
|
|
61
|
+
return options.observer === undefined
|
|
62
|
+
? undefined
|
|
63
|
+
: {
|
|
64
|
+
cwd: options.cwd,
|
|
65
|
+
open: options.observer.open,
|
|
66
|
+
render: options.observer.render ?? renderObserver,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
58
69
|
/**
|
|
59
70
|
* Run `fn` as the lifetime of at most one run. When `fn` settles the scope closes synchronously,
|
|
60
71
|
* so the scope and its run admit no further calls, even through references `fn` leaked into
|
|
@@ -100,13 +111,7 @@ export async function runScope(fn) {
|
|
|
100
111
|
const openRun = (options, runId, paths, runStatus) => {
|
|
101
112
|
const now = options.now ?? Date.now;
|
|
102
113
|
const createdAt = new Date(now()).toISOString();
|
|
103
|
-
const observer = options
|
|
104
|
-
? undefined
|
|
105
|
-
: {
|
|
106
|
-
cwd: options.cwd,
|
|
107
|
-
open: options.observer.open,
|
|
108
|
-
render: options.observer.render ?? renderObserver,
|
|
109
|
-
};
|
|
114
|
+
const observer = observerTarget(options);
|
|
110
115
|
let finishCalled = false;
|
|
111
116
|
let pointerWritten = false;
|
|
112
117
|
// One chain serializes every write. A rejection reaches only the caller of that write; the
|
|
@@ -133,7 +138,7 @@ export async function runScope(fn) {
|
|
|
133
138
|
/** run.json, then `afterBundle`, then the review and projections. */
|
|
134
139
|
const writeBundleFiles = async (bundle, afterBundle) => {
|
|
135
140
|
await validatePreparedRunArtifactPaths(paths);
|
|
136
|
-
const publicBundle = { ...bundle, cwd: PUBLIC_TARGET_CWD };
|
|
141
|
+
const publicBundle = await withPublicSandboxIds(paths, { ...bundle, cwd: PUBLIC_TARGET_CWD });
|
|
137
142
|
await writeContainedOutputFile(paths, RUN_BUNDLE_FILE, json(publicBundle), "utf8");
|
|
138
143
|
await afterBundle(publicBundle);
|
|
139
144
|
await writeContainedOutputFile(paths, "review.json", json(publicBundle.review), "utf8");
|
|
@@ -149,6 +154,7 @@ export async function runScope(fn) {
|
|
|
149
154
|
};
|
|
150
155
|
const publish = async (bundle) => {
|
|
151
156
|
await writeBundleFiles(bundle, (publicBundle) => runStatus.finish(runStatusOutcome(publicBundle)));
|
|
157
|
+
await scrubRunSandboxIds(paths);
|
|
152
158
|
await writePointer();
|
|
153
159
|
return new FinishedRun(issueKey, runId, paths, observer, runStatus);
|
|
154
160
|
};
|