@velum-labs/routekit-eval-setup 1.4.0 → 1.5.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/dist/adapters/authoring-responses-request.d.ts +5 -0
- package/dist/adapters/authoring-responses-request.js +38 -0
- package/dist/adapters/evaluation-evidence-freshness.d.ts +7 -0
- package/dist/adapters/evaluation-evidence-freshness.js +76 -0
- package/dist/adapters/git-task-history.d.ts +67 -0
- package/dist/adapters/git-task-history.js +171 -0
- package/dist/adapters/integrated-repository-history.d.ts +21 -0
- package/dist/adapters/integrated-repository-history.js +175 -0
- package/dist/adapters/repository-command-diagnostic.d.ts +8 -0
- package/dist/adapters/repository-command-diagnostic.js +46 -0
- package/dist/adapters/repository-command-evidence.d.ts +13 -0
- package/dist/adapters/repository-command-evidence.js +102 -0
- package/dist/adapters/repository-command-runner.d.ts +238 -0
- package/dist/adapters/repository-command-runner.js +1483 -0
- package/dist/adapters/repository-import-context.d.ts +47 -0
- package/dist/adapters/repository-import-context.js +469 -0
- package/dist/adapters/repository-node-test-reporter.d.ts +3 -0
- package/dist/adapters/repository-node-test-reporter.js +27 -0
- package/dist/adapters/repository-review-evidence.d.ts +39 -0
- package/dist/adapters/repository-review-evidence.js +632 -0
- package/dist/adapters/repository-seed-selection.d.ts +7 -0
- package/dist/adapters/repository-seed-selection.js +79 -0
- package/dist/adapters/repository-solution-edits.d.ts +49 -0
- package/dist/adapters/repository-solution-edits.js +136 -0
- package/dist/adapters/repository-vitest-phase-adapter.d.ts +8 -0
- package/dist/adapters/repository-vitest-phase-adapter.js +310 -0
- package/dist/adapters/repository-vitest-reporter.d.ts +24 -0
- package/dist/adapters/repository-vitest-reporter.js +314 -0
- package/dist/adapters/strict-authoring-schema.d.ts +5 -0
- package/dist/adapters/strict-authoring-schema.js +158 -0
- package/dist/adapters/test-discovery.d.ts +30 -0
- package/dist/adapters/test-discovery.js +124 -0
- package/dist/adapters/typescript-repository-index.d.ts +51 -0
- package/dist/adapters/typescript-repository-index.js +226 -0
- package/dist/agentic-capabilities-protocol.d.ts +1373 -0
- package/dist/agentic-capabilities-protocol.js +786 -0
- package/dist/case-checkpoint-store.d.ts +29 -0
- package/dist/case-checkpoint-store.js +133 -0
- package/dist/case-pipeline-protocol-v2.d.ts +184 -0
- package/dist/case-pipeline-protocol-v2.js +193 -0
- package/dist/case-pipeline-protocol.d.ts +2626 -0
- package/dist/case-pipeline-protocol.js +371 -0
- package/dist/effect-api.d.ts +74 -12
- package/dist/effect-api.js +56 -7
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +10 -0
- package/dist/eval-capability-execution-envelope.d.ts +64 -0
- package/dist/eval-capability-execution-envelope.js +98 -0
- package/dist/eval-capability-policy.d.ts +90 -0
- package/dist/eval-capability-policy.js +107 -0
- package/dist/eval-event-log.d.ts +51 -0
- package/dist/eval-event-log.js +69 -10
- package/dist/evaluation-authoring-policy.d.ts +18 -0
- package/dist/evaluation-authoring-policy.js +19 -0
- package/dist/evaluation-authoring-validation.d.ts +22 -0
- package/dist/evaluation-authoring-validation.js +72 -0
- package/dist/evaluation-evidence.d.ts +20 -0
- package/dist/evaluation-evidence.js +319 -0
- package/dist/evaluation-grader-calibration-protocol.d.ts +108 -0
- package/dist/evaluation-grader-calibration-protocol.js +80 -0
- package/dist/evaluation-grader-calibration.d.ts +18 -0
- package/dist/evaluation-grader-calibration.js +334 -0
- package/dist/evaluation-grading-policy.d.ts +24 -0
- package/dist/evaluation-grading-policy.js +54 -0
- package/dist/evaluation-proposal-policy.d.ts +4 -0
- package/dist/evaluation-proposal-policy.js +91 -0
- package/dist/evaluation-source-retrieval.d.ts +68 -0
- package/dist/evaluation-source-retrieval.js +513 -0
- package/dist/evaluation-structure-policy.d.ts +29 -0
- package/dist/evaluation-structure-policy.js +138 -0
- package/dist/index.d.ts +124 -19
- package/dist/index.js +69 -12
- package/dist/inspection.js +2 -3
- package/dist/project-artifacts.d.ts +2 -2
- package/dist/project-artifacts.js +26 -121
- package/dist/project-authoring.d.ts +66 -5
- package/dist/project-authoring.js +783 -109
- package/dist/project-contracts.d.ts +292 -82
- package/dist/project-contracts.js +65 -7
- package/dist/project-store.js +2 -1
- package/dist/project-workflow.d.ts +3 -3
- package/dist/project-workflow.js +116 -18
- package/dist/repository-adversary-protocol.d.ts +64 -0
- package/dist/repository-adversary-protocol.js +105 -0
- package/dist/repository-behavior-protocol.d.ts +188 -0
- package/dist/repository-behavior-protocol.js +202 -0
- package/dist/repository-benchmark-protocol.d.ts +487 -0
- package/dist/repository-benchmark-protocol.js +96 -0
- package/dist/repository-execution-protocol.d.ts +150 -0
- package/dist/repository-execution-protocol.js +38 -0
- package/dist/repository-fixture-instructions.d.ts +3 -0
- package/dist/repository-fixture-instructions.js +91 -0
- package/dist/repository-fixture-protocol.d.ts +79 -0
- package/dist/repository-fixture-protocol.js +79 -0
- package/dist/repository-foundry-plan-protocol.d.ts +118 -0
- package/dist/repository-foundry-plan-protocol.js +296 -0
- package/dist/repository-foundry-progress-protocol.d.ts +52 -0
- package/dist/repository-foundry-progress-protocol.js +52 -0
- package/dist/repository-improvement-protocol.d.ts +100 -0
- package/dist/repository-improvement-protocol.js +106 -0
- package/dist/repository-language-model-protocol.d.ts +43 -0
- package/dist/repository-language-model-protocol.js +146 -0
- package/dist/repository-oracle-coverage-protocol.d.ts +18 -0
- package/dist/repository-oracle-coverage-protocol.js +39 -0
- package/dist/repository-oracle-execution-binding.d.ts +27 -0
- package/dist/repository-oracle-execution-binding.js +59 -0
- package/dist/repository-oracle-protocol.d.ts +230 -0
- package/dist/repository-oracle-protocol.js +156 -0
- package/dist/repository-oracle-scope-policy.d.ts +22 -0
- package/dist/repository-oracle-scope-policy.js +92 -0
- package/dist/repository-quality-policy.d.ts +15 -0
- package/dist/repository-quality-policy.js +357 -0
- package/dist/repository-routing-benchmark-protocol.d.ts +176 -0
- package/dist/repository-routing-benchmark-protocol.js +103 -0
- package/dist/repository-routing-model-protocol.d.ts +36 -0
- package/dist/repository-routing-model-protocol.js +89 -0
- package/dist/repository-routing-plan-protocol.d.ts +112 -0
- package/dist/repository-routing-plan-protocol.js +58 -0
- package/dist/repository-routing-quality-policy.d.ts +9 -0
- package/dist/repository-routing-quality-policy.js +191 -0
- package/dist/repository-seed-qualification-progress-protocol.d.ts +205 -0
- package/dist/repository-seed-qualification-progress-protocol.js +28 -0
- package/dist/repository-semantic-calibration-protocol.d.ts +768 -0
- package/dist/repository-semantic-calibration-protocol.js +276 -0
- package/dist/repository-semantic-calibration.d.ts +163 -0
- package/dist/repository-semantic-calibration.js +581 -0
- package/dist/repository-specification-contract-facts-protocol.d.ts +224 -0
- package/dist/repository-specification-contract-facts-protocol.js +276 -0
- package/dist/repository-specification-critique-protocol.d.ts +189 -0
- package/dist/repository-specification-critique-protocol.js +103 -0
- package/dist/repository-task-family-protocol.d.ts +24 -0
- package/dist/repository-task-family-protocol.js +37 -0
- package/dist/repository-task-seed-protocol.d.ts +384 -0
- package/dist/repository-task-seed-protocol.js +236 -0
- package/dist/repository-trajectory-protocol.d.ts +20 -0
- package/dist/repository-trajectory-protocol.js +42 -0
- package/dist/service.js +1 -1
- package/dist/services/adversary/service.d.ts +64 -0
- package/dist/services/adversary/service.js +330 -0
- package/dist/services/benchmark-compiler/service.d.ts +450 -0
- package/dist/services/benchmark-compiler/service.js +9 -0
- package/dist/services/budgeted-model/service.d.ts +118 -0
- package/dist/services/budgeted-model/service.js +460 -0
- package/dist/services/case-authoring/service.d.ts +163 -0
- package/dist/services/case-authoring/service.js +1456 -0
- package/dist/services/case-finalization/service.d.ts +283 -0
- package/dist/services/case-finalization/service.js +370 -0
- package/dist/services/case-generation/service.d.ts +619 -0
- package/dist/services/case-generation/service.js +2628 -0
- package/dist/services/case-pipeline/service.d.ts +31 -0
- package/dist/services/case-pipeline/service.js +485 -0
- package/dist/services/case-pipeline-v2/service.d.ts +70 -0
- package/dist/services/case-pipeline-v2/service.js +477 -0
- package/dist/services/command-observability/service.d.ts +13 -0
- package/dist/services/command-observability/service.js +3 -0
- package/dist/services/dimension-labeling/service.d.ts +77 -0
- package/dist/services/dimension-labeling/service.js +188 -0
- package/dist/services/eval-candidate/service.d.ts +208 -0
- package/dist/services/eval-candidate/service.js +64 -0
- package/dist/services/eval-capabilities/service.d.ts +183 -0
- package/dist/services/eval-capabilities/service.js +1433 -0
- package/dist/services/eval-environment/service.d.ts +173 -0
- package/dist/services/eval-environment/service.js +127 -0
- package/dist/services/evidence-reconstruction/service.d.ts +36 -0
- package/dist/services/evidence-reconstruction/service.js +145 -0
- package/dist/services/fixture-builder/service.d.ts +62 -0
- package/dist/services/fixture-builder/service.js +36 -0
- package/dist/services/fixture-validation/service.d.ts +75 -0
- package/dist/services/fixture-validation/service.js +295 -0
- package/dist/services/foundry/service.d.ts +831 -0
- package/dist/services/foundry/service.js +442 -0
- package/dist/services/foundry-progress/service.d.ts +62 -0
- package/dist/services/foundry-progress/service.js +149 -0
- package/dist/services/foundry-v2/service.d.ts +54 -0
- package/dist/services/foundry-v2/service.js +28 -0
- package/dist/services/grounded-authoring/service.d.ts +126 -0
- package/dist/services/grounded-authoring/service.js +822 -0
- package/dist/services/historical-case/service.d.ts +722 -0
- package/dist/services/historical-case/service.js +177 -0
- package/dist/services/improvement-loop/service.d.ts +59 -0
- package/dist/services/improvement-loop/service.js +176 -0
- package/dist/services/language-model/service.d.ts +52 -0
- package/dist/services/language-model/service.js +194 -0
- package/dist/services/oracle-builder/service.d.ts +146 -0
- package/dist/services/oracle-builder/service.js +513 -0
- package/dist/services/oracle-coverage/service.d.ts +28 -0
- package/dist/services/oracle-coverage/service.js +50 -0
- package/dist/services/oracle-coverage-witness/service.d.ts +130 -0
- package/dist/services/oracle-coverage-witness/service.js +538 -0
- package/dist/services/pipeline-challenge/service.d.ts +551 -0
- package/dist/services/pipeline-challenge/service.js +427 -0
- package/dist/services/pipeline-controls/service.d.ts +130 -0
- package/dist/services/pipeline-controls/service.js +483 -0
- package/dist/services/pipeline-oracle/service.d.ts +8 -0
- package/dist/services/pipeline-oracle/service.js +256 -0
- package/dist/services/pipeline-seed/service.d.ts +298 -0
- package/dist/services/pipeline-seed/service.js +428 -0
- package/dist/services/pipeline-spec/service.d.ts +103 -0
- package/dist/services/pipeline-spec/service.js +619 -0
- package/dist/services/pipeline-tournament/service.d.ts +258 -0
- package/dist/services/pipeline-tournament/service.js +476 -0
- package/dist/services/quality-gate/service.d.ts +233 -0
- package/dist/services/quality-gate/service.js +136 -0
- package/dist/services/repository-bundle/service.d.ts +33 -0
- package/dist/services/repository-bundle/service.js +114 -0
- package/dist/services/repository-model/service.d.ts +105 -0
- package/dist/services/repository-model/service.js +250 -0
- package/dist/services/repository-public-artifact/service.d.ts +133 -0
- package/dist/services/repository-public-artifact/service.js +330 -0
- package/dist/services/routing-benchmark/service.d.ts +362 -0
- package/dist/services/routing-benchmark/service.js +96 -0
- package/dist/services/specification-critic/service.d.ts +92 -0
- package/dist/services/specification-critic/service.js +172 -0
- package/dist/services/task-family/service.d.ts +40 -0
- package/dist/services/task-family/service.js +55 -0
- package/dist/services/task-seed/service.d.ts +906 -0
- package/dist/services/task-seed/service.js +1406 -0
- package/dist/services/task-specification/service.d.ts +27 -0
- package/dist/services/task-specification/service.js +40 -0
- package/dist/services/trajectory-policy/service.d.ts +110 -0
- package/dist/services/trajectory-policy/service.js +216 -0
- package/dist/test/agentic-capabilities-protocol.test.d.ts +1 -0
- package/dist/test/agentic-capabilities-protocol.test.js +570 -0
- package/dist/test/agentic-capabilities.test.d.ts +1 -0
- package/dist/test/agentic-capabilities.test.js +1461 -0
- package/dist/test/agentic-environment.test.d.ts +1 -0
- package/dist/test/agentic-environment.test.js +213 -0
- package/dist/test/case-pipeline-foundation.test.d.ts +1 -0
- package/dist/test/case-pipeline-foundation.test.js +535 -0
- package/dist/test/case-pipeline-protocol-v2.test.d.ts +1 -0
- package/dist/test/case-pipeline-protocol-v2.test.js +124 -0
- package/dist/test/case-pipeline-v2.test.d.ts +1 -0
- package/dist/test/case-pipeline-v2.test.js +286 -0
- package/dist/test/case-pipeline.test.d.ts +1 -0
- package/dist/test/case-pipeline.test.js +851 -0
- package/dist/test/eval-capability-policy.test.d.ts +1 -0
- package/dist/test/eval-capability-policy.test.js +50 -0
- package/dist/test/eval-event-log.test.js +34 -1
- package/dist/test/evaluation-evidence-freshness.test.d.ts +1 -0
- package/dist/test/evaluation-evidence-freshness.test.js +44 -0
- package/dist/test/evaluation-evidence.test.d.ts +1 -0
- package/dist/test/evaluation-evidence.test.js +230 -0
- package/dist/test/evaluation-grader-calibration.test.d.ts +1 -0
- package/dist/test/evaluation-grader-calibration.test.js +373 -0
- package/dist/test/evaluation-proposal-digest.test.d.ts +1 -0
- package/dist/test/evaluation-proposal-digest.test.js +187 -0
- package/dist/test/evaluation-source-retrieval.test.d.ts +1 -0
- package/dist/test/evaluation-source-retrieval.test.js +237 -0
- package/dist/test/evaluation-structure-policy.test.d.ts +1 -0
- package/dist/test/evaluation-structure-policy.test.js +196 -0
- package/dist/test/fixtures/repository-resource-panel.d.ts +39 -0
- package/dist/test/fixtures/repository-resource-panel.js +111 -0
- package/dist/test/fixtures/vitest-boundary-panel.d.ts +84 -0
- package/dist/test/fixtures/vitest-boundary-panel.js +120 -0
- package/dist/test/fixtures/vitest-phase-panel.d.ts +135 -0
- package/dist/test/fixtures/vitest-phase-panel.js +213 -0
- package/dist/test/fixtures/vitest-reporter-results.d.ts +76 -0
- package/dist/test/fixtures/vitest-reporter-results.js +94 -0
- package/dist/test/grounded-authoring.test.d.ts +1 -0
- package/dist/test/grounded-authoring.test.js +565 -0
- package/dist/test/integrated-repository-history.test.d.ts +1 -0
- package/dist/test/integrated-repository-history.test.js +227 -0
- package/dist/test/project-authoring.test.js +593 -43
- package/dist/test/project-workflow.test.js +395 -32
- package/dist/test/repository-authoring-artifacts.test.d.ts +1 -0
- package/dist/test/repository-authoring-artifacts.test.js +185 -0
- package/dist/test/repository-bundle.test.d.ts +1 -0
- package/dist/test/repository-bundle.test.js +52 -0
- package/dist/test/repository-case-generation.test.d.ts +1 -0
- package/dist/test/repository-case-generation.test.js +3465 -0
- package/dist/test/repository-command-diagnostic.test.d.ts +1 -0
- package/dist/test/repository-command-diagnostic.test.js +55 -0
- package/dist/test/repository-command-signals.test.d.ts +1 -0
- package/dist/test/repository-command-signals.test.js +124 -0
- package/dist/test/repository-fixture-scope-coverage.test.d.ts +1 -0
- package/dist/test/repository-fixture-scope-coverage.test.js +127 -0
- package/dist/test/repository-fixture-validation.test.d.ts +1 -0
- package/dist/test/repository-fixture-validation.test.js +362 -0
- package/dist/test/repository-foundry-progress.test.d.ts +1 -0
- package/dist/test/repository-foundry-progress.test.js +110 -0
- package/dist/test/repository-foundry-quality.test.d.ts +1 -0
- package/dist/test/repository-foundry-quality.test.js +1138 -0
- package/dist/test/repository-import-context.test.d.ts +1 -0
- package/dist/test/repository-import-context.test.js +354 -0
- package/dist/test/repository-model-authoring.test.d.ts +1 -0
- package/dist/test/repository-model-authoring.test.js +544 -0
- package/dist/test/repository-model.test.d.ts +1 -0
- package/dist/test/repository-model.test.js +2195 -0
- package/dist/test/repository-node-test-reporter.test.d.ts +1 -0
- package/dist/test/repository-node-test-reporter.test.js +104 -0
- package/dist/test/repository-oracle-concurrency.test.d.ts +1 -0
- package/dist/test/repository-oracle-concurrency.test.js +542 -0
- package/dist/test/repository-oracle-coverage-witness.test.d.ts +1 -0
- package/dist/test/repository-oracle-coverage-witness.test.js +511 -0
- package/dist/test/repository-oracle-coverage.test.d.ts +1 -0
- package/dist/test/repository-oracle-coverage.test.js +168 -0
- package/dist/test/repository-oracle-evidence.test.d.ts +1 -0
- package/dist/test/repository-oracle-evidence.test.js +185 -0
- package/dist/test/repository-oracle-plan.test.d.ts +1 -0
- package/dist/test/repository-oracle-plan.test.js +176 -0
- package/dist/test/repository-overlay-isolation.test.d.ts +1 -0
- package/dist/test/repository-overlay-isolation.test.js +85 -0
- package/dist/test/repository-preparation-cache.test.d.ts +1 -0
- package/dist/test/repository-preparation-cache.test.js +414 -0
- package/dist/test/repository-public-artifact.test.d.ts +1 -0
- package/dist/test/repository-public-artifact.test.js +273 -0
- package/dist/test/repository-qualification-diagnostics.test.d.ts +1 -0
- package/dist/test/repository-qualification-diagnostics.test.js +524 -0
- package/dist/test/repository-reference-authoring.test.d.ts +1 -0
- package/dist/test/repository-reference-authoring.test.js +1633 -0
- package/dist/test/repository-review-evidence-v2.test.d.ts +1 -0
- package/dist/test/repository-review-evidence-v2.test.js +183 -0
- package/dist/test/repository-review-evidence.test.d.ts +1 -0
- package/dist/test/repository-review-evidence.test.js +124 -0
- package/dist/test/repository-seed-exclusions.test.d.ts +1 -0
- package/dist/test/repository-seed-exclusions.test.js +96 -0
- package/dist/test/repository-seed-selection.test.d.ts +1 -0
- package/dist/test/repository-seed-selection.test.js +504 -0
- package/dist/test/repository-semantic-calibration.test.d.ts +1 -0
- package/dist/test/repository-semantic-calibration.test.js +688 -0
- package/dist/test/repository-solution-edits.test.d.ts +1 -0
- package/dist/test/repository-solution-edits.test.js +377 -0
- package/dist/test/repository-specification-budget.test.d.ts +1 -0
- package/dist/test/repository-specification-budget.test.js +171 -0
- package/dist/test/repository-specification-contract-checkpoint.test.d.ts +1 -0
- package/dist/test/repository-specification-contract-checkpoint.test.js +228 -0
- package/dist/test/repository-specification-contract-facts.test.d.ts +1 -0
- package/dist/test/repository-specification-contract-facts.test.js +177 -0
- package/dist/test/repository-trajectory-authoring.test.d.ts +1 -0
- package/dist/test/repository-trajectory-authoring.test.js +176 -0
- package/dist/test/repository-valid-control-plan.test.d.ts +1 -0
- package/dist/test/repository-valid-control-plan.test.js +45 -0
- package/dist/test/repository-vitest-phase.test.d.ts +1 -0
- package/dist/test/repository-vitest-phase.test.js +848 -0
- package/dist/test/repository-vitest-reporter.test.d.ts +1 -0
- package/dist/test/repository-vitest-reporter.test.js +158 -0
- package/dist/test/repository-workspace-build.test.d.ts +1 -0
- package/dist/test/repository-workspace-build.test.js +160 -0
- package/dist/test/strict-authoring-schema.test.d.ts +1 -0
- package/dist/test/strict-authoring-schema.test.js +169 -0
- package/package.json +48 -7
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { cp, lstat, mkdir, mkdtemp, readdir, readFile, readlink, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { StringDecoder } from "node:string_decoder";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { Clock, Context, Effect, Exit, Option, ScopedCache } from "effect";
|
|
10
|
+
import { RepositoryFoundryError } from "../errors.js";
|
|
11
|
+
import { RepositoryFoundryEvidenceReconstruction } from "../services/evidence-reconstruction/service.js";
|
|
12
|
+
import { RepositoryCommandObservability } from "../services/command-observability/service.js";
|
|
13
|
+
import { sanitizeRepositoryCommandOutputV1 } from "./repository-command-diagnostic.js";
|
|
14
|
+
import { repositoryCommandExecutionIncompleteV1 } from "./repository-command-evidence.js";
|
|
15
|
+
import { repositoryNodeTestArgumentsV1 } from "./repository-node-test-reporter.js";
|
|
16
|
+
import { repositoryVitestReporterSourceV1, repositoryVitestTestArgumentsV1 } from "./repository-vitest-reporter.js";
|
|
17
|
+
import { repositoryVitestPhaseAdapterV1 } from "./repository-vitest-phase-adapter.js";
|
|
18
|
+
export const RepositoryCommandExecutionPort = Context.Reference("@velum-labs/routekit-eval-setup/RepositoryCommandExecutionPort", {
|
|
19
|
+
defaultValue: () => undefined
|
|
20
|
+
});
|
|
21
|
+
/** Host-provided, versioned pnpm store populated before an offline invocation. */
|
|
22
|
+
export const RepositoryReadOnlyPnpmStore = Context.Reference("@velum-labs/routekit-eval-setup/RepositoryReadOnlyPnpmStore", { defaultValue: () => undefined });
|
|
23
|
+
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
const MAX_OBSERVED_COMMAND_BYTES = 64 * 1024;
|
|
25
|
+
const MAX_OBSERVED_CHUNK_BYTES = 2 * 1024;
|
|
26
|
+
const MAX_OBSERVED_CHUNKS_PER_FLUSH = 8;
|
|
27
|
+
const OBSERVED_COMMAND_FLUSH_MS = 250;
|
|
28
|
+
const execFilePromise = promisify(execFile);
|
|
29
|
+
const ALLOWED_EXECUTABLES = new Set([
|
|
30
|
+
"corepack",
|
|
31
|
+
"node",
|
|
32
|
+
"pnpm",
|
|
33
|
+
"python",
|
|
34
|
+
"python3",
|
|
35
|
+
"go",
|
|
36
|
+
"cargo"
|
|
37
|
+
]);
|
|
38
|
+
const commandPath = [dirname(process.execPath), process.env.PATH]
|
|
39
|
+
.filter((value) => value !== undefined && value.length > 0)
|
|
40
|
+
.join(delimiter);
|
|
41
|
+
const corepackHome = process.env.COREPACK_HOME ??
|
|
42
|
+
(process.env.HOME === undefined
|
|
43
|
+
? undefined
|
|
44
|
+
: join(process.env.HOME, ".cache", "node", "corepack"));
|
|
45
|
+
const trustedPnpmStoreRoot = join(tmpdir(), `routekit-foundry-pnpm-store-${String(process.pid)}-${randomUUID()}`);
|
|
46
|
+
const failure = (operation, detail, cause) => new RepositoryFoundryError({
|
|
47
|
+
operation,
|
|
48
|
+
detail,
|
|
49
|
+
...(cause === undefined ? {} : { cause })
|
|
50
|
+
});
|
|
51
|
+
const pathWithin = (root, candidate) => {
|
|
52
|
+
const path = relative(root, candidate);
|
|
53
|
+
return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
|
|
54
|
+
};
|
|
55
|
+
const sbplPath = (path) => JSON.stringify(path);
|
|
56
|
+
const createDarwinSandboxProfiles = async (input) => {
|
|
57
|
+
const candidateProfile = join(input.root, "repository-command-candidate.sb");
|
|
58
|
+
const trustedPreparationProfile = join(input.root, "repository-command-trusted-preparation.sb");
|
|
59
|
+
const optionalReadRoots = [
|
|
60
|
+
dirname(dirname(process.execPath)),
|
|
61
|
+
process.env.PNPM_HOME,
|
|
62
|
+
corepackHome,
|
|
63
|
+
process.env.HOME === undefined ? undefined : join(process.env.HOME, "Library", "pnpm"),
|
|
64
|
+
input.trustedPnpmStore
|
|
65
|
+
].filter((candidate) => candidate !== undefined && candidate.length > 0);
|
|
66
|
+
const readRoots = [
|
|
67
|
+
input.root,
|
|
68
|
+
"/bin",
|
|
69
|
+
"/Library/Developer/CommandLineTools",
|
|
70
|
+
"/sbin",
|
|
71
|
+
"/usr/bin",
|
|
72
|
+
"/usr/local",
|
|
73
|
+
"/opt/homebrew",
|
|
74
|
+
...optionalReadRoots
|
|
75
|
+
];
|
|
76
|
+
const common = [
|
|
77
|
+
"(version 1)",
|
|
78
|
+
"(deny default)",
|
|
79
|
+
'(import "system.sb")',
|
|
80
|
+
"(allow process*)",
|
|
81
|
+
// Test runners must be able to reap their own worker processes. This does
|
|
82
|
+
// not permit signals to the host, sibling candidates or unrelated jobs.
|
|
83
|
+
"(allow signal (target children))",
|
|
84
|
+
"(allow file-read-metadata file-test-existence)",
|
|
85
|
+
`(allow file-read* file-test-existence file-map-executable ${readRoots
|
|
86
|
+
.map((candidate) => `(subpath ${sbplPath(candidate)})`)
|
|
87
|
+
.join(" ")})`
|
|
88
|
+
];
|
|
89
|
+
const writableIsolationRoots = `(allow file-write* (subpath ${sbplPath(input.checkout)}) (subpath ${sbplPath(input.home)}) (subpath ${sbplPath(input.temporary)}))`;
|
|
90
|
+
const candidateSource = [
|
|
91
|
+
...common,
|
|
92
|
+
writableIsolationRoots,
|
|
93
|
+
`(deny file-write* (subpath ${sbplPath(join(input.checkout, ".git"))}))`,
|
|
94
|
+
"(deny network*)",
|
|
95
|
+
""
|
|
96
|
+
].join("\n");
|
|
97
|
+
const trustedPreparationSource = [
|
|
98
|
+
...common,
|
|
99
|
+
`(allow file-write* (subpath ${sbplPath(input.checkout)}) (subpath ${sbplPath(input.home)}) (subpath ${sbplPath(input.temporary)})${input.trustedPnpmStore === undefined || input.trustedPnpmStoreReadOnly
|
|
100
|
+
? ""
|
|
101
|
+
: ` (subpath ${sbplPath(input.trustedPnpmStore)})`})`,
|
|
102
|
+
`(deny file-write* (subpath ${sbplPath(join(input.checkout, ".git"))}))`,
|
|
103
|
+
"(allow network*)",
|
|
104
|
+
""
|
|
105
|
+
].join("\n");
|
|
106
|
+
await Promise.all([
|
|
107
|
+
writeFile(candidateProfile, candidateSource, {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
mode: 0o600
|
|
110
|
+
}),
|
|
111
|
+
writeFile(trustedPreparationProfile, trustedPreparationSource, {
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
mode: 0o600
|
|
114
|
+
})
|
|
115
|
+
]);
|
|
116
|
+
return {
|
|
117
|
+
candidate: candidateProfile,
|
|
118
|
+
trustedPreparation: trustedPreparationProfile
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
const materializeIsolatedRepository = async (input) => {
|
|
122
|
+
const startedAt = Date.now();
|
|
123
|
+
const root = await realpath(await mkdtemp(join(tmpdir(), input.preparingTemplate ? "routekit-foundry-prepared-" : "routekit-foundry-isolated-")));
|
|
124
|
+
const checkout = join(root, "repository");
|
|
125
|
+
const home = join(root, "home");
|
|
126
|
+
const temporary = join(root, "tmp");
|
|
127
|
+
const archive = join(root, "source.tar");
|
|
128
|
+
await Promise.all([
|
|
129
|
+
mkdir(checkout, { recursive: true }),
|
|
130
|
+
mkdir(home, { recursive: true }),
|
|
131
|
+
mkdir(temporary, { recursive: true })
|
|
132
|
+
]);
|
|
133
|
+
try {
|
|
134
|
+
if (input.template !== undefined) {
|
|
135
|
+
if (process.platform === "darwin") {
|
|
136
|
+
// Native traversal avoids fs.cp's per-file JS overhead on large pnpm
|
|
137
|
+
// installations. -c uses clonefile with ordinary-copy fallback; -P keeps
|
|
138
|
+
// symlinks intact, and neither mode creates hard links to the template.
|
|
139
|
+
await execFilePromise("/bin/cp", ["-cR", "-P", `${input.template.isolated.checkout}/`, checkout], {
|
|
140
|
+
encoding: "utf8",
|
|
141
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
142
|
+
timeout: 60000,
|
|
143
|
+
env: { PATH: commandPath }
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
await cp(input.template.isolated.checkout, checkout, {
|
|
148
|
+
recursive: true,
|
|
149
|
+
mode: constants.COPYFILE_FICLONE,
|
|
150
|
+
dereference: false,
|
|
151
|
+
verbatimSymlinks: true
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// pnpm shell launchers can embed absolute NODE_PATH prefixes. A fresh clone
|
|
155
|
+
// must resolve its own dependencies, never the immutable template's files.
|
|
156
|
+
for (const path of input.template.relocatedFiles) {
|
|
157
|
+
const destination = join(checkout, path);
|
|
158
|
+
const content = await readFile(destination, "utf8");
|
|
159
|
+
await writeFile(destination, content.replaceAll(input.template.isolated.checkout, checkout));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await execFilePromise("git", [
|
|
164
|
+
"-C",
|
|
165
|
+
input.repositoryRoot,
|
|
166
|
+
"archive",
|
|
167
|
+
"--format=tar",
|
|
168
|
+
`--output=${archive}`,
|
|
169
|
+
input.commit
|
|
170
|
+
], {
|
|
171
|
+
encoding: "utf8",
|
|
172
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
173
|
+
windowsHide: true,
|
|
174
|
+
env: {
|
|
175
|
+
PATH: process.env.PATH,
|
|
176
|
+
HOME: process.env.HOME,
|
|
177
|
+
TMPDIR: process.env.TMPDIR
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
await execFilePromise("tar", ["-xf", archive, "-C", checkout], {
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
183
|
+
windowsHide: true
|
|
184
|
+
});
|
|
185
|
+
await rm(archive);
|
|
186
|
+
const gitOptions = {
|
|
187
|
+
encoding: "utf8",
|
|
188
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
189
|
+
windowsHide: true
|
|
190
|
+
};
|
|
191
|
+
await execFilePromise("git", ["-C", checkout, "init", "-q"], gitOptions);
|
|
192
|
+
await execFilePromise("git", ["-C", checkout, "config", "user.name", "RouteKit Repository Foundry"], gitOptions);
|
|
193
|
+
await execFilePromise("git", ["-C", checkout, "config", "user.email", "repository-foundry@routekit.invalid"], gitOptions);
|
|
194
|
+
await execFilePromise("git", ["-C", checkout, "config", "commit.gpgsign", "false"], gitOptions);
|
|
195
|
+
await execFilePromise("git", ["-C", checkout, "add", "-A"], gitOptions);
|
|
196
|
+
await execFilePromise("git", [
|
|
197
|
+
"-C",
|
|
198
|
+
checkout,
|
|
199
|
+
"commit",
|
|
200
|
+
"-q",
|
|
201
|
+
"--no-gpg-sign",
|
|
202
|
+
"--date=2000-01-01T00:00:00Z",
|
|
203
|
+
"-m",
|
|
204
|
+
"isolated foundry baseline"
|
|
205
|
+
], gitOptions);
|
|
206
|
+
}
|
|
207
|
+
const canonicalRoot = await realpath(root);
|
|
208
|
+
const canonicalCheckout = await realpath(checkout);
|
|
209
|
+
const canonicalHome = await realpath(home);
|
|
210
|
+
const canonicalTemporary = await realpath(temporary);
|
|
211
|
+
const sandboxProfiles = process.platform === "darwin"
|
|
212
|
+
? await createDarwinSandboxProfiles({
|
|
213
|
+
root: canonicalRoot,
|
|
214
|
+
checkout: canonicalCheckout,
|
|
215
|
+
home: canonicalHome,
|
|
216
|
+
temporary: canonicalTemporary,
|
|
217
|
+
...(input.trustedPnpmStore === undefined
|
|
218
|
+
? {}
|
|
219
|
+
: {
|
|
220
|
+
trustedPnpmStore: input.trustedPnpmStore,
|
|
221
|
+
trustedPnpmStoreReadOnly: input.trustedPnpmStoreReadOnly ?? false
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
: undefined;
|
|
225
|
+
return {
|
|
226
|
+
root: canonicalRoot,
|
|
227
|
+
checkout: canonicalCheckout,
|
|
228
|
+
home: canonicalHome,
|
|
229
|
+
temporary: canonicalTemporary,
|
|
230
|
+
materializationDurationMs: Date.now() - startedAt,
|
|
231
|
+
timings: {
|
|
232
|
+
checkoutDurationMs: Date.now() - startedAt,
|
|
233
|
+
overlayDurationMs: 0,
|
|
234
|
+
cleanupDurationMs: 0
|
|
235
|
+
},
|
|
236
|
+
...(sandboxProfiles === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: {
|
|
239
|
+
candidateSandboxProfile: sandboxProfiles.candidate,
|
|
240
|
+
trustedPreparationSandboxProfile: sandboxProfiles.trustedPreparation
|
|
241
|
+
}),
|
|
242
|
+
...(input.trustedPnpmStore === undefined
|
|
243
|
+
? {}
|
|
244
|
+
: {
|
|
245
|
+
trustedPnpmStore: input.trustedPnpmStore,
|
|
246
|
+
trustedPnpmStoreReadOnly: input.trustedPnpmStoreReadOnly ?? false
|
|
247
|
+
})
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch (cause) {
|
|
251
|
+
await rm(root, { recursive: true, force: true });
|
|
252
|
+
throw cause;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const exactPnpmSpecifier = /^pnpm@(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/u;
|
|
256
|
+
const pnpmCommandIndex = (args) => {
|
|
257
|
+
let index = 1;
|
|
258
|
+
while (args[index] === "--filter") {
|
|
259
|
+
const selector = args[index + 1];
|
|
260
|
+
if (selector === undefined || selector.length === 0 || selector.startsWith("-")) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
index += 2;
|
|
264
|
+
}
|
|
265
|
+
return index < args.length ? index : undefined;
|
|
266
|
+
};
|
|
267
|
+
const isPnpmInstallRecipe = (recipe) => recipe.executable === "corepack" &&
|
|
268
|
+
exactPnpmSpecifier.test(recipe.args[0] ?? "") &&
|
|
269
|
+
recipe.args[pnpmCommandIndex(recipe.args) ?? -1] === "install";
|
|
270
|
+
const reusableFrozenInstallRecipe = (recipe) => {
|
|
271
|
+
if (recipe.executable !== "corepack" ||
|
|
272
|
+
recipe.cwd !== "." ||
|
|
273
|
+
!exactPnpmSpecifier.test(recipe.args[0] ?? "")) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const commandIndex = pnpmCommandIndex(recipe.args);
|
|
277
|
+
if (commandIndex === undefined || recipe.args[commandIndex] !== "install")
|
|
278
|
+
return false;
|
|
279
|
+
const required = new Set(["--frozen-lockfile", "--ignore-scripts", "--ignore-pnpmfile"]);
|
|
280
|
+
for (let index = commandIndex + 1; index < recipe.args.length; index += 1) {
|
|
281
|
+
const argument = recipe.args[index];
|
|
282
|
+
if (required.delete(argument) || argument === "--prefer-offline" || argument === "--offline") {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (argument === "--filter" &&
|
|
286
|
+
(recipe.args[index + 1]?.length ?? 0) > 0 &&
|
|
287
|
+
!recipe.args[index + 1].startsWith("-")) {
|
|
288
|
+
index += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return required.size === 0;
|
|
294
|
+
};
|
|
295
|
+
const materializeExactPnpmShim = async (isolatedRoot, recipe) => {
|
|
296
|
+
const specifier = recipe.executable === "corepack" ? recipe.args[0] : undefined;
|
|
297
|
+
if (specifier === undefined || !exactPnpmSpecifier.test(specifier))
|
|
298
|
+
return undefined;
|
|
299
|
+
const directory = join(isolatedRoot, "repository-command-bin");
|
|
300
|
+
await mkdir(directory, { recursive: true });
|
|
301
|
+
if (process.platform === "win32") {
|
|
302
|
+
await writeFile(join(directory, "pnpm.cmd"), `@echo off\r\ncorepack ${specifier} %*\r\n`, {
|
|
303
|
+
encoding: "utf8",
|
|
304
|
+
mode: 0o700
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
await writeFile(join(directory, "pnpm"), `#!/bin/sh\nexec corepack ${specifier} "$@"\n`, {
|
|
309
|
+
encoding: "utf8",
|
|
310
|
+
mode: 0o700
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return directory;
|
|
314
|
+
};
|
|
315
|
+
const validateRecipe = (recipe, checkout, stage) => {
|
|
316
|
+
if (!ALLOWED_EXECUTABLES.has(recipe.executable)) {
|
|
317
|
+
throw new Error(`command executable is not allowed: ${JSON.stringify(recipe.executable)}`);
|
|
318
|
+
}
|
|
319
|
+
if (recipe.cwd.trim().length === 0 ||
|
|
320
|
+
isAbsolute(recipe.cwd) ||
|
|
321
|
+
recipe.cwd.split(/[\\/]/u).includes("..") ||
|
|
322
|
+
recipe.cwd.includes("\0")) {
|
|
323
|
+
throw new Error(`command cwd is not a canonical relative path: ${JSON.stringify(recipe.cwd)}`);
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isInteger(recipe.timeoutMs) ||
|
|
326
|
+
recipe.timeoutMs < 1 ||
|
|
327
|
+
recipe.timeoutMs > 30 * 60_000) {
|
|
328
|
+
throw new Error("command timeout must be between 1 ms and 30 minutes");
|
|
329
|
+
}
|
|
330
|
+
if (recipe.args.some((arg) => arg.includes("\0") || /^(?:--cwd|-C)$/u.test(arg) || /^--cwd=/u.test(arg))) {
|
|
331
|
+
throw new Error("command arguments must not override the reviewed cwd");
|
|
332
|
+
}
|
|
333
|
+
if (stage === "trusted-preparation" &&
|
|
334
|
+
recipe.executable === "corepack" &&
|
|
335
|
+
!exactPnpmSpecifier.test(recipe.args[0] ?? "")) {
|
|
336
|
+
throw new Error("corepack commands require an exact pnpm semantic version");
|
|
337
|
+
}
|
|
338
|
+
if (stage === "trusted-preparation" &&
|
|
339
|
+
isPnpmInstallRecipe(recipe) &&
|
|
340
|
+
!reusableFrozenInstallRecipe(recipe)) {
|
|
341
|
+
throw new Error("trusted pnpm preparation requires a frozen lockfile with scripts and pnpm hooks disabled");
|
|
342
|
+
}
|
|
343
|
+
const cwd = resolve(checkout, recipe.cwd);
|
|
344
|
+
if (!pathWithin(checkout, cwd)) {
|
|
345
|
+
throw new Error("command cwd escapes the isolated checkout");
|
|
346
|
+
}
|
|
347
|
+
return cwd;
|
|
348
|
+
};
|
|
349
|
+
const applyCandidatePatch = (checkout, patch) => {
|
|
350
|
+
if (Buffer.byteLength(patch, "utf8") > MAX_OUTPUT_BYTES) {
|
|
351
|
+
return Effect.fail(failure("prepare-checkout", "candidate patch exceeds the 2 MiB limit"));
|
|
352
|
+
}
|
|
353
|
+
return Effect.callback((resume, interruptionSignal) => {
|
|
354
|
+
const child = spawn("git", ["-C", checkout, "apply", "--whitespace=nowarn", "--recount", "-"], {
|
|
355
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
356
|
+
env: {
|
|
357
|
+
PATH: process.env.PATH,
|
|
358
|
+
HOME: process.env.HOME,
|
|
359
|
+
TMPDIR: process.env.TMPDIR
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
let settled = false;
|
|
363
|
+
let stderr = "";
|
|
364
|
+
child.stderr.setEncoding("utf8");
|
|
365
|
+
child.stderr.on("data", (chunk) => {
|
|
366
|
+
if (Buffer.byteLength(stderr) < MAX_OUTPUT_BYTES)
|
|
367
|
+
stderr += chunk;
|
|
368
|
+
});
|
|
369
|
+
child.once("error", (cause) => {
|
|
370
|
+
if (settled)
|
|
371
|
+
return;
|
|
372
|
+
settled = true;
|
|
373
|
+
resume(Effect.fail(failure("prepare-checkout", "could not apply candidate patch", cause)));
|
|
374
|
+
});
|
|
375
|
+
child.once("close", (code) => {
|
|
376
|
+
if (settled)
|
|
377
|
+
return;
|
|
378
|
+
settled = true;
|
|
379
|
+
resume(code === 0
|
|
380
|
+
? Effect.void
|
|
381
|
+
: Effect.fail(failure("prepare-checkout", `candidate patch did not apply: ${stderr.trim()}`)));
|
|
382
|
+
});
|
|
383
|
+
const interrupt = () => {
|
|
384
|
+
child.kill("SIGKILL");
|
|
385
|
+
};
|
|
386
|
+
interruptionSignal.addEventListener("abort", interrupt, { once: true });
|
|
387
|
+
child.stdin.end(patch, "utf8");
|
|
388
|
+
return Effect.sync(() => {
|
|
389
|
+
interruptionSignal.removeEventListener("abort", interrupt);
|
|
390
|
+
if (!settled)
|
|
391
|
+
child.kill("SIGKILL");
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
// Host writes are outside the candidate sandbox. A lexical path check alone
|
|
396
|
+
// cannot protect the shared dependency store or host files from linked paths.
|
|
397
|
+
// Each clone is private and no candidate runs while these overlays are installed.
|
|
398
|
+
const assertHostOverlayDestination = async (checkout, path) => {
|
|
399
|
+
if (path.length === 0 ||
|
|
400
|
+
isAbsolute(path) ||
|
|
401
|
+
path.includes("\\") ||
|
|
402
|
+
path.includes("\0") ||
|
|
403
|
+
path.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
404
|
+
throw new Error("host overlay destination is not a canonical repository-relative file path");
|
|
405
|
+
}
|
|
406
|
+
const root = await lstat(checkout);
|
|
407
|
+
if (root.isSymbolicLink() || !root.isDirectory()) {
|
|
408
|
+
throw new Error("host overlay checkout root is not a regular directory");
|
|
409
|
+
}
|
|
410
|
+
let current = checkout;
|
|
411
|
+
const parts = path.split("/");
|
|
412
|
+
for (const [index, part] of parts.entries()) {
|
|
413
|
+
current = join(current, part);
|
|
414
|
+
let metadata;
|
|
415
|
+
try {
|
|
416
|
+
metadata = await lstat(current);
|
|
417
|
+
}
|
|
418
|
+
catch (cause) {
|
|
419
|
+
if (cause !== null &&
|
|
420
|
+
typeof cause === "object" &&
|
|
421
|
+
"code" in cause &&
|
|
422
|
+
cause.code === "ENOENT") {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
throw cause;
|
|
426
|
+
}
|
|
427
|
+
if (metadata.isSymbolicLink()) {
|
|
428
|
+
throw new Error("host overlay destination traverses a symbolic link");
|
|
429
|
+
}
|
|
430
|
+
if (index < parts.length - 1 ? !metadata.isDirectory() : !metadata.isFile()) {
|
|
431
|
+
throw new Error("host overlay destination is not a regular file beneath regular directories");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const applyOverlays = Effect.fn("RepositoryFoundry.applyCommandOverlays")(function* (input) {
|
|
436
|
+
for (const overlay of input.overlays) {
|
|
437
|
+
if (overlay.path.trim().length === 0 ||
|
|
438
|
+
isAbsolute(overlay.path) ||
|
|
439
|
+
overlay.path.split(/[\\/]/u).includes("..") ||
|
|
440
|
+
overlay.path.includes("\0")) {
|
|
441
|
+
return yield* failure("prepare-checkout", `overlay path is unsafe: ${JSON.stringify(overlay.path)}`);
|
|
442
|
+
}
|
|
443
|
+
const destination = resolve(input.checkout, overlay.path);
|
|
444
|
+
if (!pathWithin(input.checkout, destination)) {
|
|
445
|
+
return yield* failure("prepare-checkout", `overlay path escapes checkout: ${JSON.stringify(overlay.path)}`);
|
|
446
|
+
}
|
|
447
|
+
const content = yield* Effect.tryPromise({
|
|
448
|
+
try: async () => {
|
|
449
|
+
const result = await execFilePromise("git", ["-C", input.repositoryRoot, "show", `${overlay.sourceCommit}:${overlay.path}`], {
|
|
450
|
+
encoding: "buffer",
|
|
451
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
452
|
+
windowsHide: true
|
|
453
|
+
});
|
|
454
|
+
return result.stdout;
|
|
455
|
+
},
|
|
456
|
+
catch: (cause) => failure("prepare-checkout", `could not read reviewed overlay ${overlay.sourceCommit}:${overlay.path}`, cause)
|
|
457
|
+
});
|
|
458
|
+
yield* Effect.tryPromise({
|
|
459
|
+
try: async () => {
|
|
460
|
+
await assertHostOverlayDestination(input.checkout, overlay.path);
|
|
461
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
462
|
+
await writeFile(destination, content);
|
|
463
|
+
},
|
|
464
|
+
catch: (cause) => failure("prepare-checkout", `could not materialize reviewed overlay ${JSON.stringify(overlay.path)}`, cause)
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
const applyFileOverrides = Effect.fn("RepositoryFoundry.applyCommandFileOverrides")(function* (input) {
|
|
469
|
+
for (const override of input.overrides) {
|
|
470
|
+
if (override.path.trim().length === 0 ||
|
|
471
|
+
isAbsolute(override.path) ||
|
|
472
|
+
override.path.split(/[\\/]/u).includes("..") ||
|
|
473
|
+
override.path.includes("\0")) {
|
|
474
|
+
return yield* failure("prepare-checkout", `file override path is unsafe: ${JSON.stringify(override.path)}`);
|
|
475
|
+
}
|
|
476
|
+
const destination = resolve(input.checkout, override.path);
|
|
477
|
+
if (!pathWithin(input.checkout, destination)) {
|
|
478
|
+
return yield* failure("prepare-checkout", `file override path escapes checkout: ${JSON.stringify(override.path)}`);
|
|
479
|
+
}
|
|
480
|
+
yield* Effect.tryPromise({
|
|
481
|
+
try: async () => {
|
|
482
|
+
await assertHostOverlayDestination(input.checkout, override.path);
|
|
483
|
+
if ("delete" in override) {
|
|
484
|
+
try {
|
|
485
|
+
const metadata = await lstat(destination);
|
|
486
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
487
|
+
throw new Error("file deletion override can only remove a regular file");
|
|
488
|
+
}
|
|
489
|
+
await rm(destination);
|
|
490
|
+
}
|
|
491
|
+
catch (cause) {
|
|
492
|
+
if (cause !== null &&
|
|
493
|
+
typeof cause === "object" &&
|
|
494
|
+
"code" in cause &&
|
|
495
|
+
cause.code === "ENOENT") {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
throw cause;
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
503
|
+
await writeFile(destination, override.content, "utf8");
|
|
504
|
+
},
|
|
505
|
+
catch: (cause) => failure("prepare-checkout", `could not materialize file override ${JSON.stringify(override.path)}`, cause)
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
const applyContentOverlays = Effect.fn("RepositoryFoundry.applyCommandContentOverlays")(function* (input) {
|
|
510
|
+
for (const overlay of input.overlays) {
|
|
511
|
+
if (overlay.path.trim().length === 0 ||
|
|
512
|
+
isAbsolute(overlay.path) ||
|
|
513
|
+
overlay.path.split(/[\\/]/u).includes("..") ||
|
|
514
|
+
overlay.path.includes("\0")) {
|
|
515
|
+
return yield* failure("prepare-checkout", `content overlay path is unsafe: ${JSON.stringify(overlay.path)}`);
|
|
516
|
+
}
|
|
517
|
+
const destination = resolve(input.checkout, overlay.path);
|
|
518
|
+
if (!pathWithin(input.checkout, destination)) {
|
|
519
|
+
return yield* failure("prepare-checkout", `content overlay path escapes checkout: ${JSON.stringify(overlay.path)}`);
|
|
520
|
+
}
|
|
521
|
+
yield* Effect.tryPromise({
|
|
522
|
+
try: async () => {
|
|
523
|
+
await assertHostOverlayDestination(input.checkout, overlay.path);
|
|
524
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
525
|
+
await writeFile(destination, overlay.content, "utf8");
|
|
526
|
+
},
|
|
527
|
+
catch: (cause) => failure("prepare-checkout", `could not materialize content overlay ${JSON.stringify(overlay.path)}`, cause)
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
const runCommand = Effect.fn("RepositoryFoundry.runCommand")(function* (input) {
|
|
532
|
+
const cwd = yield* Effect.try({
|
|
533
|
+
try: () => validateRecipe(input.recipe, input.isolated.checkout, input.stage),
|
|
534
|
+
catch: (cause) => failure("run-command", cause instanceof Error ? cause.message : String(cause), cause)
|
|
535
|
+
});
|
|
536
|
+
const vitestReporterPath = join(input.isolated.root, `vitest-reporter-${createHash("sha256").update(input.recipe.id).digest("hex")}.mjs`);
|
|
537
|
+
const vitestArguments = input.stage === "grade"
|
|
538
|
+
? repositoryVitestTestArgumentsV1(input.recipe, vitestReporterPath)
|
|
539
|
+
: undefined;
|
|
540
|
+
if (vitestArguments !== undefined) {
|
|
541
|
+
yield* Effect.tryPromise({
|
|
542
|
+
try: async () => {
|
|
543
|
+
const phaseAdapterPath = `${vitestReporterPath}.phase.mjs`;
|
|
544
|
+
const phase = await repositoryVitestPhaseAdapterV1({
|
|
545
|
+
cwd,
|
|
546
|
+
checkout: input.isolated.checkout
|
|
547
|
+
});
|
|
548
|
+
if (phase.supported) {
|
|
549
|
+
await writeFile(phaseAdapterPath, phase.source, {
|
|
550
|
+
encoding: "utf8",
|
|
551
|
+
mode: 0o600,
|
|
552
|
+
flag: "wx"
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
await writeFile(vitestReporterPath, repositoryVitestReporterSourceV1({
|
|
556
|
+
phaseAdapterPath,
|
|
557
|
+
runtimeSupported: phase.supported
|
|
558
|
+
}), {
|
|
559
|
+
encoding: "utf8",
|
|
560
|
+
mode: 0o600,
|
|
561
|
+
flag: "wx"
|
|
562
|
+
});
|
|
563
|
+
},
|
|
564
|
+
catch: (cause) => failure("run-command", "could not materialize the host-owned Vitest phase adapter", cause)
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const reporterMetadata = vitestArguments === undefined ? {} : { testReporter: "vitest-v1" };
|
|
568
|
+
const commandArgs = vitestArguments ??
|
|
569
|
+
(input.stage === "grade" ? repositoryNodeTestArgumentsV1(input.recipe) : input.recipe.args);
|
|
570
|
+
const exactPnpmBin = yield* Effect.tryPromise({
|
|
571
|
+
try: () => materializeExactPnpmShim(input.isolated.root, input.recipe),
|
|
572
|
+
catch: (cause) => failure("run-command", "could not materialize the exact pnpm command shim", cause)
|
|
573
|
+
});
|
|
574
|
+
const commandObservability = yield* Effect.serviceOption(RepositoryCommandObservability);
|
|
575
|
+
const executionPort = yield* RepositoryCommandExecutionPort;
|
|
576
|
+
if (executionPort !== undefined) {
|
|
577
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
578
|
+
const commandId = randomUUID();
|
|
579
|
+
const observe = (event) => {
|
|
580
|
+
if (Option.isSome(commandObservability)) {
|
|
581
|
+
try {
|
|
582
|
+
commandObservability.value.observe(event);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
/* telemetry never changes scientific decisions */
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
const common = {
|
|
590
|
+
version: 1,
|
|
591
|
+
commandId,
|
|
592
|
+
recipeId: input.recipe.id,
|
|
593
|
+
repetition: input.repetition,
|
|
594
|
+
stage: input.stage
|
|
595
|
+
};
|
|
596
|
+
observe({ ...common, kind: "started", at: new Date(startedAt).toISOString() });
|
|
597
|
+
const sandboxProfile = input.stage === "trusted-preparation"
|
|
598
|
+
? input.isolated.trustedPreparationSandboxProfile
|
|
599
|
+
: input.isolated.candidateSandboxProfile;
|
|
600
|
+
const result = yield* executionPort.run({
|
|
601
|
+
operationId: commandId,
|
|
602
|
+
cwd,
|
|
603
|
+
executable: sandboxProfile === undefined ? input.recipe.executable : "/usr/bin/sandbox-exec",
|
|
604
|
+
args: sandboxProfile === undefined
|
|
605
|
+
? commandArgs
|
|
606
|
+
: ["-f", sandboxProfile, input.recipe.executable, ...commandArgs],
|
|
607
|
+
environment: {
|
|
608
|
+
PATH: exactPnpmBin === undefined ? commandPath : [exactPnpmBin, commandPath].join(delimiter),
|
|
609
|
+
HOME: input.isolated.home,
|
|
610
|
+
TMPDIR: input.isolated.temporary,
|
|
611
|
+
...(corepackHome === undefined ? {} : { COREPACK_HOME: corepackHome }),
|
|
612
|
+
...(input.isolated.trustedPnpmStore === undefined
|
|
613
|
+
? {}
|
|
614
|
+
: {
|
|
615
|
+
npm_config_store_dir: input.isolated.trustedPnpmStore,
|
|
616
|
+
pnpm_config_store_dir: input.isolated.trustedPnpmStore,
|
|
617
|
+
...(input.isolated.trustedPnpmStoreReadOnly
|
|
618
|
+
? { pnpm_config_frozen_store: "true", npm_config_frozen_store: "true" }
|
|
619
|
+
: {}),
|
|
620
|
+
...(input.stage === "trusted-preparation"
|
|
621
|
+
? {}
|
|
622
|
+
: {
|
|
623
|
+
npm_config_offline: "true",
|
|
624
|
+
pnpm_config_offline: "true",
|
|
625
|
+
pnpm_config_verify_deps_before_run: "false"
|
|
626
|
+
})
|
|
627
|
+
}),
|
|
628
|
+
CI: "1",
|
|
629
|
+
NO_COLOR: "1",
|
|
630
|
+
FORCE_COLOR: "0",
|
|
631
|
+
PORTLESS: "0"
|
|
632
|
+
},
|
|
633
|
+
timeoutMs: input.recipe.timeoutMs
|
|
634
|
+
});
|
|
635
|
+
const endedAt = yield* Clock.currentTimeMillis;
|
|
636
|
+
let sequence = 0;
|
|
637
|
+
let commandBytes = 0;
|
|
638
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
639
|
+
const bytes = Buffer.from(sanitizeRepositoryCommandOutputV1(result[stream]), "utf8");
|
|
640
|
+
const decoder = new StringDecoder("utf8");
|
|
641
|
+
for (let offset = 0; offset < bytes.length; offset += MAX_OBSERVED_CHUNK_BYTES) {
|
|
642
|
+
const chunk = decoder.write(bytes.subarray(offset, offset + MAX_OBSERVED_CHUNK_BYTES)) +
|
|
643
|
+
(offset + MAX_OBSERVED_CHUNK_BYTES >= bytes.length ? decoder.end() : "");
|
|
644
|
+
commandBytes += Buffer.byteLength(chunk);
|
|
645
|
+
sequence += 1;
|
|
646
|
+
observe({
|
|
647
|
+
...common,
|
|
648
|
+
kind: "output",
|
|
649
|
+
at: new Date(endedAt).toISOString(),
|
|
650
|
+
stream,
|
|
651
|
+
sequence,
|
|
652
|
+
chunk,
|
|
653
|
+
chunkBytes: Buffer.byteLength(chunk),
|
|
654
|
+
commandBytes,
|
|
655
|
+
truncated: false
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
observe({
|
|
660
|
+
...common,
|
|
661
|
+
kind: "completed",
|
|
662
|
+
at: new Date(endedAt).toISOString(),
|
|
663
|
+
exitCode: result.exitCode,
|
|
664
|
+
timedOut: result.timedOut ?? false,
|
|
665
|
+
durationMs: endedAt - startedAt,
|
|
666
|
+
commandBytes,
|
|
667
|
+
truncated: false
|
|
668
|
+
});
|
|
669
|
+
return {
|
|
670
|
+
recipeId: input.recipe.id,
|
|
671
|
+
commit: input.commit,
|
|
672
|
+
repetition: input.repetition,
|
|
673
|
+
stage: input.stage,
|
|
674
|
+
exitCode: result.exitCode,
|
|
675
|
+
timedOut: result.timedOut ?? false,
|
|
676
|
+
outputTruncated: false,
|
|
677
|
+
stdout: sanitizeRepositoryCommandOutputV1(result.stdout),
|
|
678
|
+
stderr: sanitizeRepositoryCommandOutputV1(result.stderr),
|
|
679
|
+
durationMs: endedAt - startedAt,
|
|
680
|
+
...reporterMetadata
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return yield* Effect.callback((resume, interruptionSignal) => {
|
|
684
|
+
const startedAt = Date.now();
|
|
685
|
+
const commandId = randomUUID();
|
|
686
|
+
const observe = (event) => {
|
|
687
|
+
if (Option.isNone(commandObservability))
|
|
688
|
+
return;
|
|
689
|
+
try {
|
|
690
|
+
commandObservability.value.observe(event);
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
// Observability is best-effort and must never affect repository execution.
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
const sandboxProfile = input.stage === "trusted-preparation"
|
|
697
|
+
? input.isolated.trustedPreparationSandboxProfile
|
|
698
|
+
: input.isolated.candidateSandboxProfile;
|
|
699
|
+
const executable = sandboxProfile === undefined ? input.recipe.executable : "/usr/bin/sandbox-exec";
|
|
700
|
+
const args = sandboxProfile === undefined
|
|
701
|
+
? [...commandArgs]
|
|
702
|
+
: ["-f", sandboxProfile, input.recipe.executable, ...commandArgs];
|
|
703
|
+
const detached = process.platform !== "win32";
|
|
704
|
+
const child = spawn(executable, args, {
|
|
705
|
+
cwd,
|
|
706
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
707
|
+
detached,
|
|
708
|
+
env: {
|
|
709
|
+
PATH: exactPnpmBin === undefined ? commandPath : [exactPnpmBin, commandPath].join(delimiter),
|
|
710
|
+
HOME: input.isolated.home,
|
|
711
|
+
TMPDIR: input.isolated.temporary,
|
|
712
|
+
...(corepackHome === undefined ? {} : { COREPACK_HOME: corepackHome }),
|
|
713
|
+
...(input.isolated.trustedPnpmStore === undefined
|
|
714
|
+
? {}
|
|
715
|
+
: {
|
|
716
|
+
// pnpm 11 reads pnpm_config_ settings. Keep the npm_config_
|
|
717
|
+
// spelling as well for repositories pinned to older pnpm.
|
|
718
|
+
npm_config_store_dir: input.isolated.trustedPnpmStore,
|
|
719
|
+
pnpm_config_store_dir: input.isolated.trustedPnpmStore,
|
|
720
|
+
...(input.isolated.trustedPnpmStoreReadOnly
|
|
721
|
+
? { pnpm_config_frozen_store: "true", npm_config_frozen_store: "true" }
|
|
722
|
+
: {}),
|
|
723
|
+
...(input.stage === "trusted-preparation"
|
|
724
|
+
? {}
|
|
725
|
+
: {
|
|
726
|
+
npm_config_offline: "true",
|
|
727
|
+
pnpm_config_offline: "true",
|
|
728
|
+
pnpm_config_verify_deps_before_run: "false"
|
|
729
|
+
})
|
|
730
|
+
}),
|
|
731
|
+
CI: "1",
|
|
732
|
+
NO_COLOR: "1",
|
|
733
|
+
FORCE_COLOR: "0",
|
|
734
|
+
PORTLESS: "0"
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
observe({
|
|
738
|
+
version: 1,
|
|
739
|
+
kind: "started",
|
|
740
|
+
at: new Date(startedAt).toISOString(),
|
|
741
|
+
commandId,
|
|
742
|
+
recipeId: input.recipe.id,
|
|
743
|
+
repetition: input.repetition,
|
|
744
|
+
stage: input.stage
|
|
745
|
+
});
|
|
746
|
+
let stdout = "";
|
|
747
|
+
let stderr = "";
|
|
748
|
+
let outputBytes = 0;
|
|
749
|
+
let outputTruncated = false;
|
|
750
|
+
let observedCommandBytes = 0;
|
|
751
|
+
let observedSequence = 0;
|
|
752
|
+
let observedTruncated = false;
|
|
753
|
+
let observedContentDropped = false;
|
|
754
|
+
let observedTimer;
|
|
755
|
+
const observedPending = [];
|
|
756
|
+
const streamState = {
|
|
757
|
+
stdout: { decoder: new StringDecoder("utf8"), line: "", privateKey: false },
|
|
758
|
+
stderr: { decoder: new StringDecoder("utf8"), line: "", privateKey: false }
|
|
759
|
+
};
|
|
760
|
+
let timedOut = false;
|
|
761
|
+
let settled = false;
|
|
762
|
+
let forcedSettlement;
|
|
763
|
+
const takeUtf8Prefix = (value, maximumBytes) => {
|
|
764
|
+
if (Buffer.byteLength(value, "utf8") <= maximumBytes)
|
|
765
|
+
return value;
|
|
766
|
+
let bytes = 0;
|
|
767
|
+
let prefix = "";
|
|
768
|
+
for (const character of value) {
|
|
769
|
+
const width = Buffer.byteLength(character, "utf8");
|
|
770
|
+
if (bytes + width > maximumBytes)
|
|
771
|
+
break;
|
|
772
|
+
prefix += character;
|
|
773
|
+
bytes += width;
|
|
774
|
+
}
|
|
775
|
+
return prefix;
|
|
776
|
+
};
|
|
777
|
+
const pendingBytes = () => observedPending.reduce((total, entry) => total + Buffer.byteLength(entry.text, "utf8"), 0);
|
|
778
|
+
const flushObserved = (maximumChunks = MAX_OBSERVED_CHUNKS_PER_FLUSH, scheduleRemainder = true) => {
|
|
779
|
+
observedTimer = undefined;
|
|
780
|
+
let flushed = 0;
|
|
781
|
+
while (flushed < maximumChunks) {
|
|
782
|
+
const current = observedPending[0];
|
|
783
|
+
if (current === undefined)
|
|
784
|
+
break;
|
|
785
|
+
const chunk = takeUtf8Prefix(current.text, MAX_OBSERVED_CHUNK_BYTES);
|
|
786
|
+
if (chunk.length === 0) {
|
|
787
|
+
observedPending.shift();
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf8");
|
|
791
|
+
current.text = current.text.slice(chunk.length);
|
|
792
|
+
if (current.text.length === 0)
|
|
793
|
+
observedPending.shift();
|
|
794
|
+
observedCommandBytes += chunkBytes;
|
|
795
|
+
observe({
|
|
796
|
+
version: 1,
|
|
797
|
+
kind: "output",
|
|
798
|
+
at: new Date().toISOString(),
|
|
799
|
+
commandId,
|
|
800
|
+
recipeId: input.recipe.id,
|
|
801
|
+
repetition: input.repetition,
|
|
802
|
+
stage: input.stage,
|
|
803
|
+
stream: current.stream,
|
|
804
|
+
sequence: ++observedSequence,
|
|
805
|
+
chunk,
|
|
806
|
+
chunkBytes,
|
|
807
|
+
commandBytes: observedCommandBytes,
|
|
808
|
+
truncated: observedTruncated || observedContentDropped
|
|
809
|
+
});
|
|
810
|
+
flushed += 1;
|
|
811
|
+
}
|
|
812
|
+
if (scheduleRemainder && observedPending.length > 0) {
|
|
813
|
+
observedTimer = setTimeout(flushObserved, OBSERVED_COMMAND_FLUSH_MS);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
const scheduleObservedFlush = () => {
|
|
817
|
+
if (observedTimer !== undefined || observedPending.length === 0)
|
|
818
|
+
return;
|
|
819
|
+
observedTimer = setTimeout(flushObserved, OBSERVED_COMMAND_FLUSH_MS);
|
|
820
|
+
};
|
|
821
|
+
const enqueueObserved = (stream, value) => {
|
|
822
|
+
if (value.length === 0 || observedTruncated)
|
|
823
|
+
return;
|
|
824
|
+
const remaining = MAX_OBSERVED_COMMAND_BYTES - observedCommandBytes - pendingBytes();
|
|
825
|
+
if (remaining <= 0) {
|
|
826
|
+
observedTruncated = true;
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const sanitized = sanitizeRepositoryCommandOutputV1(value);
|
|
830
|
+
const accepted = takeUtf8Prefix(sanitized, remaining);
|
|
831
|
+
if (accepted.length === 0)
|
|
832
|
+
return;
|
|
833
|
+
if (Buffer.byteLength(accepted, "utf8") < Buffer.byteLength(sanitized, "utf8")) {
|
|
834
|
+
observedTruncated = true;
|
|
835
|
+
}
|
|
836
|
+
const last = observedPending.at(-1);
|
|
837
|
+
if (last?.stream === stream)
|
|
838
|
+
last.text += accepted;
|
|
839
|
+
else
|
|
840
|
+
observedPending.push({ stream, text: accepted });
|
|
841
|
+
scheduleObservedFlush();
|
|
842
|
+
};
|
|
843
|
+
const acceptObservedText = (stream, value, final = false) => {
|
|
844
|
+
if (observedTruncated)
|
|
845
|
+
return;
|
|
846
|
+
const state = streamState[stream];
|
|
847
|
+
state.line += value;
|
|
848
|
+
const pieces = state.line.split(/(?<=\n)/u);
|
|
849
|
+
state.line = final ? "" : (pieces.pop() ?? "");
|
|
850
|
+
if (Buffer.byteLength(state.line, "utf8") > MAX_OBSERVED_COMMAND_BYTES) {
|
|
851
|
+
// Never retain an unbounded unterminated line. Dropping the line is
|
|
852
|
+
// safer than cutting through a credential-shaped value.
|
|
853
|
+
state.line = "";
|
|
854
|
+
observedContentDropped = true;
|
|
855
|
+
}
|
|
856
|
+
let safe = "";
|
|
857
|
+
for (const line of pieces) {
|
|
858
|
+
if (state.privateKey) {
|
|
859
|
+
if (/-----END [A-Z0-9 ]*PRIVATE KEY-----/u.test(line)) {
|
|
860
|
+
safe += "[redacted]\n";
|
|
861
|
+
state.privateKey = false;
|
|
862
|
+
}
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/u.test(line)) {
|
|
866
|
+
if (/-----END [A-Z0-9 ]*PRIVATE KEY-----/u.test(line)) {
|
|
867
|
+
safe += sanitizeRepositoryCommandOutputV1(line);
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
state.privateKey = true;
|
|
871
|
+
}
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (Buffer.byteLength(line, "utf8") > MAX_OBSERVED_COMMAND_BYTES) {
|
|
875
|
+
observedContentDropped = true;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
safe += line;
|
|
879
|
+
}
|
|
880
|
+
if (final && state.privateKey) {
|
|
881
|
+
safe += "[redacted]\n";
|
|
882
|
+
state.privateKey = false;
|
|
883
|
+
}
|
|
884
|
+
enqueueObserved(stream, safe);
|
|
885
|
+
};
|
|
886
|
+
const finalizeObserved = () => {
|
|
887
|
+
if (observedTimer !== undefined) {
|
|
888
|
+
clearTimeout(observedTimer);
|
|
889
|
+
observedTimer = undefined;
|
|
890
|
+
}
|
|
891
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
892
|
+
const state = streamState[stream];
|
|
893
|
+
acceptObservedText(stream, state.decoder.end(), true);
|
|
894
|
+
}
|
|
895
|
+
if (observedTimer !== undefined) {
|
|
896
|
+
clearTimeout(observedTimer);
|
|
897
|
+
observedTimer = undefined;
|
|
898
|
+
}
|
|
899
|
+
flushObserved(Number.POSITIVE_INFINITY, false);
|
|
900
|
+
};
|
|
901
|
+
const observeCompleted = (exitCode, commandTimedOut) => {
|
|
902
|
+
finalizeObserved();
|
|
903
|
+
observe({
|
|
904
|
+
version: 1,
|
|
905
|
+
kind: "completed",
|
|
906
|
+
at: new Date().toISOString(),
|
|
907
|
+
commandId,
|
|
908
|
+
recipeId: input.recipe.id,
|
|
909
|
+
repetition: input.repetition,
|
|
910
|
+
stage: input.stage,
|
|
911
|
+
exitCode,
|
|
912
|
+
timedOut: commandTimedOut,
|
|
913
|
+
durationMs: Date.now() - startedAt,
|
|
914
|
+
commandBytes: observedCommandBytes,
|
|
915
|
+
truncated: observedTruncated || observedContentDropped
|
|
916
|
+
});
|
|
917
|
+
};
|
|
918
|
+
const killProcessTree = () => {
|
|
919
|
+
if (detached && child.pid !== undefined) {
|
|
920
|
+
try {
|
|
921
|
+
process.kill(-child.pid, "SIGKILL");
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
catch {
|
|
925
|
+
// Fall through if the process group already exited.
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
child.kill("SIGKILL");
|
|
929
|
+
};
|
|
930
|
+
const settleTimedOut = () => {
|
|
931
|
+
if (settled)
|
|
932
|
+
return;
|
|
933
|
+
settled = true;
|
|
934
|
+
child.stdout.destroy();
|
|
935
|
+
child.stderr.destroy();
|
|
936
|
+
observeCompleted(null, true);
|
|
937
|
+
resume(Effect.succeed({
|
|
938
|
+
recipeId: input.recipe.id,
|
|
939
|
+
commit: input.commit,
|
|
940
|
+
repetition: input.repetition,
|
|
941
|
+
stage: input.stage,
|
|
942
|
+
exitCode: null,
|
|
943
|
+
timedOut: true,
|
|
944
|
+
outputTruncated,
|
|
945
|
+
stdout,
|
|
946
|
+
stderr,
|
|
947
|
+
durationMs: Date.now() - startedAt,
|
|
948
|
+
...reporterMetadata
|
|
949
|
+
}));
|
|
950
|
+
};
|
|
951
|
+
const append = (target, chunk) => {
|
|
952
|
+
acceptObservedText(target, streamState[target].decoder.write(chunk));
|
|
953
|
+
const remaining = MAX_OUTPUT_BYTES - outputBytes;
|
|
954
|
+
if (remaining <= 0) {
|
|
955
|
+
outputTruncated = true;
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const accepted = chunk.byteLength <= remaining ? chunk : chunk.subarray(0, remaining);
|
|
959
|
+
outputBytes += accepted.byteLength;
|
|
960
|
+
if (accepted.byteLength < chunk.byteLength)
|
|
961
|
+
outputTruncated = true;
|
|
962
|
+
if (target === "stdout")
|
|
963
|
+
stdout += accepted.toString("utf8");
|
|
964
|
+
else
|
|
965
|
+
stderr += accepted.toString("utf8");
|
|
966
|
+
};
|
|
967
|
+
child.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
968
|
+
child.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
969
|
+
const timeout = setTimeout(() => {
|
|
970
|
+
timedOut = true;
|
|
971
|
+
killProcessTree();
|
|
972
|
+
forcedSettlement = setTimeout(settleTimedOut, 2_000);
|
|
973
|
+
}, input.recipe.timeoutMs);
|
|
974
|
+
child.once("error", (cause) => {
|
|
975
|
+
if (settled)
|
|
976
|
+
return;
|
|
977
|
+
settled = true;
|
|
978
|
+
clearTimeout(timeout);
|
|
979
|
+
if (forcedSettlement !== undefined)
|
|
980
|
+
clearTimeout(forcedSettlement);
|
|
981
|
+
observeCompleted(null, false);
|
|
982
|
+
resume(Effect.fail(failure("run-command", `could not start ${input.recipe.executable}`, cause)));
|
|
983
|
+
});
|
|
984
|
+
child.once("close", (exitCode) => {
|
|
985
|
+
if (settled)
|
|
986
|
+
return;
|
|
987
|
+
settled = true;
|
|
988
|
+
clearTimeout(timeout);
|
|
989
|
+
if (forcedSettlement !== undefined)
|
|
990
|
+
clearTimeout(forcedSettlement);
|
|
991
|
+
observeCompleted(exitCode, timedOut);
|
|
992
|
+
resume(Effect.succeed({
|
|
993
|
+
recipeId: input.recipe.id,
|
|
994
|
+
commit: input.commit,
|
|
995
|
+
repetition: input.repetition,
|
|
996
|
+
stage: input.stage,
|
|
997
|
+
exitCode,
|
|
998
|
+
timedOut,
|
|
999
|
+
outputTruncated,
|
|
1000
|
+
stdout,
|
|
1001
|
+
stderr,
|
|
1002
|
+
durationMs: Date.now() - startedAt,
|
|
1003
|
+
...reporterMetadata
|
|
1004
|
+
}));
|
|
1005
|
+
});
|
|
1006
|
+
const interrupt = () => {
|
|
1007
|
+
killProcessTree();
|
|
1008
|
+
};
|
|
1009
|
+
interruptionSignal.addEventListener("abort", interrupt, { once: true });
|
|
1010
|
+
return Effect.sync(() => {
|
|
1011
|
+
interruptionSignal.removeEventListener("abort", interrupt);
|
|
1012
|
+
clearTimeout(timeout);
|
|
1013
|
+
if (forcedSettlement !== undefined)
|
|
1014
|
+
clearTimeout(forcedSettlement);
|
|
1015
|
+
if (observedTimer !== undefined)
|
|
1016
|
+
clearTimeout(observedTimer);
|
|
1017
|
+
if (!settled)
|
|
1018
|
+
killProcessTree();
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
const commandPassed = (result) => result.exitCode === 0 && !repositoryCommandExecutionIncompleteV1(result);
|
|
1023
|
+
const runRecipeSequence = Effect.fn("RepositoryFoundry.runCommandRecipeSequence")(function* (input) {
|
|
1024
|
+
const results = [];
|
|
1025
|
+
for (const recipe of input.recipes) {
|
|
1026
|
+
const result = yield* runCommand({
|
|
1027
|
+
isolated: input.isolated,
|
|
1028
|
+
commit: input.commit,
|
|
1029
|
+
repetition: input.repetition,
|
|
1030
|
+
stage: input.stage,
|
|
1031
|
+
recipe
|
|
1032
|
+
});
|
|
1033
|
+
results.push(result);
|
|
1034
|
+
if (!commandPassed(result))
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
return results;
|
|
1038
|
+
});
|
|
1039
|
+
const terminalResult = (terminal, input) => ({
|
|
1040
|
+
...terminal,
|
|
1041
|
+
gradeExecuted: input.gradeExecuted,
|
|
1042
|
+
trustedPreparationResults: input.trustedPreparationResults,
|
|
1043
|
+
candidateValidationResults: input.candidateValidationResults,
|
|
1044
|
+
...(input.preparation === undefined ? {} : { preparation: input.preparation }),
|
|
1045
|
+
...(input.isolationTimings === undefined ? {} : { isolationTimings: input.isolationTimings })
|
|
1046
|
+
});
|
|
1047
|
+
const acquireCheckout = (root, commit, trustedPnpmStore, template, preparingTemplate) => Effect.gen(function* () {
|
|
1048
|
+
const readOnlyStore = yield* RepositoryReadOnlyPnpmStore;
|
|
1049
|
+
return yield* Effect.acquireRelease(Effect.tryPromise({
|
|
1050
|
+
try: () => materializeIsolatedRepository({
|
|
1051
|
+
repositoryRoot: root,
|
|
1052
|
+
commit,
|
|
1053
|
+
...(trustedPnpmStore === undefined
|
|
1054
|
+
? {}
|
|
1055
|
+
: { trustedPnpmStore, trustedPnpmStoreReadOnly: readOnlyStore !== undefined }),
|
|
1056
|
+
...(template === undefined ? {} : { template }),
|
|
1057
|
+
...(preparingTemplate === undefined ? {} : { preparingTemplate })
|
|
1058
|
+
}),
|
|
1059
|
+
catch: (cause) => failure("prepare-checkout", "could not materialize isolated single-commit repository", cause)
|
|
1060
|
+
}), (isolated) => Effect.tryPromise({
|
|
1061
|
+
try: async () => {
|
|
1062
|
+
const startedAt = Date.now();
|
|
1063
|
+
try {
|
|
1064
|
+
await rm(isolated.root, { recursive: true, force: true });
|
|
1065
|
+
}
|
|
1066
|
+
finally {
|
|
1067
|
+
isolated.timings.cleanupDurationMs = Date.now() - startedAt;
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
catch: () => undefined
|
|
1071
|
+
}).pipe(Effect.ignore));
|
|
1072
|
+
});
|
|
1073
|
+
const resolveTrustedPnpmStore = Effect.fn("RepositoryFoundry.resolveTrustedPnpmStore")(function* (recipes) {
|
|
1074
|
+
const specifiers = [
|
|
1075
|
+
...new Set(recipes.flatMap((recipe) => isPnpmInstallRecipe(recipe)
|
|
1076
|
+
? [recipe.args[0]]
|
|
1077
|
+
: []))
|
|
1078
|
+
];
|
|
1079
|
+
if (specifiers.length === 0)
|
|
1080
|
+
return undefined;
|
|
1081
|
+
if (specifiers.length !== 1) {
|
|
1082
|
+
return yield* failure("prepare-checkout", "trusted preparation must use one exact pnpm version");
|
|
1083
|
+
}
|
|
1084
|
+
const readOnlyStore = yield* RepositoryReadOnlyPnpmStore;
|
|
1085
|
+
if (readOnlyStore !== undefined) {
|
|
1086
|
+
if (!isAbsolute(readOnlyStore) ||
|
|
1087
|
+
!recipes.every((recipe) => isPnpmInstallRecipe(recipe) &&
|
|
1088
|
+
recipe.args.includes("--offline"))) {
|
|
1089
|
+
return yield* failure("prepare-checkout", "a host-provided read-only pnpm store requires an absolute path and offline installation");
|
|
1090
|
+
}
|
|
1091
|
+
return yield* Effect.tryPromise({
|
|
1092
|
+
try: async () => {
|
|
1093
|
+
const path = await realpath(readOnlyStore);
|
|
1094
|
+
if (!(await lstat(path)).isDirectory()) {
|
|
1095
|
+
throw new Error("the prehydrated pnpm store is not a directory");
|
|
1096
|
+
}
|
|
1097
|
+
return path;
|
|
1098
|
+
},
|
|
1099
|
+
catch: (cause) => failure("prepare-checkout", "the host-provided read-only pnpm store is unavailable", cause)
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
const path = yield* Effect.tryPromise({
|
|
1103
|
+
try: async () => {
|
|
1104
|
+
await mkdir(trustedPnpmStoreRoot, { recursive: true, mode: 0o700 });
|
|
1105
|
+
const result = await execFilePromise("corepack", [specifiers[0], "store", "path", "--silent"], {
|
|
1106
|
+
encoding: "utf8",
|
|
1107
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
1108
|
+
windowsHide: true,
|
|
1109
|
+
env: {
|
|
1110
|
+
PATH: commandPath,
|
|
1111
|
+
HOME: process.env.HOME,
|
|
1112
|
+
TMPDIR: process.env.TMPDIR,
|
|
1113
|
+
...(corepackHome === undefined ? {} : { COREPACK_HOME: corepackHome }),
|
|
1114
|
+
// pnpm derives its default store from the operating-system account
|
|
1115
|
+
// home, not HOME. Immutable sandbox images deliberately make that
|
|
1116
|
+
// account home read-only, so bind the trusted content-addressed
|
|
1117
|
+
// store to a private writable runtime directory explicitly.
|
|
1118
|
+
npm_config_store_dir: trustedPnpmStoreRoot,
|
|
1119
|
+
pnpm_config_store_dir: trustedPnpmStoreRoot
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
const requested = result.stdout.trim();
|
|
1123
|
+
if (requested.length === 0 ||
|
|
1124
|
+
!isAbsolute(requested) ||
|
|
1125
|
+
!pathWithin(trustedPnpmStoreRoot, requested)) {
|
|
1126
|
+
throw new Error("pnpm returned a store path outside the trusted runtime root");
|
|
1127
|
+
}
|
|
1128
|
+
await mkdir(requested, { recursive: true });
|
|
1129
|
+
return realpath(requested);
|
|
1130
|
+
},
|
|
1131
|
+
catch: (cause) => failure("prepare-checkout", "could not resolve the trusted pnpm content-addressed store", cause)
|
|
1132
|
+
});
|
|
1133
|
+
return path;
|
|
1134
|
+
});
|
|
1135
|
+
const reusableFrozenInstall = (recipes) => {
|
|
1136
|
+
return recipes.length === 1 && reusableFrozenInstallRecipe(recipes[0]);
|
|
1137
|
+
};
|
|
1138
|
+
/** Normalize generated links once, and inventory pnpm files needing clone-local prefixes. */
|
|
1139
|
+
const prepareTemplateRelocation = async (isolated) => {
|
|
1140
|
+
const relocatedFiles = [];
|
|
1141
|
+
const visit = async (directory) => {
|
|
1142
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
1143
|
+
const path = join(directory, entry.name);
|
|
1144
|
+
if (entry.isSymbolicLink()) {
|
|
1145
|
+
const target = await readlink(path);
|
|
1146
|
+
const resolved = resolve(directory, target);
|
|
1147
|
+
if (pathWithin(isolated.checkout, resolved)) {
|
|
1148
|
+
if (isAbsolute(target)) {
|
|
1149
|
+
await rm(path);
|
|
1150
|
+
await symlink(relative(directory, resolved), path);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
else if (isolated.trustedPnpmStore === undefined ||
|
|
1154
|
+
!pathWithin(isolated.trustedPnpmStore, resolved)) {
|
|
1155
|
+
throw new Error("pristine installation contains a symlink outside its repository and trusted store");
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
else if (entry.isDirectory()) {
|
|
1159
|
+
if (path !== join(isolated.checkout, ".git"))
|
|
1160
|
+
await visit(path);
|
|
1161
|
+
}
|
|
1162
|
+
else if (entry.isFile() &&
|
|
1163
|
+
(directory.endsWith(`${sep}node_modules${sep}.bin`) ||
|
|
1164
|
+
entry.name === ".modules.yaml" ||
|
|
1165
|
+
entry.name === ".pnpm-workspace-state-v1.json")) {
|
|
1166
|
+
if ((await readFile(path, "utf8")).includes(isolated.checkout)) {
|
|
1167
|
+
relocatedFiles.push(relative(isolated.checkout, path));
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
await visit(isolated.checkout);
|
|
1173
|
+
return relocatedFiles;
|
|
1174
|
+
};
|
|
1175
|
+
/** Installation reuse is bounded to these immutable commits and this owning Effect scope. */
|
|
1176
|
+
export const makeRepositoryPreparationCacheV1 = Effect.fn("RepositoryFoundry.makePreparationCache")(function* (input) {
|
|
1177
|
+
const repositoryRoot = yield* resolveRepositoryRoot(input.repositoryRoot);
|
|
1178
|
+
const recipes = input.trustedPreparationRecipes;
|
|
1179
|
+
const commits = new Set(input.commits);
|
|
1180
|
+
const eligible =
|
|
1181
|
+
// Both supported Unix runners materialize ordinary or copy-on-write file
|
|
1182
|
+
// copies, never hard links. Every repetition receives a private checkout
|
|
1183
|
+
// before candidate overlays are applied, while candidate commands are
|
|
1184
|
+
// forced onto the selected pnpm store in offline/frozen-store mode.
|
|
1185
|
+
(process.platform === "darwin" || process.platform === "linux") &&
|
|
1186
|
+
commits.size <= 2 &&
|
|
1187
|
+
[...commits].every((commit) => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(commit)) &&
|
|
1188
|
+
reusableFrozenInstall(recipes);
|
|
1189
|
+
const recipeIdentity = JSON.stringify(recipes);
|
|
1190
|
+
const cache = yield* ScopedCache.makeWith({
|
|
1191
|
+
capacity: Number.POSITIVE_INFINITY, // At most the two explicitly pinned historical commits.
|
|
1192
|
+
timeToLive: (exit) => (Exit.isSuccess(exit) ? "Infinity" : 0),
|
|
1193
|
+
lookup: Effect.fnUntraced(function* (commit) {
|
|
1194
|
+
const trustedPnpmStore = yield* resolveTrustedPnpmStore(recipes);
|
|
1195
|
+
const key = createHash("sha256")
|
|
1196
|
+
.update(JSON.stringify({
|
|
1197
|
+
version: 1,
|
|
1198
|
+
repositoryRoot,
|
|
1199
|
+
commit,
|
|
1200
|
+
recipes,
|
|
1201
|
+
node: process.version,
|
|
1202
|
+
executable: process.execPath,
|
|
1203
|
+
platform: process.platform,
|
|
1204
|
+
arch: process.arch,
|
|
1205
|
+
commandPath,
|
|
1206
|
+
corepackHome,
|
|
1207
|
+
trustedPnpmStore
|
|
1208
|
+
}))
|
|
1209
|
+
.digest("hex");
|
|
1210
|
+
const isolated = yield* acquireCheckout(repositoryRoot, commit, trustedPnpmStore, undefined, true);
|
|
1211
|
+
const preparationResults = yield* runRecipeSequence({
|
|
1212
|
+
isolated,
|
|
1213
|
+
commit,
|
|
1214
|
+
repetition: 0,
|
|
1215
|
+
stage: "trusted-preparation",
|
|
1216
|
+
recipes
|
|
1217
|
+
});
|
|
1218
|
+
if (preparationResults.some((result) => !commandPassed(result))) {
|
|
1219
|
+
return yield* failure("prepare-checkout", "pristine frozen installation failed; no reusable template was published", { templateKey: key, commit, trustedPreparationResults: preparationResults });
|
|
1220
|
+
}
|
|
1221
|
+
const relocatedFiles = yield* Effect.tryPromise({
|
|
1222
|
+
try: () => prepareTemplateRelocation(isolated),
|
|
1223
|
+
catch: (cause) => failure("prepare-checkout", "pristine installation cannot be safely relocated", cause)
|
|
1224
|
+
});
|
|
1225
|
+
return {
|
|
1226
|
+
isolated,
|
|
1227
|
+
key,
|
|
1228
|
+
preparationResults,
|
|
1229
|
+
relocatedFiles
|
|
1230
|
+
};
|
|
1231
|
+
})
|
|
1232
|
+
});
|
|
1233
|
+
const preparationCache = {
|
|
1234
|
+
get: (requested) => eligible &&
|
|
1235
|
+
requested.repositoryRoot === repositoryRoot &&
|
|
1236
|
+
commits.has(requested.commit) &&
|
|
1237
|
+
JSON.stringify(requested.recipes) === recipeIdentity
|
|
1238
|
+
? ScopedCache.get(cache, requested.commit)
|
|
1239
|
+
: Effect.succeed(undefined)
|
|
1240
|
+
};
|
|
1241
|
+
return preparationCache;
|
|
1242
|
+
});
|
|
1243
|
+
const prepareCheckout = Effect.fn("RepositoryFoundry.prepareCommandCheckout")(function* (input) {
|
|
1244
|
+
if (input.patch !== undefined) {
|
|
1245
|
+
yield* applyCandidatePatch(input.checkout, input.patch);
|
|
1246
|
+
}
|
|
1247
|
+
yield* applyFileOverrides({
|
|
1248
|
+
checkout: input.checkout,
|
|
1249
|
+
overrides: input.fileOverrides
|
|
1250
|
+
});
|
|
1251
|
+
yield* applyOverlays({
|
|
1252
|
+
repositoryRoot: input.repositoryRoot,
|
|
1253
|
+
checkout: input.checkout,
|
|
1254
|
+
overlays: input.overlays
|
|
1255
|
+
});
|
|
1256
|
+
yield* applyContentOverlays({
|
|
1257
|
+
checkout: input.checkout,
|
|
1258
|
+
overlays: input.contentOverlays
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
const resolveRepositoryRoot = (repositoryRoot) => Effect.tryPromise({
|
|
1262
|
+
try: () => realpath(repositoryRoot),
|
|
1263
|
+
catch: (cause) => failure("resolve-repository", `failed to resolve ${JSON.stringify(repositoryRoot)}`, cause)
|
|
1264
|
+
});
|
|
1265
|
+
export const replayRepositoryCommandsV1 = Effect.fn("RepositoryFoundry.replayRepositoryCommands")(function* (input) {
|
|
1266
|
+
const reconstruction = yield* Effect.serviceOption(RepositoryFoundryEvidenceReconstruction);
|
|
1267
|
+
if (Option.isSome(reconstruction))
|
|
1268
|
+
return yield* reconstruction.value.commands(input);
|
|
1269
|
+
if (input.recipes.length === 0 ||
|
|
1270
|
+
new Set(input.recipes.map((recipe) => recipe.id)).size !== input.recipes.length) {
|
|
1271
|
+
return yield* failure("run-command", "repository replay requires one or more uniquely identified grade recipes");
|
|
1272
|
+
}
|
|
1273
|
+
const root = yield* resolveRepositoryRoot(input.repositoryRoot);
|
|
1274
|
+
const template = input.preparationCache === undefined
|
|
1275
|
+
? undefined
|
|
1276
|
+
: yield* input.preparationCache.get({
|
|
1277
|
+
repositoryRoot: root,
|
|
1278
|
+
commit: input.commit,
|
|
1279
|
+
recipes: input.trustedPreparationRecipes ?? []
|
|
1280
|
+
});
|
|
1281
|
+
const trustedPnpmStore = template === undefined
|
|
1282
|
+
? yield* resolveTrustedPnpmStore(input.trustedPreparationRecipes ?? [])
|
|
1283
|
+
: template.isolated.trustedPnpmStore;
|
|
1284
|
+
const repetitions = Math.max(1, Math.min(input.repetitions ?? 2, 5));
|
|
1285
|
+
const repetitionStart = Math.max(1, Math.min(Math.trunc(input.repetitionStart ?? 1), 5));
|
|
1286
|
+
const results = [];
|
|
1287
|
+
for (let repetition = repetitionStart; repetition < repetitionStart + repetitions; repetition += 1) {
|
|
1288
|
+
results.push(...(yield* Effect.scoped(Effect.gen(function* () {
|
|
1289
|
+
const isolated = yield* acquireCheckout(root, input.commit, trustedPnpmStore, template);
|
|
1290
|
+
const checkout = isolated.checkout;
|
|
1291
|
+
const preparation = template === undefined
|
|
1292
|
+
? undefined
|
|
1293
|
+
: {
|
|
1294
|
+
kind: "pristine-install-template",
|
|
1295
|
+
templateKey: template.key,
|
|
1296
|
+
sourceCommit: input.commit,
|
|
1297
|
+
cloneDurationMs: isolated.materializationDurationMs,
|
|
1298
|
+
sharedResults: template.preparationResults
|
|
1299
|
+
};
|
|
1300
|
+
const trustedPreparationResults = template === undefined
|
|
1301
|
+
? yield* runRecipeSequence({
|
|
1302
|
+
isolated,
|
|
1303
|
+
commit: input.commit,
|
|
1304
|
+
repetition,
|
|
1305
|
+
stage: "trusted-preparation",
|
|
1306
|
+
recipes: input.trustedPreparationRecipes ?? []
|
|
1307
|
+
})
|
|
1308
|
+
: [];
|
|
1309
|
+
const failedPreparation = trustedPreparationResults.find((result) => !commandPassed(result));
|
|
1310
|
+
if (failedPreparation !== undefined) {
|
|
1311
|
+
return [
|
|
1312
|
+
terminalResult(failedPreparation, {
|
|
1313
|
+
gradeExecuted: false,
|
|
1314
|
+
trustedPreparationResults,
|
|
1315
|
+
candidateValidationResults: [],
|
|
1316
|
+
preparation,
|
|
1317
|
+
isolationTimings: isolated.timings
|
|
1318
|
+
})
|
|
1319
|
+
];
|
|
1320
|
+
}
|
|
1321
|
+
const overlayStartedAt = Date.now();
|
|
1322
|
+
yield* prepareCheckout({
|
|
1323
|
+
repositoryRoot: root,
|
|
1324
|
+
checkout,
|
|
1325
|
+
...(input.patch === undefined ? {} : { patch: input.patch }),
|
|
1326
|
+
overlays: input.overlays ?? [],
|
|
1327
|
+
contentOverlays: input.contentOverlays ?? [],
|
|
1328
|
+
fileOverrides: input.fileOverrides ?? []
|
|
1329
|
+
});
|
|
1330
|
+
isolated.timings.overlayDurationMs = Date.now() - overlayStartedAt;
|
|
1331
|
+
const candidateValidationResults = yield* runRecipeSequence({
|
|
1332
|
+
isolated,
|
|
1333
|
+
commit: input.commit,
|
|
1334
|
+
repetition,
|
|
1335
|
+
stage: "candidate-validation",
|
|
1336
|
+
recipes: input.candidateValidationRecipes ?? []
|
|
1337
|
+
});
|
|
1338
|
+
const failedValidation = candidateValidationResults.find((result) => !commandPassed(result));
|
|
1339
|
+
if (failedValidation !== undefined) {
|
|
1340
|
+
return [
|
|
1341
|
+
terminalResult(failedValidation, {
|
|
1342
|
+
gradeExecuted: false,
|
|
1343
|
+
trustedPreparationResults,
|
|
1344
|
+
candidateValidationResults,
|
|
1345
|
+
preparation,
|
|
1346
|
+
isolationTimings: isolated.timings
|
|
1347
|
+
})
|
|
1348
|
+
];
|
|
1349
|
+
}
|
|
1350
|
+
const grades = [];
|
|
1351
|
+
for (const recipe of input.recipes) {
|
|
1352
|
+
const grade = yield* runCommand({
|
|
1353
|
+
isolated,
|
|
1354
|
+
commit: input.commit,
|
|
1355
|
+
repetition,
|
|
1356
|
+
stage: "grade",
|
|
1357
|
+
recipe
|
|
1358
|
+
});
|
|
1359
|
+
grades.push(terminalResult(grade, {
|
|
1360
|
+
gradeExecuted: true,
|
|
1361
|
+
trustedPreparationResults,
|
|
1362
|
+
candidateValidationResults,
|
|
1363
|
+
preparation,
|
|
1364
|
+
isolationTimings: isolated.timings
|
|
1365
|
+
}));
|
|
1366
|
+
// A killed process or incomplete capture cannot yield a grade.
|
|
1367
|
+
// Retain it, release this checkout, and leave queued work unstarted.
|
|
1368
|
+
if (repositoryCommandExecutionIncompleteV1(grade) &&
|
|
1369
|
+
input.continueAfterIncompleteGrade !== true)
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
return grades;
|
|
1373
|
+
}))));
|
|
1374
|
+
}
|
|
1375
|
+
return results;
|
|
1376
|
+
});
|
|
1377
|
+
export const replayRepositoryCommandV1 = Effect.fn("RepositoryFoundry.replayRepositoryCommand")(function* (input) {
|
|
1378
|
+
return yield* replayRepositoryCommandsV1({
|
|
1379
|
+
repositoryRoot: input.repositoryRoot,
|
|
1380
|
+
commit: input.commit,
|
|
1381
|
+
recipes: [input.recipe],
|
|
1382
|
+
...(input.repetitions === undefined ? {} : { repetitions: input.repetitions }),
|
|
1383
|
+
...(input.overlays === undefined ? {} : { overlays: input.overlays }),
|
|
1384
|
+
...(input.contentOverlays === undefined ? {} : { contentOverlays: input.contentOverlays }),
|
|
1385
|
+
...(input.fileOverrides === undefined ? {} : { fileOverrides: input.fileOverrides }),
|
|
1386
|
+
...(input.patch === undefined ? {} : { patch: input.patch }),
|
|
1387
|
+
...(input.trustedPreparationRecipes === undefined
|
|
1388
|
+
? {}
|
|
1389
|
+
: {
|
|
1390
|
+
trustedPreparationRecipes: input.trustedPreparationRecipes
|
|
1391
|
+
}),
|
|
1392
|
+
...(input.candidateValidationRecipes === undefined
|
|
1393
|
+
? {}
|
|
1394
|
+
: {
|
|
1395
|
+
candidateValidationRecipes: input.candidateValidationRecipes
|
|
1396
|
+
})
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
const inspectPath = async (checkout, repositoryPath) => {
|
|
1400
|
+
if (repositoryPath.trim().length === 0 ||
|
|
1401
|
+
isAbsolute(repositoryPath) ||
|
|
1402
|
+
repositoryPath.includes("\\") ||
|
|
1403
|
+
repositoryPath.includes("\0") ||
|
|
1404
|
+
repositoryPath.split("/").some((part) => part.length === 0 || part === "." || part === "..")) {
|
|
1405
|
+
throw new Error(`inspection path is not a canonical repository-relative path: ${JSON.stringify(repositoryPath)}`);
|
|
1406
|
+
}
|
|
1407
|
+
const destination = resolve(checkout, repositoryPath);
|
|
1408
|
+
if (!pathWithin(checkout, destination)) {
|
|
1409
|
+
throw new Error(`inspection path escapes checkout: ${JSON.stringify(repositoryPath)}`);
|
|
1410
|
+
}
|
|
1411
|
+
let current = checkout;
|
|
1412
|
+
const parts = repositoryPath.split("/");
|
|
1413
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
1414
|
+
current = join(current, parts[index]);
|
|
1415
|
+
let metadata;
|
|
1416
|
+
try {
|
|
1417
|
+
metadata = await lstat(current);
|
|
1418
|
+
}
|
|
1419
|
+
catch (cause) {
|
|
1420
|
+
if (cause !== null &&
|
|
1421
|
+
typeof cause === "object" &&
|
|
1422
|
+
"code" in cause &&
|
|
1423
|
+
cause.code === "ENOENT") {
|
|
1424
|
+
return "missing";
|
|
1425
|
+
}
|
|
1426
|
+
throw cause;
|
|
1427
|
+
}
|
|
1428
|
+
const final = index === parts.length - 1;
|
|
1429
|
+
if (metadata.isSymbolicLink()) {
|
|
1430
|
+
if (final)
|
|
1431
|
+
return "symlink";
|
|
1432
|
+
throw new Error(`inspection path traverses a symbolic link: ${JSON.stringify(repositoryPath)}`);
|
|
1433
|
+
}
|
|
1434
|
+
if (!final && !metadata.isDirectory())
|
|
1435
|
+
return "missing";
|
|
1436
|
+
if (final) {
|
|
1437
|
+
if (metadata.isFile())
|
|
1438
|
+
return "file";
|
|
1439
|
+
if (metadata.isDirectory())
|
|
1440
|
+
return "directory";
|
|
1441
|
+
return "missing";
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return "missing";
|
|
1445
|
+
};
|
|
1446
|
+
export const inspectRepositorySolutionPathsV1 = Effect.fn("RepositoryFoundry.inspectRepositorySolutionPaths")(function* (input) {
|
|
1447
|
+
if (input.paths.length === 0) {
|
|
1448
|
+
return yield* failure("prepare-checkout", "repository path inspection requires at least one path");
|
|
1449
|
+
}
|
|
1450
|
+
const paths = [...new Set(input.paths)];
|
|
1451
|
+
const root = yield* resolveRepositoryRoot(input.repositoryRoot);
|
|
1452
|
+
const repetitions = Math.max(1, Math.min(input.repetitions ?? 2, 5));
|
|
1453
|
+
const results = [];
|
|
1454
|
+
for (let repetition = 1; repetition <= repetitions; repetition += 1) {
|
|
1455
|
+
results.push(...(yield* Effect.scoped(Effect.gen(function* () {
|
|
1456
|
+
const isolated = yield* acquireCheckout(root, input.commit);
|
|
1457
|
+
const checkout = isolated.checkout;
|
|
1458
|
+
yield* prepareCheckout({
|
|
1459
|
+
repositoryRoot: root,
|
|
1460
|
+
checkout,
|
|
1461
|
+
...(input.patch === undefined ? {} : { patch: input.patch }),
|
|
1462
|
+
overlays: [],
|
|
1463
|
+
contentOverlays: [],
|
|
1464
|
+
fileOverrides: input.fileOverrides ?? []
|
|
1465
|
+
});
|
|
1466
|
+
const inspected = [];
|
|
1467
|
+
for (const repositoryPath of paths) {
|
|
1468
|
+
const kind = yield* Effect.tryPromise({
|
|
1469
|
+
try: () => inspectPath(checkout, repositoryPath),
|
|
1470
|
+
catch: (cause) => failure("prepare-checkout", cause instanceof Error ? cause.message : String(cause), cause)
|
|
1471
|
+
});
|
|
1472
|
+
inspected.push({
|
|
1473
|
+
commit: input.commit,
|
|
1474
|
+
repetition,
|
|
1475
|
+
path: repositoryPath,
|
|
1476
|
+
kind
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
return inspected;
|
|
1480
|
+
}))));
|
|
1481
|
+
}
|
|
1482
|
+
return results;
|
|
1483
|
+
});
|