humanish 0.124.0 → 0.126.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/CONTEXT.md +1 -1
- package/README.md +2 -2
- package/dist/actors/computer-use/actor.d.ts +2 -0
- package/dist/actors/computer-use/actor.js +1 -0
- package/dist/actors/computer-use/loop/clock.d.ts +9 -8
- package/dist/actors/computer-use/loop/clock.js +27 -17
- package/dist/actors/computer-use/loop/debrief.d.ts +1 -1
- package/dist/actors/computer-use/loop/debrief.js +15 -5
- package/dist/actors/computer-use/loop/ending.d.ts +8 -5
- package/dist/actors/computer-use/loop/ending.js +8 -5
- package/dist/actors/computer-use/loop/observation.d.ts +6 -0
- package/dist/actors/computer-use/loop/observation.js +19 -1
- package/dist/actors/computer-use/loop/session.d.ts +4 -2
- package/dist/actors/computer-use/loop/session.js +4 -1
- package/dist/actors/computer-use/loop/trace.js +4 -2
- package/dist/actors/computer-use/loop/types.d.ts +22 -8
- package/dist/actors/computer-use/loop.js +5 -5
- package/dist/actors/contract.d.ts +31 -8
- package/dist/actors/contract.js +3 -0
- package/dist/actors/stop-cause.js +1 -0
- package/dist/analysis/evidence.js +2 -2
- package/dist/cli/commands/study.js +8 -0
- package/dist/evidence/redaction.d.ts +1 -0
- package/dist/evidence/redaction.js +1 -0
- package/dist/evidence/trace-page.d.ts +6 -0
- package/dist/evidence/trace-page.js +90 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/observer/feed-cursors.d.ts +14 -0
- package/dist/observer/feed-cursors.js +61 -0
- package/dist/observer/participant-items.d.ts +7 -0
- package/dist/observer/pinned-files.d.ts +4 -1
- package/dist/observer/pinned-files.js +10 -7
- package/dist/observer/run-routes.d.ts +1 -1
- package/dist/observer/run-routes.js +39 -17
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle.js +4 -0
- package/dist/routes/computer-use/diagnostics.d.ts +1 -1
- package/dist/routes/computer-use/diagnostics.js +3 -3
- package/dist/routes/computer-use/e2b-desktop/desktop.js +2 -1
- package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +6 -0
- package/dist/routes/computer-use/e2b-desktop/prepare.js +47 -1
- package/dist/routes/computer-use/e2b-desktop/start.d.ts +2 -1
- package/dist/routes/computer-use/e2b-desktop/start.js +15 -3
- package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -1
- package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
- package/dist/routes/computer-use/fanout-records.js +4 -0
- package/dist/routes/computer-use/live-flush.d.ts +9 -4
- package/dist/routes/computer-use/live-flush.js +16 -3
- package/dist/routes/computer-use/participant-deps.d.ts +1 -1
- package/dist/routes/computer-use/participant-desktop.d.ts +1 -1
- package/dist/routes/computer-use/participant-model.js +16 -6
- package/dist/routes/computer-use/participant-runs.d.ts +2 -2
- package/dist/routes/computer-use/participant-runs.js +11 -7
- package/dist/routes/computer-use/plan.js +39 -1
- package/dist/routes/computer-use/setup.d.ts +1 -0
- package/dist/routes/computer-use/setup.js +28 -0
- package/dist/routes/computer-use/single-bundle.d.ts +5 -1
- package/dist/routes/computer-use/single-bundle.js +2 -0
- package/dist/routes/computer-use/types.d.ts +16 -10
- package/dist/routes/shared-world/bundle-records.js +3 -0
- package/dist/routes/shared-world/participant-specs.js +3 -2
- package/dist/routes/shared-world/plan.js +5 -0
- package/dist/run/bundle-shape.js +2 -2
- package/dist/run/bundle.d.ts +10 -3
- package/dist/run/bundle.js +11 -2
- package/dist/run/contained-output.d.ts +26 -0
- package/dist/run/contained-output.js +101 -1
- package/dist/run/evidence-files.d.ts +12 -1
- package/dist/run/evidence-files.js +70 -40
- package/dist/run/judge.d.ts +7 -4
- package/dist/run/judge.js +13 -8
- package/dist/run/live-items.d.ts +67 -0
- package/dist/run/live-items.js +262 -0
- package/dist/run/locate.d.ts +10 -0
- package/dist/run/locate.js +10 -0
- package/dist/run/run.d.ts +3 -2
- package/dist/run/run.js +29 -8
- package/dist/run/sandbox-ids.d.ts +0 -5
- package/dist/run/sandbox-ids.js +0 -7
- package/dist/run/stream-trace.d.ts +40 -10
- package/dist/run/stream-trace.js +151 -22
- package/dist/run/streams.d.ts +33 -2
- package/dist/study/arrivals.d.ts +32 -14
- package/dist/study/arrivals.js +47 -21
- package/dist/study/bundle-estimate.d.ts +6 -5
- package/dist/study/bundle-estimate.js +12 -7
- package/dist/study/desktop-files.d.ts +37 -0
- package/dist/study/desktop-files.js +174 -0
- package/dist/study/keys.d.ts +8 -0
- package/dist/study/keys.js +3 -0
- package/dist/study/migrate/v2.js +12 -3
- package/dist/study/parse/actors.d.ts +1 -1
- package/dist/study/parse/actors.js +51 -26
- package/dist/study/parse/clock.d.ts +18 -0
- package/dist/study/parse/clock.js +36 -0
- package/dist/study/parse/execution.js +6 -0
- package/dist/study/plan-base.js +4 -1
- package/dist/study/plan-participants.d.ts +19 -5
- package/dist/study/plan-participants.js +17 -1
- package/dist/study/plan.js +24 -13
- package/dist/study/preflight.js +4 -0
- package/dist/study/types.d.ts +34 -5
- package/dist/study/validation.js +3 -0
- package/dist/study/warnings.d.ts +2 -2
- package/dist/study/warnings.js +7 -0
- package/dist/subject/local-tree-archive.d.ts +7 -0
- package/dist/subject/local-tree-archive.js +12 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +4 -1
- package/dist/substrates/e2b/desktop-browser.js +6 -2
- package/dist/substrates/e2b/desktop-clock.d.ts +10 -0
- package/dist/substrates/e2b/desktop-clock.js +29 -0
- package/dist/substrates/e2b/sdk.d.ts +6 -0
- package/dist/verify/artifacts.js +39 -51
- package/dist/verify/pages.d.ts +7 -0
- package/dist/verify/pages.js +50 -0
- package/dist/verify/screenshot-reads.d.ts +21 -0
- package/dist/verify/screenshot-reads.js +46 -0
- package/dist/verify/verify.js +17 -9
- package/docs/architecture/actor-contract.md +2 -0
- package/docs/architecture/external-public-shared-world.md +4 -1
- package/docs/architecture/github-feedback-loop.md +1 -1
- package/docs/architecture/observer.md +10 -1
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/policy.md +7 -0
- package/docs/contracts/run-bundle.md +106 -14
- package/docs/contracts/schemas.md +110 -40
- package/package.json +4 -2
|
@@ -13,7 +13,7 @@ import { type CuaDiagnostics } from "./diagnostics.js";
|
|
|
13
13
|
import type { ActorCompletionReason, ActorStatus, ActorStopCause, ActorTokenUsage, ActorTraceItem } from "../../actors/contract.js";
|
|
14
14
|
import { type CuaActorDescriptor } from "../../actors/registry.js";
|
|
15
15
|
import type { StudyDeps } from "../../study/study-deps.js";
|
|
16
|
-
import type { ArrivalPlan, ParticipantArrival } from "../../study/arrivals.js";
|
|
16
|
+
import type { ArrivalPlan, ParticipantArrival, ParticipantEnd } from "../../study/arrivals.js";
|
|
17
17
|
import type { StudyEvent, ParticipantRef } from "../../study/run-study-events.js";
|
|
18
18
|
import type { InProcessDriver, ProviderFactory, RunStudyHomes } from "../../study/run-study-homes.js";
|
|
19
19
|
import { type BrowserScorer } from "../../study/adapter-extension.js";
|
|
@@ -23,9 +23,10 @@ import { type DesktopResourceObservation } from "../../substrates/e2b/desktop-re
|
|
|
23
23
|
import { type DetachedTimers } from "../../substrates/detached.js";
|
|
24
24
|
import type { Brain, ComputerUsePlan, ComputerUseRunner, ResidualConfig } from "../../study/plan-types.js";
|
|
25
25
|
import { type StudyCommsEmail } from "../../study/types.js";
|
|
26
|
+
import type { DesktopFile } from "../../study/desktop-files.js";
|
|
26
27
|
import { type ObserverResult } from "../../observer/render.js";
|
|
27
28
|
import { type BundleRun, type RunBundle, type RunRerunLineage, type RunScorerProvenance, type RunSubjectProvenance, type RunSubjectStateStepRecord } from "../../run/bundle.js";
|
|
28
|
-
import { type RunDesktopGeometry } from "../../run/streams.js";
|
|
29
|
+
import { type RunDesktopFile, type RunDesktopGeometry } from "../../run/streams.js";
|
|
29
30
|
import { type PreparedOutputRoot } from "../../run/contained-output.js";
|
|
30
31
|
import type { ComputerUseParticipant, SharedWorldParticipant } from "../../study/plan-participants.js";
|
|
31
32
|
import type { ResolvedParticipant } from "../../run/participant.js";
|
|
@@ -114,6 +115,8 @@ export interface CuaParticipantPlanEntry {
|
|
|
114
115
|
startAfterMs?: number;
|
|
115
116
|
/** The participant's declared end, on the same clock. */
|
|
116
117
|
endAfterMs?: number;
|
|
118
|
+
/** The participant's session cap, from the start of its session. */
|
|
119
|
+
maxSessionMs?: number;
|
|
117
120
|
}
|
|
118
121
|
/** The pre-flight spend and participant plan (pure; printed to stderr + recorded as a bundle event before
|
|
119
122
|
* any sandbox or provider call; identical in dry-run, marked $0). */
|
|
@@ -195,7 +198,7 @@ export interface CuaParticipantSummary {
|
|
|
195
198
|
concurrency: number;
|
|
196
199
|
waves: number;
|
|
197
200
|
}
|
|
198
|
-
export type CuaActorStudyErrorCode = StudyResultErrorCode | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_LOCAL_CAPACITY_EXCEEDED" | "HUMANISH_COMPUTER_USE_DESKTOP_OUT_OF_MEMORY" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
|
|
201
|
+
export type CuaActorStudyErrorCode = StudyResultErrorCode | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_LOCAL_CAPACITY_EXCEEDED" | "HUMANISH_COMPUTER_USE_DESKTOP_OUT_OF_MEMORY" | "HUMANISH_COMPUTER_USE_DESKTOP_FILES_INVALID" | "HUMANISH_COMPUTER_USE_TIME_ZONE_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
|
|
199
202
|
export interface CuaActorStudyResult extends AutomaticAnalysisResult, StudyResultIdentity<"computer-use"> {
|
|
200
203
|
/** True when the Observer verified the bundle, all live participants passed credibility checks
|
|
201
204
|
* (or this is a dry-run), and no declared adapter/scorer verdict failed. */
|
|
@@ -316,6 +319,9 @@ export interface E2BDesktopDeps {
|
|
|
316
319
|
/** Local-tree route only: the once-per-run packed archive bytes, shared byte-identically
|
|
317
320
|
* across every fan-out participant's upload step. Absent on dry-run and every other route. */
|
|
318
321
|
localTreeArchiveBuffer?: ArrayBuffer;
|
|
322
|
+
/** Computer use on a hosted desktop: each participant's files (participants[].files) by id,
|
|
323
|
+
* read on this machine before any sandbox. A participant without an entry receives none. */
|
|
324
|
+
desktopFiles?: ReadonlyMap<string, readonly DesktopFile[]>;
|
|
319
325
|
env: Record<string, string | undefined>;
|
|
320
326
|
e2bApiKey: string;
|
|
321
327
|
requestTimeoutMs: number;
|
|
@@ -367,14 +373,12 @@ export interface ParticipantModelDeps {
|
|
|
367
373
|
openaiApiKey: string;
|
|
368
374
|
timeoutMs: number;
|
|
369
375
|
/**
|
|
370
|
-
*
|
|
371
|
-
* (scheduledParticipantDeps)
|
|
376
|
+
* How the participant's session ends, when it declares participants[].endAfterMs or maxSessionMs
|
|
377
|
+
* (scheduledParticipantDeps): at its scheduled end on `now`'s clock, with stop cause
|
|
378
|
+
* `scheduled_end`, or at its cap after the session starts, with stop cause `session_cap`.
|
|
372
379
|
*/
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
readonly windDownMs: number;
|
|
376
|
-
};
|
|
377
|
-
/** The run's clock. A participant with a scheduled end runs its session on it. */
|
|
380
|
+
end?: ParticipantEnd;
|
|
381
|
+
/** The run's clock. A participant with an end or a cap runs its session on it. */
|
|
378
382
|
now: () => number;
|
|
379
383
|
participantCount: number;
|
|
380
384
|
artifactRoot: PreparedOutputRoot;
|
|
@@ -449,6 +453,8 @@ export interface ParticipantRunOutcome {
|
|
|
449
453
|
/** Requested + measured desktop/browser geometry. Viewport is absent when measurement failed. */
|
|
450
454
|
desktopGeometry?: RunDesktopGeometry;
|
|
451
455
|
recording?: RunDesktopRecording;
|
|
456
|
+
/** participants[].files confirmed on the participant's desktop. */
|
|
457
|
+
desktopFiles?: RunDesktopFile[];
|
|
452
458
|
stateStepRecords: RunSubjectStateStepRecord[];
|
|
453
459
|
/** Completed subject-phase records (clone/upload/extract/install/build/ready/state groups),
|
|
454
460
|
* folded into bundle.events at build time. Empty on the in-process route (no provisioning). */
|
|
@@ -122,6 +122,9 @@ function sharedWorldStream(ctx, spec, index, view) {
|
|
|
122
122
|
},
|
|
123
123
|
}),
|
|
124
124
|
desktopGeometry,
|
|
125
|
+
...(spec.planned.limits.timeZone === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { timeZone: spec.planned.limits.timeZone }),
|
|
125
128
|
ui: {
|
|
126
129
|
route: view.route,
|
|
127
130
|
intent: `Watch ${name} use the shared app at the same time as the others.`,
|
|
@@ -140,10 +140,11 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
|
|
|
140
140
|
studyCwd: ctx.cwd,
|
|
141
141
|
redactScreenshots: ctx.redactScreenshots,
|
|
142
142
|
scrubKnownValues,
|
|
143
|
-
// A session that throws returns no trace; the run still records that it started.
|
|
143
|
+
// A session that throws returns no trace; the run still records that it started. Observed
|
|
144
|
+
// URLs land in a shared-world bundle only as digests, so no session records its pages.
|
|
144
145
|
runSession: (options) => {
|
|
145
146
|
ctx.run.participantStarted();
|
|
146
|
-
return ctx.runSession(options);
|
|
147
|
+
return ctx.runSession({ ...options, recordPages: false });
|
|
147
148
|
},
|
|
148
149
|
liveFlush: () => live.flush?.flush,
|
|
149
150
|
onStream: participantStreams(ctx.input.onStream, live),
|
|
@@ -8,6 +8,8 @@ import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
|
|
|
8
8
|
import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
9
9
|
import { brainOf, planCaps, desktopRequirements, isNonEmpty, planBase, } from "../../study/plan-base.js";
|
|
10
10
|
import { sharedWorldParticipants } from "../../study/plan-participants.js";
|
|
11
|
+
import { DESKTOP_FILES_ROUTE_REASON } from "../../study/desktop-files.js";
|
|
12
|
+
import { participantList } from "../../study/study-fields.js";
|
|
11
13
|
import { PUBLIC_TARGET_OWNER_PATTERN } from "../../study/parse/subject.js";
|
|
12
14
|
import { REPO_SLUG_PATTERN } from "../../study/parse/values.js";
|
|
13
15
|
import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
|
|
@@ -41,6 +43,9 @@ export function planSharedWorldStudy(config, input) {
|
|
|
41
43
|
const tasksReason = taskProtocolValidationReason(config, false);
|
|
42
44
|
if (tasksReason)
|
|
43
45
|
return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
46
|
+
// The parser refuses these; a library caller's config skips it.
|
|
47
|
+
if (participantList(config)?.some((entry) => entry.files !== undefined))
|
|
48
|
+
return refuse(invalid, `participants[].files: ${DESKTOP_FILES_ROUTE_REASON}. Remove them.`);
|
|
44
49
|
const actorType = config.actor?.type ?? "";
|
|
45
50
|
const descriptor = actorRegistry[actorType];
|
|
46
51
|
if (!descriptor || !isCuaActorDescriptor(descriptor))
|
package/dist/run/bundle-shape.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isCommsReceivingEvidence } from "../comms/receiving-evidence.js";
|
|
2
2
|
import { isCapturedGitState } from "./git-state.js";
|
|
3
|
-
import { PUBLIC_TARGET_CWD, REVIEW_SCHEMA,
|
|
3
|
+
import { PUBLIC_TARGET_CWD, REVIEW_SCHEMA, isReadRunBundleSchema, } from "./bundle.js";
|
|
4
4
|
import { CLEANUP_SCHEMA, } from "./results.js";
|
|
5
5
|
import { isRunSimulationStatus, isRunStream, isRunStreamKind } from "./stream-shape.js";
|
|
6
6
|
import { isLocalEvidenceArtifactPath } from "./paths.js";
|
|
@@ -10,7 +10,7 @@ import { isSharedWorldEvidence } from "./shared-world-shape.js";
|
|
|
10
10
|
import { isNonNegativeSafeInteger, isPositiveSafeInteger, isRecord } from "./type-guards.js";
|
|
11
11
|
export function isRunBundle(value) {
|
|
12
12
|
return (isRecord(value) &&
|
|
13
|
-
(value.schema
|
|
13
|
+
isReadRunBundleSchema(value.schema) &&
|
|
14
14
|
(value.commsReceiving === undefined || isCommsReceivingEvidence(value.commsReceiving)) &&
|
|
15
15
|
(value.publication === undefined ||
|
|
16
16
|
(isRecord(value.publication) &&
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -5,10 +5,17 @@ import type { SharedWorldEvidence } from "./shared-world-evidence.js";
|
|
|
5
5
|
import { type RunStudyProvenance } from "./study-provenance.js";
|
|
6
6
|
import type { RunInterruptSignal } from "./status.js";
|
|
7
7
|
import type { RunSimulationStatus, RunStream, RunStreamKind } from "./streams.js";
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The run bundle version this release writes: each stream's trace, and each running participant's
|
|
10
|
+
* items so far, are files run.json indexes.
|
|
11
|
+
*/
|
|
12
|
+
export declare const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v3";
|
|
13
|
+
/** The version 0.123.0 and 0.124.0 wrote, with a running participant's items inline. It is still read. */
|
|
14
|
+
export declare const RUN_BUNDLE_V2_SCHEMA = "humanish.run-bundle.v2";
|
|
10
15
|
/** The version 0.122.0 and earlier wrote, with every trace inline. It is still read. */
|
|
11
16
|
export declare const RUN_BUNDLE_V1_SCHEMA = "humanish.run-bundle.v1";
|
|
17
|
+
/** Whether `value` names a run bundle version this release reads: v3, v2 or v1. */
|
|
18
|
+
export declare function isReadRunBundleSchema(value: unknown): value is RunBundle["schema"];
|
|
12
19
|
/** The run bundle file, relative to the run directory. */
|
|
13
20
|
export declare const RUN_BUNDLE_FILE = "run.json";
|
|
14
21
|
export declare const REVIEW_SCHEMA = "humanish.review.v1";
|
|
@@ -231,7 +238,7 @@ export interface RunBundle {
|
|
|
231
238
|
restrictions: ["real-communications"];
|
|
232
239
|
};
|
|
233
240
|
commsReceiving?: CommsReceivingEvidence;
|
|
234
|
-
schema: typeof RUN_BUNDLE_SCHEMA | typeof RUN_BUNDLE_V1_SCHEMA;
|
|
241
|
+
schema: typeof RUN_BUNDLE_SCHEMA | typeof RUN_BUNDLE_V2_SCHEMA | typeof RUN_BUNDLE_V1_SCHEMA;
|
|
235
242
|
runId: string;
|
|
236
243
|
mode: "dry-run" | "live";
|
|
237
244
|
simCount: number;
|
package/dist/run/bundle.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { captureGitState } from "./git-state.js";
|
|
3
3
|
import { studyFields } from "./study-provenance.js";
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The run bundle version this release writes: each stream's trace, and each running participant's
|
|
6
|
+
* items so far, are files run.json indexes.
|
|
7
|
+
*/
|
|
8
|
+
export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v3";
|
|
9
|
+
/** The version 0.123.0 and 0.124.0 wrote, with a running participant's items inline. It is still read. */
|
|
10
|
+
export const RUN_BUNDLE_V2_SCHEMA = "humanish.run-bundle.v2";
|
|
6
11
|
/** The version 0.122.0 and earlier wrote, with every trace inline. It is still read. */
|
|
7
12
|
export const RUN_BUNDLE_V1_SCHEMA = "humanish.run-bundle.v1";
|
|
13
|
+
/** Whether `value` names a run bundle version this release reads: v3, v2 or v1. */
|
|
14
|
+
export function isReadRunBundleSchema(value) {
|
|
15
|
+
return (value === RUN_BUNDLE_SCHEMA || value === RUN_BUNDLE_V2_SCHEMA || value === RUN_BUNDLE_V1_SCHEMA);
|
|
16
|
+
}
|
|
8
17
|
/** The run bundle file, relative to the run directory. */
|
|
9
18
|
export const RUN_BUNDLE_FILE = "run.json";
|
|
10
19
|
export const REVIEW_SCHEMA = "humanish.review.v1";
|
|
@@ -46,6 +46,21 @@ export declare function writeContainedOutputFile(rootInput: PreparedOutputRoot,
|
|
|
46
46
|
* writeContainedOutputFile renames a finished file into place instead, replacing what is there.
|
|
47
47
|
*/
|
|
48
48
|
export declare function createContainedOutputFile(rootInput: PreparedOutputRoot, relativePath: string, data: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Appends `data` to an existing single-link regular file inside the root that holds exactly
|
|
51
|
+
* `size` bytes, the count the caller says it wrote, so an append never lands after bytes the
|
|
52
|
+
* caller did not write. The checks writeContainedOutputFile makes run before the open; the opened
|
|
53
|
+
* file must be the one checked, and the root and its folders are checked again before the write.
|
|
54
|
+
* The data is synced before this resolves, so a run.json written after it never indexes bytes the
|
|
55
|
+
* disk may not hold.
|
|
56
|
+
*/
|
|
57
|
+
export declare function appendContainedOutputFile(rootInput: PreparedOutputRoot, relativePath: string, data: Uint8Array, size: number): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Removes a single-link regular file or an empty folder inside the root. Nothing at the path, or a
|
|
60
|
+
* folder on it missing, is not an error. A link, a hard-linked file or a folder that holds anything
|
|
61
|
+
* is refused and kept.
|
|
62
|
+
*/
|
|
63
|
+
export declare function removeContainedOutputFile(rootInput: PreparedOutputRoot, relativePath: string): Promise<void>;
|
|
49
64
|
/**
|
|
50
65
|
* A file in a run directory or a preflight journal. verify reads every run file whole to scan it,
|
|
51
66
|
* and a frame whole to check it, so this covers the largest screenshot src/evidence/image.ts
|
|
@@ -99,6 +114,17 @@ export declare function readContainedRegularFile(rootInput: PreparedOutputRoot,
|
|
|
99
114
|
* after it was opened is never read whole.
|
|
100
115
|
*/
|
|
101
116
|
export declare function readOpenedAtMost(handle: FileHandle, maxBytes: number): Promise<Buffer | null>;
|
|
117
|
+
/**
|
|
118
|
+
* The first `length` bytes of one regular file, under the checks readContainedRegularFile makes,
|
|
119
|
+
* or all of a file that holds fewer. Bytes past `length`, such as a later append, are not read.
|
|
120
|
+
* With `start`, the bytes from `start` to `length`.
|
|
121
|
+
*/
|
|
122
|
+
export declare function readContainedPrefix(rootInput: PreparedOutputRoot, relativePath: string, length: number, start?: number): Promise<ContainedRead>;
|
|
123
|
+
/**
|
|
124
|
+
* The first `length` bytes of an opened file, read by position, or all of a shorter one. With
|
|
125
|
+
* `start`, the bytes from `start` to `length`.
|
|
126
|
+
*/
|
|
127
|
+
export declare function readOpenedPrefix(handle: FileHandle, length: number, start?: number): Promise<Buffer>;
|
|
102
128
|
/** The caller owns this checked descriptor and must close it after reading/streaming. */
|
|
103
129
|
export declare function openContainedRegularFile(rootInput: PreparedOutputRoot, relativePath: string): Promise<FileHandle | null>;
|
|
104
130
|
export declare function assertSafeOutputPathSegment(value: string, label?: string): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, realpath, rename, rmdir, unlink, } from "node:fs/promises";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { assertDirectoryIdentity, assertRegularFileOrMissing, isPathInside, prepareHumanishStorageDirectory, resolveExistingHumanishStorageDirectory, validatePreparedRunArtifactPaths, validatePreparedRunRootIdentity, } from "./paths.js";
|
|
@@ -150,6 +150,70 @@ export async function createContainedOutputFile(rootInput, relativePath, data) {
|
|
|
150
150
|
}
|
|
151
151
|
await handle.close();
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Appends `data` to an existing single-link regular file inside the root that holds exactly
|
|
155
|
+
* `size` bytes, the count the caller says it wrote, so an append never lands after bytes the
|
|
156
|
+
* caller did not write. The checks writeContainedOutputFile makes run before the open; the opened
|
|
157
|
+
* file must be the one checked, and the root and its folders are checked again before the write.
|
|
158
|
+
* The data is synced before this resolves, so a run.json written after it never indexes bytes the
|
|
159
|
+
* disk may not hold.
|
|
160
|
+
*/
|
|
161
|
+
export async function appendContainedOutputFile(rootInput, relativePath, data, size) {
|
|
162
|
+
const filePath = await prepareContainedOutputFile(rootInput, relativePath);
|
|
163
|
+
const root = await resolveOutputRoot(rootInput);
|
|
164
|
+
await assertContainedDirectoryChain(root, path.dirname(filePath));
|
|
165
|
+
const before = await lstat(filePath, { bigint: true });
|
|
166
|
+
if (!before.isFile() || before.nlink > 1n)
|
|
167
|
+
throw new Error(`${relativePath} is not a single-link regular file.`);
|
|
168
|
+
// O_NONBLOCK: a file swapped for a FIFO after the lstat fails the open at once (no reader), where
|
|
169
|
+
// a blocking open would wait for a reader and hold up every later write in the run's queue.
|
|
170
|
+
// Writes to a regular file ignore it.
|
|
171
|
+
const handle = await open(filePath, constants.O_WRONLY | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
172
|
+
try {
|
|
173
|
+
const after = await handle.stat({ bigint: true });
|
|
174
|
+
if (!after.isFile() || after.nlink > 1n || after.dev !== before.dev || after.ino !== before.ino)
|
|
175
|
+
throw new Error(`${relativePath} changed while it was opened.`);
|
|
176
|
+
if (after.size !== BigInt(size))
|
|
177
|
+
throw new Error(`${relativePath} holds ${after.size} bytes, not ${size}.`);
|
|
178
|
+
if ((await resolveOutputRoot(rootInput)) !== root)
|
|
179
|
+
throw new Error("Output root changed after it was prepared.");
|
|
180
|
+
await assertContainedDirectoryChain(root, path.dirname(filePath));
|
|
181
|
+
const { bytesWritten } = await handle.write(data, 0, data.length);
|
|
182
|
+
if (bytesWritten !== data.length)
|
|
183
|
+
throw new Error(`${relativePath} was not appended in full.`);
|
|
184
|
+
await handle.datasync();
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await handle.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Removes a single-link regular file or an empty folder inside the root. Nothing at the path, or a
|
|
192
|
+
* folder on it missing, is not an error. A link, a hard-linked file or a folder that holds anything
|
|
193
|
+
* is refused and kept.
|
|
194
|
+
*/
|
|
195
|
+
export async function removeContainedOutputFile(rootInput, relativePath) {
|
|
196
|
+
assertSafeRelativeOutputPath(relativePath, false);
|
|
197
|
+
const root = await resolveOutputRoot(rootInput);
|
|
198
|
+
const candidate = path.resolve(root, normalizeRelativeOutputPath(relativePath));
|
|
199
|
+
if (!isPathInside(root, candidate) || candidate === root)
|
|
200
|
+
throw new Error("Output file must stay inside its selected root.");
|
|
201
|
+
let stats;
|
|
202
|
+
try {
|
|
203
|
+
await assertContainedDirectoryChain(root, path.dirname(candidate));
|
|
204
|
+
stats = await lstat(candidate, { bigint: true });
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
208
|
+
return;
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
if (stats.isDirectory())
|
|
212
|
+
return rmdir(candidate);
|
|
213
|
+
if (!stats.isFile() || stats.nlink > 1n)
|
|
214
|
+
throw new Error(`${relativePath} is not a single-link regular file.`);
|
|
215
|
+
await unlink(candidate);
|
|
216
|
+
}
|
|
153
217
|
// The most humanish reads of one file of each kind. A larger file is refused unread.
|
|
154
218
|
/**
|
|
155
219
|
* A file in a run directory or a preflight journal. verify reads every run file whole to scan it,
|
|
@@ -240,6 +304,42 @@ export async function readOpenedAtMost(handle, maxBytes) {
|
|
|
240
304
|
chunks.push(chunk.subarray(0, bytesRead));
|
|
241
305
|
}
|
|
242
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* The first `length` bytes of one regular file, under the checks readContainedRegularFile makes,
|
|
309
|
+
* or all of a file that holds fewer. Bytes past `length`, such as a later append, are not read.
|
|
310
|
+
* With `start`, the bytes from `start` to `length`.
|
|
311
|
+
*/
|
|
312
|
+
export async function readContainedPrefix(rootInput, relativePath, length, start = 0) {
|
|
313
|
+
const opened = await openContained(rootInput, relativePath);
|
|
314
|
+
if (opened.status === "missing")
|
|
315
|
+
return opened;
|
|
316
|
+
if (opened.status === "refused")
|
|
317
|
+
return { status: "refused", reason: opened.reason, limit: length };
|
|
318
|
+
try {
|
|
319
|
+
return { status: "read", bytes: await readOpenedPrefix(opened.handle, length, start) };
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return { status: "refused", reason: "unreadable", limit: length };
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
await opened.handle.close().catch(() => { });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The first `length` bytes of an opened file, read by position, or all of a shorter one. With
|
|
330
|
+
* `start`, the bytes from `start` to `length`.
|
|
331
|
+
*/
|
|
332
|
+
export async function readOpenedPrefix(handle, length, start = 0) {
|
|
333
|
+
const bytes = Buffer.allocUnsafe(Math.max(0, length - start));
|
|
334
|
+
let total = 0;
|
|
335
|
+
while (total < bytes.length) {
|
|
336
|
+
const { bytesRead } = await handle.read(bytes, total, bytes.length - total, start + total);
|
|
337
|
+
if (bytesRead === 0)
|
|
338
|
+
break;
|
|
339
|
+
total += bytesRead;
|
|
340
|
+
}
|
|
341
|
+
return bytes.subarray(0, total);
|
|
342
|
+
}
|
|
243
343
|
/** The caller owns this checked descriptor and must close it after reading/streaming. */
|
|
244
344
|
export async function openContainedRegularFile(rootInput, relativePath) {
|
|
245
345
|
const opened = await openContained(rootInput, relativePath);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
1
2
|
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
2
3
|
/**
|
|
3
4
|
* Analysis inputs are retained local artifacts, never URLs or caller-selected outputs. The path is
|
|
@@ -17,6 +18,8 @@ export declare function decodesToPlainRelativePath(value: string, maxLength: num
|
|
|
17
18
|
* authority to select another file or initiate a network request.
|
|
18
19
|
*/
|
|
19
20
|
export declare function readBoundedFile(root: PreparedOutputRoot, relativePath: string, maxBytes: number): Promise<Buffer | null>;
|
|
21
|
+
/** The fields of a file's lstat that sameFile compares: which file it is, its size and times. */
|
|
22
|
+
export type FileStamp = Pick<BigIntStats, "dev" | "ino" | "size" | "mtimeNs" | "ctimeNs">;
|
|
20
23
|
/**
|
|
21
24
|
* After a bounded read returned nothing: true when the file is absent, false when something is
|
|
22
25
|
* there that could not be read. Errors other than ENOENT propagate.
|
|
@@ -25,6 +28,7 @@ export declare function pathMissing(filePath: string): Promise<boolean>;
|
|
|
25
28
|
export type BoundedFileResult = {
|
|
26
29
|
state: "read";
|
|
27
30
|
bytes: Buffer;
|
|
31
|
+
stamp: FileStamp;
|
|
28
32
|
} | {
|
|
29
33
|
state: "limit";
|
|
30
34
|
size: bigint;
|
|
@@ -37,6 +41,13 @@ export declare function readBoundedFileResult(root: PreparedOutputRoot, relative
|
|
|
37
41
|
* At most `maxBytes` of a contained single-link regular file that stays the same file, with the
|
|
38
42
|
* same size and times, from the first check to the last. Size refusals are distinguished only after
|
|
39
43
|
* the same checks. `relativePath` uses `/`, and any name a contained read accepts
|
|
40
|
-
* (isSafeRelativeFilePath) is read: verify and export read the names they always did.
|
|
44
|
+
* (isSafeRelativeFilePath) is read: verify and export read the names they always did. A read
|
|
45
|
+
* returns the stamp it held the file to.
|
|
41
46
|
*/
|
|
42
47
|
export declare function readUnchangedFile(root: PreparedOutputRoot, relativePath: string, maxBytes: number): Promise<BoundedFileResult>;
|
|
48
|
+
/**
|
|
49
|
+
* True when the file readUnchangedFile would read at `relativePath` is still the file `stamp`
|
|
50
|
+
* describes: it passes the same root, parent and leaf checks, before and after its lstat matches
|
|
51
|
+
* the stamp. Opens nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isUnchangedFile(root: PreparedOutputRoot, relativePath: string, stamp: FileStamp): Promise<boolean>;
|
|
@@ -99,47 +99,19 @@ export async function readBoundedFileResult(root, relativePath, maxBytes) {
|
|
|
99
99
|
* At most `maxBytes` of a contained single-link regular file that stays the same file, with the
|
|
100
100
|
* same size and times, from the first check to the last. Size refusals are distinguished only after
|
|
101
101
|
* the same checks. `relativePath` uses `/`, and any name a contained read accepts
|
|
102
|
-
* (isSafeRelativeFilePath) is read: verify and export read the names they always did.
|
|
102
|
+
* (isSafeRelativeFilePath) is read: verify and export read the names they always did. A read
|
|
103
|
+
* returns the stamp it held the file to.
|
|
103
104
|
*/
|
|
104
105
|
export async function readUnchangedFile(root, relativePath, maxBytes) {
|
|
105
|
-
if (!
|
|
106
|
-
relativePath.includes("\\") ||
|
|
107
|
-
!Number.isSafeInteger(maxBytes) ||
|
|
108
|
-
maxBytes < 1)
|
|
106
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
|
|
109
107
|
return unavailable;
|
|
110
|
-
const validateRoot = async () => {
|
|
111
|
-
if ("physicalRunRoot" in root) {
|
|
112
|
-
await validatePreparedRunRootIdentity(root);
|
|
113
|
-
return root.physicalRunRoot;
|
|
114
|
-
}
|
|
115
|
-
await assertPreparedSelectedOutputDirectory(root);
|
|
116
|
-
return root.physicalPath;
|
|
117
|
-
};
|
|
118
108
|
try {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
if (!isPathInside(physicalRoot, candidate) || candidate === physicalRoot)
|
|
122
|
-
return unavailable;
|
|
123
|
-
const validateParents = async () => {
|
|
124
|
-
let current = physicalRoot;
|
|
125
|
-
for (const segment of relativePath.split("/").slice(0, -1)) {
|
|
126
|
-
current = path.join(current, segment);
|
|
127
|
-
const info = await lstat(current);
|
|
128
|
-
if (!info.isDirectory() || info.isSymbolicLink())
|
|
129
|
-
throw new Error("Unsafe analysis input directory.");
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
await validateParents();
|
|
133
|
-
const before = await lstat(candidate, { bigint: true });
|
|
134
|
-
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n)
|
|
135
|
-
return unavailable;
|
|
136
|
-
if ((await realpath(candidate)) !== candidate)
|
|
109
|
+
const located = await locateContainedFile(root, relativePath);
|
|
110
|
+
if (located === null)
|
|
137
111
|
return unavailable;
|
|
112
|
+
const { candidate, before, stillSameFile } = located;
|
|
138
113
|
if (before.size > BigInt(maxBytes)) {
|
|
139
|
-
if ((await
|
|
140
|
-
return unavailable;
|
|
141
|
-
await validateParents();
|
|
142
|
-
if (!sameFile(before, await lstat(candidate, { bigint: true })))
|
|
114
|
+
if (!(await stillSameFile(before)))
|
|
143
115
|
return unavailable;
|
|
144
116
|
return { state: "limit", size: before.size };
|
|
145
117
|
}
|
|
@@ -153,12 +125,10 @@ export async function readUnchangedFile(root, relativePath, maxBytes) {
|
|
|
153
125
|
!sameFile(before, await handle.stat({ bigint: true })) ||
|
|
154
126
|
bytes.length !== Number(before.size))
|
|
155
127
|
return unavailable;
|
|
156
|
-
if ((await
|
|
157
|
-
return unavailable;
|
|
158
|
-
await validateParents();
|
|
159
|
-
if (!sameFile(before, await lstat(candidate, { bigint: true })))
|
|
128
|
+
if (!(await stillSameFile(before)))
|
|
160
129
|
return unavailable;
|
|
161
|
-
|
|
130
|
+
const { dev, ino, size, mtimeNs, ctimeNs } = before;
|
|
131
|
+
return { state: "read", bytes, stamp: { dev, ino, size, mtimeNs, ctimeNs } };
|
|
162
132
|
}
|
|
163
133
|
finally {
|
|
164
134
|
await handle.close();
|
|
@@ -168,3 +138,63 @@ export async function readUnchangedFile(root, relativePath, maxBytes) {
|
|
|
168
138
|
return unavailable;
|
|
169
139
|
}
|
|
170
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* True when the file readUnchangedFile would read at `relativePath` is still the file `stamp`
|
|
143
|
+
* describes: it passes the same root, parent and leaf checks, before and after its lstat matches
|
|
144
|
+
* the stamp. Opens nothing.
|
|
145
|
+
*/
|
|
146
|
+
export async function isUnchangedFile(root, relativePath, stamp) {
|
|
147
|
+
try {
|
|
148
|
+
const located = await locateContainedFile(root, relativePath);
|
|
149
|
+
return (located !== null && sameFile(stamp, located.before) && (await located.stillSameFile(stamp)));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The contained single-link regular file at `relativePath`: its path, its lstat, and a recheck that
|
|
157
|
+
* the root, its parents and the leaf are unchanged since. Null for a path or leaf a read refuses;
|
|
158
|
+
* a root or parent that fails its check throws.
|
|
159
|
+
*/
|
|
160
|
+
async function locateContainedFile(root, relativePath) {
|
|
161
|
+
if (!isSafeRelativeFilePath(relativePath) || relativePath.includes("\\"))
|
|
162
|
+
return null;
|
|
163
|
+
const validateRoot = async () => {
|
|
164
|
+
if ("physicalRunRoot" in root) {
|
|
165
|
+
await validatePreparedRunRootIdentity(root);
|
|
166
|
+
return root.physicalRunRoot;
|
|
167
|
+
}
|
|
168
|
+
await assertPreparedSelectedOutputDirectory(root);
|
|
169
|
+
return root.physicalPath;
|
|
170
|
+
};
|
|
171
|
+
const physicalRoot = await validateRoot();
|
|
172
|
+
const candidate = path.join(physicalRoot, relativePath);
|
|
173
|
+
if (!isPathInside(physicalRoot, candidate) || candidate === physicalRoot)
|
|
174
|
+
return null;
|
|
175
|
+
const validateParents = async () => {
|
|
176
|
+
let current = physicalRoot;
|
|
177
|
+
for (const segment of relativePath.split("/").slice(0, -1)) {
|
|
178
|
+
current = path.join(current, segment);
|
|
179
|
+
const info = await lstat(current);
|
|
180
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
181
|
+
throw new Error("Unsafe analysis input directory.");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
await validateParents();
|
|
185
|
+
const before = await lstat(candidate, { bigint: true });
|
|
186
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n)
|
|
187
|
+
return null;
|
|
188
|
+
if ((await realpath(candidate)) !== candidate)
|
|
189
|
+
return null;
|
|
190
|
+
return {
|
|
191
|
+
candidate,
|
|
192
|
+
before,
|
|
193
|
+
stillSameFile: async (stamp) => {
|
|
194
|
+
if ((await validateRoot()) !== physicalRoot)
|
|
195
|
+
return false;
|
|
196
|
+
await validateParents();
|
|
197
|
+
return sameFile(stamp, await lstat(candidate, { bigint: true }));
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
package/dist/run/judge.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface BlockerFacts {
|
|
|
6
6
|
completionReason: ActorCompletionReason;
|
|
7
7
|
/** A harness-owned stop condition ended the session: a matched stopWhen or a completed dwell window. */
|
|
8
8
|
stopConditionMatched: boolean;
|
|
9
|
-
/** What the participant declared
|
|
9
|
+
/** What the participant declared, when it declared an outcome (ActorTrace.declaredOutcome). */
|
|
10
10
|
declaredOutcome?: ParticipantDeclaredOutcome;
|
|
11
11
|
/** The route's reading of the closing report: it describes a blocker or asks for instructions. */
|
|
12
12
|
closingReportReadsBlocked: boolean;
|
|
@@ -22,9 +22,12 @@ export interface SessionEnding extends BlockerFacts {
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function hollowCompletion(ending: SessionEnding): boolean;
|
|
24
24
|
/**
|
|
25
|
-
* The
|
|
26
|
-
* word and wins.
|
|
27
|
-
* session: a matched stopWhen is structured completion evidence and overrides
|
|
25
|
+
* The session ended goal_satisfied but the participant reported a blocker. A declared `reached`
|
|
26
|
+
* or `blocked` is its own word and wins. Otherwise the closing report is read, unless a stop
|
|
27
|
+
* condition ended the session: a matched stopWhen is structured completion evidence and overrides
|
|
28
|
+
* the text. A declared `not_reached` is read the same way: it ends the session as gave_up when
|
|
29
|
+
* the participant ends it, so here it comes from a closing report at a scheduled end or a session
|
|
30
|
+
* cap, and it does not say whether something stopped the participant.
|
|
28
31
|
*/
|
|
29
32
|
export declare function selfReportedBlocker(ending: BlockerFacts): boolean;
|
|
30
33
|
/** What a participant's pass reads: its session's end, or why it has none, and the two judgments above. */
|
package/dist/run/judge.js
CHANGED
|
@@ -25,16 +25,21 @@ export function hollowCompletion(ending) {
|
|
|
25
25
|
!ending.stopConditionMatched);
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* The
|
|
29
|
-
* word and wins.
|
|
30
|
-
* session: a matched stopWhen is structured completion evidence and overrides
|
|
28
|
+
* The session ended goal_satisfied but the participant reported a blocker. A declared `reached`
|
|
29
|
+
* or `blocked` is its own word and wins. Otherwise the closing report is read, unless a stop
|
|
30
|
+
* condition ended the session: a matched stopWhen is structured completion evidence and overrides
|
|
31
|
+
* the text. A declared `not_reached` is read the same way: it ends the session as gave_up when
|
|
32
|
+
* the participant ends it, so here it comes from a closing report at a scheduled end or a session
|
|
33
|
+
* cap, and it does not say whether something stopped the participant.
|
|
31
34
|
*/
|
|
32
35
|
export function selfReportedBlocker(ending) {
|
|
33
|
-
if (ending.
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
if (ending.completionReason !== "goal_satisfied")
|
|
37
|
+
return false;
|
|
38
|
+
if (ending.declaredOutcome === "blocked")
|
|
39
|
+
return true;
|
|
40
|
+
if (ending.declaredOutcome === "reached")
|
|
41
|
+
return false;
|
|
42
|
+
return ending.closingReportReadsBlocked && !ending.stopConditionMatched;
|
|
38
43
|
}
|
|
39
44
|
/** Judge participant evidence for the bundle and the result in the same order. */
|
|
40
45
|
export function judgeParticipantRecords(participants, wording = {}) {
|