mimetic-cli 0.1.3 → 0.1.5
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 +67 -12
- package/dist/env-file.d.ts +14 -0
- package/dist/env-file.js +108 -0
- package/dist/env-file.js.map +1 -0
- package/dist/feedback.d.ts +7 -5
- package/dist/feedback.js +61 -4
- package/dist/feedback.js.map +1 -1
- package/dist/init-templates.js +29 -0
- package/dist/init-templates.js.map +1 -1
- package/dist/lab-app-runner.d.ts +78 -0
- package/dist/lab-app-runner.js +403 -0
- package/dist/lab-app-runner.js.map +1 -0
- package/dist/labs.d.ts +67 -0
- package/dist/labs.js +257 -0
- package/dist/labs.js.map +1 -0
- package/dist/observer-assets.js +473 -25
- package/dist/observer-assets.js.map +1 -1
- package/dist/observer.d.ts +6 -0
- package/dist/observer.js +49 -8
- package/dist/observer.js.map +1 -1
- package/dist/oss-lab.d.ts +1 -1
- package/dist/oss-lab.js +6 -6
- package/dist/oss-lab.js.map +1 -1
- package/dist/oss-meta-lab.d.ts +113 -1
- package/dist/oss-meta-lab.js +2753 -200
- package/dist/oss-meta-lab.js.map +1 -1
- package/dist/oss-remote-telemetry.d.ts +77 -0
- package/dist/oss-remote-telemetry.js +393 -0
- package/dist/oss-remote-telemetry.js.map +1 -0
- package/dist/program.d.ts +8 -0
- package/dist/program.js +668 -70
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +105 -3
- package/dist/run.js +684 -22
- package/dist/run.js.map +1 -1
- package/docs/architecture/local-codex-tui-actor.md +9 -6
- package/docs/architecture/oss-lab-poc.md +119 -47
- package/docs/architecture/project-layout.md +40 -6
- package/docs/assets/mimetic-oss-lab-observer.png +0 -0
- package/docs/contracts/feedback.md +15 -12
- package/docs/contracts/policy.md +9 -2
- package/docs/contracts/run-bundle.md +62 -0
- package/docs/contracts/schemas.md +21 -0
- package/docs/goals/current.md +50 -17
- package/docs/product/open-source-install-experience.md +63 -8
- package/docs/ramp/README.md +26 -8
- package/docs/roadmap/world-class-open-source-v0.md +41 -20
- package/package.json +9 -6
- package/skills/mimetic-cli/SKILL.md +89 -4
- package/skills/mimetic-cli/agents/openai.yaml +1 -1
package/dist/run.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
6
7
|
import { buildObserverData } from "./observer-data.js";
|
|
7
8
|
export const RUN_BUNDLE_SCHEMA = "mimetic.run-bundle.v1";
|
|
8
9
|
export const REVIEW_SCHEMA = "mimetic.review.v1";
|
|
@@ -10,13 +11,18 @@ export const VERIFY_SCHEMA = "mimetic.verify-result.v1";
|
|
|
10
11
|
export const RUNS_SCHEMA = "mimetic.runs-result.v1";
|
|
11
12
|
export const DOCTOR_SCHEMA = "mimetic.doctor-result.v1";
|
|
12
13
|
const sensitivePatterns = [
|
|
13
|
-
/sk-[a-z0-9_-]{
|
|
14
|
-
/
|
|
14
|
+
/sk-[a-z0-9_-]{20,}/i,
|
|
15
|
+
/e2b_[a-z0-9_-]{12,}/i,
|
|
16
|
+
/gh[pousr]_[a-z0-9_]{12,}/i,
|
|
17
|
+
/https?:\/\/[^/\s]*e2b[^)\s]+/i,
|
|
15
18
|
/BEGIN (RSA|OPENSSH|PRIVATE) KEY/i
|
|
16
19
|
];
|
|
17
20
|
const LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS = 240_000;
|
|
18
21
|
const LOCAL_CODEX_TUI_MAX_TIMEOUT_MS = 600_000;
|
|
19
22
|
const LOCAL_ACTOR_TRANSCRIPT_MAX_CHARS = 80_000;
|
|
23
|
+
const LOCAL_CODEX_EXEC_DEFAULT_MAX_CONCURRENCY = 4;
|
|
24
|
+
const BROWSER_APP_DEFAULT_TIMEOUT_MS = 60_000;
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
20
26
|
const builtinPersona = {
|
|
21
27
|
id: "builtin-synthetic-new-user",
|
|
22
28
|
name: "Built-in Synthetic New User",
|
|
@@ -55,7 +61,7 @@ export async function runDryRun(options) {
|
|
|
55
61
|
}
|
|
56
62
|
};
|
|
57
63
|
}
|
|
58
|
-
const simCount = normalizeSimCount(options.simCount);
|
|
64
|
+
const simCount = normalizeSimCount(options.appUrl ? options.simCount ?? 2 : options.simCount);
|
|
59
65
|
if (simCount === null) {
|
|
60
66
|
return {
|
|
61
67
|
schema: "mimetic.run-result.v1",
|
|
@@ -64,10 +70,37 @@ export async function runDryRun(options) {
|
|
|
64
70
|
warnings,
|
|
65
71
|
error: {
|
|
66
72
|
code: "MIMETIC_INVALID_SIM_COUNT",
|
|
67
|
-
message: "--sims must be
|
|
73
|
+
message: "--sims must be a positive integer."
|
|
68
74
|
}
|
|
69
75
|
};
|
|
70
76
|
}
|
|
77
|
+
if (options.appUrl !== undefined) {
|
|
78
|
+
if (options.dryRun) {
|
|
79
|
+
return {
|
|
80
|
+
schema: "mimetic.run-result.v1",
|
|
81
|
+
ok: false,
|
|
82
|
+
cwd,
|
|
83
|
+
warnings,
|
|
84
|
+
error: {
|
|
85
|
+
code: "MIMETIC_APP_URL_OPTION_CONFLICT",
|
|
86
|
+
message: "Use --app-url for a live browser app proof; remove --dry-run."
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (simCount > 2) {
|
|
91
|
+
return {
|
|
92
|
+
schema: "mimetic.run-result.v1",
|
|
93
|
+
ok: false,
|
|
94
|
+
cwd,
|
|
95
|
+
warnings,
|
|
96
|
+
error: {
|
|
97
|
+
code: "MIMETIC_INVALID_SIM_COUNT",
|
|
98
|
+
message: "--sims must be 1 or 2 when --app-url is used."
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return runBrowserAppProof({ ...options, appUrl: options.appUrl, cwd, simCount });
|
|
103
|
+
}
|
|
71
104
|
if (!options.dryRun) {
|
|
72
105
|
const actor = resolveRequestedLocalCodexActor(options.actor);
|
|
73
106
|
if (actor === "codex-tui") {
|
|
@@ -187,6 +220,498 @@ export async function runDryRun(options) {
|
|
|
187
220
|
warnings
|
|
188
221
|
};
|
|
189
222
|
}
|
|
223
|
+
const browserSurfaces = [
|
|
224
|
+
{
|
|
225
|
+
id: "desktop",
|
|
226
|
+
label: "Desktop browser surface",
|
|
227
|
+
viewport: {
|
|
228
|
+
width: 1440,
|
|
229
|
+
height: 960,
|
|
230
|
+
deviceScaleFactor: 1,
|
|
231
|
+
isMobile: false
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "mobile",
|
|
236
|
+
label: "Mobile browser surface",
|
|
237
|
+
viewport: {
|
|
238
|
+
width: 390,
|
|
239
|
+
height: 844,
|
|
240
|
+
deviceScaleFactor: 2,
|
|
241
|
+
isMobile: true
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
async function runBrowserAppProof(options) {
|
|
246
|
+
const warnings = [];
|
|
247
|
+
const appUrl = normalizeLocalAppUrl(options.appUrl);
|
|
248
|
+
if (!appUrl) {
|
|
249
|
+
return {
|
|
250
|
+
schema: "mimetic.run-result.v1",
|
|
251
|
+
ok: false,
|
|
252
|
+
cwd: options.cwd,
|
|
253
|
+
warnings,
|
|
254
|
+
error: {
|
|
255
|
+
code: "MIMETIC_INVALID_APP_URL",
|
|
256
|
+
message: "--app-url must be an http(s) loopback URL such as http://127.0.0.1:5173."
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const browserCommand = await resolveBrowserCommand();
|
|
261
|
+
if (!browserCommand) {
|
|
262
|
+
return {
|
|
263
|
+
schema: "mimetic.run-result.v1",
|
|
264
|
+
ok: false,
|
|
265
|
+
cwd: options.cwd,
|
|
266
|
+
warnings,
|
|
267
|
+
error: {
|
|
268
|
+
code: "MIMETIC_BROWSER_APP_CAPTURE_FAILED",
|
|
269
|
+
message: "No Chrome/Chromium browser command was found. Set MIMETIC_BROWSER_COMMAND to a browser binary that supports --headless and --screenshot."
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const now = new Date();
|
|
274
|
+
const createdAt = now.toISOString();
|
|
275
|
+
const runId = options.runId ?? `browser-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
276
|
+
const artifactRoot = path.join(".mimetic", "runs", runId);
|
|
277
|
+
const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
|
|
278
|
+
const packageName = await readPackageName(options.cwd);
|
|
279
|
+
const mimeticSource = await directoryExists(path.join(options.cwd, "mimetic")) ? "present" : "missing";
|
|
280
|
+
const selection = await loadDryRunSelection(options.cwd, mimeticSource);
|
|
281
|
+
if (mimeticSource === "missing") {
|
|
282
|
+
warnings.push("Committed mimetic/ source was not found; using built-in synthetic browser-app defaults.");
|
|
283
|
+
}
|
|
284
|
+
warnings.push(...selection.warnings);
|
|
285
|
+
await mkdir(path.join(absoluteArtifactRoot, "screenshots"), { recursive: true });
|
|
286
|
+
await mkdir(path.join(absoluteArtifactRoot, "traces"), { recursive: true });
|
|
287
|
+
const surfaces = browserSurfaces.slice(0, options.simCount);
|
|
288
|
+
const captures = await Promise.all(surfaces.map((surface) => captureBrowserSurface({
|
|
289
|
+
absoluteArtifactRoot,
|
|
290
|
+
appUrl,
|
|
291
|
+
browserCommand,
|
|
292
|
+
surface,
|
|
293
|
+
timeoutMs: options.timeoutMs ?? BROWSER_APP_DEFAULT_TIMEOUT_MS
|
|
294
|
+
})));
|
|
295
|
+
const completedAt = new Date().toISOString();
|
|
296
|
+
const events = buildBrowserAppEvents({ appUrl, captures, createdAt });
|
|
297
|
+
const allPassed = captures.every((capture) => capture.ok);
|
|
298
|
+
const review = createBrowserAppReviewSummary({ appUrl, captures });
|
|
299
|
+
const bundle = {
|
|
300
|
+
schema: RUN_BUNDLE_SCHEMA,
|
|
301
|
+
runId,
|
|
302
|
+
mode: "live",
|
|
303
|
+
simCount: captures.length,
|
|
304
|
+
createdAt,
|
|
305
|
+
cwd: options.cwd,
|
|
306
|
+
artifactRoot,
|
|
307
|
+
source: {
|
|
308
|
+
packageName,
|
|
309
|
+
mimeticSource,
|
|
310
|
+
git: {
|
|
311
|
+
status: "not_captured",
|
|
312
|
+
note: "Browser app proof records local app evidence only; host git state capture is a later primitive."
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
persona: {
|
|
316
|
+
id: selection.persona.id,
|
|
317
|
+
name: selection.persona.name,
|
|
318
|
+
source: selection.persona.source,
|
|
319
|
+
sourceDigest: selection.persona.sourceDigest
|
|
320
|
+
},
|
|
321
|
+
scenario: {
|
|
322
|
+
id: "browser-app-surface-smoke",
|
|
323
|
+
title: "Browser App Surface Smoke",
|
|
324
|
+
goal: "Capture desktop and mobile browser evidence against a running local app URL.",
|
|
325
|
+
source: "builtin:browser-app-surface-smoke",
|
|
326
|
+
sourceDigest: digestText(appUrl)
|
|
327
|
+
},
|
|
328
|
+
lifecycle: [
|
|
329
|
+
{
|
|
330
|
+
at: createdAt,
|
|
331
|
+
event: "run.created",
|
|
332
|
+
message: `Live browser app proof created for ${appUrl}.`
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
at: createdAt,
|
|
336
|
+
event: "app.url.accepted",
|
|
337
|
+
message: "Accepted public-safe loopback app URL for browser surface capture."
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
at: completedAt,
|
|
341
|
+
event: "review.created",
|
|
342
|
+
message: allPassed
|
|
343
|
+
? "Created review from desktop/mobile browser screenshot evidence."
|
|
344
|
+
: "Created review with missing or blocked browser screenshot evidence."
|
|
345
|
+
}
|
|
346
|
+
],
|
|
347
|
+
simulations: captures.map((capture, index) => {
|
|
348
|
+
const simId = `browser-${capture.surface.id}`;
|
|
349
|
+
const streamId = `${simId}-stream`;
|
|
350
|
+
return {
|
|
351
|
+
id: simId,
|
|
352
|
+
index: index + 1,
|
|
353
|
+
personaId: selection.persona.id,
|
|
354
|
+
scenarioId: "browser-app-surface-smoke",
|
|
355
|
+
status: capture.ok ? "passed" : "blocked",
|
|
356
|
+
streamKind: "browser",
|
|
357
|
+
mode: "browser-sim",
|
|
358
|
+
progress: 100,
|
|
359
|
+
currentStep: capture.ok
|
|
360
|
+
? `${capture.surface.label} captured`
|
|
361
|
+
: `${capture.surface.label} blocked`,
|
|
362
|
+
summary: capture.reason,
|
|
363
|
+
streamIds: [streamId],
|
|
364
|
+
startedAt: createdAt,
|
|
365
|
+
updatedAt: capture.capturedAt
|
|
366
|
+
};
|
|
367
|
+
}),
|
|
368
|
+
streams: captures.map((capture) => {
|
|
369
|
+
const simId = `browser-${capture.surface.id}`;
|
|
370
|
+
const streamId = `${simId}-stream`;
|
|
371
|
+
const screenshotUrl = `../${capture.screenshotPath}`;
|
|
372
|
+
return {
|
|
373
|
+
id: streamId,
|
|
374
|
+
simId,
|
|
375
|
+
kind: "browser",
|
|
376
|
+
label: capture.surface.label,
|
|
377
|
+
status: capture.ok ? "passed" : "blocked",
|
|
378
|
+
transport: "snapshot",
|
|
379
|
+
updatedAt: capture.capturedAt,
|
|
380
|
+
embed: {
|
|
381
|
+
kind: "screenshot",
|
|
382
|
+
url: screenshotUrl,
|
|
383
|
+
title: capture.surface.label
|
|
384
|
+
},
|
|
385
|
+
viewport: capture.surface.viewport,
|
|
386
|
+
ui: {
|
|
387
|
+
appStatus: capture.ok ? "running" : "blocked",
|
|
388
|
+
appUrl,
|
|
389
|
+
route: appUrl,
|
|
390
|
+
intent: "Verify that the running app renders in a real browser surface for this viewport.",
|
|
391
|
+
screenshotUrl,
|
|
392
|
+
state: capture.reason,
|
|
393
|
+
visualStatus: capture.ok ? "visible" : "blocked"
|
|
394
|
+
},
|
|
395
|
+
completion: {
|
|
396
|
+
checkedAt: capture.capturedAt,
|
|
397
|
+
exitCode: capture.ok ? 0 : 1,
|
|
398
|
+
reason: capture.reason,
|
|
399
|
+
status: capture.ok ? "passed" : "blocked"
|
|
400
|
+
},
|
|
401
|
+
artifacts: [
|
|
402
|
+
{ label: "run bundle", path: "run.json", kind: "bundle" },
|
|
403
|
+
{ label: "review", path: "review.md", kind: "review" },
|
|
404
|
+
{ label: "event log", path: "events.ndjson", kind: "events" },
|
|
405
|
+
{ label: `${capture.surface.id} screenshot`, path: capture.screenshotPath, kind: "screenshot" },
|
|
406
|
+
{ label: `${capture.surface.id} browser trace`, path: capture.tracePath, kind: "trace" }
|
|
407
|
+
]
|
|
408
|
+
};
|
|
409
|
+
}),
|
|
410
|
+
events,
|
|
411
|
+
redaction: {
|
|
412
|
+
status: "passed",
|
|
413
|
+
notes: "Browser app proof stores loopback app URLs, screenshots, and generated traces only; secret-like text is rejected by verify."
|
|
414
|
+
},
|
|
415
|
+
artifacts: {
|
|
416
|
+
run: "run.json",
|
|
417
|
+
reviewJson: "review.json",
|
|
418
|
+
reviewMarkdown: "review.md",
|
|
419
|
+
observerData: "observer/observer-data.json",
|
|
420
|
+
events: "events.ndjson"
|
|
421
|
+
},
|
|
422
|
+
review,
|
|
423
|
+
feedbackCandidates: []
|
|
424
|
+
};
|
|
425
|
+
await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
|
|
426
|
+
await writeJson(path.join(options.cwd, ".mimetic", "runs", "latest.json"), {
|
|
427
|
+
schema: "mimetic.latest-run.v1",
|
|
428
|
+
runId,
|
|
429
|
+
path: artifactRoot,
|
|
430
|
+
updatedAt: completedAt
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
schema: "mimetic.run-result.v1",
|
|
434
|
+
ok: allPassed,
|
|
435
|
+
runId,
|
|
436
|
+
mode: "live",
|
|
437
|
+
simCount: captures.length,
|
|
438
|
+
cwd: options.cwd,
|
|
439
|
+
artifactRoot,
|
|
440
|
+
bundlePath: path.join(artifactRoot, "run.json"),
|
|
441
|
+
reviewPath: path.join(artifactRoot, "review.md"),
|
|
442
|
+
latestPath: path.join(".mimetic", "runs", "latest.json"),
|
|
443
|
+
warnings,
|
|
444
|
+
...(allPassed
|
|
445
|
+
? {}
|
|
446
|
+
: {
|
|
447
|
+
error: {
|
|
448
|
+
code: "MIMETIC_BROWSER_APP_CAPTURE_FAILED",
|
|
449
|
+
message: review.summary
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async function captureBrowserSurface(args) {
|
|
455
|
+
const started = Date.now();
|
|
456
|
+
const screenshotPath = path.join("screenshots", `${args.surface.id}.png`);
|
|
457
|
+
const tracePath = path.join("traces", `${args.surface.id}.json`);
|
|
458
|
+
const absoluteScreenshotPath = path.join(args.absoluteArtifactRoot, screenshotPath);
|
|
459
|
+
const absoluteTracePath = path.join(args.absoluteArtifactRoot, tracePath);
|
|
460
|
+
const httpProbe = await probeAppUrl(args.appUrl, Math.min(args.timeoutMs, 15_000));
|
|
461
|
+
const capturedAt = new Date().toISOString();
|
|
462
|
+
const profileDir = await mkdtemp(path.join(os.tmpdir(), "mimetic-browser-profile-"));
|
|
463
|
+
try {
|
|
464
|
+
await captureScreenshotWithBrowser({
|
|
465
|
+
args: [
|
|
466
|
+
"--headless=new",
|
|
467
|
+
"--disable-background-networking",
|
|
468
|
+
"--disable-default-apps",
|
|
469
|
+
"--disable-extensions",
|
|
470
|
+
"--disable-gpu",
|
|
471
|
+
"--disable-dev-shm-usage",
|
|
472
|
+
"--no-first-run",
|
|
473
|
+
"--no-default-browser-check",
|
|
474
|
+
"--hide-scrollbars",
|
|
475
|
+
`--user-data-dir=${profileDir}`,
|
|
476
|
+
`--window-size=${args.surface.viewport.width},${args.surface.viewport.height}`,
|
|
477
|
+
`--force-device-scale-factor=${args.surface.viewport.deviceScaleFactor}`,
|
|
478
|
+
`--screenshot=${absoluteScreenshotPath}`,
|
|
479
|
+
args.appUrl
|
|
480
|
+
],
|
|
481
|
+
browserCommand: args.browserCommand,
|
|
482
|
+
screenshotPath: absoluteScreenshotPath,
|
|
483
|
+
timeoutMs: args.timeoutMs
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
const reason = `Browser screenshot command failed for ${args.surface.id}: ${compactBrowserError(error)}`;
|
|
488
|
+
await writeJson(absoluteTracePath, buildBrowserTrace({
|
|
489
|
+
appUrl: args.appUrl,
|
|
490
|
+
browserCommand: path.basename(args.browserCommand),
|
|
491
|
+
capturedAt,
|
|
492
|
+
durationMs: Date.now() - started,
|
|
493
|
+
...(httpProbe.status === undefined ? {} : { httpStatus: httpProbe.status }),
|
|
494
|
+
ok: false,
|
|
495
|
+
reason,
|
|
496
|
+
screenshotPath,
|
|
497
|
+
surface: args.surface
|
|
498
|
+
}));
|
|
499
|
+
return {
|
|
500
|
+
capturedAt,
|
|
501
|
+
durationMs: Date.now() - started,
|
|
502
|
+
...(httpProbe.status === undefined ? {} : { httpStatus: httpProbe.status }),
|
|
503
|
+
ok: false,
|
|
504
|
+
reason,
|
|
505
|
+
screenshotPath,
|
|
506
|
+
surface: args.surface,
|
|
507
|
+
tracePath
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
await rm(profileDir, { force: true, recursive: true }).catch(() => undefined);
|
|
512
|
+
}
|
|
513
|
+
const screenshotStats = await stat(absoluteScreenshotPath).catch(() => null);
|
|
514
|
+
const ok = Boolean(screenshotStats?.isFile() && screenshotStats.size > 0 && httpProbe.ok);
|
|
515
|
+
const reason = ok
|
|
516
|
+
? `${args.surface.label} screenshot captured from ${args.appUrl}${httpProbe.status === undefined ? "" : ` with HTTP ${httpProbe.status}`}.`
|
|
517
|
+
: screenshotStats?.isFile() && screenshotStats.size > 0
|
|
518
|
+
? `${args.surface.label} screenshot exists, but app HTTP readiness was not proven: ${httpProbe.reason}.`
|
|
519
|
+
: `${args.surface.label} screenshot artifact was missing or empty.`;
|
|
520
|
+
const completedAt = new Date().toISOString();
|
|
521
|
+
const durationMs = Date.now() - started;
|
|
522
|
+
await writeJson(absoluteTracePath, buildBrowserTrace({
|
|
523
|
+
appUrl: args.appUrl,
|
|
524
|
+
browserCommand: path.basename(args.browserCommand),
|
|
525
|
+
capturedAt: completedAt,
|
|
526
|
+
durationMs,
|
|
527
|
+
...(httpProbe.status === undefined ? {} : { httpStatus: httpProbe.status }),
|
|
528
|
+
ok,
|
|
529
|
+
reason,
|
|
530
|
+
screenshotPath,
|
|
531
|
+
surface: args.surface
|
|
532
|
+
}));
|
|
533
|
+
return {
|
|
534
|
+
capturedAt: completedAt,
|
|
535
|
+
durationMs,
|
|
536
|
+
...(httpProbe.status === undefined ? {} : { httpStatus: httpProbe.status }),
|
|
537
|
+
ok,
|
|
538
|
+
reason,
|
|
539
|
+
screenshotPath,
|
|
540
|
+
surface: args.surface,
|
|
541
|
+
tracePath
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function buildBrowserTrace(args) {
|
|
545
|
+
return {
|
|
546
|
+
schema: "mimetic.browser-surface-trace.v1",
|
|
547
|
+
capturedAt: args.capturedAt,
|
|
548
|
+
appUrl: args.appUrl,
|
|
549
|
+
browserCommand: args.browserCommand,
|
|
550
|
+
durationMs: args.durationMs,
|
|
551
|
+
...(args.httpStatus === undefined ? {} : { httpStatus: args.httpStatus }),
|
|
552
|
+
ok: args.ok,
|
|
553
|
+
reason: args.reason,
|
|
554
|
+
screenshotPath: args.screenshotPath,
|
|
555
|
+
surface: args.surface,
|
|
556
|
+
redaction: "passed"
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
async function captureScreenshotWithBrowser(args) {
|
|
560
|
+
const child = spawn(args.browserCommand, args.args, {
|
|
561
|
+
detached: true,
|
|
562
|
+
stdio: "ignore"
|
|
563
|
+
});
|
|
564
|
+
let exitCode = null;
|
|
565
|
+
let signal = null;
|
|
566
|
+
child.once("exit", (code, childSignal) => {
|
|
567
|
+
exitCode = code;
|
|
568
|
+
signal = childSignal;
|
|
569
|
+
});
|
|
570
|
+
child.unref();
|
|
571
|
+
const deadline = Date.now() + args.timeoutMs;
|
|
572
|
+
while (Date.now() <= deadline) {
|
|
573
|
+
const stats = await stat(args.screenshotPath).catch(() => null);
|
|
574
|
+
if (stats?.isFile() && stats.size > 0) {
|
|
575
|
+
terminateProcessGroup(child.pid);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (exitCode !== null || signal !== null) {
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
await wait(250);
|
|
582
|
+
}
|
|
583
|
+
terminateProcessGroup(child.pid, true);
|
|
584
|
+
const stats = await stat(args.screenshotPath).catch(() => null);
|
|
585
|
+
if (stats?.isFile() && stats.size > 0) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
throw new Error(exitCode !== null || signal !== null
|
|
589
|
+
? `browser exited before screenshot was written (exit=${exitCode ?? "null"} signal=${signal ?? "null"})`
|
|
590
|
+
: `timed out after ${args.timeoutMs}ms waiting for screenshot`);
|
|
591
|
+
}
|
|
592
|
+
function terminateProcessGroup(pid, force = false) {
|
|
593
|
+
if (!pid) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
process.kill(-pid, force ? "SIGKILL" : "SIGTERM");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
catch { }
|
|
601
|
+
try {
|
|
602
|
+
process.kill(pid, force ? "SIGKILL" : "SIGTERM");
|
|
603
|
+
}
|
|
604
|
+
catch { }
|
|
605
|
+
}
|
|
606
|
+
async function wait(ms) {
|
|
607
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
608
|
+
}
|
|
609
|
+
function buildBrowserAppEvents(args) {
|
|
610
|
+
const events = [
|
|
611
|
+
{
|
|
612
|
+
id: "event-001",
|
|
613
|
+
at: args.createdAt,
|
|
614
|
+
level: "info",
|
|
615
|
+
type: "browser-app.run.created",
|
|
616
|
+
message: "Created live browser app proof run against a loopback URL."
|
|
617
|
+
}
|
|
618
|
+
];
|
|
619
|
+
args.captures.forEach((capture) => {
|
|
620
|
+
events.push({
|
|
621
|
+
id: `event-${String(events.length + 1).padStart(3, "0")}`,
|
|
622
|
+
at: capture.capturedAt,
|
|
623
|
+
level: capture.ok ? "info" : "warn",
|
|
624
|
+
type: capture.ok ? "browser-app.screenshot.captured" : "browser-app.screenshot.blocked",
|
|
625
|
+
message: `${capture.surface.id}: ${capture.reason}`,
|
|
626
|
+
simId: `browser-${capture.surface.id}`,
|
|
627
|
+
streamId: `browser-${capture.surface.id}-stream`
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
return events;
|
|
631
|
+
}
|
|
632
|
+
function createBrowserAppReviewSummary(args) {
|
|
633
|
+
const passed = args.captures.filter((capture) => capture.ok).length;
|
|
634
|
+
const allPassed = passed === args.captures.length;
|
|
635
|
+
return {
|
|
636
|
+
schema: REVIEW_SCHEMA,
|
|
637
|
+
verdict: allPassed ? "pass" : "blocked",
|
|
638
|
+
summary: allPassed
|
|
639
|
+
? `Captured ${passed}/${args.captures.length} live browser app surface${args.captures.length === 1 ? "" : "s"} from ${args.appUrl}.`
|
|
640
|
+
: `Captured ${passed}/${args.captures.length} live browser app surfaces from ${args.appUrl}; at least one required surface was blocked.`,
|
|
641
|
+
gaps: [
|
|
642
|
+
"This browser app proof captures render evidence and HTTP readiness; it does not yet prove autonomous persona navigation through multi-step product flows.",
|
|
643
|
+
"Only loopback app URLs are accepted so generated bundles do not preserve private external targets.",
|
|
644
|
+
...args.captures
|
|
645
|
+
.filter((capture) => !capture.ok)
|
|
646
|
+
.map((capture) => `${capture.surface.id}: ${capture.reason}`)
|
|
647
|
+
]
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
async function probeAppUrl(appUrl, timeoutMs) {
|
|
651
|
+
const controller = new AbortController();
|
|
652
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
653
|
+
try {
|
|
654
|
+
const response = await fetch(appUrl, {
|
|
655
|
+
signal: controller.signal
|
|
656
|
+
});
|
|
657
|
+
return {
|
|
658
|
+
ok: response.status < 500,
|
|
659
|
+
reason: `HTTP ${response.status}`,
|
|
660
|
+
status: response.status
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
return {
|
|
665
|
+
ok: false,
|
|
666
|
+
reason: compactBrowserError(error)
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
finally {
|
|
670
|
+
clearTimeout(timer);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async function resolveBrowserCommand() {
|
|
674
|
+
const candidates = [
|
|
675
|
+
process.env.MIMETIC_BROWSER_COMMAND,
|
|
676
|
+
"google-chrome",
|
|
677
|
+
"chromium",
|
|
678
|
+
"chromium-browser",
|
|
679
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
680
|
+
].filter((candidate) => Boolean(candidate?.trim()));
|
|
681
|
+
for (const candidate of candidates) {
|
|
682
|
+
try {
|
|
683
|
+
await execFileAsync(candidate, ["--version"], {
|
|
684
|
+
timeout: 5_000,
|
|
685
|
+
maxBuffer: 256 * 1024
|
|
686
|
+
});
|
|
687
|
+
return candidate;
|
|
688
|
+
}
|
|
689
|
+
catch { }
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
function normalizeLocalAppUrl(value) {
|
|
694
|
+
try {
|
|
695
|
+
const url = new URL(value);
|
|
696
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(url.hostname)) {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
url.hash = "";
|
|
703
|
+
return url.toString();
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function compactBrowserError(error) {
|
|
710
|
+
if (error instanceof Error) {
|
|
711
|
+
return redactSensitiveText(error.message).replace(/\s+/g, " ").slice(0, 240);
|
|
712
|
+
}
|
|
713
|
+
return redactSensitiveText(String(error)).replace(/\s+/g, " ").slice(0, 240);
|
|
714
|
+
}
|
|
190
715
|
function isLocalCodexActor(value) {
|
|
191
716
|
return value === "codex-tui" || value === "codex-exec";
|
|
192
717
|
}
|
|
@@ -212,7 +737,7 @@ async function runLocalCodexTui(options) {
|
|
|
212
737
|
warnings,
|
|
213
738
|
error: {
|
|
214
739
|
code: "MIMETIC_ACTOR_FANOUT_UNIMPLEMENTED",
|
|
215
|
-
message: "Local Codex TUI actor support is
|
|
740
|
+
message: "Local Codex TUI actor support is currently single-lane because it owns one PTY/UI session. Use codex-exec for bounded-concurrency fanout."
|
|
216
741
|
}
|
|
217
742
|
};
|
|
218
743
|
}
|
|
@@ -638,28 +1163,29 @@ function buildLocalCodexExecBundle(args) {
|
|
|
638
1163
|
}
|
|
639
1164
|
async function runLocalCodexExec(options) {
|
|
640
1165
|
const warnings = [];
|
|
641
|
-
|
|
1166
|
+
const timeoutMs = normalizeActorTimeout(options.timeoutMs ?? readEnvInteger("MIMETIC_CODEX_ACTOR_TIMEOUT_MS") ?? LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS);
|
|
1167
|
+
if (timeoutMs === null) {
|
|
642
1168
|
return {
|
|
643
1169
|
schema: "mimetic.run-result.v1",
|
|
644
1170
|
ok: false,
|
|
645
1171
|
cwd: options.cwd,
|
|
646
1172
|
warnings,
|
|
647
1173
|
error: {
|
|
648
|
-
code: "
|
|
649
|
-
message:
|
|
1174
|
+
code: "MIMETIC_INVALID_TIMEOUT",
|
|
1175
|
+
message: `--timeout-ms must be an integer between 1 and ${LOCAL_CODEX_TUI_MAX_TIMEOUT_MS}.`
|
|
650
1176
|
}
|
|
651
1177
|
};
|
|
652
1178
|
}
|
|
653
|
-
const
|
|
654
|
-
if (
|
|
1179
|
+
const maxConcurrency = normalizePositiveInteger(readEnvInteger("MIMETIC_LOCAL_CODEX_EXEC_MAX_CONCURRENCY") ?? LOCAL_CODEX_EXEC_DEFAULT_MAX_CONCURRENCY);
|
|
1180
|
+
if (maxConcurrency === null) {
|
|
655
1181
|
return {
|
|
656
1182
|
schema: "mimetic.run-result.v1",
|
|
657
1183
|
ok: false,
|
|
658
1184
|
cwd: options.cwd,
|
|
659
1185
|
warnings,
|
|
660
1186
|
error: {
|
|
661
|
-
code: "
|
|
662
|
-
message:
|
|
1187
|
+
code: "MIMETIC_INVALID_ACTOR_CONCURRENCY",
|
|
1188
|
+
message: "MIMETIC_LOCAL_CODEX_EXEC_MAX_CONCURRENCY must be a positive integer."
|
|
663
1189
|
}
|
|
664
1190
|
};
|
|
665
1191
|
}
|
|
@@ -720,7 +1246,7 @@ async function runLocalCodexExec(options) {
|
|
|
720
1246
|
{
|
|
721
1247
|
at: createdAt,
|
|
722
1248
|
event: "run.created",
|
|
723
|
-
message: `Live local Codex exec run created with ${options.simCount} explicit opt-in actor${options.simCount === 1 ? "" : "s"}.`
|
|
1249
|
+
message: `Live local Codex exec run created with ${options.simCount} explicit opt-in actor${options.simCount === 1 ? "" : "s"} and max concurrency ${maxConcurrency}.`
|
|
724
1250
|
},
|
|
725
1251
|
{
|
|
726
1252
|
at: createdAt,
|
|
@@ -744,7 +1270,7 @@ async function runLocalCodexExec(options) {
|
|
|
744
1270
|
{
|
|
745
1271
|
at: runningAt,
|
|
746
1272
|
event: "actor.running",
|
|
747
|
-
message:
|
|
1273
|
+
message: `Local Codex exec actor lanes are running with max concurrency ${maxConcurrency}; Observer data will refresh as sanitized evidence arrives.`
|
|
748
1274
|
}
|
|
749
1275
|
],
|
|
750
1276
|
lanes: lanes.map((lane) => ({
|
|
@@ -767,7 +1293,7 @@ async function runLocalCodexExec(options) {
|
|
|
767
1293
|
review: createLocalActorRunningReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout")
|
|
768
1294
|
});
|
|
769
1295
|
await writeRunBundleArtifacts(absoluteArtifactRoot, runningBundle);
|
|
770
|
-
const laneResults = await
|
|
1296
|
+
const laneResults = await mapWithConcurrency(lanes, maxConcurrency, async (lane) => {
|
|
771
1297
|
const actor = await executeLocalActorCommand(lane.command, {
|
|
772
1298
|
cwd: options.cwd,
|
|
773
1299
|
timeoutMs
|
|
@@ -807,7 +1333,7 @@ async function runLocalCodexExec(options) {
|
|
|
807
1333
|
tracePath,
|
|
808
1334
|
transcriptPath
|
|
809
1335
|
};
|
|
810
|
-
})
|
|
1336
|
+
});
|
|
811
1337
|
const completedAt = new Date().toISOString();
|
|
812
1338
|
const laneStatuses = laneResults.map((result) => result.actor.status);
|
|
813
1339
|
const status = aggregateActorStatus(laneStatuses);
|
|
@@ -1336,7 +1862,13 @@ function isSamePath(candidatePath, targetPath) {
|
|
|
1336
1862
|
return candidate === target;
|
|
1337
1863
|
}
|
|
1338
1864
|
function normalizeActorTimeout(value) {
|
|
1339
|
-
if (
|
|
1865
|
+
if (normalizePositiveInteger(value) === null || value === undefined || value > LOCAL_CODEX_TUI_MAX_TIMEOUT_MS) {
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
return value;
|
|
1869
|
+
}
|
|
1870
|
+
function normalizePositiveInteger(value) {
|
|
1871
|
+
if (!Number.isInteger(value) || value === undefined || value < 1) {
|
|
1340
1872
|
return null;
|
|
1341
1873
|
}
|
|
1342
1874
|
return value;
|
|
@@ -1348,6 +1880,26 @@ function readEnvInteger(name) {
|
|
|
1348
1880
|
}
|
|
1349
1881
|
return /^\d+$/.test(value) ? Number.parseInt(value, 10) : Number.NaN;
|
|
1350
1882
|
}
|
|
1883
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
1884
|
+
const results = new Array(items.length);
|
|
1885
|
+
let nextIndex = 0;
|
|
1886
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
1887
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
1888
|
+
while (true) {
|
|
1889
|
+
const index = nextIndex;
|
|
1890
|
+
nextIndex += 1;
|
|
1891
|
+
if (index >= items.length) {
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const item = items[index];
|
|
1895
|
+
if (item === undefined) {
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
results[index] = await mapper(item, index);
|
|
1899
|
+
}
|
|
1900
|
+
}));
|
|
1901
|
+
return results;
|
|
1902
|
+
}
|
|
1351
1903
|
function buildLocalCodexTuiPrompt(selection, verdictNonce) {
|
|
1352
1904
|
return [
|
|
1353
1905
|
"You are a Mimetic local Codex TUI dogfood actor.",
|
|
@@ -1511,7 +2063,7 @@ function normalizeSimCount(value) {
|
|
|
1511
2063
|
if (value === undefined) {
|
|
1512
2064
|
return 1;
|
|
1513
2065
|
}
|
|
1514
|
-
if (!Number.
|
|
2066
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
1515
2067
|
return null;
|
|
1516
2068
|
}
|
|
1517
2069
|
return value;
|
|
@@ -1557,10 +2109,23 @@ export async function verifyRun(cwdInput, runInput) {
|
|
|
1557
2109
|
ok: reviewJson !== null && reviewMarkdown !== null,
|
|
1558
2110
|
message: "review.json and review.md must exist"
|
|
1559
2111
|
});
|
|
2112
|
+
const publicSafetyFindings = await scanRunPublicSafetyArtifacts(resolved);
|
|
1560
2113
|
checks.push({
|
|
1561
2114
|
name: "public-safety scan",
|
|
1562
|
-
ok:
|
|
1563
|
-
message:
|
|
2115
|
+
ok: publicSafetyFindings.length === 0,
|
|
2116
|
+
message: publicSafetyFindings.length === 0
|
|
2117
|
+
? "run text artifacts and public-proof paths must not match known secret or browser-profile patterns"
|
|
2118
|
+
: `public-safety findings: ${publicSafetyFindings.slice(0, 5).join(", ")}`
|
|
2119
|
+
});
|
|
2120
|
+
const missingEvidenceArtifacts = isRunBundle(bundle)
|
|
2121
|
+
? await missingLocalEvidenceArtifacts(resolved, bundle)
|
|
2122
|
+
: [];
|
|
2123
|
+
checks.push({
|
|
2124
|
+
name: "local evidence artifacts exist",
|
|
2125
|
+
ok: missingEvidenceArtifacts.length === 0,
|
|
2126
|
+
message: missingEvidenceArtifacts.length === 0
|
|
2127
|
+
? "referenced local screenshot/trace/log/filesystem artifacts are present"
|
|
2128
|
+
: `missing local evidence artifacts: ${missingEvidenceArtifacts.join(", ")}`
|
|
1564
2129
|
});
|
|
1565
2130
|
const ok = checks.every((check) => check.ok);
|
|
1566
2131
|
return {
|
|
@@ -1848,9 +2413,106 @@ async function writeRunBundleArtifacts(absoluteArtifactRoot, bundle) {
|
|
|
1848
2413
|
await writeJson(path.join(absoluteArtifactRoot, "run.json"), bundle);
|
|
1849
2414
|
await writeJson(path.join(absoluteArtifactRoot, "review.json"), bundle.review);
|
|
1850
2415
|
await writeFile(path.join(absoluteArtifactRoot, "review.md"), renderReviewMarkdown(bundle), "utf8");
|
|
2416
|
+
await writeFile(path.join(absoluteArtifactRoot, "events.ndjson"), `${bundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
1851
2417
|
await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
|
|
1852
2418
|
await writeJson(path.join(absoluteArtifactRoot, "observer", "observer-data.json"), buildObserverData(bundle));
|
|
1853
2419
|
}
|
|
2420
|
+
async function missingLocalEvidenceArtifacts(runRoot, bundle) {
|
|
2421
|
+
const requiredPaths = new Set();
|
|
2422
|
+
for (const stream of bundle.streams) {
|
|
2423
|
+
for (const artifact of stream.artifacts) {
|
|
2424
|
+
if ((artifact.kind === "screenshot" || artifact.kind === "trace" || artifact.kind === "log" || artifact.kind === "filesystem")
|
|
2425
|
+
&& isLocalEvidenceArtifactPath(artifact.path)) {
|
|
2426
|
+
requiredPaths.add(artifact.path);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
const embedPath = normalizeLocalEvidenceReference(stream.embed?.kind === "screenshot" ? stream.embed.url : undefined);
|
|
2430
|
+
if (embedPath) {
|
|
2431
|
+
requiredPaths.add(embedPath);
|
|
2432
|
+
}
|
|
2433
|
+
const uiScreenshotPath = normalizeLocalEvidenceReference(stream.ui?.screenshotUrl);
|
|
2434
|
+
if (uiScreenshotPath) {
|
|
2435
|
+
requiredPaths.add(uiScreenshotPath);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
const missing = [];
|
|
2439
|
+
for (const artifactPath of requiredPaths) {
|
|
2440
|
+
const absolutePath = path.join(runRoot, artifactPath);
|
|
2441
|
+
const stats = await stat(absolutePath).catch(() => null);
|
|
2442
|
+
if (!stats?.isFile() || stats.size <= 0) {
|
|
2443
|
+
missing.push(artifactPath);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
return missing;
|
|
2447
|
+
}
|
|
2448
|
+
const riskyPublicArtifactPathSegments = new Set([
|
|
2449
|
+
".git",
|
|
2450
|
+
"Cookies",
|
|
2451
|
+
"Login Data",
|
|
2452
|
+
"Local Storage",
|
|
2453
|
+
"Preferences",
|
|
2454
|
+
"Secure Preferences",
|
|
2455
|
+
"profiles"
|
|
2456
|
+
]);
|
|
2457
|
+
async function scanRunPublicSafetyArtifacts(runRoot) {
|
|
2458
|
+
const findings = [];
|
|
2459
|
+
await scanRunPublicSafetyDirectory(runRoot, runRoot, findings);
|
|
2460
|
+
return findings;
|
|
2461
|
+
}
|
|
2462
|
+
async function scanRunPublicSafetyDirectory(root, current, findings) {
|
|
2463
|
+
if (findings.length >= 50) {
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2467
|
+
for (const entry of entries) {
|
|
2468
|
+
const absolutePath = path.join(current, entry.name);
|
|
2469
|
+
const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/");
|
|
2470
|
+
if (isRiskyPublicArtifactPath(relativePath) || containsSensitivePattern(relativePath)) {
|
|
2471
|
+
findings.push(`risky artifact path ${relativePath}`);
|
|
2472
|
+
if (findings.length >= 50)
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (entry.isDirectory()) {
|
|
2476
|
+
await scanRunPublicSafetyDirectory(root, absolutePath, findings);
|
|
2477
|
+
if (findings.length >= 50)
|
|
2478
|
+
return;
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
if (!entry.isFile() || !shouldScanTextArtifact(relativePath)) {
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
const text = await readFile(absolutePath, "utf8").catch(() => null);
|
|
2485
|
+
if (text !== null && containsSensitivePattern(text)) {
|
|
2486
|
+
findings.push(`sensitive text ${relativePath}`);
|
|
2487
|
+
if (findings.length >= 50)
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
function isRiskyPublicArtifactPath(relativePath) {
|
|
2493
|
+
return relativePath.split(/[\\/]/).some((segment) => riskyPublicArtifactPathSegments.has(segment));
|
|
2494
|
+
}
|
|
2495
|
+
function shouldScanTextArtifact(relativePath) {
|
|
2496
|
+
const extension = path.extname(relativePath).toLowerCase();
|
|
2497
|
+
return ![".png", ".jpg", ".jpeg", ".webp", ".gif", ".tgz", ".gz", ".zip"].includes(extension);
|
|
2498
|
+
}
|
|
2499
|
+
function isLocalEvidenceArtifactPath(value) {
|
|
2500
|
+
return value.length > 0
|
|
2501
|
+
&& !path.isAbsolute(value)
|
|
2502
|
+
&& !value.includes("://")
|
|
2503
|
+
&& !value.startsWith("..")
|
|
2504
|
+
&& !value.split(/[\\/]/).includes("..");
|
|
2505
|
+
}
|
|
2506
|
+
function normalizeLocalEvidenceReference(value) {
|
|
2507
|
+
if (!value || value.includes("://") || path.isAbsolute(value)) {
|
|
2508
|
+
return null;
|
|
2509
|
+
}
|
|
2510
|
+
const normalized = value.replace(/\\/g, "/");
|
|
2511
|
+
if (normalized.startsWith("../")) {
|
|
2512
|
+
return isLocalEvidenceArtifactPath(normalized.slice(3)) ? normalized.slice(3) : null;
|
|
2513
|
+
}
|
|
2514
|
+
return isLocalEvidenceArtifactPath(normalized) ? normalized : null;
|
|
2515
|
+
}
|
|
1854
2516
|
async function validateCwd(cwd) {
|
|
1855
2517
|
try {
|
|
1856
2518
|
const stats = await stat(cwd);
|