humanish 0.88.1 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/actor-contract.d.ts +1 -1
- package/dist/actor-stop-cause.js +1 -0
- package/dist/actor-stop-cause.js.map +1 -1
- package/dist/computer-use-actor.d.ts +2 -0
- package/dist/computer-use-actor.js +6 -3
- package/dist/computer-use-actor.js.map +1 -1
- package/dist/computer-use.d.ts +5 -2
- package/dist/computer-use.js +38 -3
- package/dist/computer-use.js.map +1 -1
- package/dist/cua-actor-lab.js +3 -1
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/cua-diagnostics.d.ts +1 -1
- package/dist/cua-diagnostics.js +1 -1
- package/dist/cua-diagnostics.js.map +1 -1
- package/dist/export-bundle.js +6 -0
- package/dist/export-bundle.js.map +1 -1
- package/dist/export.js +67 -10
- package/dist/export.js.map +1 -1
- package/dist/feedback.d.ts +10 -0
- package/dist/feedback.js +58 -1
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lab-config.d.ts +1 -1
- package/dist/observer-app.html +10 -9
- package/dist/observer.d.ts +2 -0
- package/dist/observer.js +52 -5
- package/dist/observer.js.map +1 -1
- package/dist/openai-responses-cu.d.ts +3 -0
- package/dist/openai-responses-cu.js +4 -1
- package/dist/openai-responses-cu.js.map +1 -1
- package/dist/pricing.js +2 -2
- package/dist/pricing.js.map +1 -1
- package/dist/program.js +146 -3
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +19 -1
- package/dist/run.js +151 -21
- package/dist/run.js.map +1 -1
- package/dist/shared-world-lab.d.ts +2 -0
- package/dist/shared-world-lab.js +90 -5
- package/dist/shared-world-lab.js.map +1 -1
- package/dist/study-analysis-engine.d.ts +34 -0
- package/dist/study-analysis-engine.js +217 -0
- package/dist/study-analysis-engine.js.map +1 -0
- package/dist/study-analysis-evidence.d.ts +27 -0
- package/dist/study-analysis-evidence.js +420 -0
- package/dist/study-analysis-evidence.js.map +1 -0
- package/dist/study-analysis-provider.d.ts +38 -0
- package/dist/study-analysis-provider.js +140 -0
- package/dist/study-analysis-provider.js.map +1 -0
- package/dist/study-analysis-service.d.ts +45 -0
- package/dist/study-analysis-service.js +207 -0
- package/dist/study-analysis-service.js.map +1 -0
- package/dist/study-analysis-sharing.d.ts +11 -0
- package/dist/study-analysis-sharing.js +27 -0
- package/dist/study-analysis-sharing.js.map +1 -0
- package/dist/study-analysis-store.d.ts +30 -0
- package/dist/study-analysis-store.js +343 -0
- package/dist/study-analysis-store.js.map +1 -0
- package/dist/study-analysis-validation.d.ts +448 -0
- package/dist/study-analysis-validation.js +331 -0
- package/dist/study-analysis-validation.js.map +1 -0
- package/dist/study-analysis.d.ts +167 -0
- package/dist/study-analysis.js +4 -0
- package/dist/study-analysis.js.map +1 -0
- package/docs/architecture/observer.md +10 -1
- package/docs/contracts/feedback.md +9 -3
- package/docs/contracts/schemas.md +52 -3
- package/docs/contracts/study-analysis.md +150 -0
- package/docs/goals/current.md +4 -3
- package/docs/ramp/README.md +13 -2
- package/docs/release/0.88.2-sequential-study-budgets.md +57 -0
- package/docs/release/0.89.0-study-findings.md +79 -0
- package/package.json +3 -2
package/dist/run.d.ts
CHANGED
|
@@ -646,6 +646,20 @@ export interface SharedWorldTurn {
|
|
|
646
646
|
seedDigest: string;
|
|
647
647
|
}
|
|
648
648
|
export type SharedWorldTimelineEntry = SharedWorldCheckpoint | SharedWorldTurn;
|
|
649
|
+
/** Declared seats that were never started after one executed sequential role stopped the run.
|
|
650
|
+
* The executed timeline plus this ordered tail must account for every declared sim/stream. */
|
|
651
|
+
export interface SharedWorldSkippedTail {
|
|
652
|
+
afterRoleId: string;
|
|
653
|
+
roles: Array<{
|
|
654
|
+
roleId: string;
|
|
655
|
+
simId: string;
|
|
656
|
+
streamId: string;
|
|
657
|
+
}>;
|
|
658
|
+
cause: "harness_error" | "session_error" | "usage_unreported" | "study_spend_limit";
|
|
659
|
+
/** Present only for a measured aggregate threshold; estimates, not provider billing. */
|
|
660
|
+
maxTotalUsd?: number;
|
|
661
|
+
estimatedTotalUsd?: number;
|
|
662
|
+
}
|
|
649
663
|
/**
|
|
650
664
|
* The shared-world evidence block (`humanish.shared-world.v1`). TWO variants discriminated by
|
|
651
665
|
* `topologyMode` (FIX-8 — renamed off `RunBundle.mode` to avoid the dry-run|live collision):
|
|
@@ -690,6 +704,8 @@ export interface SharedWorldEvidence {
|
|
|
690
704
|
/** The role ids that actually took a turn, in declared order. */
|
|
691
705
|
sequence?: string[];
|
|
692
706
|
timeline?: SharedWorldTimelineEntry[];
|
|
707
|
+
/** Explicit unstarted suffix; absent on historical/full-execution bundles. */
|
|
708
|
+
skippedTail?: SharedWorldSkippedTail;
|
|
693
709
|
/** Per-actor harness-clocked windows (overlap proves simultaneity). */
|
|
694
710
|
laneWindows?: SharedWorldLaneWindow[];
|
|
695
711
|
/** Cadence digests of the shared world under load (baseline + periodic + final). */
|
|
@@ -1055,6 +1071,8 @@ export interface RunResult {
|
|
|
1055
1071
|
export interface VerifyResult {
|
|
1056
1072
|
schema: typeof VERIFY_SCHEMA;
|
|
1057
1073
|
ok: boolean;
|
|
1074
|
+
/** Only present when source evidence verifies but derived analysis failed the public-safety scan. */
|
|
1075
|
+
recordingOk?: boolean;
|
|
1058
1076
|
cwd: string;
|
|
1059
1077
|
run: string;
|
|
1060
1078
|
bundlePath?: string;
|
|
@@ -1066,7 +1084,7 @@ export interface VerifyResult {
|
|
|
1066
1084
|
shareSafety: {
|
|
1067
1085
|
status: "share_ready" | "local_only" | "blocked";
|
|
1068
1086
|
reasons: Array<{
|
|
1069
|
-
code: "VERIFY_FAILED" | "PUBLIC_SAFETY_FINDINGS" | "RAW_SCREENSHOTS";
|
|
1087
|
+
code: "VERIFY_FAILED" | "PUBLIC_SAFETY_FINDINGS" | "ANALYSIS_UNVERIFIED" | "RAW_SCREENSHOTS";
|
|
1070
1088
|
message: string;
|
|
1071
1089
|
}>;
|
|
1072
1090
|
};
|
package/dist/run.js
CHANGED
|
@@ -22,6 +22,8 @@ import { screenshotEvidenceError } from "./image-evidence.js";
|
|
|
22
22
|
import { buildObserverData } from "./observer-data.js";
|
|
23
23
|
import { parseResolvedPersona, personaToDirectives, renderPersonaPromptSection } from "./persona.js";
|
|
24
24
|
import { round6 } from "./pricing.js";
|
|
25
|
+
import { loadStudyAnalysis, listStudyAnalysisExecutions } from "./study-analysis-store.js";
|
|
26
|
+
import { isStudyAnalysisRecordPath, studyAnalysisSharingProblems } from "./study-analysis-sharing.js";
|
|
25
27
|
import { containsSensitive, digestText, redactText, redactToSecretLabel, tailText } from "./redaction.js";
|
|
26
28
|
import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, prepareRunArtifactPaths, resolveExistingRunDirectory, resolveLatestRunDirectory, resolveRunsRoot, validatePreparedRunArtifactPaths } from "./run-paths.js";
|
|
27
29
|
import { probeKeySources } from "./key-resolution.js";
|
|
@@ -2612,7 +2614,13 @@ async function verifyPreparedRun(cwd, runInput, runPaths) {
|
|
|
2612
2614
|
ok: reviewJson !== null && reviewMarkdown !== null,
|
|
2613
2615
|
message: "review.json and review.md must exist"
|
|
2614
2616
|
});
|
|
2615
|
-
const
|
|
2617
|
+
const derivedPublicSafetyFindings = [];
|
|
2618
|
+
const publicSafetyFindings = await scanRunPublicSafetyArtifacts(runPaths, derivedPublicSafetyFindings);
|
|
2619
|
+
// JSON escapes can hide a sensitive value from the byte scan while the
|
|
2620
|
+
// decoded recording exposes it to Observer, feedback, or analysis input.
|
|
2621
|
+
if (publicSafetyFindings.length < 50 && bundle !== null && containsSensitivePattern(JSON.stringify(bundle))) {
|
|
2622
|
+
publicSafetyFindings.push("sensitive decoded run.json");
|
|
2623
|
+
}
|
|
2616
2624
|
checks.push({
|
|
2617
2625
|
name: "public-safety scan",
|
|
2618
2626
|
ok: publicSafetyFindings.length === 0,
|
|
@@ -2716,6 +2724,10 @@ async function verifyPreparedRun(cwd, runInput, runPaths) {
|
|
|
2716
2724
|
? "cost figures are absent, or every claimed estimate carries its ratesAsOf date + source and the total matches its known lines (estimates never presented as exact)"
|
|
2717
2725
|
: `cost labeling findings: ${costFindings.join(", ")}`
|
|
2718
2726
|
});
|
|
2727
|
+
const recordingOk = checks.every((check) => check.ok);
|
|
2728
|
+
if (derivedPublicSafetyFindings.length > 0)
|
|
2729
|
+
checks.push({ name: "derived analysis public-safety scan", ok: false,
|
|
2730
|
+
message: "Derived analysis contains sensitive text or unsafe artifact paths; sharing is blocked, original recording remains independently verifiable." });
|
|
2719
2731
|
const ok = checks.every((check) => check.ok);
|
|
2720
2732
|
const warnings = isRunBundle(bundle)
|
|
2721
2733
|
? [...rawScreenshotPostureWarnings(bundle), ...undeclaredSubjectStateWarnings(bundle), ...desktopGeometryWarnings(bundle)]
|
|
@@ -2731,9 +2743,23 @@ async function verifyPreparedRun(cwd, runInput, runPaths) {
|
|
|
2731
2743
|
}
|
|
2732
2744
|
]
|
|
2733
2745
|
};
|
|
2746
|
+
// Interpretation validity is independent of run validity. Keep recordings usable,
|
|
2747
|
+
// while refusing to silently promote stale or malformed derived text into sharing.
|
|
2748
|
+
const analysis = await loadStudyAnalysis(runPaths);
|
|
2749
|
+
const analysisSharing = studyAnalysisSharingProblems(analysis);
|
|
2750
|
+
const executionHistory = await listStudyAnalysisExecutions(runPaths);
|
|
2751
|
+
if (analysisSharing.unverified || analysisSharing.sensitive || executionHistory.warnings.length > 0) {
|
|
2752
|
+
warnings.push("Some study analysis or correction records could not be validated against current evidence.");
|
|
2753
|
+
shareSafety.reasons.push({ code: "ANALYSIS_UNVERIFIED", message: "Derived analysis or corrections need review against the current evidence before sharing." });
|
|
2754
|
+
if (analysisSharing.sensitive)
|
|
2755
|
+
shareSafety.status = "blocked";
|
|
2756
|
+
else if (shareSafety.status === "share_ready")
|
|
2757
|
+
shareSafety.status = "local_only";
|
|
2758
|
+
}
|
|
2734
2759
|
return {
|
|
2735
2760
|
schema: VERIFY_SCHEMA,
|
|
2736
2761
|
ok,
|
|
2762
|
+
...(!ok && recordingOk ? { recordingOk: true } : {}),
|
|
2737
2763
|
cwd,
|
|
2738
2764
|
run: runInput,
|
|
2739
2765
|
bundlePath: path.relative(cwd, bundlePath),
|
|
@@ -4414,7 +4440,7 @@ const SHARED_WORLD_STATESERIES_KEYS = new Set(["timestamp", "digest"]);
|
|
|
4414
4440
|
*/
|
|
4415
4441
|
function sharedWorldEvidenceFindings(bundle) {
|
|
4416
4442
|
if (bundle.mode !== "live") {
|
|
4417
|
-
return [];
|
|
4443
|
+
return bundle.sharedWorld?.skippedTail === undefined ? [] : ["skippedTail requires a live executed interruption"];
|
|
4418
4444
|
}
|
|
4419
4445
|
const sw = bundle.sharedWorld;
|
|
4420
4446
|
if (!sw) {
|
|
@@ -4426,6 +4452,9 @@ function sharedWorldEvidenceFindings(bundle) {
|
|
|
4426
4452
|
}
|
|
4427
4453
|
// FIX-8: dispatch on topologyMode FIRST; unknown/missing → fail closed.
|
|
4428
4454
|
const topologyMode = sw.topologyMode;
|
|
4455
|
+
if (topologyMode !== "sequential" && sw.skippedTail !== undefined) {
|
|
4456
|
+
return ["skippedTail is only valid on sequential shared-world evidence"];
|
|
4457
|
+
}
|
|
4429
4458
|
if (topologyMode === "sequential") {
|
|
4430
4459
|
return sequentialSharedWorldFindings(bundle, sw);
|
|
4431
4460
|
}
|
|
@@ -4457,6 +4486,95 @@ function sharedWorldCommonFindings(bundle, sw) {
|
|
|
4457
4486
|
}
|
|
4458
4487
|
return findings;
|
|
4459
4488
|
}
|
|
4489
|
+
/** Validate the declared suffix against the executed prefix and existing participant evidence. */
|
|
4490
|
+
function sequentialSkippedTailFindings(bundle, sw, sequence, turns) {
|
|
4491
|
+
const failures = [];
|
|
4492
|
+
const reject = (message) => { failures.push(`skippedTail: ${message}`); };
|
|
4493
|
+
const tail = sw.skippedTail;
|
|
4494
|
+
if (!isRecord(tail) || !Array.isArray(tail.roles) || tail.roles.length === 0 || !tail.roles.every(isRecord)) {
|
|
4495
|
+
return ["skippedTail: a nonempty declared role suffix is required"];
|
|
4496
|
+
}
|
|
4497
|
+
const roster = [...turns, ...tail.roles];
|
|
4498
|
+
if (!Number.isSafeInteger(sw.roleCount) || sw.roleCount < 1
|
|
4499
|
+
|| roster.length !== sw.roleCount || bundle.simCount !== sw.roleCount
|
|
4500
|
+
|| bundle.simulations.length !== sw.roleCount || bundle.streams.length !== sw.roleCount
|
|
4501
|
+
|| sequence.length !== turns.length || turns.length === 0) {
|
|
4502
|
+
reject("executed prefix and blocked suffix must account for every declared simulation and stream");
|
|
4503
|
+
}
|
|
4504
|
+
for (const key of ["roleId", "simId", "streamId"]) {
|
|
4505
|
+
const ids = roster.map(role => role[key]);
|
|
4506
|
+
if (ids.some(id => typeof id !== "string" || id.length === 0) || new Set(ids).size !== ids.length) {
|
|
4507
|
+
reject(`declared ${key} values must be nonempty and unique`);
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
if (tail.afterRoleId !== sequence.at(-1) || tail.afterRoleId !== turns.at(-1)?.roleId) {
|
|
4511
|
+
reject("blocker must be the immediately preceding executed role");
|
|
4512
|
+
}
|
|
4513
|
+
if (bundle.review.verdict === "pass")
|
|
4514
|
+
reject("blocked participants cannot accompany a passed run review");
|
|
4515
|
+
roster.forEach((role, index) => {
|
|
4516
|
+
const sim = bundle.simulations[index], stream = bundle.streams[index];
|
|
4517
|
+
if (!sim || !stream || sim.index !== index + 1 || role.simId !== sim.id || role.streamId !== stream.id
|
|
4518
|
+
|| stream.simId !== sim.id || sim.streamIds.length !== 1 || sim.streamIds[0] !== stream.id) {
|
|
4519
|
+
reject("ordered role, simulation and stream identities must agree");
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
const roleEvents = bundle.events.filter(event => event.simId === sim.id && event.streamId === stream.id);
|
|
4523
|
+
if (index < turns.length) {
|
|
4524
|
+
if (!stream.actor && !roleEvents.some(event => event.type === "shared-world.session.error")) {
|
|
4525
|
+
reject("an executed role needs an actor or an explicit attempted-session error");
|
|
4526
|
+
}
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
if (sim.status !== "blocked" || stream.status !== "blocked"
|
|
4530
|
+
|| stream.actor !== undefined || stream.liveActor !== undefined || stream.embed?.kind !== "placeholder"
|
|
4531
|
+
|| stream.ui?.actorStatus !== undefined || stream.ui?.screenshotUrl !== undefined
|
|
4532
|
+
|| stream.artifacts.some(artifact => artifact.kind === "trace" || artifact.kind === "screenshot")
|
|
4533
|
+
|| typeof sim.currentStep !== "string" || sim.currentStep.length === 0 || stream.ui?.state !== sim.currentStep) {
|
|
4534
|
+
reject("an unstarted role must be blocked with a reason and no actor, trace or screenshot");
|
|
4535
|
+
}
|
|
4536
|
+
const sessionEvents = roleEvents.filter(event => event.type.startsWith("shared-world.session."));
|
|
4537
|
+
if (sessionEvents.length !== 1 || sessionEvents[0]?.type !== "shared-world.session.blocked") {
|
|
4538
|
+
reject("each unstarted role needs exactly one blocked session event");
|
|
4539
|
+
}
|
|
4540
|
+
});
|
|
4541
|
+
const predecessor = bundle.streams[turns.length - 1];
|
|
4542
|
+
const actor = predecessor?.actor;
|
|
4543
|
+
if (tail.cause === "session_error") {
|
|
4544
|
+
if (!predecessor || !bundle.events.some(event => event.type === "shared-world.session.error"
|
|
4545
|
+
&& event.simId === predecessor.simId && event.streamId === predecessor.id)) {
|
|
4546
|
+
reject("session_error requires the predecessor's explicit orchestration error");
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
else if (tail.cause === "harness_error") {
|
|
4550
|
+
if (actor?.completionReason !== "harness_error")
|
|
4551
|
+
reject("harness_error must match the predecessor actor");
|
|
4552
|
+
}
|
|
4553
|
+
else if (tail.cause === "usage_unreported") {
|
|
4554
|
+
if (!actor || !(actor.interactionUsageIncomplete === true || actor.debrief?.usageReported === false
|
|
4555
|
+
|| actor.estimatedCost?.estimatedCostUsd === null))
|
|
4556
|
+
reject("usage_unreported requires recorded unavailable usage");
|
|
4557
|
+
}
|
|
4558
|
+
else if (tail.cause === "study_spend_limit") {
|
|
4559
|
+
const estimates = bundle.streams.slice(0, turns.length).map(stream => stream.actor?.estimatedCost?.estimatedCostUsd);
|
|
4560
|
+
const allKnown = estimates.every(value => typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
4561
|
+
&& bundle.streams.slice(0, turns.length).every(stream => stream.actor?.interactionUsageIncomplete !== true
|
|
4562
|
+
&& stream.actor?.debrief?.usageReported !== false);
|
|
4563
|
+
const sum = estimates.reduce((total, value) => total + (value ?? 0), 0);
|
|
4564
|
+
if (!allKnown || typeof tail.maxTotalUsd !== "number" || !Number.isFinite(tail.maxTotalUsd) || tail.maxTotalUsd < 0
|
|
4565
|
+
|| typeof tail.estimatedTotalUsd !== "number" || !Number.isFinite(tail.estimatedTotalUsd)
|
|
4566
|
+
|| tail.estimatedTotalUsd !== sum || !(sum > tail.maxTotalUsd)) {
|
|
4567
|
+
reject("study_spend_limit requires known prefix estimates exceeding the recorded finite threshold");
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
else {
|
|
4571
|
+
reject("a supported typed interruption cause is required");
|
|
4572
|
+
}
|
|
4573
|
+
if (tail.cause !== "study_spend_limit" && (tail.maxTotalUsd !== undefined || tail.estimatedTotalUsd !== undefined)) {
|
|
4574
|
+
reject("budget figures require a measured study_spend_limit cause");
|
|
4575
|
+
}
|
|
4576
|
+
return failures;
|
|
4577
|
+
}
|
|
4460
4578
|
/**
|
|
4461
4579
|
* SEQUENTIAL branch (the PoC #164): the alternating timeline must be well-formed, single-plane,
|
|
4462
4580
|
* digest-only, and carry the sequential attributionLimits. FIX-8: a sequential bundle must NOT
|
|
@@ -4485,8 +4603,12 @@ function sequentialSharedWorldFindings(bundle, sw) {
|
|
|
4485
4603
|
}
|
|
4486
4604
|
const checkpoints = rawTimeline.filter((entry) => isRecord(entry) && entry.kind === "checkpoint");
|
|
4487
4605
|
const turns = rawTimeline.filter((entry) => isRecord(entry) && entry.kind === "turn");
|
|
4488
|
-
//
|
|
4489
|
-
|
|
4606
|
+
// Historical full-execution bundles keep the original equality rule. A shorter executed
|
|
4607
|
+
// prefix requires explicit blocked-tail evidence, never an inference from absent actors.
|
|
4608
|
+
if (sw.skippedTail !== undefined) {
|
|
4609
|
+
findings.push(...sequentialSkippedTailFindings(bundle, sw, sequence, turns));
|
|
4610
|
+
}
|
|
4611
|
+
else if (!(sequence.length === sw.roleCount && turns.length === sw.roleCount)) {
|
|
4490
4612
|
findings.push(`phantom/dropped role: sequence length (${sequence.length}), roleCount (${sw.roleCount}), and timeline turn count (${turns.length}) must all match`);
|
|
4491
4613
|
}
|
|
4492
4614
|
// Timeline well-formed: starts with cp-baseline, strictly alternates checkpoint → turn →
|
|
@@ -4517,6 +4639,12 @@ function sequentialSharedWorldFindings(bundle, sw) {
|
|
|
4517
4639
|
if (turn.roleId !== sequence[index]) {
|
|
4518
4640
|
findings.push(`turn order does not match the declared sequence at position ${index} (turn "${String(turn.roleId)}" vs sequence "${String(sequence[index])}")`);
|
|
4519
4641
|
}
|
|
4642
|
+
if (sw.skippedTail !== undefined) {
|
|
4643
|
+
const checkpoint = rawTimeline[index * 2 + 2];
|
|
4644
|
+
if (!isRecord(checkpoint) || checkpoint.name !== `cp-after-${String(turn.roleId)}`) {
|
|
4645
|
+
findings.push("skippedTail: each after-checkpoint must belong to its executed role, never an unstarted seat");
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4520
4648
|
});
|
|
4521
4649
|
// Checkpoints: digest is sha256-16 and the record carries NO value-shaped field (digest-only).
|
|
4522
4650
|
for (const checkpoint of checkpoints) {
|
|
@@ -4938,15 +5066,17 @@ const riskyPublicArtifactPathSegments = new Set([
|
|
|
4938
5066
|
"Secure Preferences",
|
|
4939
5067
|
"profiles"
|
|
4940
5068
|
]);
|
|
4941
|
-
async function scanRunPublicSafetyArtifacts(runPaths) {
|
|
5069
|
+
async function scanRunPublicSafetyArtifacts(runPaths, derivedFindings) {
|
|
4942
5070
|
const findings = [];
|
|
4943
5071
|
await validatePreparedRunArtifactPaths(runPaths);
|
|
4944
|
-
await scanRunPublicSafetyDirectory(runPaths, "", findings);
|
|
5072
|
+
await scanRunPublicSafetyDirectory(runPaths, "", findings, derivedFindings);
|
|
4945
5073
|
await validatePreparedRunArtifactPaths(runPaths);
|
|
4946
5074
|
return findings;
|
|
4947
5075
|
}
|
|
4948
|
-
async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, findings) {
|
|
4949
|
-
|
|
5076
|
+
async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, findings, derivedFindings) {
|
|
5077
|
+
// Each authority has its own finding budget. Derived files must never consume
|
|
5078
|
+
// the source scan's budget and make an unscanned recording appear verified.
|
|
5079
|
+
if (findings.length >= 50 && derivedFindings.length >= 50) {
|
|
4950
5080
|
return;
|
|
4951
5081
|
}
|
|
4952
5082
|
const current = relativeDirectory
|
|
@@ -4955,33 +5085,33 @@ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, finding
|
|
|
4955
5085
|
const entries = await readdir(current).catch(() => []);
|
|
4956
5086
|
for (const entryName of entries) {
|
|
4957
5087
|
const relativePath = relativeDirectory ? `${relativeDirectory}/${entryName}` : entryName;
|
|
5088
|
+
const stats = await lstat(path.join(current, entryName), { bigint: true }).catch(() => null);
|
|
5089
|
+
const selectedFindings = !stats?.isDirectory()
|
|
5090
|
+
&& (relativePath === "observer/study-analysis.json" || isStudyAnalysisRecordPath(relativePath)) ? derivedFindings : findings;
|
|
4958
5091
|
if (isRiskyPublicArtifactPath(relativePath) || containsSensitivePattern(relativePath)) {
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
return;
|
|
5092
|
+
if (selectedFindings.length < 50)
|
|
5093
|
+
selectedFindings.push(`risky artifact path ${relativePath}`);
|
|
4962
5094
|
}
|
|
4963
|
-
const stats = await lstat(path.join(current, entryName), { bigint: true }).catch(() => null);
|
|
4964
5095
|
if (!stats || stats.isSymbolicLink() || (!stats.isDirectory() && !stats.isFile()) || (stats.isFile() && stats.nlink > 1n)) {
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
return;
|
|
5096
|
+
if (selectedFindings.length < 50)
|
|
5097
|
+
selectedFindings.push(`unsafe artifact leaf ${relativePath}`);
|
|
4968
5098
|
continue;
|
|
4969
5099
|
}
|
|
4970
5100
|
if (stats.isDirectory()) {
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
5101
|
+
// A directory named analysis.json is not an owned record. Its children
|
|
5102
|
+
// can contain source evidence even after derived findings are saturated.
|
|
5103
|
+
await scanRunPublicSafetyDirectory(runPaths, relativePath, findings, derivedFindings);
|
|
4974
5104
|
continue;
|
|
4975
5105
|
}
|
|
5106
|
+
if (selectedFindings.length >= 50)
|
|
5107
|
+
continue;
|
|
4976
5108
|
if (!shouldScanTextArtifact(relativePath)) {
|
|
4977
5109
|
continue;
|
|
4978
5110
|
}
|
|
4979
5111
|
const bytes = await readSafeRunArtifactBytes(runPaths, relativePath);
|
|
4980
5112
|
const text = bytes?.toString("utf8") ?? null;
|
|
4981
5113
|
if (text !== null && containsSensitivePattern(text)) {
|
|
4982
|
-
|
|
4983
|
-
if (findings.length >= 50)
|
|
4984
|
-
return;
|
|
5114
|
+
selectedFindings.push(`sensitive text ${relativePath}`);
|
|
4985
5115
|
}
|
|
4986
5116
|
}
|
|
4987
5117
|
}
|