humanish 0.109.1 → 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 +11 -0
- package/README.md +1 -1
- package/dist/cli/commands/comms.js +25 -7
- package/dist/cli/commands/observe.js +4 -2
- package/dist/cli/commands/run-command.js +4 -3
- package/dist/cli/commands/runs.js +11 -5
- 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 +7 -3
- package/dist/evidence/redaction.js +68 -24
- package/dist/feedback/export-bundle.js +6 -1
- 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/live-phase.d.ts +1 -0
- package/dist/routes/computer-use/preflight.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/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 +18 -4
- package/package.json +3 -2
- package/skills/humanish/SKILL.md +2 -2
|
@@ -60,12 +60,65 @@ function redactLocalPaths(text, label) {
|
|
|
60
60
|
* passes only this value at a sandbox-id key.
|
|
61
61
|
*/
|
|
62
62
|
export const REDACTED_SANDBOX_ID = "[redacted-sandbox-id]";
|
|
63
|
-
/** The keys run writers put a sandbox id under;
|
|
63
|
+
/** The keys run writers put a sandbox id under; resource entries hold one as `id`. */
|
|
64
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
|
+
}
|
|
65
118
|
/**
|
|
66
|
-
* `value` with every sandbox id replaced by REDACTED_SANDBOX_ID: strings at
|
|
67
|
-
* `subjectSandboxId`, and the `id` of each `providerResources` entry.
|
|
68
|
-
*
|
|
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.
|
|
69
122
|
*/
|
|
70
123
|
export function redactSandboxIds(value) {
|
|
71
124
|
if (Array.isArray(value)) {
|
|
@@ -74,28 +127,19 @@ export function redactSandboxIds(value) {
|
|
|
74
127
|
}
|
|
75
128
|
if (value === null || typeof value !== "object")
|
|
76
129
|
return value;
|
|
77
|
-
let changed = false;
|
|
78
130
|
// fromEntries keeps a key such as __proto__ an own property, as JSON.parse made it.
|
|
79
|
-
const entries =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
? Object.fromEntries(Object.entries(redactSandboxIds(resource)).map(([field, entry]) => [
|
|
89
|
-
field,
|
|
90
|
-
field === "id" ? REDACTED_SANDBOX_ID : entry,
|
|
91
|
-
]))
|
|
92
|
-
: redactSandboxIds(resource));
|
|
93
|
-
else
|
|
94
|
-
next = redactSandboxIds(child);
|
|
95
|
-
if (next !== child && JSON.stringify(next) !== JSON.stringify(child))
|
|
96
|
-
changed = true;
|
|
97
|
-
return [key, next];
|
|
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)];
|
|
98
140
|
});
|
|
141
|
+
const changed = entries.length !== Object.keys(value).length ||
|
|
142
|
+
entries.some(([key, child]) => child !== value[key]);
|
|
99
143
|
return changed ? Object.fromEntries(entries) : value;
|
|
100
144
|
}
|
|
101
145
|
/** Redact secrets to [REDACTED_SECRET] and local paths to their path labels. */
|
|
@@ -8,6 +8,7 @@ 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 { inventorySandboxIds, scrubSandboxIdBytes } from "../run/sandbox-ids.js";
|
|
11
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";
|
|
@@ -300,6 +301,7 @@ function keptPngPaths(source, bundle) {
|
|
|
300
301
|
async function writeDerivative(source, stagePaths, bundle) {
|
|
301
302
|
const entries = [];
|
|
302
303
|
const imagePaths = keptPngPaths(source, bundle);
|
|
304
|
+
const sandboxIds = inventorySandboxIds(source.files);
|
|
303
305
|
let images = 0;
|
|
304
306
|
for (const file of source.files) {
|
|
305
307
|
const isPng = path.extname(file.path).toLowerCase() === ".png";
|
|
@@ -343,6 +345,7 @@ async function writeDerivative(source, stagePaths, bundle) {
|
|
|
343
345
|
}
|
|
344
346
|
bytes = jsonBytes(transformed);
|
|
345
347
|
}
|
|
348
|
+
bytes = scrubSandboxIdBytes(file.path, bytes, sandboxIds);
|
|
346
349
|
action = bytes.equals(file.bytes) ? "copied" : "updated";
|
|
347
350
|
}
|
|
348
351
|
await writeContainedOutputFile(stagePaths, file.path, bytes);
|
|
@@ -496,7 +499,9 @@ async function verifyFrozenSource(workspace, source, runId) {
|
|
|
496
499
|
verified.shareSafety.reasons.every(
|
|
497
500
|
// Both are repaired in the copy: frames are blurred, and an unread PNG is dropped or
|
|
498
501
|
// refused by keptPngPaths. Any other unread file fails as an unsupported format.
|
|
499
|
-
(reason) => reason.code === "RAW_SCREENSHOTS" ||
|
|
502
|
+
(reason) => reason.code === "RAW_SCREENSHOTS" ||
|
|
503
|
+
reason.code === "RAW_SANDBOX_ID" ||
|
|
504
|
+
reason.code === "UNSCANNED_ARTIFACT")))) {
|
|
500
505
|
return { shareSafety: verified.shareSafety };
|
|
501
506
|
}
|
|
502
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 };
|
|
@@ -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.
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
2
|
+
import { type PreparedRunArtifactPaths } from "./paths.js";
|
|
3
|
+
/** `text` with every occurrence of each raw id replaced by its label, longest id first. */
|
|
4
|
+
export declare function scrubSandboxIds(text: string, ids: Iterable<string>, label?: (id: string) => string): string;
|
|
5
|
+
/**
|
|
6
|
+
* `value` as it may be written or returned: ids at the sandbox-id keys become the marker and a
|
|
7
|
+
* digest, and any raw id left in free text or a URL becomes its label.
|
|
8
|
+
*/
|
|
9
|
+
export declare function publicSandboxView<T>(value: T, ids: Iterable<string>): T;
|
|
10
|
+
/**
|
|
11
|
+
* The raw ids at sandbox-id keys in `value`, such as a run.json recorded before 0.110. A value
|
|
12
|
+
* shorter than an E2B id is left to the key rule, so the text scrub never rewrites a common word.
|
|
13
|
+
*/
|
|
14
|
+
export declare function keyedSandboxIds(value: unknown): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Every raw sandbox id a run's files name: the receipts', and any at a sandbox-id key in its
|
|
17
|
+
* JSON, which is where a run from before 0.110 that has no receipts still records them.
|
|
18
|
+
*/
|
|
19
|
+
export declare function inventorySandboxIds(files: readonly {
|
|
20
|
+
path: string;
|
|
21
|
+
bytes: Buffer;
|
|
22
|
+
}[]): string[];
|
|
23
|
+
/** Whether a run file's JSON holds a raw id at a sandbox-id key, which no receipt may name. */
|
|
24
|
+
export declare function holdsKeyedSandboxId(file: string, text: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The raw sandbox ids a run's receipts journal, with any this process receipted whose append
|
|
27
|
+
* failed; empty when there are none or the journal cannot be read.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readRunSandboxIds(root: PreparedOutputRoot): Promise<string[]>;
|
|
30
|
+
/**
|
|
31
|
+
* `value` as a run writes it: raw sandbox ids stay in the run's sandbox-receipts.ndjson, and the
|
|
32
|
+
* file names each by marker and digest.
|
|
33
|
+
*/
|
|
34
|
+
export declare function withPublicSandboxIds<T>(root: PreparedOutputRoot, value: T): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* The bytes of `file` with each raw id replaced by its label: the same buffer when none occurs, or
|
|
37
|
+
* when the bytes are not the plain text verify and export read as text.
|
|
38
|
+
*/
|
|
39
|
+
export declare function scrubSandboxIdBytes(file: string, bytes: Buffer, ids: readonly string[]): Buffer;
|
|
40
|
+
/**
|
|
41
|
+
* Rewrite each file of a finished run that names one of its raw sandbox ids, apart from the
|
|
42
|
+
* receipts, with the label in place of the id. Writers such as a participant's actor.json can
|
|
43
|
+
* quote an SDK error that names the sandbox. Run.finish calls it before analysis reads the run.
|
|
44
|
+
* Best effort: verify grades a file this misses.
|
|
45
|
+
*/
|
|
46
|
+
export declare function scrubRunSandboxIds(paths: PreparedRunArtifactPaths): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* A run's result as it may be returned or printed: the same object when it names no sandbox,
|
|
49
|
+
* otherwise its public view, scrubbed of the ids the run's receipts journal.
|
|
50
|
+
*/
|
|
51
|
+
export declare function publicRunResult<T extends {
|
|
52
|
+
runId?: string;
|
|
53
|
+
}>(result: T, cwd: string): Promise<T>;
|