@hue-run/sdk 0.2.2 → 0.3.1

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.
Files changed (42) hide show
  1. package/CLI.md +52 -0
  2. package/ENVIRONMENTS.md +24 -6
  3. package/EVALUATIONS.md +69 -2
  4. package/README.md +20 -0
  5. package/dist/environment/client.d.ts +2 -2
  6. package/dist/environment/types.d.ts +33 -1
  7. package/dist/evals/client.d.ts +30 -1
  8. package/dist/evals/client.js +16 -0
  9. package/dist/evals/environment-target.d.ts +84 -0
  10. package/dist/evals/environment-target.js +201 -0
  11. package/dist/evals/local-worker.d.ts +88 -0
  12. package/dist/evals/local-worker.js +171 -0
  13. package/dist/evals/runner.d.ts +1 -1
  14. package/dist/evals/runner.js +22 -12
  15. package/dist/evals/scorer-publication.js +17 -1
  16. package/dist/evals/scorers.d.ts +10 -4
  17. package/dist/evals/scorers.js +11 -8
  18. package/dist/evals/simulation.d.ts +6 -6
  19. package/dist/evals/simulation.js +45 -222
  20. package/dist/evals/types.d.ts +39 -1
  21. package/dist/evals.d.ts +2 -0
  22. package/dist/evals.js +1 -0
  23. package/dist/setup/checkpoint.d.ts +14 -0
  24. package/dist/setup/checkpoint.js +186 -0
  25. package/dist/setup/cli.d.ts +2 -0
  26. package/dist/setup/cli.js +150 -0
  27. package/dist/setup/detect.d.ts +3 -0
  28. package/dist/setup/detect.js +146 -0
  29. package/dist/setup/machine.d.ts +109 -0
  30. package/dist/setup/machine.js +43 -0
  31. package/dist/setup/render.d.ts +16 -0
  32. package/dist/setup/render.js +111 -0
  33. package/dist/setup/runner.d.ts +101 -0
  34. package/dist/setup/runner.js +117 -0
  35. package/dist/setup/types.d.ts +145 -0
  36. package/dist/setup/types.js +2 -0
  37. package/dist/setup.d.ts +6 -0
  38. package/dist/setup.js +6 -0
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/package.json +12 -1
  42. package/setup-events.schema.json +134 -0
@@ -1,56 +1,25 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
3
  import { HueEnvironmentError } from "../environment/client.js";
4
- import { bindEnvironmentTools } from "../environment/tools.js";
5
4
  import { HueApiError } from "./client.js";
6
5
  import { CheckpointStore } from "./checkpoint.js";
7
6
  import { MAX_ENVIRONMENT_STEPS } from "./environment-evidence.js";
8
7
  import { aggregateBounds, digest, json } from "./json.js";
9
8
  import { normalizeScorerDefinitionForPublication } from "./scorer-publication.js";
10
- import { actualAgentManifestV2, attemptBaselineV2, projectMcpConnectionV2, requestedAttemptProvidersV2, validateAttemptConnectionBundleV2, } from "./attempt.js";
11
- import { runExperiment, TargetCancelledError, TargetOutcomeUncertainError, } from "./runner.js";
9
+ import { pinRequestedAttemptV2, requestedAttemptV2, runEnvironmentTarget, } from "./environment-target.js";
10
+ import { runExperiment } from "./runner.js";
12
11
  const scorerDefinition = (entry) => "definition" in entry.scorer ? entry.scorer.definition : entry.scorer;
