humanish 0.15.0 → 0.15.2
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/README.md +33 -31
- package/dist/claude-agent-sdk.js +14 -7
- package/dist/claude-agent-sdk.js.map +1 -1
- package/dist/codex-app-server-ui.d.ts +1 -1
- package/dist/codex-app-server-ui.js +69 -39
- package/dist/codex-app-server-ui.js.map +1 -1
- package/dist/codex-app-server.d.ts +3 -0
- package/dist/codex-app-server.js +15 -8
- package/dist/codex-app-server.js.map +1 -1
- package/dist/concurrent-shared-world-lab.js +18 -16
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/core/git-state.d.ts +2 -0
- package/dist/core/git-state.js +152 -17
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/git-workspace.d.ts +31 -0
- package/dist/core/git-workspace.js +447 -0
- package/dist/core/git-workspace.js.map +1 -0
- package/dist/cua-actor-lab.d.ts +3 -2
- package/dist/cua-actor-lab.js +34 -23
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/e2b-terminal-lab.js +111 -46
- package/dist/e2b-terminal-lab.js.map +1 -1
- package/dist/feedback.js +139 -101
- package/dist/feedback.js.map +1 -1
- package/dist/init.d.ts +1 -1
- package/dist/init.js +108 -22
- package/dist/init.js.map +1 -1
- package/dist/lab-config.d.ts +5 -0
- package/dist/lab-config.js +38 -0
- package/dist/lab-config.js.map +1 -1
- package/dist/labs.js +258 -52
- package/dist/labs.js.map +1 -1
- package/dist/observer-assets.js +3 -2
- package/dist/observer-assets.js.map +1 -1
- package/dist/observer-static.js +131 -17
- package/dist/observer-static.js.map +1 -1
- package/dist/observer.js +263 -66
- package/dist/observer.js.map +1 -1
- package/dist/openai-responses-cu.js +18 -4
- package/dist/openai-responses-cu.js.map +1 -1
- package/dist/oss-lab.js +59 -14
- package/dist/oss-lab.js.map +1 -1
- package/dist/oss-meta-lab.d.ts +2 -4
- package/dist/oss-meta-lab.js +117 -477
- package/dist/oss-meta-lab.js.map +1 -1
- package/dist/program.js +23 -21
- package/dist/program.js.map +1 -1
- package/dist/run-paths.d.ts +56 -0
- package/dist/run-paths.js +340 -0
- package/dist/run-paths.js.map +1 -0
- package/dist/run.d.ts +14 -4
- package/dist/run.js +595 -306
- package/dist/run.js.map +1 -1
- package/dist/scripted-browser-actor.d.ts +7 -4
- package/dist/scripted-browser-actor.js +154 -41
- package/dist/scripted-browser-actor.js.map +1 -1
- package/dist/scripted-browser-lab.js +95 -45
- package/dist/scripted-browser-lab.js.map +1 -1
- package/dist/selected-output-paths.d.ts +49 -0
- package/dist/selected-output-paths.js +354 -0
- package/dist/selected-output-paths.js.map +1 -0
- package/dist/shared-world-lab.js +13 -12
- package/dist/shared-world-lab.js.map +1 -1
- package/dist/source-archive.js +44 -1
- package/dist/source-archive.js.map +1 -1
- package/docs/architecture/oss-lab-poc.md +8 -0
- package/docs/assets/humanish-observer-hero.png +0 -0
- package/docs/contracts/policy.md +7 -1
- package/docs/goals/current.md +20 -1
- package/docs/product/open-source-install-experience.md +9 -1
- package/docs/ramp/README.md +14 -7
- package/package.json +1 -1
package/dist/run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { lstat, readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
@@ -8,17 +8,19 @@ import { parse as parseYaml } from "yaml";
|
|
|
8
8
|
// (moved there so the actor registry can reuse it without a run.ts import cycle). This file
|
|
9
9
|
// keeps the `run --app-url` orchestration; behavior is byte-identical.
|
|
10
10
|
import { browserSurfaces, builtinBrowserPersonaJourney, captureBrowserSurface, normalizeLocalAppUrl, parseBrowserPersonaJourneyFromScenario, resolveBrowserCommand } from "./scripted-browser-actor.js";
|
|
11
|
-
import { CODEX_APP_SERVER_TRACE_SCHEMA } from "./codex-app-server.js";
|
|
11
|
+
import { CODEX_APP_SERVER_TRACE_SCHEMA, runCodexAppServerSessionInPreparedRoot } from "./codex-app-server.js";
|
|
12
12
|
import { getActor } from "./actor-registry.js";
|
|
13
13
|
import { artifactReferenceIfWritten, hasWrittenScreenshot } from "./artifact-reference.js";
|
|
14
14
|
import { ACTOR_TRACE_SCHEMA } from "./actor-contract.js";
|
|
15
15
|
import { captureGitState, GIT_STATE_SCHEMA } from "./core/git-state.js";
|
|
16
|
+
import { inspectVerifiedGitWorkspace } from "./core/git-workspace.js";
|
|
16
17
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
17
18
|
import { screenshotEvidenceError } from "./image-evidence.js";
|
|
18
19
|
import { buildObserverData } from "./observer-data.js";
|
|
19
20
|
import { parseResolvedPersona, personaToDirectives, renderPersonaPromptSection } from "./persona.js";
|
|
20
21
|
import { containsSensitive, digestText, redactText, redactToSecretLabel, tailText } from "./redaction.js";
|
|
21
|
-
import {
|
|
22
|
+
import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, prepareRunArtifactPaths, resolveExistingRunDirectory, resolveLatestRunDirectory, resolveRunsRoot, validatePreparedRunArtifactPaths } from "./run-paths.js";
|
|
23
|
+
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, bindExistingManagedHumanishOutputDirectory, prepareContainedOutputDirectory, prepareContainedOutputDirectoryRoot, prepareContainedOutputFile, prepareSelectedOutputDirectory, readContainedRegularFile, writeContainedOutputFile, writePreparedRunLatestPointer } from "./selected-output-paths.js";
|
|
22
24
|
export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v1";
|
|
23
25
|
export const SHARED_WORLD_SCHEMA = "humanish.shared-world.v1";
|
|
24
26
|
export const REVIEW_SCHEMA = "humanish.review.v1";
|
|
@@ -29,8 +31,17 @@ export const CLEANUP_SCHEMA = "humanish.cleanup-result.v1";
|
|
|
29
31
|
export const PUBLIC_TARGET_CWD = "[target-cwd]";
|
|
30
32
|
const SAFE_GIT_NOTES = new Set([
|
|
31
33
|
"Git command could not be started.",
|
|
34
|
+
"Git HEAD capture timed out.",
|
|
35
|
+
"Git HEAD command could not be started.",
|
|
36
|
+
"Git metadata could not be inspected safely.",
|
|
32
37
|
"Git status command could not be captured.",
|
|
33
38
|
"Git status command could not be started.",
|
|
39
|
+
"Git status could not be captured.",
|
|
40
|
+
"Git status capture timed out.",
|
|
41
|
+
"Git metadata failed containment validation.",
|
|
42
|
+
"Git ref-state capture timed out.",
|
|
43
|
+
"Git ref-state command could not be started.",
|
|
44
|
+
"Git work-tree detection timed out.",
|
|
34
45
|
"Git work tree had changes; only counts were captured, not branch names, remotes, paths, or file names.",
|
|
35
46
|
"Git work tree was clean; branch names, remotes, paths, and file names were not captured.",
|
|
36
47
|
"No git work tree was detected.",
|
|
@@ -102,6 +113,7 @@ export async function runDryRun(options) {
|
|
|
102
113
|
}
|
|
103
114
|
};
|
|
104
115
|
}
|
|
116
|
+
const projectRoot = await prepareSelectedOutputDirectory(path.dirname(cwd), cwd);
|
|
105
117
|
if (options.appUrl !== undefined) {
|
|
106
118
|
if (options.dryRun) {
|
|
107
119
|
return {
|
|
@@ -127,18 +139,18 @@ export async function runDryRun(options) {
|
|
|
127
139
|
}
|
|
128
140
|
};
|
|
129
141
|
}
|
|
130
|
-
return runBrowserAppProof({ ...options, appUrl: options.appUrl, cwd, simCount });
|
|
142
|
+
return runBrowserAppProof({ ...options, appUrl: options.appUrl, cwd, projectRoot, simCount });
|
|
131
143
|
}
|
|
132
144
|
if (!options.dryRun) {
|
|
133
145
|
const actor = resolveRequestedLocalCodexActor(options.actor);
|
|
134
146
|
if (actor === "codex-tui") {
|
|
135
|
-
return runLocalCodexTui({ ...options, actor, cwd, simCount });
|
|
147
|
+
return runLocalCodexTui({ ...options, actor, cwd, projectRoot, simCount });
|
|
136
148
|
}
|
|
137
149
|
if (actor === "codex-exec") {
|
|
138
|
-
return runLocalCodexExec({ ...options, actor, cwd, simCount });
|
|
150
|
+
return runLocalCodexExec({ ...options, actor, cwd, projectRoot, simCount });
|
|
139
151
|
}
|
|
140
152
|
if (actor === "codex-app-server") {
|
|
141
|
-
return runLocalCodexAppServer({ ...options, actor, cwd, simCount });
|
|
153
|
+
return runLocalCodexAppServer({ ...options, actor, cwd, projectRoot, simCount });
|
|
142
154
|
}
|
|
143
155
|
return {
|
|
144
156
|
schema: "humanish.run-result.v1",
|
|
@@ -154,12 +166,13 @@ export async function runDryRun(options) {
|
|
|
154
166
|
const now = new Date();
|
|
155
167
|
const createdAt = now.toISOString();
|
|
156
168
|
const runId = options.runId ?? `dryrun-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const packageName = await readPackageName(cwd);
|
|
160
|
-
const humanishSource = await directoryExists(path.join(cwd, "humanish")) ? "present" : "missing";
|
|
169
|
+
const packageName = await readPackageName(projectRoot);
|
|
170
|
+
const humanishSource = await implicitProjectDirectoryExists(projectRoot, "humanish") ? "present" : "missing";
|
|
161
171
|
const source = await buildRunSource({ cwd, capturedAt: createdAt, humanishSource, packageName });
|
|
162
|
-
const selection = await loadDryRunSelection(
|
|
172
|
+
const selection = await loadDryRunSelection(projectRoot, humanishSource);
|
|
173
|
+
await assertPreparedSelectedOutputDirectory(projectRoot);
|
|
174
|
+
const runPaths = await prepareRunArtifactPaths(cwd, runId);
|
|
175
|
+
const artifactRoot = runPaths.relativeRunRoot;
|
|
163
176
|
if (humanishSource === "missing") {
|
|
164
177
|
warnings.push("Committed humanish/ source was not found; using built-in synthetic dry-run defaults.");
|
|
165
178
|
}
|
|
@@ -220,14 +233,13 @@ export async function runDryRun(options) {
|
|
|
220
233
|
review: createReviewSummary(),
|
|
221
234
|
feedbackCandidates: []
|
|
222
235
|
};
|
|
223
|
-
await
|
|
224
|
-
await
|
|
225
|
-
await writeJson(path.join(cwd, ".humanish", "runs", "latest.json"), {
|
|
236
|
+
await writeRunBundleArtifacts(runPaths, bundle);
|
|
237
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
226
238
|
schema: "humanish.latest-run.v1",
|
|
227
239
|
runId,
|
|
228
240
|
path: artifactRoot,
|
|
229
241
|
updatedAt: createdAt
|
|
230
|
-
});
|
|
242
|
+
}, null, 2)}\n`, "utf8");
|
|
231
243
|
return {
|
|
232
244
|
schema: "humanish.run-result.v1",
|
|
233
245
|
ok: true,
|
|
@@ -238,7 +250,7 @@ export async function runDryRun(options) {
|
|
|
238
250
|
artifactRoot,
|
|
239
251
|
bundlePath: path.join(artifactRoot, "run.json"),
|
|
240
252
|
reviewPath: path.join(artifactRoot, "review.md"),
|
|
241
|
-
latestPath:
|
|
253
|
+
latestPath: runPaths.relativeLatestPointer,
|
|
242
254
|
warnings
|
|
243
255
|
};
|
|
244
256
|
}
|
|
@@ -273,12 +285,13 @@ async function runBrowserAppProof(options) {
|
|
|
273
285
|
const now = new Date();
|
|
274
286
|
const createdAt = now.toISOString();
|
|
275
287
|
const runId = options.runId ?? `browser-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
const packageName = await readPackageName(options.cwd);
|
|
279
|
-
const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
|
|
288
|
+
const packageName = await readPackageName(options.projectRoot);
|
|
289
|
+
const humanishSource = await implicitProjectDirectoryExists(options.projectRoot, "humanish") ? "present" : "missing";
|
|
280
290
|
const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
|
|
281
|
-
const selection = await loadDryRunSelection(options.
|
|
291
|
+
const selection = await loadDryRunSelection(options.projectRoot, humanishSource);
|
|
292
|
+
await assertPreparedSelectedOutputDirectory(options.projectRoot);
|
|
293
|
+
const runPaths = await prepareRunArtifactPaths(options.cwd, runId);
|
|
294
|
+
const artifactRoot = runPaths.relativeRunRoot;
|
|
282
295
|
if (selection.browserJourneyFailure) {
|
|
283
296
|
return {
|
|
284
297
|
schema: "humanish.run-result.v1",
|
|
@@ -299,17 +312,18 @@ async function runBrowserAppProof(options) {
|
|
|
299
312
|
warnings.push("No executable browser scenario manifest was found; using built-in browser persona two-step journey.");
|
|
300
313
|
}
|
|
301
314
|
warnings.push(...selection.warnings);
|
|
302
|
-
await
|
|
303
|
-
await
|
|
315
|
+
await prepareContainedOutputDirectory(runPaths, "screenshots");
|
|
316
|
+
await prepareContainedOutputDirectory(runPaths, "traces");
|
|
304
317
|
const surfaces = browserSurfaces.slice(0, options.simCount);
|
|
305
318
|
const captures = await Promise.all(surfaces.map((surface) => captureBrowserSurface({
|
|
306
|
-
absoluteArtifactRoot,
|
|
319
|
+
absoluteArtifactRoot: runPaths,
|
|
307
320
|
appUrl,
|
|
308
321
|
browserCommand,
|
|
309
322
|
browserJourney,
|
|
310
323
|
surface,
|
|
311
324
|
timeoutMs: options.timeoutMs ?? BROWSER_APP_DEFAULT_TIMEOUT_MS
|
|
312
325
|
})));
|
|
326
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
313
327
|
const completedAt = new Date().toISOString();
|
|
314
328
|
const events = buildBrowserAppEvents({ appUrl, captures, createdAt });
|
|
315
329
|
const allPassed = captures.every((capture) => capture.ok);
|
|
@@ -445,13 +459,13 @@ async function runBrowserAppProof(options) {
|
|
|
445
459
|
review,
|
|
446
460
|
feedbackCandidates: []
|
|
447
461
|
};
|
|
448
|
-
await writeRunBundleArtifacts(
|
|
449
|
-
await
|
|
462
|
+
await writeRunBundleArtifacts(runPaths, bundle);
|
|
463
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
450
464
|
schema: "humanish.latest-run.v1",
|
|
451
465
|
runId,
|
|
452
466
|
path: artifactRoot,
|
|
453
467
|
updatedAt: completedAt
|
|
454
|
-
});
|
|
468
|
+
}, null, 2)}\n`, "utf8");
|
|
455
469
|
return {
|
|
456
470
|
schema: "humanish.run-result.v1",
|
|
457
471
|
ok: allPassed,
|
|
@@ -462,7 +476,7 @@ async function runBrowserAppProof(options) {
|
|
|
462
476
|
artifactRoot,
|
|
463
477
|
bundlePath: path.join(artifactRoot, "run.json"),
|
|
464
478
|
reviewPath: path.join(artifactRoot, "review.md"),
|
|
465
|
-
latestPath:
|
|
479
|
+
latestPath: runPaths.relativeLatestPointer,
|
|
466
480
|
warnings,
|
|
467
481
|
...(allPassed
|
|
468
482
|
? {}
|
|
@@ -577,15 +591,28 @@ async function runLocalCodexTui(options) {
|
|
|
577
591
|
const now = new Date();
|
|
578
592
|
const createdAt = now.toISOString();
|
|
579
593
|
const runId = options.runId ?? `codex-tui-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
594
|
+
const usesDefaultCodexCommand = options.actorCommand === undefined && process.env.HUMANISH_CODEX_ACTOR_COMMAND === undefined;
|
|
595
|
+
const trustPreflight = usesDefaultCodexCommand ? await checkCodexWorkspaceTrust(options.cwd) : { ok: true };
|
|
596
|
+
const packageName = await readPackageName(options.projectRoot);
|
|
597
|
+
const humanishSource = await implicitProjectDirectoryExists(options.projectRoot, "humanish") ? "present" : "missing";
|
|
598
|
+
const source = !trustPreflight.ok && trustPreflight.unsafeMetadata
|
|
599
|
+
? {
|
|
600
|
+
packageName,
|
|
601
|
+
humanishSource,
|
|
602
|
+
git: {
|
|
603
|
+
schema: GIT_STATE_SCHEMA,
|
|
604
|
+
status: "unavailable",
|
|
605
|
+
capturedAt: createdAt,
|
|
606
|
+
head: { shortSha: null, refState: "unknown" },
|
|
607
|
+
changes: { staged: 0, unstaged: 0, untracked: 0, total: 0 },
|
|
608
|
+
note: "Git metadata failed containment validation."
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
: await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
|
|
612
|
+
const selection = await loadDryRunSelection(options.projectRoot, humanishSource);
|
|
613
|
+
await assertPreparedSelectedOutputDirectory(options.projectRoot);
|
|
614
|
+
const runPaths = await prepareRunArtifactPaths(options.cwd, runId);
|
|
615
|
+
const artifactRoot = runPaths.relativeRunRoot;
|
|
589
616
|
if (humanishSource === "missing") {
|
|
590
617
|
warnings.push("Committed humanish/ source was not found; using built-in synthetic local actor defaults.");
|
|
591
618
|
}
|
|
@@ -594,7 +621,6 @@ async function runLocalCodexTui(options) {
|
|
|
594
621
|
const prompt = buildLocalCodexTuiPrompt(selection, verdictNonce);
|
|
595
622
|
const promptDigest = digestText(prompt);
|
|
596
623
|
const command = resolveLocalCodexTuiCommand(options.cwd, prompt, options.actorCommand);
|
|
597
|
-
const usesDefaultCodexCommand = options.actorCommand === undefined && process.env.HUMANISH_CODEX_ACTOR_COMMAND === undefined;
|
|
598
624
|
const simId = "sim-01";
|
|
599
625
|
const streamId = "sim-01-codex-tui";
|
|
600
626
|
const events = [];
|
|
@@ -608,11 +634,9 @@ async function runLocalCodexTui(options) {
|
|
|
608
634
|
simId,
|
|
609
635
|
streamId
|
|
610
636
|
});
|
|
611
|
-
await
|
|
637
|
+
await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
612
638
|
};
|
|
613
|
-
await mkdir(path.dirname(transcriptPath), { recursive: true });
|
|
614
639
|
let actor;
|
|
615
|
-
const trustPreflight = usesDefaultCodexCommand ? await checkCodexWorkspaceTrust(options.cwd) : { ok: true };
|
|
616
640
|
if (!trustPreflight.ok) {
|
|
617
641
|
actor = {
|
|
618
642
|
durationMs: 0,
|
|
@@ -719,19 +743,20 @@ async function runLocalCodexTui(options) {
|
|
|
719
743
|
review: createLocalActorRunningReviewSummary("Codex TUI"),
|
|
720
744
|
feedbackCandidates: []
|
|
721
745
|
};
|
|
722
|
-
await writeRunBundleArtifacts(
|
|
723
|
-
await
|
|
746
|
+
await writeRunBundleArtifacts(runPaths, runningBundle);
|
|
747
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
724
748
|
schema: "humanish.latest-run.v1",
|
|
725
749
|
runId,
|
|
726
750
|
path: artifactRoot,
|
|
727
751
|
updatedAt: runningAt
|
|
728
|
-
});
|
|
752
|
+
}, null, 2)}\n`, "utf8");
|
|
729
753
|
actor = await executeLocalActorCommand(command, {
|
|
730
754
|
cwd: options.cwd,
|
|
731
755
|
timeoutMs,
|
|
732
756
|
verdictNonce
|
|
733
757
|
});
|
|
734
758
|
}
|
|
759
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
735
760
|
const completedAt = new Date().toISOString();
|
|
736
761
|
const redactedTranscript = redactSensitiveText(actor.transcript);
|
|
737
762
|
const tail = tailText(redactedTranscript, 6_000);
|
|
@@ -741,8 +766,8 @@ async function runLocalCodexTui(options) {
|
|
|
741
766
|
// block the reason embeds the absolute workspace path; the live CLI reason can
|
|
742
767
|
// still show the real path, but the public-safe bundle must not.
|
|
743
768
|
const verdictReason = redactSensitiveText(actor.reason);
|
|
744
|
-
await
|
|
745
|
-
await
|
|
769
|
+
await writeContainedOutputFile(runPaths, "transcripts/codex-tui-sanitized.txt", redactedTranscript.length > 0 ? redactedTranscript : "No transcript output captured.\n", "utf8");
|
|
770
|
+
await writeContainedOutputFile(runPaths, "actor.json", `${JSON.stringify({
|
|
746
771
|
schema: "humanish.local-codex-tui-actor.v1",
|
|
747
772
|
actor: "codex-tui",
|
|
748
773
|
commandName: command.name,
|
|
@@ -758,7 +783,7 @@ async function runLocalCodexTui(options) {
|
|
|
758
783
|
transcriptBytes: actor.transcriptBytes,
|
|
759
784
|
transcriptPath: "transcripts/codex-tui-sanitized.txt",
|
|
760
785
|
redaction: "passed"
|
|
761
|
-
});
|
|
786
|
+
}, null, 2)}\n`, "utf8");
|
|
762
787
|
await appendEvent("actor.observation", `Captured ${actor.transcriptBytes} output byte${actor.transcriptBytes === 1 ? "" : "s"}; sanitized transcript tail recorded with redaction=passed.`);
|
|
763
788
|
await appendEvent("actor.artifact", "Wrote sanitized Codex TUI transcript and actor trace artifacts under the ignored run directory.");
|
|
764
789
|
await appendEvent("actor.verdict", `Local Codex TUI actor verdict ${status}: ${verdictReason}`, status === "passed" ? "info" : status === "timed_out" ? "warn" : "error");
|
|
@@ -862,13 +887,13 @@ async function runLocalCodexTui(options) {
|
|
|
862
887
|
review: createLocalActorReviewSummary("Codex TUI", status, verdictReason),
|
|
863
888
|
feedbackCandidates: []
|
|
864
889
|
};
|
|
865
|
-
await writeRunBundleArtifacts(
|
|
866
|
-
await
|
|
890
|
+
await writeRunBundleArtifacts(runPaths, bundle);
|
|
891
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
867
892
|
schema: "humanish.latest-run.v1",
|
|
868
893
|
runId,
|
|
869
894
|
path: artifactRoot,
|
|
870
895
|
updatedAt: completedAt
|
|
871
|
-
});
|
|
896
|
+
}, null, 2)}\n`, "utf8");
|
|
872
897
|
return {
|
|
873
898
|
schema: "humanish.run-result.v1",
|
|
874
899
|
ok: status === "passed",
|
|
@@ -879,7 +904,7 @@ async function runLocalCodexTui(options) {
|
|
|
879
904
|
artifactRoot,
|
|
880
905
|
bundlePath: path.join(artifactRoot, "run.json"),
|
|
881
906
|
reviewPath: path.join(artifactRoot, "review.md"),
|
|
882
|
-
latestPath:
|
|
907
|
+
latestPath: runPaths.relativeLatestPointer,
|
|
883
908
|
warnings,
|
|
884
909
|
...(status === "passed"
|
|
885
910
|
? {}
|
|
@@ -996,13 +1021,13 @@ async function runLocalCodexExec(options) {
|
|
|
996
1021
|
const now = new Date();
|
|
997
1022
|
const createdAt = now.toISOString();
|
|
998
1023
|
const runId = options.runId ?? `codex-exec-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1001
|
-
const eventsPath = path.join(absoluteArtifactRoot, "events.ndjson");
|
|
1002
|
-
const packageName = await readPackageName(options.cwd);
|
|
1003
|
-
const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
|
|
1024
|
+
const packageName = await readPackageName(options.projectRoot);
|
|
1025
|
+
const humanishSource = await implicitProjectDirectoryExists(options.projectRoot, "humanish") ? "present" : "missing";
|
|
1004
1026
|
const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
|
|
1005
|
-
const selection = await loadDryRunSelection(options.
|
|
1027
|
+
const selection = await loadDryRunSelection(options.projectRoot, humanishSource);
|
|
1028
|
+
await assertPreparedSelectedOutputDirectory(options.projectRoot);
|
|
1029
|
+
const runPaths = await prepareRunArtifactPaths(options.cwd, runId);
|
|
1030
|
+
const artifactRoot = runPaths.relativeRunRoot;
|
|
1006
1031
|
if (humanishSource === "missing") {
|
|
1007
1032
|
warnings.push("Committed humanish/ source was not found; using built-in synthetic local actor defaults.");
|
|
1008
1033
|
}
|
|
@@ -1020,15 +1045,12 @@ async function runLocalCodexExec(options) {
|
|
|
1020
1045
|
...(streamId === undefined ? {} : { streamId })
|
|
1021
1046
|
});
|
|
1022
1047
|
};
|
|
1023
|
-
await
|
|
1024
|
-
await mkdir(path.join(absoluteArtifactRoot, "actors"), { recursive: true });
|
|
1025
|
-
await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
|
|
1026
|
-
await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
|
|
1048
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
1027
1049
|
schema: "humanish.latest-run.v1",
|
|
1028
1050
|
runId,
|
|
1029
1051
|
path: artifactRoot,
|
|
1030
1052
|
updatedAt: createdAt
|
|
1031
|
-
});
|
|
1053
|
+
}, null, 2)}\n`, "utf8");
|
|
1032
1054
|
const lanes = Array.from({ length: options.simCount }, (_, index) => {
|
|
1033
1055
|
const focus = localCodexExecFocus(index);
|
|
1034
1056
|
const simId = `sim-${String(index + 1).padStart(2, "0")}`;
|
|
@@ -1047,7 +1069,7 @@ async function runLocalCodexExec(options) {
|
|
|
1047
1069
|
for (const lane of lanes) {
|
|
1048
1070
|
pushEvent("actor.running", "Published local Codex exec running snapshot for Observer polling.", "info", lane.simId, lane.streamId);
|
|
1049
1071
|
}
|
|
1050
|
-
await
|
|
1072
|
+
await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
1051
1073
|
const baseLifecycle = [
|
|
1052
1074
|
{
|
|
1053
1075
|
at: createdAt,
|
|
@@ -1099,7 +1121,7 @@ async function runLocalCodexExec(options) {
|
|
|
1099
1121
|
events,
|
|
1100
1122
|
review: createLocalActorRunningReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout")
|
|
1101
1123
|
});
|
|
1102
|
-
await writeRunBundleArtifacts(
|
|
1124
|
+
await writeRunBundleArtifacts(runPaths, runningBundle);
|
|
1103
1125
|
const laneResults = await mapWithConcurrency(lanes, maxConcurrency, async (lane) => {
|
|
1104
1126
|
const actor = await executeLocalActorCommand(lane.command, {
|
|
1105
1127
|
cwd: options.cwd,
|
|
@@ -1112,30 +1134,12 @@ async function runLocalCodexExec(options) {
|
|
|
1112
1134
|
? "transcripts/codex-exec-sanitized.jsonl"
|
|
1113
1135
|
: `transcripts/${lane.streamId}-sanitized.jsonl`;
|
|
1114
1136
|
const tracePath = options.simCount === 1 ? "actor.json" : `actors/${lane.streamId}.json`;
|
|
1115
|
-
await writeFile(path.join(absoluteArtifactRoot, transcriptPath), redactedTranscript.length > 0 ? redactedTranscript : "No transcript output captured.\n", "utf8");
|
|
1116
|
-
await writeJson(path.join(absoluteArtifactRoot, tracePath), {
|
|
1117
|
-
schema: "humanish.local-codex-exec-actor.v1",
|
|
1118
|
-
actor: "codex-exec",
|
|
1119
|
-
commandName: lane.command.name,
|
|
1120
|
-
focusId: lane.focus.id,
|
|
1121
|
-
promptDigest: lane.promptDigest,
|
|
1122
|
-
verdictNonce,
|
|
1123
|
-
startedAt: createdAt,
|
|
1124
|
-
completedAt: new Date().toISOString(),
|
|
1125
|
-
durationMs: actor.durationMs,
|
|
1126
|
-
exitCode: actor.exitCode,
|
|
1127
|
-
signal: actor.signal,
|
|
1128
|
-
status: actor.status,
|
|
1129
|
-
timeoutMs,
|
|
1130
|
-
transcriptBytes: actor.transcriptBytes,
|
|
1131
|
-
transcriptPath,
|
|
1132
|
-
redaction: "passed"
|
|
1133
|
-
});
|
|
1134
1137
|
return {
|
|
1135
1138
|
actor,
|
|
1136
1139
|
command: lane.command,
|
|
1137
1140
|
focus: lane.focus,
|
|
1138
1141
|
promptDigest: lane.promptDigest,
|
|
1142
|
+
redactedTranscript,
|
|
1139
1143
|
simId: lane.simId,
|
|
1140
1144
|
streamId: lane.streamId,
|
|
1141
1145
|
tail,
|
|
@@ -1144,6 +1148,28 @@ async function runLocalCodexExec(options) {
|
|
|
1144
1148
|
};
|
|
1145
1149
|
});
|
|
1146
1150
|
const completedAt = new Date().toISOString();
|
|
1151
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
1152
|
+
for (const result of laneResults) {
|
|
1153
|
+
await writeContainedOutputFile(runPaths, result.transcriptPath, result.redactedTranscript.length > 0 ? result.redactedTranscript : "No transcript output captured.\n", "utf8");
|
|
1154
|
+
await writeContainedOutputFile(runPaths, result.tracePath, `${JSON.stringify({
|
|
1155
|
+
schema: "humanish.local-codex-exec-actor.v1",
|
|
1156
|
+
actor: "codex-exec",
|
|
1157
|
+
commandName: result.command.name,
|
|
1158
|
+
focusId: result.focus.id,
|
|
1159
|
+
promptDigest: result.promptDigest,
|
|
1160
|
+
verdictNonce,
|
|
1161
|
+
startedAt: createdAt,
|
|
1162
|
+
completedAt,
|
|
1163
|
+
durationMs: result.actor.durationMs,
|
|
1164
|
+
exitCode: result.actor.exitCode,
|
|
1165
|
+
signal: result.actor.signal,
|
|
1166
|
+
status: result.actor.status,
|
|
1167
|
+
timeoutMs,
|
|
1168
|
+
transcriptBytes: result.actor.transcriptBytes,
|
|
1169
|
+
transcriptPath: result.transcriptPath,
|
|
1170
|
+
redaction: "passed"
|
|
1171
|
+
}, null, 2)}\n`, "utf8");
|
|
1172
|
+
}
|
|
1147
1173
|
const laneStatuses = laneResults.map((result) => result.actor.status);
|
|
1148
1174
|
const status = aggregateActorStatus(laneStatuses);
|
|
1149
1175
|
const verdictReason = options.simCount === 1
|
|
@@ -1157,7 +1183,7 @@ async function runLocalCodexExec(options) {
|
|
|
1157
1183
|
? `Actor timed out after ${timeoutMs}ms; last safe observation retained.`
|
|
1158
1184
|
: `Actor exited with code ${result.actor.exitCode ?? "null"}${result.actor.signal ? ` and signal ${result.actor.signal}` : ""}.`, result.actor.status === "passed" ? "info" : "warn", result.simId, result.streamId);
|
|
1159
1185
|
}
|
|
1160
|
-
await
|
|
1186
|
+
await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
1161
1187
|
const bundle = buildLocalCodexExecBundle({
|
|
1162
1188
|
runId,
|
|
1163
1189
|
simCount: options.simCount,
|
|
@@ -1200,7 +1226,13 @@ async function runLocalCodexExec(options) {
|
|
|
1200
1226
|
events,
|
|
1201
1227
|
review: createLocalActorReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout", status, verdictReason)
|
|
1202
1228
|
});
|
|
1203
|
-
await writeRunBundleArtifacts(
|
|
1229
|
+
await writeRunBundleArtifacts(runPaths, bundle);
|
|
1230
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
1231
|
+
schema: "humanish.latest-run.v1",
|
|
1232
|
+
runId,
|
|
1233
|
+
path: artifactRoot,
|
|
1234
|
+
updatedAt: completedAt
|
|
1235
|
+
}, null, 2)}\n`, "utf8");
|
|
1204
1236
|
return {
|
|
1205
1237
|
schema: "humanish.run-result.v1",
|
|
1206
1238
|
ok: status === "passed",
|
|
@@ -1211,7 +1243,7 @@ async function runLocalCodexExec(options) {
|
|
|
1211
1243
|
artifactRoot,
|
|
1212
1244
|
bundlePath: path.join(artifactRoot, "run.json"),
|
|
1213
1245
|
reviewPath: path.join(artifactRoot, "review.md"),
|
|
1214
|
-
latestPath:
|
|
1246
|
+
latestPath: runPaths.relativeLatestPointer,
|
|
1215
1247
|
warnings,
|
|
1216
1248
|
...(status === "passed"
|
|
1217
1249
|
? {}
|
|
@@ -1342,13 +1374,13 @@ async function runLocalCodexAppServer(options) {
|
|
|
1342
1374
|
const now = new Date();
|
|
1343
1375
|
const createdAt = now.toISOString();
|
|
1344
1376
|
const runId = options.runId ?? `codex-app-server-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
1347
|
-
const eventsPath = path.join(absoluteArtifactRoot, "events.ndjson");
|
|
1348
|
-
const packageName = await readPackageName(options.cwd);
|
|
1349
|
-
const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
|
|
1377
|
+
const packageName = await readPackageName(options.projectRoot);
|
|
1378
|
+
const humanishSource = await implicitProjectDirectoryExists(options.projectRoot, "humanish") ? "present" : "missing";
|
|
1350
1379
|
const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
|
|
1351
|
-
const selection = await loadDryRunSelection(options.
|
|
1380
|
+
const selection = await loadDryRunSelection(options.projectRoot, humanishSource);
|
|
1381
|
+
await assertPreparedSelectedOutputDirectory(options.projectRoot);
|
|
1382
|
+
const runPaths = await prepareRunArtifactPaths(options.cwd, runId);
|
|
1383
|
+
const artifactRoot = runPaths.relativeRunRoot;
|
|
1352
1384
|
if (humanishSource === "missing") {
|
|
1353
1385
|
warnings.push("Committed humanish/ source was not found; using built-in synthetic Codex app-server actor defaults.");
|
|
1354
1386
|
}
|
|
@@ -1365,14 +1397,12 @@ async function runLocalCodexAppServer(options) {
|
|
|
1365
1397
|
...(streamId === undefined ? {} : { streamId })
|
|
1366
1398
|
});
|
|
1367
1399
|
};
|
|
1368
|
-
await
|
|
1369
|
-
await mkdir(path.join(absoluteArtifactRoot, "actors"), { recursive: true });
|
|
1370
|
-
await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
|
|
1400
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
1371
1401
|
schema: "humanish.latest-run.v1",
|
|
1372
1402
|
runId,
|
|
1373
1403
|
path: artifactRoot,
|
|
1374
1404
|
updatedAt: createdAt
|
|
1375
|
-
});
|
|
1405
|
+
}, null, 2)}\n`, "utf8");
|
|
1376
1406
|
const lanes = Array.from({ length: options.simCount }, (_, index) => {
|
|
1377
1407
|
const focus = localCodexExecFocus(index);
|
|
1378
1408
|
const simId = `sim-${String(index + 1).padStart(2, "0")}`;
|
|
@@ -1397,7 +1427,7 @@ async function runLocalCodexAppServer(options) {
|
|
|
1397
1427
|
for (const lane of lanes) {
|
|
1398
1428
|
pushEvent("codex-app-server.running", "Published Codex app-server running snapshot for Observer polling.", "info", lane.simId, lane.streamId);
|
|
1399
1429
|
}
|
|
1400
|
-
await
|
|
1430
|
+
await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
1401
1431
|
const baseLifecycle = [
|
|
1402
1432
|
{
|
|
1403
1433
|
at: createdAt,
|
|
@@ -1450,13 +1480,15 @@ async function runLocalCodexAppServer(options) {
|
|
|
1450
1480
|
events,
|
|
1451
1481
|
review: createLocalActorRunningReviewSummary("Codex app-server")
|
|
1452
1482
|
});
|
|
1453
|
-
await writeRunBundleArtifacts(
|
|
1483
|
+
await writeRunBundleArtifacts(runPaths, runningBundle);
|
|
1454
1484
|
const laneResults = await mapWithConcurrency(lanes, options.simCount, async (lane) => {
|
|
1455
|
-
const laneRunRoot = lane.prefix
|
|
1456
|
-
|
|
1485
|
+
const laneRunRoot = lane.prefix
|
|
1486
|
+
? await prepareContainedOutputDirectoryRoot(runPaths, lane.prefix)
|
|
1487
|
+
: runPaths;
|
|
1488
|
+
const sessionOptions = {
|
|
1457
1489
|
cwd: options.cwd,
|
|
1458
1490
|
prompt: lane.prompt,
|
|
1459
|
-
runRoot: laneRunRoot,
|
|
1491
|
+
runRoot: "physicalRunRoot" in laneRunRoot ? laneRunRoot.physicalRunRoot : laneRunRoot.physicalPath,
|
|
1460
1492
|
timeoutMs,
|
|
1461
1493
|
...(options.actorCommand === undefined ? {} : { actorCommand: options.actorCommand }),
|
|
1462
1494
|
approvalPolicy: "never",
|
|
@@ -1464,12 +1496,14 @@ async function runLocalCodexAppServer(options) {
|
|
|
1464
1496
|
...(process.env.HUMANISH_CODEX_APP_SERVER_MODEL ? { model: process.env.HUMANISH_CODEX_APP_SERVER_MODEL } : {}),
|
|
1465
1497
|
sandbox: readCodexAppServerSandboxFromEnv(),
|
|
1466
1498
|
serviceName: "humanish"
|
|
1467
|
-
}
|
|
1499
|
+
};
|
|
1500
|
+
const result = await runCodexAppServerSessionInPreparedRoot(sessionOptions, laneRunRoot);
|
|
1468
1501
|
return {
|
|
1469
1502
|
lane,
|
|
1470
1503
|
result: prefixCodexAppServerResultPaths(result, lane.prefix)
|
|
1471
1504
|
};
|
|
1472
1505
|
});
|
|
1506
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
1473
1507
|
const completedAt = new Date().toISOString();
|
|
1474
1508
|
const statuses = laneResults.map((entry) => entry.result.status);
|
|
1475
1509
|
const status = aggregateActorStatus(statuses);
|
|
@@ -1480,7 +1514,7 @@ async function runLocalCodexAppServer(options) {
|
|
|
1480
1514
|
pushEvent("codex-app-server.artifact", `Captured ${entry.result.counts.envelopes} app-server envelope${entry.result.counts.envelopes === 1 ? "" : "s"}; trace summary and transcript were redacted before persistence.`, "info", entry.lane.simId, entry.lane.streamId);
|
|
1481
1515
|
pushEvent("codex-app-server.verdict", `Codex app-server actor lane ${entry.lane.focus.label} verdict ${entry.result.status}: ${entry.result.reason}`, entry.result.status === "passed" ? "info" : entry.result.status === "timed_out" ? "warn" : "error", entry.lane.simId, entry.lane.streamId);
|
|
1482
1516
|
}
|
|
1483
|
-
await
|
|
1517
|
+
await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
1484
1518
|
const bundle = buildLocalCodexAppServerBundle({
|
|
1485
1519
|
runId,
|
|
1486
1520
|
simCount: options.simCount,
|
|
@@ -1523,7 +1557,13 @@ async function runLocalCodexAppServer(options) {
|
|
|
1523
1557
|
events,
|
|
1524
1558
|
review: createLocalActorReviewSummary(options.simCount === 1 ? "Codex app-server" : "Codex app-server fanout", status, verdictReason)
|
|
1525
1559
|
});
|
|
1526
|
-
await writeRunBundleArtifacts(
|
|
1560
|
+
await writeRunBundleArtifacts(runPaths, bundle);
|
|
1561
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
1562
|
+
schema: "humanish.latest-run.v1",
|
|
1563
|
+
runId,
|
|
1564
|
+
path: artifactRoot,
|
|
1565
|
+
updatedAt: completedAt
|
|
1566
|
+
}, null, 2)}\n`, "utf8");
|
|
1527
1567
|
return {
|
|
1528
1568
|
schema: "humanish.run-result.v1",
|
|
1529
1569
|
ok: status === "passed",
|
|
@@ -1534,7 +1574,7 @@ async function runLocalCodexAppServer(options) {
|
|
|
1534
1574
|
artifactRoot,
|
|
1535
1575
|
bundlePath: path.join(artifactRoot, "run.json"),
|
|
1536
1576
|
reviewPath: path.join(artifactRoot, "review.md"),
|
|
1537
|
-
latestPath:
|
|
1577
|
+
latestPath: runPaths.relativeLatestPointer,
|
|
1538
1578
|
warnings,
|
|
1539
1579
|
...(status === "passed"
|
|
1540
1580
|
? {}
|
|
@@ -1934,13 +1974,23 @@ async function checkCodexWorkspaceTrust(cwd) {
|
|
|
1934
1974
|
if (process.env.HUMANISH_SKIP_CODEX_TRUST_PREFLIGHT === "1") {
|
|
1935
1975
|
return { ok: true };
|
|
1936
1976
|
}
|
|
1937
|
-
const
|
|
1938
|
-
if (!
|
|
1977
|
+
const detected = await detectCodexTrustRoot(cwd);
|
|
1978
|
+
if (!detected) {
|
|
1939
1979
|
return { ok: true };
|
|
1940
1980
|
}
|
|
1981
|
+
if (detected.unsafe) {
|
|
1982
|
+
return {
|
|
1983
|
+
ok: false,
|
|
1984
|
+
trustRoot: detected.worktreeRoot,
|
|
1985
|
+
unsafeMetadata: true,
|
|
1986
|
+
message: `Codex workspace trust preflight blocked local TUI launch; Git worktree metadata failed containment validation: ${detected.worktreeRoot}`,
|
|
1987
|
+
recoveryCommand: `codex --no-alt-screen -C ${shellQuote(detected.worktreeRoot)}`
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
const trustRoot = detected.trustRoot;
|
|
1941
1991
|
const configPath = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "config.toml");
|
|
1942
1992
|
const configText = await readTextIfExists(configPath);
|
|
1943
|
-
if (configText && codexConfigTrustsProject(configText, trustRoot)) {
|
|
1993
|
+
if (configText && await codexConfigTrustsProject(configText, trustRoot)) {
|
|
1944
1994
|
return { ok: true };
|
|
1945
1995
|
}
|
|
1946
1996
|
return {
|
|
@@ -1951,42 +2001,16 @@ async function checkCodexWorkspaceTrust(cwd) {
|
|
|
1951
2001
|
};
|
|
1952
2002
|
}
|
|
1953
2003
|
async function detectCodexTrustRoot(cwd) {
|
|
1954
|
-
const
|
|
1955
|
-
if (
|
|
2004
|
+
const inspection = await inspectVerifiedGitWorkspace(cwd);
|
|
2005
|
+
if (inspection.status === "missing") {
|
|
1956
2006
|
return null;
|
|
1957
2007
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
return worktreeRoot;
|
|
1961
|
-
}
|
|
1962
|
-
const gitFile = await readTextIfExists(dotGitPath);
|
|
1963
|
-
if (!gitFile?.startsWith("gitdir:")) {
|
|
1964
|
-
return worktreeRoot;
|
|
1965
|
-
}
|
|
1966
|
-
const gitDir = gitFile.slice("gitdir:".length).trim();
|
|
1967
|
-
const absoluteGitDir = path.isAbsolute(gitDir) ? gitDir : path.resolve(worktreeRoot, gitDir);
|
|
1968
|
-
const commonDirText = await readTextIfExists(path.join(absoluteGitDir, "commondir"));
|
|
1969
|
-
if (!commonDirText) {
|
|
1970
|
-
return worktreeRoot;
|
|
1971
|
-
}
|
|
1972
|
-
const commonDir = commonDirText.trim();
|
|
1973
|
-
const absoluteCommonDir = path.resolve(absoluteGitDir, commonDir);
|
|
1974
|
-
return path.basename(absoluteCommonDir) === ".git" ? path.dirname(absoluteCommonDir) : worktreeRoot;
|
|
1975
|
-
}
|
|
1976
|
-
async function findGitWorktreeRoot(cwd) {
|
|
1977
|
-
let current = path.resolve(cwd);
|
|
1978
|
-
while (true) {
|
|
1979
|
-
if (await fileExists(path.join(current, ".git")) || await directoryExists(path.join(current, ".git"))) {
|
|
1980
|
-
return current;
|
|
1981
|
-
}
|
|
1982
|
-
const parent = path.dirname(current);
|
|
1983
|
-
if (parent === current) {
|
|
1984
|
-
return null;
|
|
1985
|
-
}
|
|
1986
|
-
current = parent;
|
|
2008
|
+
if (inspection.status === "unsafe") {
|
|
2009
|
+
return { unsafe: true, worktreeRoot: inspection.worktreeRoot };
|
|
1987
2010
|
}
|
|
2011
|
+
return { unsafe: false, trustRoot: inspection.workspace.trustRoot };
|
|
1988
2012
|
}
|
|
1989
|
-
function codexConfigTrustsProject(configText, trustRoot) {
|
|
2013
|
+
async function codexConfigTrustsProject(configText, trustRoot) {
|
|
1990
2014
|
const sectionPattern = /^\[projects\."((?:\\.|[^"\\])*)"\]\s*$/gm;
|
|
1991
2015
|
let sectionMatch;
|
|
1992
2016
|
while ((sectionMatch = sectionPattern.exec(configText)) !== null) {
|
|
@@ -1994,7 +2018,7 @@ function codexConfigTrustsProject(configText, trustRoot) {
|
|
|
1994
2018
|
const afterSection = configText.slice(sectionMatch.index + sectionMatch[0].length);
|
|
1995
2019
|
const nextSectionIndex = afterSection.search(/^\[/m);
|
|
1996
2020
|
const sectionBody = nextSectionIndex === -1 ? afterSection : afterSection.slice(0, nextSectionIndex);
|
|
1997
|
-
if (/^trust_level\s*=\s*"trusted"\s*$/m.test(sectionBody) &&
|
|
2021
|
+
if (/^trust_level\s*=\s*"trusted"\s*$/m.test(sectionBody) && await isSamePhysicalPath(projectPath, trustRoot)) {
|
|
1998
2022
|
return true;
|
|
1999
2023
|
}
|
|
2000
2024
|
}
|
|
@@ -2003,10 +2027,17 @@ function codexConfigTrustsProject(configText, trustRoot) {
|
|
|
2003
2027
|
function unescapeTomlString(value) {
|
|
2004
2028
|
return value.replace(/\\(["\\])/g, "$1");
|
|
2005
2029
|
}
|
|
2006
|
-
function
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2030
|
+
async function isSamePhysicalPath(candidatePath, targetPath) {
|
|
2031
|
+
try {
|
|
2032
|
+
const [candidate, target] = await Promise.all([
|
|
2033
|
+
realpath(path.resolve(candidatePath)),
|
|
2034
|
+
realpath(path.resolve(targetPath))
|
|
2035
|
+
]);
|
|
2036
|
+
return candidate === target;
|
|
2037
|
+
}
|
|
2038
|
+
catch {
|
|
2039
|
+
return false;
|
|
2040
|
+
}
|
|
2010
2041
|
}
|
|
2011
2042
|
function normalizeActorTimeout(value) {
|
|
2012
2043
|
if (normalizePositiveInteger(value) === null || value === undefined || value > LOCAL_CODEX_TUI_MAX_TIMEOUT_MS) {
|
|
@@ -2251,9 +2282,40 @@ function normalizeSimCount(value) {
|
|
|
2251
2282
|
}
|
|
2252
2283
|
export async function verifyRun(cwdInput, runInput) {
|
|
2253
2284
|
const cwd = path.resolve(cwdInput);
|
|
2285
|
+
let runPaths;
|
|
2286
|
+
try {
|
|
2287
|
+
runPaths = await resolveRunPath(cwd, runInput);
|
|
2288
|
+
}
|
|
2289
|
+
catch {
|
|
2290
|
+
return invalidRunStorageVerifyResult(cwd, runInput);
|
|
2291
|
+
}
|
|
2292
|
+
return verifyPreparedRun(cwd, runInput, runPaths);
|
|
2293
|
+
}
|
|
2294
|
+
function invalidRunStorageVerifyResult(cwd, runInput) {
|
|
2295
|
+
return {
|
|
2296
|
+
schema: VERIFY_SCHEMA,
|
|
2297
|
+
ok: false,
|
|
2298
|
+
cwd,
|
|
2299
|
+
run: runInput,
|
|
2300
|
+
checks: [{
|
|
2301
|
+
name: "run storage containment",
|
|
2302
|
+
ok: false,
|
|
2303
|
+
message: "run storage must contain only identity-bound directories and single-link regular files"
|
|
2304
|
+
}],
|
|
2305
|
+
shareSafety: {
|
|
2306
|
+
status: "blocked",
|
|
2307
|
+
reasons: [{ code: "VERIFY_FAILED", message: "Run storage failed containment validation." }]
|
|
2308
|
+
},
|
|
2309
|
+
warnings: [],
|
|
2310
|
+
error: {
|
|
2311
|
+
code: "HUMANISH_INVALID_RUN_BUNDLE",
|
|
2312
|
+
message: "Run storage failed containment validation."
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
async function verifyPreparedRun(cwd, runInput, runPaths) {
|
|
2254
2317
|
const checks = [];
|
|
2255
|
-
|
|
2256
|
-
if (!resolved) {
|
|
2318
|
+
if (!runPaths) {
|
|
2257
2319
|
return {
|
|
2258
2320
|
schema: VERIFY_SCHEMA,
|
|
2259
2321
|
ok: false,
|
|
@@ -2276,11 +2338,11 @@ export async function verifyRun(cwdInput, runInput) {
|
|
|
2276
2338
|
}
|
|
2277
2339
|
};
|
|
2278
2340
|
}
|
|
2279
|
-
const bundlePath = path.join(
|
|
2280
|
-
const bundle = await
|
|
2281
|
-
const cleanupJson = await
|
|
2282
|
-
const reviewJson = await
|
|
2283
|
-
const reviewMarkdown = await
|
|
2341
|
+
const bundlePath = path.join(runPaths.absoluteRunRoot, "run.json");
|
|
2342
|
+
const bundle = await readRunJsonIfExists(runPaths, "run.json");
|
|
2343
|
+
const cleanupJson = await readRunJsonIfExists(runPaths, "cleanup.json");
|
|
2344
|
+
const reviewJson = await readRunJsonIfExists(runPaths, "review.json");
|
|
2345
|
+
const reviewMarkdown = await readRunTextIfExists(runPaths, "review.md");
|
|
2284
2346
|
checks.push({
|
|
2285
2347
|
name: "run.json exists",
|
|
2286
2348
|
ok: bundle !== null,
|
|
@@ -2306,7 +2368,7 @@ export async function verifyRun(cwdInput, runInput) {
|
|
|
2306
2368
|
ok: reviewJson !== null && reviewMarkdown !== null,
|
|
2307
2369
|
message: "review.json and review.md must exist"
|
|
2308
2370
|
});
|
|
2309
|
-
const publicSafetyFindings = await scanRunPublicSafetyArtifacts(
|
|
2371
|
+
const publicSafetyFindings = await scanRunPublicSafetyArtifacts(runPaths);
|
|
2310
2372
|
checks.push({
|
|
2311
2373
|
name: "public-safety scan",
|
|
2312
2374
|
ok: publicSafetyFindings.length === 0,
|
|
@@ -2315,7 +2377,7 @@ export async function verifyRun(cwdInput, runInput) {
|
|
|
2315
2377
|
: `public-safety findings: ${publicSafetyFindings.slice(0, 5).join(", ")}`
|
|
2316
2378
|
});
|
|
2317
2379
|
const missingEvidenceArtifacts = isRunBundle(bundle)
|
|
2318
|
-
? await missingLocalEvidenceArtifacts(
|
|
2380
|
+
? await missingLocalEvidenceArtifacts(runPaths, bundle)
|
|
2319
2381
|
: [];
|
|
2320
2382
|
const invalidEvidenceReferences = isRunBundle(bundle)
|
|
2321
2383
|
? invalidRunEvidenceReferences(bundle)
|
|
@@ -2330,7 +2392,7 @@ export async function verifyRun(cwdInput, runInput) {
|
|
|
2330
2392
|
: `missing local evidence artifacts: ${missingEvidenceArtifacts.join(", ")}`
|
|
2331
2393
|
});
|
|
2332
2394
|
const terminalProductFindings = isRunBundle(bundle)
|
|
2333
|
-
? await validateTerminalProductEvidence(
|
|
2395
|
+
? await validateTerminalProductEvidence(runPaths, bundle)
|
|
2334
2396
|
: [];
|
|
2335
2397
|
checks.push({
|
|
2336
2398
|
name: "terminal-product evidence",
|
|
@@ -2340,7 +2402,7 @@ export async function verifyRun(cwdInput, runInput) {
|
|
|
2340
2402
|
: `terminal-product findings: ${terminalProductFindings.join(", ")}`
|
|
2341
2403
|
});
|
|
2342
2404
|
const codexAppServerFindings = isRunBundle(bundle)
|
|
2343
|
-
? await validateCodexAppServerEvidence(
|
|
2405
|
+
? await validateCodexAppServerEvidence(runPaths, bundle)
|
|
2344
2406
|
: [];
|
|
2345
2407
|
checks.push({
|
|
2346
2408
|
name: "codex app-server evidence",
|
|
@@ -2435,7 +2497,27 @@ export async function verifyRun(cwdInput, runInput) {
|
|
|
2435
2497
|
export async function cleanupRun(cwdInput, runInput, hooks = {}) {
|
|
2436
2498
|
const cwd = path.resolve(cwdInput);
|
|
2437
2499
|
const checkedAt = (hooks.now ?? (() => new Date()))().toISOString();
|
|
2438
|
-
|
|
2500
|
+
let resolved;
|
|
2501
|
+
try {
|
|
2502
|
+
resolved = await resolveRunPath(cwd, runInput);
|
|
2503
|
+
}
|
|
2504
|
+
catch {
|
|
2505
|
+
return {
|
|
2506
|
+
schema: CLEANUP_SCHEMA,
|
|
2507
|
+
ok: false,
|
|
2508
|
+
cwd,
|
|
2509
|
+
run: runInput,
|
|
2510
|
+
checkedAt,
|
|
2511
|
+
summary: { resources: 0, killed: 0, alreadyClean: 0, failed: 0, skipped: 0 },
|
|
2512
|
+
resources: [],
|
|
2513
|
+
adapterResults: [],
|
|
2514
|
+
warnings: [],
|
|
2515
|
+
error: {
|
|
2516
|
+
code: "HUMANISH_INVALID_RUN_BUNDLE",
|
|
2517
|
+
message: "Run storage failed containment validation."
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2439
2521
|
if (!resolved) {
|
|
2440
2522
|
return {
|
|
2441
2523
|
schema: CLEANUP_SCHEMA,
|
|
@@ -2453,9 +2535,20 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
|
|
|
2453
2535
|
}
|
|
2454
2536
|
};
|
|
2455
2537
|
}
|
|
2456
|
-
const
|
|
2457
|
-
const
|
|
2458
|
-
const
|
|
2538
|
+
const runPaths = resolved;
|
|
2539
|
+
const bundlePath = path.join(runPaths.absoluteRunRoot, "run.json");
|
|
2540
|
+
const cleanupPath = path.join(runPaths.absoluteRunRoot, "cleanup.json");
|
|
2541
|
+
await prepareContainedOutputFile(runPaths, "cleanup.json");
|
|
2542
|
+
const bundleBytes = await readContainedRegularFile(runPaths, "run.json");
|
|
2543
|
+
let bundle = null;
|
|
2544
|
+
if (bundleBytes) {
|
|
2545
|
+
try {
|
|
2546
|
+
bundle = JSON.parse(bundleBytes.toString("utf8"));
|
|
2547
|
+
}
|
|
2548
|
+
catch {
|
|
2549
|
+
bundle = null;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2459
2552
|
if (!isRunBundle(bundle)) {
|
|
2460
2553
|
return {
|
|
2461
2554
|
schema: CLEANUP_SCHEMA,
|
|
@@ -2476,7 +2569,6 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
|
|
|
2476
2569
|
}
|
|
2477
2570
|
const resources = [];
|
|
2478
2571
|
const warnings = [];
|
|
2479
|
-
let desktopModule = null;
|
|
2480
2572
|
const providerResources = bundle.providerResources ?? [];
|
|
2481
2573
|
for (const resource of providerResources) {
|
|
2482
2574
|
if (resource.provider !== "e2b-desktop" || resource.kind !== "sandbox") {
|
|
@@ -2499,41 +2591,22 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
|
|
|
2499
2591
|
});
|
|
2500
2592
|
continue;
|
|
2501
2593
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
status: "failed",
|
|
2510
|
-
message: "installed @e2b/desktop SDK does not expose Sandbox.kill"
|
|
2511
|
-
});
|
|
2512
|
-
continue;
|
|
2513
|
-
}
|
|
2514
|
-
await desktopModule.Sandbox.kill(resource.id, { requestTimeoutMs: 60_000 });
|
|
2515
|
-
resources.push({
|
|
2516
|
-
provider: resource.provider,
|
|
2517
|
-
kind: resource.kind,
|
|
2518
|
-
id: resource.id,
|
|
2519
|
-
status: "killed",
|
|
2520
|
-
message: "resource killed by exact recorded id"
|
|
2521
|
-
});
|
|
2522
|
-
}
|
|
2523
|
-
catch (error) {
|
|
2524
|
-
resources.push({
|
|
2525
|
-
provider: resource.provider,
|
|
2526
|
-
kind: resource.kind,
|
|
2527
|
-
id: resource.id,
|
|
2528
|
-
status: "failed",
|
|
2529
|
-
message: error instanceof Error ? error.message : String(error)
|
|
2530
|
-
});
|
|
2531
|
-
}
|
|
2594
|
+
resources.push({
|
|
2595
|
+
provider: resource.provider,
|
|
2596
|
+
kind: resource.kind,
|
|
2597
|
+
id: resource.id,
|
|
2598
|
+
status: "failed",
|
|
2599
|
+
message: "automatic provider cleanup requires a verified resource lease"
|
|
2600
|
+
});
|
|
2532
2601
|
}
|
|
2533
2602
|
let adapterResults = [];
|
|
2534
2603
|
if (hooks.cleanupAdapterResources) {
|
|
2535
2604
|
try {
|
|
2536
|
-
adapterResults = await hooks.cleanupAdapterResources({
|
|
2605
|
+
adapterResults = await hooks.cleanupAdapterResources({
|
|
2606
|
+
cwd,
|
|
2607
|
+
runDir: runPaths.physicalRunRoot,
|
|
2608
|
+
bundle
|
|
2609
|
+
});
|
|
2537
2610
|
}
|
|
2538
2611
|
catch (error) {
|
|
2539
2612
|
adapterResults = [{
|
|
@@ -2542,9 +2615,10 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
|
|
|
2542
2615
|
message: error instanceof Error ? error.message : String(error)
|
|
2543
2616
|
}];
|
|
2544
2617
|
}
|
|
2618
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
2545
2619
|
}
|
|
2546
2620
|
if (providerResources.length === 0 && adapterResults.length === 0) {
|
|
2547
|
-
warnings.push("Run bundle recorded no
|
|
2621
|
+
warnings.push("Run bundle recorded no provider resource evidence; nothing to inspect.");
|
|
2548
2622
|
}
|
|
2549
2623
|
const summary = {
|
|
2550
2624
|
resources: resources.length,
|
|
@@ -2568,35 +2642,58 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
|
|
|
2568
2642
|
adapterResults,
|
|
2569
2643
|
warnings
|
|
2570
2644
|
};
|
|
2571
|
-
await
|
|
2645
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
2646
|
+
await writeContainedOutputFile(runPaths, "cleanup.json", `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
2572
2647
|
return result;
|
|
2573
2648
|
}
|
|
2574
2649
|
export async function loadRunBundle(cwdInput, runInput) {
|
|
2575
2650
|
const cwd = path.resolve(cwdInput);
|
|
2576
|
-
const
|
|
2577
|
-
if (!
|
|
2651
|
+
const runPaths = await resolveRunPath(cwd, runInput).catch(() => null);
|
|
2652
|
+
if (!runPaths) {
|
|
2578
2653
|
return null;
|
|
2579
2654
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2655
|
+
return loadRunBundlePrepared(cwd, runPaths);
|
|
2656
|
+
}
|
|
2657
|
+
/** Internal continuity seam for callers that already bound one run identity. */
|
|
2658
|
+
export async function loadRunBundlePrepared(cwdInput, runPaths) {
|
|
2659
|
+
const cwd = path.resolve(cwdInput);
|
|
2660
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
2661
|
+
const bundlePath = path.join(runPaths.absoluteRunRoot, "run.json");
|
|
2662
|
+
const bundle = await readRunJsonIfExists(runPaths, "run.json");
|
|
2582
2663
|
if (!isRunBundle(bundle)) {
|
|
2583
2664
|
return null;
|
|
2584
2665
|
}
|
|
2585
2666
|
return {
|
|
2586
2667
|
bundle,
|
|
2587
2668
|
bundlePath: path.relative(cwd, bundlePath),
|
|
2588
|
-
runDir:
|
|
2669
|
+
runDir: runPaths.absoluteRunRoot
|
|
2589
2670
|
};
|
|
2590
2671
|
}
|
|
2672
|
+
/** Internal continuity seam for callers that already bound one run identity. */
|
|
2673
|
+
export async function verifyRunPrepared(cwdInput, runInput, runPaths) {
|
|
2674
|
+
const cwd = path.resolve(cwdInput);
|
|
2675
|
+
try {
|
|
2676
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
2677
|
+
}
|
|
2678
|
+
catch {
|
|
2679
|
+
return invalidRunStorageVerifyResult(cwd, runInput);
|
|
2680
|
+
}
|
|
2681
|
+
return verifyPreparedRun(cwd, runInput, runPaths);
|
|
2682
|
+
}
|
|
2591
2683
|
export async function listRuns(cwdInput) {
|
|
2592
2684
|
const cwd = path.resolve(cwdInput);
|
|
2593
|
-
const
|
|
2685
|
+
const runsRootPath = resolveRunsRoot(cwd);
|
|
2594
2686
|
// ENOENT (no .humanish/runs yet) is a normal empty state: ok:true, no runs. Any
|
|
2595
2687
|
// other readdir failure (e.g. permission denied) is a real I/O failure and must
|
|
2596
2688
|
// not be swallowed into a false "no runs" report.
|
|
2597
2689
|
let entries;
|
|
2690
|
+
let runsRoot = null;
|
|
2598
2691
|
try {
|
|
2599
|
-
|
|
2692
|
+
runsRoot = await bindExistingManagedHumanishOutputDirectory(cwd, "runs");
|
|
2693
|
+
entries = runsRoot ? await readdir(runsRoot.physicalPath) : [];
|
|
2694
|
+
if (runsRoot) {
|
|
2695
|
+
await assertPreparedSelectedOutputDirectory(runsRoot);
|
|
2696
|
+
}
|
|
2600
2697
|
}
|
|
2601
2698
|
catch (error) {
|
|
2602
2699
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
@@ -2608,24 +2705,53 @@ export async function listRuns(cwdInput) {
|
|
|
2608
2705
|
}
|
|
2609
2706
|
let latest;
|
|
2610
2707
|
try {
|
|
2611
|
-
latest = await readLatest(
|
|
2708
|
+
latest = runsRoot ? await readLatest(runsRoot) : null;
|
|
2612
2709
|
}
|
|
2613
2710
|
catch (error) {
|
|
2614
2711
|
return runsUnavailableResult(cwd, error);
|
|
2615
2712
|
}
|
|
2616
2713
|
const runs = [];
|
|
2617
|
-
for (const
|
|
2618
|
-
if (!
|
|
2714
|
+
for (const entryName of entries) {
|
|
2715
|
+
if (entryName === "latest.json" || !isSafeRunIdSegment(entryName)) {
|
|
2619
2716
|
continue;
|
|
2620
2717
|
}
|
|
2621
|
-
const
|
|
2718
|
+
const entryPath = path.join(runsRootPath, entryName);
|
|
2719
|
+
const entryStats = await lstat(entryPath, { bigint: true }).catch(() => null);
|
|
2720
|
+
if (!entryStats) {
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
if (entryStats.isSymbolicLink() || (!entryStats.isDirectory() && !entryStats.isFile()) || (entryStats.isFile() && entryStats.nlink > 1n)) {
|
|
2724
|
+
return runsUnavailableResult(cwd, new Error(`Unsafe Humanish runs entry: ${entryName}`));
|
|
2725
|
+
}
|
|
2726
|
+
if (!entryStats.isDirectory()) {
|
|
2727
|
+
continue;
|
|
2728
|
+
}
|
|
2729
|
+
let entryRunPaths;
|
|
2730
|
+
try {
|
|
2731
|
+
entryRunPaths = await bindExistingRunArtifactPaths(cwd, entryName);
|
|
2732
|
+
}
|
|
2733
|
+
catch (error) {
|
|
2734
|
+
return runsUnavailableResult(cwd, error);
|
|
2735
|
+
}
|
|
2736
|
+
if (runsRoot && entryRunPaths.physicalRunsRoot !== runsRoot.physicalPath) {
|
|
2737
|
+
return runsUnavailableResult(cwd, new Error("Humanish runs root changed physical destination."));
|
|
2738
|
+
}
|
|
2739
|
+
const bundle = await readRunJsonIfExists(entryRunPaths, "run.json");
|
|
2622
2740
|
runs.push({
|
|
2623
|
-
runId:
|
|
2741
|
+
runId: entryName,
|
|
2624
2742
|
createdAt: isRecord(bundle) && typeof bundle.createdAt === "string" ? bundle.createdAt : null,
|
|
2625
2743
|
mode: isRecord(bundle) && typeof bundle.mode === "string" ? bundle.mode : null,
|
|
2626
|
-
path: path.join(
|
|
2744
|
+
path: path.join(RUNS_RELATIVE_ROOT, entryName)
|
|
2627
2745
|
});
|
|
2628
2746
|
}
|
|
2747
|
+
if (runsRoot) {
|
|
2748
|
+
try {
|
|
2749
|
+
await assertPreparedSelectedOutputDirectory(runsRoot);
|
|
2750
|
+
}
|
|
2751
|
+
catch (error) {
|
|
2752
|
+
return runsUnavailableResult(cwd, error);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2629
2755
|
return {
|
|
2630
2756
|
schema: RUNS_SCHEMA,
|
|
2631
2757
|
ok: true,
|
|
@@ -2648,13 +2774,19 @@ function runsUnavailableResult(cwd, error) {
|
|
|
2648
2774
|
};
|
|
2649
2775
|
}
|
|
2650
2776
|
export async function readReview(cwdInput, runInput) {
|
|
2651
|
-
const
|
|
2777
|
+
const cwd = path.resolve(cwdInput);
|
|
2778
|
+
let runPaths;
|
|
2779
|
+
try {
|
|
2780
|
+
runPaths = await resolveRunPath(cwd, runInput);
|
|
2781
|
+
}
|
|
2782
|
+
catch {
|
|
2783
|
+
return invalidRunStorageVerifyResult(cwd, runInput);
|
|
2784
|
+
}
|
|
2785
|
+
const verified = await verifyPreparedRun(cwd, runInput, runPaths);
|
|
2652
2786
|
if (!verified.ok || !verified.bundlePath) {
|
|
2653
2787
|
return verified;
|
|
2654
2788
|
}
|
|
2655
|
-
const
|
|
2656
|
-
const runDir = path.dirname(path.join(cwd, verified.bundlePath));
|
|
2657
|
-
const review = await readJsonIfExists(path.join(runDir, "review.json"));
|
|
2789
|
+
const review = runPaths ? await readRunJsonIfExists(runPaths, "review.json") : null;
|
|
2658
2790
|
if (!isReviewSummary(review)) {
|
|
2659
2791
|
return {
|
|
2660
2792
|
...verified,
|
|
@@ -2667,32 +2799,63 @@ export async function readReview(cwdInput, runInput) {
|
|
|
2667
2799
|
}
|
|
2668
2800
|
return {
|
|
2669
2801
|
...review,
|
|
2670
|
-
path: path.relative(cwd, path.join(
|
|
2671
|
-
runId: path.basename(
|
|
2802
|
+
path: path.relative(cwd, path.join(runPaths.absoluteRunRoot, "review.json")),
|
|
2803
|
+
runId: path.basename(runPaths.absoluteRunRoot)
|
|
2672
2804
|
};
|
|
2673
2805
|
}
|
|
2674
2806
|
export async function doctor(cwdInput) {
|
|
2675
2807
|
const cwd = path.resolve(cwdInput);
|
|
2808
|
+
const cwdOk = await validateCwd(cwd).then((error) => error === null).catch(() => false);
|
|
2809
|
+
if (!cwdOk) {
|
|
2810
|
+
const checks = [
|
|
2811
|
+
{ name: "target cwd", ok: false, message: "target directory exists" },
|
|
2812
|
+
{ name: "package.json", ok: false, message: "package.json is present and safe to read" },
|
|
2813
|
+
{ name: "humanish source", ok: false, message: "committed humanish/ source directory is present and safe to read" },
|
|
2814
|
+
{ name: "runtime ignore", ok: false, message: ".gitignore safely contains .humanish/" }
|
|
2815
|
+
];
|
|
2816
|
+
return { schema: DOCTOR_SCHEMA, ok: false, cwd, checks };
|
|
2817
|
+
}
|
|
2818
|
+
let projectRoot;
|
|
2819
|
+
try {
|
|
2820
|
+
projectRoot = await prepareSelectedOutputDirectory(path.dirname(cwd), cwd);
|
|
2821
|
+
}
|
|
2822
|
+
catch {
|
|
2823
|
+
const checks = [
|
|
2824
|
+
{ name: "target cwd", ok: false, message: "target directory failed containment validation" },
|
|
2825
|
+
{ name: "package.json", ok: false, message: "package.json is present and safe to read" },
|
|
2826
|
+
{ name: "humanish source", ok: false, message: "committed humanish/ source directory is present and safe to read" },
|
|
2827
|
+
{ name: "runtime ignore", ok: false, message: ".gitignore safely contains .humanish/" }
|
|
2828
|
+
];
|
|
2829
|
+
return { schema: DOCTOR_SCHEMA, ok: false, cwd, checks };
|
|
2830
|
+
}
|
|
2831
|
+
const safeCheck = async (check) => {
|
|
2832
|
+
try {
|
|
2833
|
+
return await check();
|
|
2834
|
+
}
|
|
2835
|
+
catch {
|
|
2836
|
+
return false;
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2676
2839
|
const checks = [
|
|
2677
2840
|
{
|
|
2678
2841
|
name: "target cwd",
|
|
2679
|
-
ok:
|
|
2842
|
+
ok: true,
|
|
2680
2843
|
message: "target directory exists"
|
|
2681
2844
|
},
|
|
2682
2845
|
{
|
|
2683
2846
|
name: "package.json",
|
|
2684
|
-
ok: await
|
|
2685
|
-
message: "package.json is present"
|
|
2847
|
+
ok: await safeCheck(async () => await readImplicitProjectFile(projectRoot, "package.json") !== null),
|
|
2848
|
+
message: "package.json is present and safe to read"
|
|
2686
2849
|
},
|
|
2687
2850
|
{
|
|
2688
2851
|
name: "humanish source",
|
|
2689
|
-
ok: await
|
|
2690
|
-
message: "committed humanish/ source directory is present"
|
|
2852
|
+
ok: await safeCheck(() => implicitProjectDirectoryExists(projectRoot, "humanish")),
|
|
2853
|
+
message: "committed humanish/ source directory is present and safe to read"
|
|
2691
2854
|
},
|
|
2692
2855
|
{
|
|
2693
2856
|
name: "runtime ignore",
|
|
2694
|
-
ok: (await
|
|
2695
|
-
message: ".gitignore contains .humanish/"
|
|
2857
|
+
ok: await safeCheck(async () => (await readImplicitProjectFile(projectRoot, ".gitignore"))?.includes(".humanish/") ?? false),
|
|
2858
|
+
message: ".gitignore safely contains .humanish/"
|
|
2696
2859
|
}
|
|
2697
2860
|
];
|
|
2698
2861
|
return {
|
|
@@ -2749,7 +2912,83 @@ function createLocalActorReviewSummary(actorLabel, status, reason) {
|
|
|
2749
2912
|
]
|
|
2750
2913
|
};
|
|
2751
2914
|
}
|
|
2752
|
-
async function
|
|
2915
|
+
async function inspectImplicitProjectPath(projectRoot, relativePath) {
|
|
2916
|
+
const segments = relativePath.replace(/\\/g, "/").split("/");
|
|
2917
|
+
if (segments.length === 0 || segments.some((segment) => segment.length === 0)) {
|
|
2918
|
+
throw new Error("Implicit project path must be a non-empty relative path.");
|
|
2919
|
+
}
|
|
2920
|
+
await assertPreparedSelectedOutputDirectory(projectRoot);
|
|
2921
|
+
let current = projectRoot.physicalPath;
|
|
2922
|
+
for (const [index, segment] of segments.entries()) {
|
|
2923
|
+
assertSafeOutputPathSegment(segment, "Implicit project path segment");
|
|
2924
|
+
current = path.join(current, segment);
|
|
2925
|
+
let stats;
|
|
2926
|
+
try {
|
|
2927
|
+
stats = await lstat(current, { bigint: true });
|
|
2928
|
+
}
|
|
2929
|
+
catch (error) {
|
|
2930
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
throw error;
|
|
2934
|
+
}
|
|
2935
|
+
if (stats.isSymbolicLink()) {
|
|
2936
|
+
throw new Error(`Implicit project path must not contain symbolic links: ${relativePath}`);
|
|
2937
|
+
}
|
|
2938
|
+
if (!stats.isDirectory() && !stats.isFile()) {
|
|
2939
|
+
throw new Error(`Implicit project path must contain only regular files and directories: ${relativePath}`);
|
|
2940
|
+
}
|
|
2941
|
+
if (stats.isFile() && stats.nlink > 1n) {
|
|
2942
|
+
throw new Error(`Implicit project files must be single-link regular files: ${relativePath}`);
|
|
2943
|
+
}
|
|
2944
|
+
if (index < segments.length - 1 && !stats.isDirectory()) {
|
|
2945
|
+
throw new Error(`Implicit project path parent must be a directory: ${relativePath}`);
|
|
2946
|
+
}
|
|
2947
|
+
if (index === segments.length - 1) {
|
|
2948
|
+
await assertPreparedSelectedOutputDirectory(projectRoot);
|
|
2949
|
+
return stats;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
return null;
|
|
2953
|
+
}
|
|
2954
|
+
async function implicitProjectDirectoryExists(projectRoot, relativePath) {
|
|
2955
|
+
const stats = await inspectImplicitProjectPath(projectRoot, relativePath);
|
|
2956
|
+
if (!stats) {
|
|
2957
|
+
return false;
|
|
2958
|
+
}
|
|
2959
|
+
if (!stats.isDirectory()) {
|
|
2960
|
+
throw new Error(`Implicit project directory has the wrong type: ${relativePath}`);
|
|
2961
|
+
}
|
|
2962
|
+
return true;
|
|
2963
|
+
}
|
|
2964
|
+
async function readImplicitProjectFile(projectRoot, relativePath) {
|
|
2965
|
+
const stats = await inspectImplicitProjectPath(projectRoot, relativePath);
|
|
2966
|
+
if (!stats) {
|
|
2967
|
+
return null;
|
|
2968
|
+
}
|
|
2969
|
+
if (!stats.isFile() || stats.nlink !== 1n) {
|
|
2970
|
+
throw new Error(`Implicit project file must be a single-link regular file: ${relativePath}`);
|
|
2971
|
+
}
|
|
2972
|
+
const bytes = await readContainedRegularFile(projectRoot, relativePath.replace(/\\/g, "/"));
|
|
2973
|
+
if (!bytes) {
|
|
2974
|
+
throw new Error(`Implicit project file changed while it was being read: ${relativePath}`);
|
|
2975
|
+
}
|
|
2976
|
+
return bytes.toString("utf8");
|
|
2977
|
+
}
|
|
2978
|
+
async function listImplicitProjectDirectory(projectRoot, relativePath) {
|
|
2979
|
+
if (!await implicitProjectDirectoryExists(projectRoot, relativePath)) {
|
|
2980
|
+
return [];
|
|
2981
|
+
}
|
|
2982
|
+
const directory = path.join(projectRoot.physicalPath, ...relativePath.replace(/\\/g, "/").split("/"));
|
|
2983
|
+
const names = await readdir(directory);
|
|
2984
|
+
await assertPreparedSelectedOutputDirectory(projectRoot);
|
|
2985
|
+
for (const name of names) {
|
|
2986
|
+
assertSafeOutputPathSegment(name, "Implicit project directory entry");
|
|
2987
|
+
await inspectImplicitProjectPath(projectRoot, `${relativePath.replace(/\\/g, "/")}/${name}`);
|
|
2988
|
+
}
|
|
2989
|
+
return names;
|
|
2990
|
+
}
|
|
2991
|
+
async function loadDryRunSelection(projectRoot, humanishSource) {
|
|
2753
2992
|
const warnings = [];
|
|
2754
2993
|
if (humanishSource === "missing") {
|
|
2755
2994
|
return {
|
|
@@ -2761,9 +3000,9 @@ async function loadDryRunSelection(cwd, humanishSource) {
|
|
|
2761
3000
|
}
|
|
2762
3001
|
const personaPath = "humanish/personas/synthetic-new-user.yaml";
|
|
2763
3002
|
const scenarioPath = "humanish/scenarios/first-run-smoke.yaml";
|
|
2764
|
-
const personaText = await
|
|
2765
|
-
const scenarioText = await
|
|
2766
|
-
const browserJourneySelection = await loadBrowserPersonaJourneySelection(
|
|
3003
|
+
const personaText = await readImplicitProjectFile(projectRoot, personaPath);
|
|
3004
|
+
const scenarioText = await readImplicitProjectFile(projectRoot, scenarioPath);
|
|
3005
|
+
const browserJourneySelection = await loadBrowserPersonaJourneySelection(projectRoot);
|
|
2767
3006
|
if (personaText === null) {
|
|
2768
3007
|
warnings.push(`${personaPath} was not found; using built-in persona defaults.`);
|
|
2769
3008
|
}
|
|
@@ -2808,15 +3047,9 @@ async function loadDryRunSelection(cwd, humanishSource) {
|
|
|
2808
3047
|
warnings: [...warnings, ...browserJourneySelection.warnings]
|
|
2809
3048
|
};
|
|
2810
3049
|
}
|
|
2811
|
-
async function loadBrowserPersonaJourneySelection(
|
|
3050
|
+
async function loadBrowserPersonaJourneySelection(projectRoot) {
|
|
2812
3051
|
const warnings = [];
|
|
2813
|
-
const
|
|
2814
|
-
const names = await readdir(scenarioDir).catch((error) => {
|
|
2815
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
2816
|
-
return [];
|
|
2817
|
-
}
|
|
2818
|
-
throw error;
|
|
2819
|
-
});
|
|
3052
|
+
const names = await listImplicitProjectDirectory(projectRoot, "humanish/scenarios");
|
|
2820
3053
|
const files = names
|
|
2821
3054
|
.filter((name) => name.endsWith(".yaml") || name.endsWith(".yml"))
|
|
2822
3055
|
.sort((left, right) => {
|
|
@@ -2828,8 +3061,7 @@ async function loadBrowserPersonaJourneySelection(cwd) {
|
|
|
2828
3061
|
});
|
|
2829
3062
|
for (const name of files) {
|
|
2830
3063
|
const relativePath = path.join("humanish", "scenarios", name);
|
|
2831
|
-
const
|
|
2832
|
-
const text = await readTextIfExists(absolutePath);
|
|
3064
|
+
const text = await readImplicitProjectFile(projectRoot, relativePath);
|
|
2833
3065
|
if (text === null) {
|
|
2834
3066
|
continue;
|
|
2835
3067
|
}
|
|
@@ -2904,25 +3136,71 @@ function escapeRegExp(value) {
|
|
|
2904
3136
|
}
|
|
2905
3137
|
async function resolveRunPath(cwd, runInput) {
|
|
2906
3138
|
if (runInput === "latest") {
|
|
2907
|
-
const
|
|
2908
|
-
|
|
3139
|
+
const runsRoot = await bindExistingManagedHumanishOutputDirectory(cwd, "runs");
|
|
3140
|
+
if (!runsRoot) {
|
|
3141
|
+
return null;
|
|
3142
|
+
}
|
|
3143
|
+
const latest = await readLatest(runsRoot);
|
|
3144
|
+
const expected = latest ? resolveLatestRunDirectory(cwd, latest) : null;
|
|
3145
|
+
if (!latest || !expected) {
|
|
3146
|
+
return null;
|
|
3147
|
+
}
|
|
3148
|
+
const runPaths = await bindExistingRunArtifactPaths(cwd, latest.runId);
|
|
3149
|
+
if (runPaths.absoluteRunRoot !== expected
|
|
3150
|
+
|| runPaths.physicalRunsRoot !== runsRoot.physicalPath) {
|
|
3151
|
+
throw new Error("Latest run pointer changed physical runs root.");
|
|
3152
|
+
}
|
|
3153
|
+
await assertPreparedSelectedOutputDirectory(runsRoot);
|
|
3154
|
+
return runPaths;
|
|
3155
|
+
}
|
|
3156
|
+
if (!isSafeRunIdSegment(runInput) || !await resolveExistingRunDirectory(cwd, runInput)) {
|
|
3157
|
+
return null;
|
|
2909
3158
|
}
|
|
2910
|
-
|
|
2911
|
-
return await directoryExists(direct) ? direct : null;
|
|
3159
|
+
return bindExistingRunArtifactPaths(cwd, runInput);
|
|
2912
3160
|
}
|
|
2913
|
-
async function readLatest(
|
|
2914
|
-
const
|
|
2915
|
-
|
|
2916
|
-
|
|
3161
|
+
async function readLatest(runsRoot) {
|
|
3162
|
+
const latestPath = path.join(runsRoot.physicalPath, "latest.json");
|
|
3163
|
+
let latestStats;
|
|
3164
|
+
try {
|
|
3165
|
+
latestStats = await lstat(latestPath, { bigint: true });
|
|
2917
3166
|
}
|
|
2918
|
-
|
|
3167
|
+
catch (error) {
|
|
3168
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
3169
|
+
return null;
|
|
3170
|
+
}
|
|
3171
|
+
throw error;
|
|
3172
|
+
}
|
|
3173
|
+
if (latestStats.isSymbolicLink() || !latestStats.isFile() || latestStats.nlink !== 1n) {
|
|
3174
|
+
throw new Error("Latest run pointer must be a single-link regular file.");
|
|
3175
|
+
}
|
|
3176
|
+
const bytes = await readContainedRegularFile(runsRoot, "latest.json");
|
|
3177
|
+
if (!bytes) {
|
|
3178
|
+
throw new Error("Latest run pointer changed while it was being read.");
|
|
3179
|
+
}
|
|
3180
|
+
let latest;
|
|
3181
|
+
try {
|
|
3182
|
+
latest = JSON.parse(bytes.toString("utf8"));
|
|
3183
|
+
}
|
|
3184
|
+
catch {
|
|
3185
|
+
return null;
|
|
3186
|
+
}
|
|
3187
|
+
return isRunPointer(latest) ? latest : null;
|
|
2919
3188
|
}
|
|
2920
|
-
async function readPackageName(
|
|
2921
|
-
const
|
|
2922
|
-
|
|
3189
|
+
async function readPackageName(projectRoot) {
|
|
3190
|
+
const text = await readImplicitProjectFile(projectRoot, "package.json");
|
|
3191
|
+
if (text === null) {
|
|
3192
|
+
return null;
|
|
3193
|
+
}
|
|
3194
|
+
try {
|
|
3195
|
+
const packageJson = JSON.parse(text);
|
|
3196
|
+
return isRecord(packageJson) && typeof packageJson.name === "string" ? packageJson.name : null;
|
|
3197
|
+
}
|
|
3198
|
+
catch {
|
|
3199
|
+
return null;
|
|
3200
|
+
}
|
|
2923
3201
|
}
|
|
2924
|
-
async function
|
|
2925
|
-
const text = await
|
|
3202
|
+
async function readRunJsonIfExists(runPaths, ...segments) {
|
|
3203
|
+
const text = await readRunTextIfExists(runPaths, ...segments);
|
|
2926
3204
|
if (text === null) {
|
|
2927
3205
|
return null;
|
|
2928
3206
|
}
|
|
@@ -2933,6 +3211,33 @@ async function readJsonIfExists(filePath) {
|
|
|
2933
3211
|
return null;
|
|
2934
3212
|
}
|
|
2935
3213
|
}
|
|
3214
|
+
async function readRunTextIfExists(runPaths, ...segments) {
|
|
3215
|
+
const bytes = await readContainedRegularFile(runPaths, segments.join("/"));
|
|
3216
|
+
return bytes?.toString("utf8") ?? null;
|
|
3217
|
+
}
|
|
3218
|
+
async function readSafeRunArtifactBytes(runPaths, relativePath) {
|
|
3219
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
3220
|
+
const segments = normalized.split("/");
|
|
3221
|
+
if (path.isAbsolute(relativePath)
|
|
3222
|
+
|| path.win32.isAbsolute(relativePath)
|
|
3223
|
+
|| segments.length === 0
|
|
3224
|
+
|| segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
return readContainedRegularFile(runPaths, normalized);
|
|
3228
|
+
}
|
|
3229
|
+
async function readSafeRunArtifactJson(runPaths, relativePath) {
|
|
3230
|
+
const bytes = await readSafeRunArtifactBytes(runPaths, relativePath);
|
|
3231
|
+
if (!bytes) {
|
|
3232
|
+
return null;
|
|
3233
|
+
}
|
|
3234
|
+
try {
|
|
3235
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
3236
|
+
}
|
|
3237
|
+
catch {
|
|
3238
|
+
return null;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
2936
3241
|
async function readTextIfExists(filePath) {
|
|
2937
3242
|
try {
|
|
2938
3243
|
return await readFile(filePath, "utf8");
|
|
@@ -2944,22 +3249,18 @@ async function readTextIfExists(filePath) {
|
|
|
2944
3249
|
throw error;
|
|
2945
3250
|
}
|
|
2946
3251
|
}
|
|
2947
|
-
async function
|
|
2948
|
-
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
2949
|
-
}
|
|
2950
|
-
async function writeRunBundleArtifacts(absoluteArtifactRoot, bundle) {
|
|
3252
|
+
async function writeRunBundleArtifacts(runPaths, bundle) {
|
|
2951
3253
|
const publicBundle = {
|
|
2952
3254
|
...bundle,
|
|
2953
3255
|
cwd: PUBLIC_TARGET_CWD
|
|
2954
3256
|
};
|
|
2955
|
-
await
|
|
2956
|
-
await
|
|
2957
|
-
await
|
|
2958
|
-
await
|
|
2959
|
-
await
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
async function missingLocalEvidenceArtifacts(runRoot, bundle) {
|
|
3257
|
+
await writeContainedOutputFile(runPaths, "run.json", `${JSON.stringify(publicBundle, null, 2)}\n`, "utf8");
|
|
3258
|
+
await writeContainedOutputFile(runPaths, "review.json", `${JSON.stringify(publicBundle.review, null, 2)}\n`, "utf8");
|
|
3259
|
+
await writeContainedOutputFile(runPaths, "review.md", renderReviewMarkdown(publicBundle), "utf8");
|
|
3260
|
+
await writeContainedOutputFile(runPaths, "events.ndjson", `${publicBundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
3261
|
+
await writeContainedOutputFile(runPaths, "observer/observer-data.json", `${JSON.stringify(buildObserverData(publicBundle), null, 2)}\n`, "utf8");
|
|
3262
|
+
}
|
|
3263
|
+
async function missingLocalEvidenceArtifacts(runPaths, bundle) {
|
|
2963
3264
|
const requiredPaths = new Map();
|
|
2964
3265
|
const addRequiredPath = (artifactPath, options = {}) => {
|
|
2965
3266
|
const existing = requiredPaths.get(artifactPath);
|
|
@@ -2990,15 +3291,13 @@ async function missingLocalEvidenceArtifacts(runRoot, bundle) {
|
|
|
2990
3291
|
}
|
|
2991
3292
|
const missing = [];
|
|
2992
3293
|
for (const [artifactPath, requirements] of requiredPaths) {
|
|
2993
|
-
const
|
|
2994
|
-
|
|
2995
|
-
if (!stats?.isFile() || stats.size <= 0) {
|
|
3294
|
+
const bytes = await readSafeRunArtifactBytes(runPaths, artifactPath);
|
|
3295
|
+
if (!bytes || bytes.length === 0) {
|
|
2996
3296
|
missing.push(artifactPath);
|
|
2997
3297
|
continue;
|
|
2998
3298
|
}
|
|
2999
3299
|
if (requirements.screenshot) {
|
|
3000
|
-
const
|
|
3001
|
-
const imageError = bytes ? screenshotEvidenceError(artifactPath, bytes) : "could not read screenshot bytes";
|
|
3300
|
+
const imageError = screenshotEvidenceError(artifactPath, bytes);
|
|
3002
3301
|
if (imageError) {
|
|
3003
3302
|
missing.push(`${artifactPath} (${imageError})`);
|
|
3004
3303
|
}
|
|
@@ -3060,7 +3359,7 @@ const TERMINAL_TRANSCRIPT_FILE = "terminal-transcript.txt";
|
|
|
3060
3359
|
* file is already caught by scanRunPublicSafetyArtifacts; this check enforces the STRUCTURAL
|
|
3061
3360
|
* evidence + the proven-teardown invariant. Dry-run/contract bundles are exempt (mode !== live).
|
|
3062
3361
|
*/
|
|
3063
|
-
async function validateTerminalProductEvidence(
|
|
3362
|
+
async function validateTerminalProductEvidence(runPaths, bundle) {
|
|
3064
3363
|
if (bundle.mode !== "live") {
|
|
3065
3364
|
return [];
|
|
3066
3365
|
}
|
|
@@ -3073,7 +3372,7 @@ async function validateTerminalProductEvidence(runRoot, bundle) {
|
|
|
3073
3372
|
return findings;
|
|
3074
3373
|
}
|
|
3075
3374
|
// The lane writes exactly one terminal run's ledgers/evidence at fixed paths in the run root.
|
|
3076
|
-
const ledgers = await
|
|
3375
|
+
const ledgers = await readSafeRunArtifactJson(runPaths, TERMINAL_LEDGERS_FILE);
|
|
3077
3376
|
if (!isRecord(ledgers) || ledgers.schema !== "humanish.terminal-ledgers.v1") {
|
|
3078
3377
|
findings.push(`missing or malformed ${TERMINAL_LEDGERS_FILE} (humanish.terminal-ledgers.v1)`);
|
|
3079
3378
|
return findings;
|
|
@@ -3108,17 +3407,17 @@ async function validateTerminalProductEvidence(runRoot, bundle) {
|
|
|
3108
3407
|
// The redacted exec-stream + normalized transcript artifacts must be WRITTEN (the producer
|
|
3109
3408
|
// always writes them on the live path, even empty for a no-output blocked run — so absence is a
|
|
3110
3409
|
// real evidence gap, while emptiness is legitimate and keeps blocked runs verifiable).
|
|
3111
|
-
if (!(await
|
|
3410
|
+
if (!(await readSafeRunArtifactBytes(runPaths, TERMINAL_EVENTS_FILE))) {
|
|
3112
3411
|
findings.push(`missing terminal event stream artifact (${TERMINAL_EVENTS_FILE})`);
|
|
3113
3412
|
}
|
|
3114
|
-
if (!(await
|
|
3413
|
+
if (!(await readSafeRunArtifactBytes(runPaths, TERMINAL_TRANSCRIPT_FILE))) {
|
|
3115
3414
|
findings.push(`missing normalized terminal transcript artifact (${TERMINAL_TRANSCRIPT_FILE})`);
|
|
3116
3415
|
}
|
|
3117
3416
|
// The provider-neutral actor trace must be on the terminal lane with redaction passed.
|
|
3118
3417
|
for (const stream of terminalStreams) {
|
|
3119
3418
|
const traceArtifact = stream.artifacts.find((artifact) => artifact.kind === "trace");
|
|
3120
3419
|
const tracePath = traceArtifact?.path ?? "actor.json";
|
|
3121
|
-
const trace = await
|
|
3420
|
+
const trace = await readSafeRunArtifactJson(runPaths, tracePath);
|
|
3122
3421
|
if (!isRecord(trace) || trace.lane !== "terminal") {
|
|
3123
3422
|
findings.push(`${stream.id} missing terminal-lane actor trace`);
|
|
3124
3423
|
continue;
|
|
@@ -3203,7 +3502,7 @@ function validateTerminalCostEvidence(ledgers) {
|
|
|
3203
3502
|
}
|
|
3204
3503
|
return findings;
|
|
3205
3504
|
}
|
|
3206
|
-
async function validateCodexAppServerEvidence(
|
|
3505
|
+
async function validateCodexAppServerEvidence(runPaths, bundle) {
|
|
3207
3506
|
if (bundle.mode !== "live") {
|
|
3208
3507
|
return [];
|
|
3209
3508
|
}
|
|
@@ -3228,7 +3527,7 @@ async function validateCodexAppServerEvidence(runRoot, bundle) {
|
|
|
3228
3527
|
findings.push(`${stream.id} missing codex app-server trace artifact`);
|
|
3229
3528
|
continue;
|
|
3230
3529
|
}
|
|
3231
|
-
const trace = await
|
|
3530
|
+
const trace = await readSafeRunArtifactJson(runPaths, traceArtifact.path);
|
|
3232
3531
|
if (!isRecord(trace) || ![CODEX_APP_SERVER_TRACE_SCHEMA, CODEX_APP_SERVER_PROJECTED_TRACE_SCHEMA].includes(String(trace.schema))) {
|
|
3233
3532
|
findings.push(`${stream.id} trace artifact must use ${CODEX_APP_SERVER_TRACE_SCHEMA} or ${CODEX_APP_SERVER_PROJECTED_TRACE_SCHEMA}`);
|
|
3234
3533
|
}
|
|
@@ -3878,34 +4177,46 @@ const riskyPublicArtifactPathSegments = new Set([
|
|
|
3878
4177
|
"Secure Preferences",
|
|
3879
4178
|
"profiles"
|
|
3880
4179
|
]);
|
|
3881
|
-
async function scanRunPublicSafetyArtifacts(
|
|
4180
|
+
async function scanRunPublicSafetyArtifacts(runPaths) {
|
|
3882
4181
|
const findings = [];
|
|
3883
|
-
await
|
|
4182
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
4183
|
+
await scanRunPublicSafetyDirectory(runPaths, "", findings);
|
|
4184
|
+
await validatePreparedRunArtifactPaths(runPaths);
|
|
3884
4185
|
return findings;
|
|
3885
4186
|
}
|
|
3886
|
-
async function scanRunPublicSafetyDirectory(
|
|
4187
|
+
async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, findings) {
|
|
3887
4188
|
if (findings.length >= 50) {
|
|
3888
4189
|
return;
|
|
3889
4190
|
}
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
4191
|
+
const current = relativeDirectory
|
|
4192
|
+
? path.join(runPaths.physicalRunRoot, ...relativeDirectory.split("/"))
|
|
4193
|
+
: runPaths.physicalRunRoot;
|
|
4194
|
+
const entries = await readdir(current).catch(() => []);
|
|
4195
|
+
for (const entryName of entries) {
|
|
4196
|
+
const relativePath = relativeDirectory ? `${relativeDirectory}/${entryName}` : entryName;
|
|
3894
4197
|
if (isRiskyPublicArtifactPath(relativePath) || containsSensitivePattern(relativePath)) {
|
|
3895
4198
|
findings.push(`risky artifact path ${relativePath}`);
|
|
3896
4199
|
if (findings.length >= 50)
|
|
3897
4200
|
return;
|
|
3898
4201
|
}
|
|
3899
|
-
|
|
3900
|
-
|
|
4202
|
+
const stats = await lstat(path.join(current, entryName), { bigint: true }).catch(() => null);
|
|
4203
|
+
if (!stats || stats.isSymbolicLink() || (!stats.isDirectory() && !stats.isFile()) || (stats.isFile() && stats.nlink > 1n)) {
|
|
4204
|
+
findings.push(`unsafe artifact leaf ${relativePath}`);
|
|
4205
|
+
if (findings.length >= 50)
|
|
4206
|
+
return;
|
|
4207
|
+
continue;
|
|
4208
|
+
}
|
|
4209
|
+
if (stats.isDirectory()) {
|
|
4210
|
+
await scanRunPublicSafetyDirectory(runPaths, relativePath, findings);
|
|
3901
4211
|
if (findings.length >= 50)
|
|
3902
4212
|
return;
|
|
3903
4213
|
continue;
|
|
3904
4214
|
}
|
|
3905
|
-
if (!
|
|
4215
|
+
if (!shouldScanTextArtifact(relativePath)) {
|
|
3906
4216
|
continue;
|
|
3907
4217
|
}
|
|
3908
|
-
const
|
|
4218
|
+
const bytes = await readSafeRunArtifactBytes(runPaths, relativePath);
|
|
4219
|
+
const text = bytes?.toString("utf8") ?? null;
|
|
3909
4220
|
if (text !== null && containsSensitivePattern(text)) {
|
|
3910
4221
|
findings.push(`sensitive text ${relativePath}`);
|
|
3911
4222
|
if (findings.length >= 50)
|
|
@@ -3961,28 +4272,6 @@ async function validateCwd(cwd) {
|
|
|
3961
4272
|
throw error;
|
|
3962
4273
|
}
|
|
3963
4274
|
}
|
|
3964
|
-
async function directoryExists(directoryPath) {
|
|
3965
|
-
try {
|
|
3966
|
-
return (await stat(directoryPath)).isDirectory();
|
|
3967
|
-
}
|
|
3968
|
-
catch (error) {
|
|
3969
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
3970
|
-
return false;
|
|
3971
|
-
}
|
|
3972
|
-
throw error;
|
|
3973
|
-
}
|
|
3974
|
-
}
|
|
3975
|
-
async function fileExists(filePath) {
|
|
3976
|
-
try {
|
|
3977
|
-
return (await stat(filePath)).isFile();
|
|
3978
|
-
}
|
|
3979
|
-
catch (error) {
|
|
3980
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
3981
|
-
return false;
|
|
3982
|
-
}
|
|
3983
|
-
throw error;
|
|
3984
|
-
}
|
|
3985
|
-
}
|
|
3986
4275
|
function containsSensitivePattern(text) {
|
|
3987
4276
|
return containsSensitive(text);
|
|
3988
4277
|
}
|