13
- function requestedAttempt(options) {
14
- const requested = options.requestedProviders !== undefined;
15
- const selected = options.mcpSurface !== undefined;
16
- if (!requested && !selected) {
17
- if (options.actualAgentManifest !== undefined)
18
- throw new TypeError("actualAgentManifest requires requestedProviders and mcpSurface");
19
- return undefined;
20
- }
21
- if (!requested || !selected)
22
- throw new TypeError("requestedProviders and mcpSurface must be supplied together");
23
- const requestedProviders = requestedAttemptProvidersV2.parse(options.requestedProviders);
24
- const mcpSurface = options.mcpSurface;
25
- const provider = requestedProviders.find((candidate) => candidate.providerInstanceKey === mcpSurface.providerInstanceKey);
26
- if (!provider?.surfaceKeys.includes(mcpSurface.surfaceKey))
27
- throw new TypeError("mcpSurface must identify an exactly requested MCP surface");
28
- const actualAgentManifest = typeof options.actualAgentManifest === "function"
29
- ? options.actualAgentManifest
30
- : actualAgentManifestV2.parse(options.actualAgentManifest);
31
- return {
32
- actualAgentManifest,
33
- requestedProviders,
34
- mcpSurface: { ...mcpSurface },
35
- };
36
- }
37
- function expectedManifestDigest(config) {
38
- if (!config || typeof config !== "object" || Array.isArray(config))
39
- throw new TypeError("Provider-profile simulations require an immutable V2 attempt baseline");
40
- const source = config;
41
- const baseline = attemptBaselineV2.safeParse(source.attemptBaselineV2);
42
- if (!baseline.success) {
43
- if (source.attemptBaselineV2 === undefined && source.attemptBaselineV1 !== undefined)
44
- throw new TypeError("Legacy V1 attempts require a fresh experiment with a V2 baseline");
45
- if (source.attemptBaselineV2 !== undefined)
46
- throw new TypeError("The immutable V2 attempt baseline is invalid");
47
- throw new TypeError("Provider-profile simulations require an immutable V2 attempt baseline");
48
- }
49
- return baseline.data.expectedAgentManifestDigest;
50
- }
51
12
  function normalizedEnvironmentDefinition(definition) {
52
13
  return json({
53
14
  ...definition,
15
+ ...(definition.schemaVersion === 2
16
+ ? {
17
+ providerInstances: definition.providerInstances.map((instance) => ({
18
+ ...instance,
19
+ syntheticPrincipalId: instance.syntheticPrincipalId.toLowerCase(),
20
+ })),
21
+ }
22
+ : {}),
54
23
  determinism: {
55
24
  clock: {
56
25
  startNs: definition.determinism?.clock?.startNs ?? "0",
@@ -341,24 +310,11 @@ async function resolveExperiment(options, idempotencyKey) {
341
310
  bindings: [...scorers.bindings, ...(options.localScorers ?? [])],
342
311
  };
343
312
  }
344
- async function seal(client, runId, executionId, status) {
345
- try {
346
- await client.finishRun(runId, {
347
- idempotencyKey: `execution:${executionId}:${status}`,
348
- status,
349
- });
350
- }
351
- catch (error) {
352
- const recovered = await client.getRun(runId).catch(() => undefined);
353
- if (recovered?.status !== status)
354
- throw error;
355
- }
356
- }
357
313
  /** Run an existing agent callback against one fresh hosted world per case. The helper
358
314
  * owns immutable resolution, execution linkage, finalization, scoring and resumable uploads.
359
315
  */
360
316
  export async function runSimulation(options) {
361
- const requestedConfiguration = requestedAttempt(options);
317
+ const requestedConfiguration = requestedAttemptV2(options);
362
318
  if (options.maxSteps !== undefined &&
363
319
  (!Number.isInteger(options.maxSteps) ||
364
320
  options.maxSteps < 1 ||
@@ -408,10 +364,7 @@ export async function runSimulation(options) {
408
364
  const runUrl = new URL(`/experiments/${experimentId}`, options.client.baseUrl).toString();
409
365
  await options.onProgress?.({ type: "run_created", experimentId, runUrl });
410
366
  const requested = requestedConfiguration
411
- ? {
412
- ...requestedConfiguration,
413
- expectedAgentManifestDigest: expectedManifestDigest((await options.client.getExperiment(experimentId)).config),
414
- }
367
+ ? pinRequestedAttemptV2(requestedConfiguration, (await options.client.getExperiment(experimentId)).config)
415
368
  : undefined;
416
369
  const report = await runExperiment({
417
370
  client: options.client,
@@ -424,172 +377,42 @@ export async function runSimulation(options) {
424
377
  scorers: bindings,
425
378
  concurrency: options.concurrency,
426
379
  schemaTimeoutMillis: options.schemaTimeoutMillis,
427
- target: async (inputs, context) => {
428
- const environmentVersionId = context.item.environmentVersionId;
429
- if (!environmentVersionId)
430
- throw new Error("The simulation case has no pinned environment version");
431
- const run = await options.environmentClient.createRun({
432
- idempotencyKey: `execution:${context.executionId}`,
433
- environmentVersionId,
434
- executionId: context.executionId,
435
- maxSteps: options.maxSteps,
436
- ttlSeconds: options.ttlSeconds,
437
- });
438
- const progress = (type) => options.onProgress?.({
439
- type,
380
+ target: (inputs, context) => runEnvironmentTarget({
381
+ client: options.client,
382
+ environmentClient: options.environmentClient,
383
+ hue: options.hue,
384
+ inputs,
385
+ context,
386
+ requested,
387
+ maxSteps: options.maxSteps,
388
+ ttlSeconds: options.ttlSeconds,
389
+ signal: options.signal,
390
+ onProgress: (event) => options.onProgress?.({
391
+ ...event,
440
392
  experimentId,
441
393
  executionId: context.executionId,
442
394
  caseId: context.item.id,
443
- environmentRunId: run.id,
444
- });
445
- let finalized = false;
446
- try {
447
- await progress("world_created");
448
- if (options.signal?.aborted)
449
- throw new TargetCancelledError();
450
- const tools = bindEnvironmentTools({
451
- hue: options.hue,
452
- client: options.environmentClient,
453
- run,
454
- parentContext: context.span.context,
455
- });
456
- let connectionBundle;
457
- let mcp;
458
- if (requested) {
459
- const actualManifest = actualAgentManifestV2.parse(typeof requested.actualAgentManifest === "function"
460
- ? await requested.actualAgentManifest({
461
- config: context.config,
462
- item: structuredClone(context.item),
463
- signal: options.signal,
464
- })
465
- : requested.actualAgentManifest);
466
- let prepared;
467
- try {
468
- prepared = await options.client.prepareAttempt({
469
- schemaVersion: 2,
470
- idempotencyKey: randomUUID(),
471
- executionId: context.executionId,
472
- environmentRunId: run.id,
473
- expectedAgentManifestDigest: requested.expectedAgentManifestDigest,
474
- actualManifest,
475
- requestedProviders: requested.requestedProviders,
476
- });
477
- }
478
- catch (error) {
479
- // A transport failure or malformed credential-bearing response may
480
- // follow a committed decision. Preserve the running checkpoint and
481
- // never reacquire credentials or replay the target on resume.
482
- throw new TargetOutcomeUncertainError(context.executionId, { cause: error });
483
- }
484
- await options.onProgress?.({
485
- type: "attempt_prepared",
486
- experimentId,
487
- executionId: context.executionId,
488
- caseId: context.item.id,
489
- environmentRunId: run.id,
490
- bindingId: prepared.status === "ready" ? prepared.bundle.bindingId : prepared.bindingId,
491
- status: prepared.status,
492
- findingCodes: prepared.preflightReport.findings.map((finding) => finding.code),
493
- ...(prepared.status === "ready"
494
- ? {
495
- executionManifestDigest: prepared.bundle.parity.executionManifestDigest,
496
- }
497
- : {}),
498
- });
499
- if (prepared.status === "environment_incomplete") {
500
- try {
501
- await seal(options.environmentClient, run.id, context.executionId, "completed");
502
- }
503
- catch (error) {
504
- throw new TargetOutcomeUncertainError(context.executionId, { cause: error });
505
- }
506
- finalized = true;
507
- await Promise.resolve(progress("world_sealed")).catch(() => undefined);
508
- return undefined;
509
- }
510
- connectionBundle = validateAttemptConnectionBundleV2(prepared.bundle, {
511
- requireFresh: true,
512
- });
513
- const projected = projectMcpConnectionV2(connectionBundle, requested.mcpSurface.providerInstanceKey);
514
- if (!projected)
515
- throw new TypeError("The prepared attempt has no selected MCP surface");
516
- mcp = projected;
517
- }
518
- else {
519
- mcp = await options.client.createSimulationMcpCapability({
520
- runId: run.id,
521
- executionId: context.executionId,
522
- });
523
- }
524
- if (options.signal?.aborted)
525
- throw new TargetCancelledError();
526
- await progress("target_started");
527
- const output = await options.target(inputs, {
528
- config: context.config,
529
- item: context.item,
530
- executionId: context.executionId,
531
- environmentRunId: run.id,
532
- tools,
533
- mcp,
534
- ...(connectionBundle ? { connectionBundle } : {}),
535
- signal: options.signal,
536
- });
537
- try {
538
- await seal(options.environmentClient, run.id, context.executionId, "completed");
539
- }
540
- catch (error) {
541
- throw new TargetOutcomeUncertainError(context.executionId, { cause: error });
542
- }
543
- finalized = true;
544
- await Promise.resolve(progress("world_sealed")).catch(() => undefined);
545
- return output;
546
- }
547
- catch (error) {
548
- if (error instanceof TargetOutcomeUncertainError || finalized)
549
- throw error;
550
- let environmentIncomplete;
551
- try {
552
- environmentIncomplete =
553
- (await options.environmentClient.getRun(run.id)).validity ===
554
- "environment_incomplete";
555
- }
556
- catch (inspectionError) {
557
- // A target error can be the adapter surfacing a coverage gap. If the
558
- // authoritative run cannot be read, do not guess that it was an agent
559
- // failure or replay the target on resume.
560
- throw new TargetOutcomeUncertainError(context.executionId, {
561
- cause: new AggregateError([error, inspectionError]),
562
- });
563
- }
564
- // A durable coverage gap invalidates parity independently of caller timing;
565
- // do not let a racing local abort hide it as an ordinary cancellation.
566
- if (environmentIncomplete) {
567
- try {
568
- await seal(options.environmentClient, run.id, context.executionId, "completed");
569
- }
570
- catch (finalizationError) {
571
- throw new TargetOutcomeUncertainError(context.executionId, {
572
- cause: new AggregateError([error, finalizationError]),
573
- });
574
- }
575
- finalized = true;
576
- await Promise.resolve(progress("world_sealed")).catch(() => undefined);
577
- return undefined;
578
- }
579
- try {
580
- await seal(options.environmentClient, run.id, context.executionId, "abandoned");
581
- }
582
- catch (finalizationError) {
583
- throw new TargetOutcomeUncertainError(context.executionId, {
584
- cause: new AggregateError([error, finalizationError]),
585
- });
586
- }
587
- await Promise.resolve(progress("world_sealed")).catch(() => undefined);
588
- if (options.signal?.aborted && !(error instanceof TargetCancelledError))
589
- throw new TargetCancelledError();
590
- throw error;
591
- }
592
- },
395
+ }),
396
+ target: (targetInputs, targetContext) => options.target(structuredClone(targetInputs), {
397
+ config: structuredClone(targetContext.config),
398
+ item: {
399
+ id: targetContext.item.id,
400
+ externalKey: targetContext.item.externalKey,
401
+ },
402
+ executionId: targetContext.executionId,
403
+ environmentRunId: targetContext.environmentRunId,
404
+ tools: targetContext.tools,
405
+ mcp: {
406
+ url: targetContext.mcp.url,
407
+ token: targetContext.mcp.token,
408
+ expiresAt: targetContext.mcp.expiresAt,
409
+ },
410
+ ...(targetContext.connectionBundle
411
+ ? { connectionBundle: structuredClone(targetContext.connectionBundle) }
412
+ : {}),
413
+ signal: targetContext.signal,
414
+ }),
415
+ }),
593
416
  });
594
417
  const complete = { ...report, experimentId, runUrl };
595
418
  attempt.stage = "completed";
@@ -119,7 +119,7 @@ export type MetricDefinition = {
119
119
  /** Allowed values. */
120
120
  categories: string[];
121
121
  };
122
- /** A pinned scorer definition: a Hue built-in, trusted local code, a manual rubric or a hosted judge. */
122
+ /** A pinned scorer definition executed locally, by a person, or by Hue. */
123
123
  export type ScorerDefinition = {
124
124
  /** Hue built-in scorer. */
125
125
  kind: "builtin";
@@ -158,6 +158,13 @@ export type ScorerDefinition = {
158
158
  sourceDigest: string;
159
159
  /** Metrics the callback reports. */
160
160
  metrics: MetricDefinition[];
161
+ } | {
162
+ /** Scored inside Hue using immutable world evidence; the local runner defers it. */
163
+ kind: "world_outcome";
164
+ /** Pinned Hue-executed outcome evaluator. */
165
+ entry: "hue.conversion_outcome.v1";
166
+ /** The seven fixed boolean metrics defined by the entry. */
167
+ metrics: MetricDefinition[];
161
168
  } | {
162
169
  /** Scored by a person in Hue; the local runner defers it. */
163
170
  kind: "manual";
@@ -603,3 +610,34 @@ export interface SimulationMcpCapability {
603
610
  /** Credential expiry timestamp. */
604
611
  expiresAt: string;
605
612
  }
613
+ /** Identity and capabilities of one fixed local agent entry point. */
614
+ export interface LocalAgentRegistration {
615
+ /** Stable application-selected agent key. */
616
+ key: string;
617
+ /** Display name. */
618
+ name: string;
619
+ /** Application-selected revision of the agent configuration. */
620
+ revision: string;
621
+ /** Supported execution contracts; defaults to environment:v1 in the worker. */
622
+ capabilities?: string[];
623
+ /** Local scorer source digests available in this process. */
624
+ scorerDigests?: string[];
625
+ }
626
+ /** Server registration and heartbeat timestamps for a local agent. */
627
+ export interface RegisteredLocalAgent extends Required<LocalAgentRegistration> {
628
+ /** Registered agent identity. */
629
+ id: string;
630
+ /** Whether Hue permits this registration to receive runs. */
631
+ enabled: boolean;
632
+ /** Latest registration heartbeat, as an ISO timestamp. */
633
+ lastSeenAt: string;
634
+ /** Registration creation timestamp. */
635
+ createdAt: string;
636
+ }
637
+ /** Queue claim connecting a local run to a pinned experiment. */
638
+ export interface LocalAgentClaim {
639
+ /** Claimed queue-run identity. */
640
+ runId: string;
641
+ /** Pinned experiment to execute. */
642
+ experimentId: string;
643
+ }
package/dist/evals.d.ts CHANGED
@@ -9,3 +9,5 @@ export type { ActualAgentManifestInputV2, ActualAgentManifestV2, AttemptBaseline
9
9
  export { builtins, defineLocalScorer, scoreLocally } from "./evals/scorers.js";
10
10
  export { sourceDigest } from "./evals/json.js";
11
11
  export type * from "./evals/types.js";
12
+ export { runLocalAgent } from "./evals/local-worker.js";
13
+ export type { LocalAgentTargetContext, RunLocalAgentOptions } from "./evals/local-worker.js";
package/dist/evals.js CHANGED
@@ -4,3 +4,4 @@ export { runSimulation } from "./evals/simulation.js";
4
4
  export { actualAgentManifestV2, agentManifestDigestV2, attemptBaselineV2, attemptBindingRead, attemptConnectionBundleV2, attemptIdentityV2, dependencyManifestV2, dependencyProviderV2, expectedAgentManifestV2, executionManifestDigestV2, parityEvidenceV2, preflightFindingV2, preflightReportV2, prepareAttemptInputV2, projectMcpConnectionV2, secretFreeBindingV2, surfaceBindingV2, } from "./evals/attempt.js";
5
5
  export { builtins, defineLocalScorer, scoreLocally } from "./evals/scorers.js";
6
6
  export { sourceDigest } from "./evals/json.js";
7
+ export { runLocalAgent } from "./evals/local-worker.js";
@@ -0,0 +1,14 @@
1
+ import type { SetupCheckpointAdapter } from "./runner.js";
2
+ import type { SetupMachineState } from "./machine.js";
3
+ export declare function defaultSetupStateDirectory(env?: NodeJS.ProcessEnv): string;
4
+ /** Stable installer-session identifier; it is not a Hue Run and reveals no path or file contents. */
5
+ export declare function setupRunId(projectRoot: string): string;
6
+ export declare class FileSetupCheckpointAdapter implements SetupCheckpointAdapter {
7
+ readonly directory: string;
8
+ private readonly runtimePlatform;
9
+ constructor(directory?: string, runtimePlatform?: NodeJS.Platform);
10
+ private get enforcesPosixPermissions();
11
+ private pathFor;
12
+ load(runId: string, projectRoot: string): Promise<SetupMachineState | undefined>;
13
+ save(state: SetupMachineState): Promise<void>;
14
+ }
@@ -0,0 +1,186 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, realpath, rename } from "node:fs/promises";
4
+ import { homedir, platform } from "node:os";
5
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ const MAX_CHECKPOINT_BYTES = 256 * 1024;
7
+ const STEPS = ["detect-project", "configure-telemetry", "verify-receipt", "claim-project"];
8
+ function isInside(parent, child) {
9
+ const path = relative(parent, child);
10
+ return path === "" || (!path.startsWith("..") && !isAbsolute(path));
11
+ }
12
+ export function defaultSetupStateDirectory(env = process.env) {
13
+ if (platform() === "darwin")
14
+ return join(homedir(), "Library", "Application Support", "Hue", "setup");
15
+ if (platform() === "win32")
16
+ return join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "Hue", "setup");
17
+ return join(env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "hue", "setup");
18
+ }
19
+ /** Stable installer-session identifier; it is not a Hue Run and reveals no path or file contents. */
20
+ export function setupRunId(projectRoot) {
21
+ return `setup_${createHash("sha256")
22
+ .update(`hue-setup-v1\0${resolve(projectRoot)}`)
23
+ .digest("hex")
24
+ .slice(0, 24)}`;
25
+ }
26
+ function digest(value) {
27
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
28
+ }
29
+ function hasExactKeys(value, keys) {
30
+ return Object.keys(value).sort().join("\0") === [...keys].sort().join("\0");
31
+ }
32
+ function validDetection(value, projectRoot) {
33
+ if (!value || typeof value !== "object" || Array.isArray(value))
34
+ return false;
35
+ const item = value;
36
+ if (!hasExactKeys(item, [
37
+ "root",
38
+ "fingerprint",
39
+ "languages",
40
+ "packageManagers",
41
+ "frameworks",
42
+ "hue",
43
+ "openTelemetry",
44
+ ]))
45
+ return false;
46
+ const allowed = (items, values, maximum) => Array.isArray(items) &&
47
+ items.length <= maximum &&
48
+ new Set(items).size === items.length &&
49
+ items.every((entry) => typeof entry === "string" && values.includes(entry));
50
+ return (item.root === projectRoot &&
51
+ typeof item.fingerprint === "string" &&
52
+ /^[a-f0-9]{64}$/u.test(item.fingerprint) &&
53
+ allowed(item.languages, ["typescript", "python"], 2) &&
54
+ allowed(item.packageManagers, ["bun", "npm", "pnpm", "yarn", "uv", "poetry", "pip"], 7) &&
55
+ allowed(item.frameworks, ["nextjs", "nestjs", "express", "fastapi", "django", "flask", "vercel-ai-sdk"], 7) &&
56
+ ["absent", "typescript", "python", "multiple"].includes(item.hue) &&
57
+ ["absent", "typescript", "python", "multiple"].includes(item.openTelemetry));
58
+ }
59
+ function validState(value, runId, projectRoot) {
60
+ if (!value || typeof value !== "object" || Array.isArray(value))
61
+ return false;
62
+ const state = value;
63
+ if (state.format !== 1 || state.runId !== runId || state.projectRoot !== projectRoot)
64
+ return false;
65
+ if (state.phase === "created" || state.phase === "detecting")
66
+ return hasExactKeys(state, ["format", "phase", "runId", "projectRoot"]);
67
+ if (state.phase !== "local-ready" ||
68
+ !hasExactKeys(state, ["format", "phase", "runId", "projectRoot", "project", "plan"]) ||
69
+ !validDetection(state.project, projectRoot))
70
+ return false;
71
+ const plan = state.plan;
72
+ return (!!plan &&
73
+ typeof plan === "object" &&
74
+ !Array.isArray(plan) &&
75
+ hasExactKeys(plan, ["steps", "mutatesProject", "backendRequired"]) &&
76
+ JSON.stringify(plan.steps) === JSON.stringify(STEPS) &&
77
+ plan.mutatesProject === false &&
78
+ plan.backendRequired === true);
79
+ }
80
+ export class FileSetupCheckpointAdapter {
81
+ directory;
82
+ runtimePlatform;
83
+ constructor(directory = defaultSetupStateDirectory(), runtimePlatform = platform()) {
84
+ this.directory = directory;
85
+ this.runtimePlatform = runtimePlatform;
86
+ }
87
+ get enforcesPosixPermissions() {
88
+ return this.runtimePlatform !== "win32";
89
+ }
90
+ async pathFor(runId, projectRoot) {
91
+ if (!/^setup_[a-f0-9]{24}$/u.test(runId))
92
+ throw new Error("Invalid setup run identifier");
93
+ const root = resolve(this.directory);
94
+ const project = await realpath(projectRoot);
95
+ if (isInside(project, root))
96
+ throw new Error("Setup checkpoints must be outside the project repository");
97
+ let info;
98
+ try {
99
+ info = await lstat(root);
100
+ }
101
+ catch (error) {
102
+ if (error.code !== "ENOENT")
103
+ throw error;
104
+ await mkdir(root, { recursive: true, mode: 0o700 });
105
+ info = await lstat(root);
106
+ }
107
+ if (!info.isDirectory() || info.isSymbolicLink())
108
+ throw new Error("Setup checkpoint directory must be private (mode 0700, no symlink)");
109
+ if (this.enforcesPosixPermissions) {
110
+ await chmod(root, 0o700);
111
+ info = await lstat(root);
112
+ if ((info.mode & 0o077) !== 0)
113
+ throw new Error("Setup checkpoint directory must be private (mode 0700, no symlink)");
114
+ }
115
+ return join(root, `${runId}.json`);
116
+ }
117
+ async load(runId, projectRoot) {
118
+ const path = await this.pathFor(runId, projectRoot);
119
+ let handle;
120
+ try {
121
+ if (this.runtimePlatform === "win32") {
122
+ const entry = await lstat(path);
123
+ if (entry.isSymbolicLink())
124
+ throw new Error("Unsafe setup checkpoint symlink");
125
+ }
126
+ const noFollow = this.runtimePlatform === "win32" ? 0 : constants.O_NOFOLLOW;
127
+ handle = await open(path, constants.O_RDONLY | noFollow);
128
+ }
129
+ catch (error) {
130
+ if (error.code === "ENOENT")
131
+ return undefined;
132
+ throw error;
133
+ }
134
+ try {
135
+ const info = await handle.stat();
136
+ if (!info.isFile() ||
137
+ info.size > MAX_CHECKPOINT_BYTES ||
138
+ (this.enforcesPosixPermissions && (info.mode & 0o077) !== 0))
139
+ throw new Error("Unsafe or oversized setup checkpoint");
140
+ const envelope = JSON.parse(await handle.readFile("utf8"));
141
+ if (!envelope ||
142
+ typeof envelope !== "object" ||
143
+ Array.isArray(envelope) ||
144
+ !hasExactKeys(envelope, ["state", "digest"]))
145
+ throw new Error("Invalid setup checkpoint envelope");
146
+ const { state, digest: expected } = envelope;
147
+ if (typeof expected !== "string" || expected !== digest(state))
148
+ throw new Error("Setup checkpoint integrity check failed");
149
+ if (!validState(state, runId, await realpath(projectRoot)))
150
+ throw new Error("Setup checkpoint identity or shape does not match this project");
151
+ return state;
152
+ }
153
+ finally {
154
+ await handle.close();
155
+ }
156
+ }
157
+ async save(state) {
158
+ const path = await this.pathFor(state.runId, state.projectRoot);
159
+ const encoded = `${JSON.stringify({ state, digest: digest(state) })}\n`;
160
+ if (Buffer.byteLength(encoded) > MAX_CHECKPOINT_BYTES)
161
+ throw new Error("Setup checkpoint exceeds 256 KiB");
162
+ const temporary = join(dirname(path), `.${state.runId}.${randomUUID()}.tmp`);
163
+ const handle = await open(temporary, "wx", 0o600);
164
+ try {
165
+ await handle.writeFile(encoded);
166
+ await handle.sync();
167
+ }
168
+ finally {
169
+ await handle.close();
170
+ }
171
+ await rename(temporary, path);
172
+ // Windows cannot open a directory as a file handle for fsync. The atomic rename and
173
+ // per-user state directory still provide resumability there; POSIX additionally fsyncs
174
+ // the containing directory so the rename survives a sudden interruption.
175
+ if (this.runtimePlatform !== "win32") {
176
+ await chmod(path, 0o600);
177
+ const directory = await open(dirname(path), "r");
178
+ try {
179
+ await directory.sync();
180
+ }
181
+ finally {
182
+ await directory.close();
183
+ }
184
+ }
185
+ }
186
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};