@deksden-com/dd-flow-cli 0.9.0-beta.15 → 0.9.0-beta.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +907 -0
- package/README.md +139 -10
- package/dist/build-info.json +8 -8
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +386 -0
- package/dist/cli/command-metadata.js +232 -0
- package/dist/cli/command-option-descriptions.js +192 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +396 -19
- package/dist/cli/hook-ingress.js +102 -0
- package/dist/cli/input-preparation.js +165 -0
- package/dist/cli/memory-flow-admission.js +50 -0
- package/dist/cli/memory-init-invocation.js +85 -0
- package/dist/cli/memory-init.js +249 -0
- package/dist/cli/memory-storage-admission.js +54 -0
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +90 -0
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +72 -0
- package/dist/cli/run-cli.js +1734 -550
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +52 -0
- package/dist/cli/runtime-process.js +153 -0
- package/dist/cli/setup.js +150 -0
- package/dist/cli.js +89 -2
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/domain/stage-handler-identities.js +28 -0
- package/dist/harness-runtime/DD-ZCODE.md +112 -0
- package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +79 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +67 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +462 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +1092 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +86 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +231 -0
- package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +954 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +626 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +555 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +180 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +627 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +390 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +752 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +1200 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +14 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +166 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +140 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +37 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +273 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +16 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +698 -0
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/model-observations.mjs +113 -0
- package/dist/harness-runtime/lib/native-children.d.mts +34 -0
- package/dist/harness-runtime/lib/native-children.mjs +265 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +95 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +85 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.d.mts +16 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +127 -0
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +114 -0
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +201 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +23 -0
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +242 -0
- package/dist/harness-runtime/lib/runner-events.mjs +202 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +53 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
- package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
- package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-decision.schema.json +9 -0
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-verification.schema.json +7 -5
- package/dist/schemas/code-work-batch.schema.json +2 -2
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +211 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +29 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +56 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/plan-review-decision.schema.json +12 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-control-receipt.schema.json +118 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/accepted-intent.js +139 -0
- package/dist/services/branch-context.js +3 -3
- package/dist/services/canon.js +54 -15
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +277 -0
- package/dist/services/check-runner-proof.js +305 -0
- package/dist/services/cleanup.js +133 -40
- package/dist/services/cli-operation-classifier.js +26 -63
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +1401 -119
- package/dist/services/codex-hook-delivery.js +32 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/compatibility-preflight.js +16 -3
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-child-capacity.js +71 -0
- package/dist/services/controller-child-observer.js +121 -0
- package/dist/services/controller-execution.js +94 -0
- package/dist/services/controller-fanout.js +226 -0
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +143 -58
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +107 -0
- package/dist/services/descriptor-run-start.js +475 -0
- package/dist/services/descriptor-stage-coordinator.js +250 -0
- package/dist/services/descriptor-stage-settlement.js +223 -0
- package/dist/services/descriptor-stage-start.js +175 -0
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +354 -163
- package/dist/services/eval-snapshots.js +1316 -150
- package/dist/services/execution-bundles.js +275 -0
- package/dist/services/execution-policy.js +185 -0
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +310 -0
- package/dist/services/flow-catalog.js +30 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +387 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +355 -36
- package/dist/services/harness-config.js +2 -0
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +791 -326
- package/dist/services/hybrid-contracts.js +474 -0
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +226 -0
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +1 -288
- package/dist/services/lifecycle-contract.js +1 -0
- package/dist/services/lifecycle-invocations.js +2148 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-lease-monitor.js +133 -0
- package/dist/services/managed-processes.js +449 -69
- package/dist/services/memory-accepted-composition.js +78 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +328 -0
- package/dist/services/memory-compose-handler.js +400 -0
- package/dist/services/memory-composition-acceptance.js +375 -0
- package/dist/services/memory-composition-graph.js +165 -0
- package/dist/services/memory-composition-registration.js +161 -0
- package/dist/services/memory-composition-work-custody.js +156 -0
- package/dist/services/memory-composition-work-results.js +108 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +37 -0
- package/dist/services/memory-corrected-review-findings.js +108 -0
- package/dist/services/memory-corrected-review-inputs.js +215 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +178 -0
- package/dist/services/memory-corrected-review-registration.js +192 -0
- package/dist/services/memory-correction-check-receipt.js +235 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +457 -0
- package/dist/services/memory-finish-handler.js +229 -0
- package/dist/services/memory-finish-integration.js +450 -0
- package/dist/services/memory-finish-readiness.js +169 -0
- package/dist/services/memory-git-integration.js +220 -0
- package/dist/services/memory-historical-corrected-review.js +212 -0
- package/dist/services/memory-historical-original-review.js +143 -0
- package/dist/services/memory-historical-repair-registration.js +105 -0
- package/dist/services/memory-historical-second-review.js +147 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +131 -0
- package/dist/services/memory-init-controller-start.js +103 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +165 -0
- package/dist/services/memory-init-lint-capability.js +203 -0
- package/dist/services/memory-init-native-capacity.js +309 -0
- package/dist/services/memory-init-preflight.js +334 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +213 -0
- package/dist/services/memory-init-request-input.js +82 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +135 -0
- package/dist/services/memory-init-start.js +181 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +91 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +174 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +221 -0
- package/dist/services/memory-lint-config.js +194 -0
- package/dist/services/memory-native-review-copy.js +103 -0
- package/dist/services/memory-normal-flow-entry.js +87 -0
- package/dist/services/memory-ordinary-defaults.js +129 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +274 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +488 -0
- package/dist/services/memory-repair-history.js +496 -0
- package/dist/services/memory-repair-physical-binding.js +194 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +162 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +795 -0
- package/dist/services/memory-repair-registration.js +240 -0
- package/dist/services/memory-repair-work-baseline.js +415 -0
- package/dist/services/memory-repair-work-completion.js +117 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +108 -0
- package/dist/services/memory-repair-workspace.js +261 -0
- package/dist/services/memory-repair-writer-copy.js +736 -0
- package/dist/services/memory-result-binding.js +239 -0
- package/dist/services/memory-retained-review-authority.js +138 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +162 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +206 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +246 -0
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +117 -0
- package/dist/services/memory-review-work-custody.js +126 -0
- package/dist/services/memory-review-work-input.js +91 -0
- package/dist/services/memory-review-work-results.js +95 -0
- package/dist/services/memory-second-corrected-review-inputs.js +167 -0
- package/dist/services/memory-second-correction-plan.js +77 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +65 -0
- package/dist/services/memory-stage-results.js +290 -0
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +156 -0
- package/dist/services/memory-verification-checks.js +317 -0
- package/dist/services/memory-verification-receipt.js +160 -0
- package/dist/services/memory-verify-handler.js +137 -0
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +64 -0
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +163 -35
- package/dist/services/migrations.js +164 -114
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/native-daemon-history.js +50 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +17 -87
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/prompts.js +28 -17
- package/dist/services/protocols.js +163 -87
- package/dist/services/provider-capacity.js +463 -0
- package/dist/services/recovery-observation-budget.js +121 -0
- package/dist/services/recovery-snapshot-database.js +588 -0
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +150 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +31 -0
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/review-copy.js +165 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +428 -0
- package/dist/services/run-control.js +1092 -0
- package/dist/services/run-controller-adapter.js +400 -0
- package/dist/services/run-controller-capture.js +186 -0
- package/dist/services/run-controller-process.js +205 -0
- package/dist/services/run-controller-recovery.js +295 -0
- package/dist/services/run-controller-state.js +97 -0
- package/dist/services/run-controller.js +1395 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +147 -0
- package/dist/services/run-health.js +149 -0
- package/dist/services/run-observations.js +152 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-guard.js +107 -0
- package/dist/services/run-recovery-runtime.js +112 -0
- package/dist/services/run-recovery.js +456 -0
- package/dist/services/run-start-custody.js +189 -0
- package/dist/services/runs.js +1023 -246
- package/dist/services/runtime-budget.js +446 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +468 -0
- package/dist/services/runtime-scope-resume.js +605 -0
- package/dist/services/runtime-scope-stop.js +211 -0
- package/dist/services/runtime-scope-worker.js +314 -0
- package/dist/services/runtime-service.js +19 -14
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +27 -0
- package/dist/services/sessions.js +36 -91
- package/dist/services/setup.js +427 -0
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +240 -18
- package/dist/services/stage-handlers.js +89 -0
- package/dist/services/stage-lifecycle.js +152 -116
- package/dist/services/stage-pause.js +202 -91
- package/dist/services/stage-report-renderer.js +234 -6
- package/dist/services/stage-work-graph.js +52 -0
- package/dist/services/tooling.js +41 -30
- package/dist/services/usage.js +49 -101
- package/dist/services/verification-matrix.js +393 -0
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +235 -88
- package/dist/services/vnext-code.js +327 -226
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +80 -5
- package/dist/services/vnext-fanout.js +355 -21
- package/dist/services/vnext-merge.js +332 -111
- package/dist/services/vnext-plan-review.js +287 -132
- package/dist/services/vnext-plan.js +228 -104
- package/dist/services/vnext-protocolize.js +533 -95
- package/dist/services/vnext-specify.js +123 -81
- package/dist/services/vnext-workspace-policy.js +7 -2
- package/dist/services/work-registry.js +1706 -200
- package/dist/services/workspace-bootstrap.js +53 -15
- package/dist/services/workspace-files.js +85 -0
- package/dist/services/worktrees.js +105 -48
- package/dist/shared/entity-references.js +1 -0
- package/dist/shared/errors.js +27 -0
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +930 -40
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +159 -0
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +96 -0
- package/dist/storage/writer-migration.js +346 -0
- package/dist/storage/writer-restore.js +217 -0
- package/dist/verification-components.json +299 -0
- package/package.json +51 -13
- package/tools/create-canon-release.mjs +33 -0
- package/tools/probe-codex-history.mjs +37 -0
- package/tools/produce-hybrid-release.mjs +41 -0
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -1,25 +1,97 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerEvidenceDetailed, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
3
|
+
export { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
|
|
4
|
+
import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
|
|
2
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
3
7
|
import fs from "node:fs";
|
|
4
8
|
import path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
9
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10
|
+
import { parse } from "shell-quote";
|
|
11
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
12
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
13
|
+
import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
|
|
14
|
+
import { createRenewalState, maintenanceRetryable, renewalConfirmed, renewalDelay, renewalFailure, renewalRemaining, RENEWAL_POLICY } from "../harness-runtime/lib/lease-renewal.mjs";
|
|
15
|
+
import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, managedProcessRuntimeHome, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
|
|
16
|
+
import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
|
|
17
|
+
import { getResourceDatabase } from "../storage/database.js";
|
|
18
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
19
|
+
import { ownedCheckEnvironment, validateCheckEnvironment } from "./check-environment.js";
|
|
20
|
+
import { browserCheckCapabilityEnvironment } from "./stage-context.js";
|
|
7
21
|
const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
|
|
8
22
|
// Receipt reservation precedes child spawn so an interrupted CLI leaves an
|
|
9
23
|
// auditable attempt. A second caller must not mistake that small handoff window
|
|
10
24
|
// for a dead check and overwrite its outcome.
|
|
11
25
|
const receiptStartingGraceMs = 30_000;
|
|
12
|
-
|
|
26
|
+
/** The check writer proves an empty tree before marking its registration terminal. */
|
|
27
|
+
export function terminalCheckSettled(context, record) {
|
|
28
|
+
if (record.kind !== "check" || !record.check_id || record.owner_id !== `check:${record.check_id}` || !record.project_id || !record.run_id
|
|
29
|
+
|| !record.pid || !record.pid_started_at || !["stopped", "failed"].includes(record.state) || !record.finished_at)
|
|
30
|
+
return null;
|
|
31
|
+
let metadata;
|
|
32
|
+
try {
|
|
33
|
+
metadata = JSON.parse(record.metadata_json);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (metadata.role !== "check" || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
|
|
39
|
+
return null;
|
|
40
|
+
let resource;
|
|
41
|
+
try {
|
|
42
|
+
resource = getResourceDatabase(context.env.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome, "read_existing");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const current = resource.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
|
|
49
|
+
if (!current || current.lease_token !== record.lease_token || current.owner_id !== record.owner_id || current.pid !== record.pid
|
|
50
|
+
|| current.pid_started_at !== record.pid_started_at || current.metadata_json !== record.metadata_json || current.state !== record.state
|
|
51
|
+
|| current.project_id !== record.project_id || current.run_id !== record.run_id || current.work_id !== record.work_id
|
|
52
|
+
|| current.check_id !== record.check_id || current.finished_at !== record.finished_at)
|
|
53
|
+
return null;
|
|
54
|
+
const receipt = context.db.get("SELECT status, finished_at, work_id FROM check_receipts WHERE id = ? AND project_id = ? AND run_id = ?", [record.check_id, record.project_id, record.run_id]);
|
|
55
|
+
return receipt && receipt.work_id === record.work_id && ["passed", "failed", "aborted"].includes(receipt.status) && receipt.finished_at
|
|
56
|
+
? { status: receipt.status, finished_at: receipt.finished_at } : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
resource.close?.();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
|
|
66
|
+
const selected = validateCheckEnvironment(checkEnvironment);
|
|
67
|
+
const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
|
|
68
|
+
// The managed CLI's Node must win over a provider app's PATH: a Rosetta
|
|
69
|
+
// provider can otherwise run x64 checks against arm64-installed modules.
|
|
70
|
+
const environment = { ...process.env, PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`, DD_FLOW_LOCAL_DATABASE_SUFFIX: suffix };
|
|
71
|
+
delete environment.NODE_PATH;
|
|
72
|
+
delete environment.DD_FLOW_BROWSER_CAPABILITY_PATH;
|
|
73
|
+
delete environment.DD_FLOW_BROWSER_CAPABILITY_SHA256;
|
|
74
|
+
for (const key of ["DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH", "DD_FLOW_CHECK_RUNNER_PROGRESS_PATH", "DD_FLOW_CHECK_INVOCATION_ID", "DD_FLOW_CHECK_ID", "DD_FLOW_CHECK_INPUT_HASH", "DD_FLOW_CHECK_VERIFICATION_EPOCH", "DD_FLOW_CHECK_COMMAND"])
|
|
75
|
+
delete environment[key];
|
|
76
|
+
Object.assign(environment, selected);
|
|
77
|
+
return environment;
|
|
78
|
+
}
|
|
79
|
+
export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
|
|
13
80
|
export function readCodeCheckProfile(workspaceRoot) {
|
|
14
81
|
const file = path.join(workspaceRoot, profileRelativePath);
|
|
15
82
|
if (!fs.existsSync(file))
|
|
16
83
|
return { profile: null, hash: null, file };
|
|
17
84
|
const bytes = fs.readFileSync(file);
|
|
18
85
|
const value = JSON.parse(bytes.toString("utf8"));
|
|
19
|
-
if (
|
|
20
|
-
throw new AppError("invalid_code_check_profile", "CODE check profile has an unsupported schema", 2, { file, schema_id: value.schema_id ?? null,
|
|
21
|
-
if (!
|
|
22
|
-
throw new AppError("invalid_code_check_profile", "
|
|
86
|
+
if (!["dd-flow/code-check-profile@6", "dd-flow/code-check-profile@7"].includes(value.schema_id ?? ""))
|
|
87
|
+
throw new AppError("invalid_code_check_profile", "CODE check profile has an unsupported schema", 2, { file, schema_id: value.schema_id ?? null, supported: ["dd-flow/code-check-profile@6", "dd-flow/code-check-profile@7"] });
|
|
88
|
+
if (!runnerContractsValid(value.runner_evidence_by_alias) || value.schema_id === "dd-flow/code-check-profile@6" && value.runner_evidence_by_alias !== undefined)
|
|
89
|
+
throw new AppError("invalid_code_check_profile", "Runner evidence requires valid profile@7 relative artifact contracts", 2, { file });
|
|
90
|
+
for (const alias of Object.keys(value.runner_evidence_by_alias ?? {}))
|
|
91
|
+
if (!value.aliases?.[alias])
|
|
92
|
+
throw new AppError("invalid_code_check_profile", "Runner evidence references an unknown alias", 2, { file, alias });
|
|
93
|
+
if (!isStringRecord(value.aliases) || !isStringRecord(value.require_alias_for) || !isGateRecord(value.mandatory_by_gate) || !isPortAliasRecord(value.ports_by_alias) || !isInputAliasRecord(workspaceRoot, value.inputs_by_alias))
|
|
94
|
+
throw new AppError("invalid_code_check_profile", "CODE check profile aliases, require_alias_for, mandatory_by_gate, ports_by_alias or inputs_by_alias are invalid", 2, { file });
|
|
23
95
|
for (const [gate, aliases] of Object.entries(value.mandatory_by_gate))
|
|
24
96
|
for (const alias of aliases ?? [])
|
|
25
97
|
if (!value.aliases[alias])
|
|
@@ -27,19 +99,76 @@ export function readCodeCheckProfile(workspaceRoot) {
|
|
|
27
99
|
for (const alias of Object.keys(value.ports_by_alias ?? {}))
|
|
28
100
|
if (!value.aliases[alias])
|
|
29
101
|
throw new AppError("invalid_code_check_profile", "A port declaration references an unknown alias", 2, { file, alias });
|
|
102
|
+
for (const alias of Object.keys(value.inputs_by_alias ?? {}))
|
|
103
|
+
if (!value.aliases[alias])
|
|
104
|
+
throw new AppError("invalid_code_check_profile", "An input declaration references an unknown alias", 2, { file, alias });
|
|
30
105
|
return { profile: value, hash: crypto.createHash("sha256").update(bytes).digest("hex"), file };
|
|
31
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Planned aliases are qualification-owned extensions: their provider Work is
|
|
109
|
+
* expected to materialize them after PLAN. Freeze the project policy and
|
|
110
|
+
* existing aliases, while still validating every planned alias definition at
|
|
111
|
+
* execution time. Unknown edits and changes to existing policy remain part
|
|
112
|
+
* of the hash and therefore fail closed.
|
|
113
|
+
*/
|
|
114
|
+
export function codeCheckProfileHash(workspaceRoot, checks = []) {
|
|
115
|
+
const { profile } = readCodeCheckProfile(workspaceRoot);
|
|
116
|
+
if (!profile)
|
|
117
|
+
return null;
|
|
118
|
+
const planned = new Set(checks.filter(check => check.availability === "planned" && check.command.startsWith("@check/")).map(check => check.command));
|
|
119
|
+
const serialize = (value) => profile.schema_id === "dd-flow/code-check-profile@7"
|
|
120
|
+
? JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([left], [right]) => left.localeCompare(right))) : item)
|
|
121
|
+
: JSON.stringify(value);
|
|
122
|
+
if (!planned.size)
|
|
123
|
+
return crypto.createHash("sha256").update(serialize(profile)).digest("hex");
|
|
124
|
+
const canonical = {
|
|
125
|
+
...profile,
|
|
126
|
+
aliases: Object.fromEntries(Object.entries(profile.aliases).filter(([alias]) => !planned.has(alias))),
|
|
127
|
+
ports_by_alias: Object.fromEntries(Object.entries(profile.ports_by_alias ?? {}).filter(([alias]) => !planned.has(alias))),
|
|
128
|
+
inputs_by_alias: Object.fromEntries(Object.entries(profile.inputs_by_alias ?? {}).filter(([alias]) => !planned.has(alias))),
|
|
129
|
+
...(profile.schema_id === "dd-flow/code-check-profile@7" ? { runner_evidence_by_alias: Object.fromEntries(Object.entries(profile.runner_evidence_by_alias ?? {}).filter(([alias]) => !planned.has(alias))) } : {})
|
|
130
|
+
};
|
|
131
|
+
return crypto.createHash("sha256").update(serialize(canonical)).digest("hex");
|
|
132
|
+
}
|
|
133
|
+
/** The integration target starts at its baseline profile; the reviewed source
|
|
134
|
+
* may add aliases, but must not change existing check policy or bindings. */
|
|
135
|
+
export function mergeCheckProfileHash(sourceWorkspace, targetWorkspace, checks) {
|
|
136
|
+
const source = readCodeCheckProfile(sourceWorkspace).profile;
|
|
137
|
+
const target = readCodeCheckProfile(targetWorkspace).profile;
|
|
138
|
+
if (!source && !target)
|
|
139
|
+
return null;
|
|
140
|
+
if (!source || !target)
|
|
141
|
+
throw new AppError("check_profile_drift", "Source and integration check profiles differ", 1);
|
|
142
|
+
const retained = structuredClone(source);
|
|
143
|
+
for (const field of ["aliases", "ports_by_alias", "inputs_by_alias", "runner_evidence_by_alias"]) {
|
|
144
|
+
const old = target[field];
|
|
145
|
+
if (!old) {
|
|
146
|
+
delete retained[field];
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const current = source[field] ?? {};
|
|
150
|
+
retained[field] = Object.fromEntries(Object.keys(old).filter(key => key in current).map(key => [key, current[key]]));
|
|
151
|
+
}
|
|
152
|
+
const existingAliases = new Set(Object.keys(target.aliases));
|
|
153
|
+
const changedExistingMetadata = ["ports_by_alias", "inputs_by_alias", "runner_evidence_by_alias"].some(field => Object.keys(source[field] ?? {}).some(alias => existingAliases.has(alias) && !(alias in (target[field] ?? {}))));
|
|
154
|
+
if (changedExistingMetadata || !isDeepStrictEqual(retained, target)) {
|
|
155
|
+
throw new AppError("check_profile_drift", "Source check profile changes existing integration policy", 1, { source_profile_hash: codeCheckProfileHash(sourceWorkspace, checks), target_profile_hash: codeCheckProfileHash(targetWorkspace, checks) });
|
|
156
|
+
}
|
|
157
|
+
return codeCheckProfileHash(sourceWorkspace, checks);
|
|
158
|
+
}
|
|
32
159
|
export function validateCodeCheckCommands(workspaceRoot, commands) {
|
|
33
160
|
const { profile, file } = readCodeCheckProfile(workspaceRoot);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
161
|
+
return commands.map((command) => {
|
|
162
|
+
if (command.startsWith("@check/")) {
|
|
163
|
+
if (!profile?.aliases[command]?.trim())
|
|
164
|
+
throw new AppError("unknown_code_check_alias", "CODE check alias is not defined by the project profile", 2, { alias: command, file });
|
|
165
|
+
return command;
|
|
166
|
+
}
|
|
167
|
+
for (const [prefix, alias] of Object.entries(profile?.require_alias_for ?? {}))
|
|
168
|
+
if (command === prefix || command.startsWith(`${prefix} `))
|
|
169
|
+
throw new AppError("raw_code_check_forbidden", "CODE check must use the project alias instead of a raw guarded command", 2, { command, required_alias: alias, file });
|
|
39
170
|
return command;
|
|
40
|
-
}
|
|
41
|
-
if (command === prefix || command.startsWith(`${prefix} `))
|
|
42
|
-
throw new AppError("raw_code_check_forbidden", "CODE check must use the project alias instead of a raw guarded command", 2, { command, required_alias: alias, file }); return command; });
|
|
171
|
+
});
|
|
43
172
|
}
|
|
44
173
|
export function resolveCodeCheckCommands(workspaceRoot, runId, commands) {
|
|
45
174
|
const validated = validateCodeCheckCommands(workspaceRoot, commands);
|
|
@@ -50,7 +179,11 @@ export function resolveCodeCheckCommands(workspaceRoot, runId, commands) {
|
|
|
50
179
|
}
|
|
51
180
|
export function projectPolicyCheckDeclarations(workspaceRoot, gate) {
|
|
52
181
|
const { profile } = readCodeCheckProfile(workspaceRoot);
|
|
53
|
-
return (profile
|
|
182
|
+
return projectPolicyCheckDeclarationsFromProfile(profile, gate);
|
|
183
|
+
}
|
|
184
|
+
/** The caller may already have independently admitted exact profile bytes. */
|
|
185
|
+
export function projectPolicyCheckDeclarationsFromProfile(profile, gate) {
|
|
186
|
+
return (profile?.mandatory_by_gate[gate] ?? []).map((alias) => { const ref = `POLICY/${gate}/${alias.slice(7).replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase()}`; return { id: ref, canonical_ref: ref, source: "project_policy", command: alias, purpose: `Mandatory project ${gate} gate.`, run_at: gate, availability: "available", ...(profile?.ports_by_alias?.[alias] ? { ports: profile.ports_by_alias[alias] } : {}), ...(profile?.inputs_by_alias?.[alias] ? { inputs: profile.inputs_by_alias[alias] } : {}) }; });
|
|
54
187
|
}
|
|
55
188
|
export function effectiveCheckDeclarations(workspaceRoot, declared, gates) { return gates.flatMap((gate) => [...declared.filter((check) => check.run_at === gate), ...projectPolicyCheckDeclarations(workspaceRoot, gate)]); }
|
|
56
189
|
export function aggregateCheckDeclarations(workspaceRoot) { return projectPolicyCheckDeclarations(workspaceRoot, "code"); }
|
|
@@ -60,12 +193,21 @@ export function validateCheckPlacement(workspaceRoot, checks) { const { profile
|
|
|
60
193
|
if (check.run_at === "work" && aggregateCommands.has(check.command))
|
|
61
194
|
throw new AppError("aggregate_check_requires_code_gate", "A project aggregate check must run at CODE or readiness, not inside one scoped Work", 2, { check_id: check.id, command: check.command, run_at: check.run_at }); }
|
|
62
195
|
export function validateCheckDeclaration(workspaceRoot, check) {
|
|
63
|
-
if (check.
|
|
64
|
-
|
|
196
|
+
if ((check.required_artifacts ?? []).some(file => path.posix.normalize(file.replaceAll("\\", "/")).split("/")[0] === "engine-outcome.json"))
|
|
197
|
+
throw new AppError("invalid_code_check_profile", "engine-outcome.json is reserved for engine execution evidence, not a candidate artifact", 2, { check_id: check.id });
|
|
198
|
+
if (!["work", "code", "readiness", "merge", "release"].includes(check.run_at))
|
|
199
|
+
throw new AppError("unsupported_check_gate", "Check requires an autonomous executable gate; replan the incompatible declaration", 2, { check_id: check.id, run_at: check.run_at });
|
|
200
|
+
if (check.runner_evidence !== undefined && !runnerContractsValid({ "@check/declaration": check.runner_evidence }))
|
|
201
|
+
throw new AppError("invalid_code_check_profile", "Check runner evidence contract is invalid", 2, { check_id: check.id });
|
|
65
202
|
const { profile } = readCodeCheckProfile(workspaceRoot);
|
|
203
|
+
const resolvedCommand = profile?.aliases[check.command] ?? check.command;
|
|
204
|
+
validateTerminatingCheckCommand(resolvedCommand, check.id);
|
|
66
205
|
if (check.availability === "planned" && check.command.startsWith("@check/") && !profile?.aliases[check.command])
|
|
67
206
|
throw new AppError("planned_check_not_materialized", "A planned check alias was not materialized by its provider Work", 2, { check_id: check.id, command: check.command, provider: check.provided_by ?? null });
|
|
68
207
|
validateCodeCheckCommands(workspaceRoot, [check.command]);
|
|
208
|
+
if (check.availability === "available" && !check.command.startsWith("@check/"))
|
|
209
|
+
assertDirectCheckExecutable(workspaceRoot, check.command, check.id);
|
|
210
|
+
validateCheckInputs(workspaceRoot, check.inputs ?? []);
|
|
69
211
|
if (check.command.startsWith("@check/") && check.definition && profile?.aliases[check.command] !== check.definition)
|
|
70
212
|
throw new AppError("check_definition_drift", "The accepted check alias definition differs from the current project profile", 2, { check_id: check.id, alias: check.command, expected: check.definition, actual: profile?.aliases[check.command] ?? null });
|
|
71
213
|
if (check.availability !== "planned")
|
|
@@ -77,36 +219,417 @@ export function validateCheckDeclaration(workspaceRoot, check) {
|
|
|
77
219
|
if (!profile?.aliases[check.command])
|
|
78
220
|
throw new AppError("planned_check_not_materialized", "A planned check alias was not materialized by its provider Work", 2, { check_id: check.id, command: check.command, provider: check.provided_by ?? null });
|
|
79
221
|
}
|
|
222
|
+
export function validateTerminatingCheckCommand(command, checkId) {
|
|
223
|
+
if (isBareServiceScript(command))
|
|
224
|
+
throw new AppError("non_terminating_check", "A check must terminate; run a finite test instead of a bare dev/start/serve/watch service script", 2, { check_id: checkId, command });
|
|
225
|
+
}
|
|
226
|
+
function isBareServiceScript(command) {
|
|
227
|
+
const words = parse(command.trim());
|
|
228
|
+
if (words.some((word) => typeof word !== "string"))
|
|
229
|
+
return false;
|
|
230
|
+
let args = words;
|
|
231
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(args[0] ?? ""))
|
|
232
|
+
args = args.slice(1);
|
|
233
|
+
const manager = path.basename(args.shift() ?? "");
|
|
234
|
+
if (!["pnpm", "npm", "yarn", "bun"].includes(manager))
|
|
235
|
+
return false;
|
|
236
|
+
while (args.length && ["--filter", "-F", "--dir", "-C", "--cwd"].includes(args[0]))
|
|
237
|
+
args.splice(0, 2);
|
|
238
|
+
if (args[0] === "run")
|
|
239
|
+
args.shift();
|
|
240
|
+
return ["dev", "start", "serve", "watch"].includes(args[0] ?? "");
|
|
241
|
+
}
|
|
242
|
+
/** Reject a plan-time typo or a fictitious "manual" command before it becomes
|
|
243
|
+
* a late shell exit. Compound shell programs remain valid: only a simple,
|
|
244
|
+
* directly named executable is preflighted here. */
|
|
245
|
+
function assertDirectCheckExecutable(workspaceRoot, command, checkId) {
|
|
246
|
+
const trimmed = command.trim();
|
|
247
|
+
// A shell operator or expansion makes the launch path dynamic. Do not
|
|
248
|
+
// guess there; this preflight is deliberately limited to a direct program
|
|
249
|
+
// with ordinary arguments and optional literal environment assignments.
|
|
250
|
+
if (/[|&;<>()`$\n\r~]/.test(trimmed))
|
|
251
|
+
return;
|
|
252
|
+
const parsed = parse(trimmed);
|
|
253
|
+
if (parsed.some(word => typeof word !== "string"))
|
|
254
|
+
return;
|
|
255
|
+
const words = parsed;
|
|
256
|
+
let index = 0;
|
|
257
|
+
const env = codeExecutionEnvironment(workspaceRoot);
|
|
258
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[index] ?? "")) {
|
|
259
|
+
const assignment = words[index++];
|
|
260
|
+
const equals = assignment.indexOf("=");
|
|
261
|
+
env[assignment.slice(0, equals)] = assignment.slice(equals + 1);
|
|
262
|
+
}
|
|
263
|
+
const program = words[index];
|
|
264
|
+
if (!program)
|
|
265
|
+
return;
|
|
266
|
+
const probe = spawnSync("/bin/sh", ["-c", "command -v -- \"$1\" >/dev/null", "dd-flow-check", program], { cwd: workspaceRoot, env, encoding: "utf8", timeout: 5000 });
|
|
267
|
+
if (probe.error || probe.signal)
|
|
268
|
+
throw new AppError("check_command_probe_unavailable", "Executable probe could not be observed; availability is unknown", 2, { check_id: checkId, executable: program, cause: probe.error?.message, signal: probe.signal });
|
|
269
|
+
if (probe.status !== 0)
|
|
270
|
+
throw new AppError("check_command_unavailable", "Available CODE check names an executable that is unavailable in this environment", 2, { check_id: checkId, command, executable: program });
|
|
271
|
+
}
|
|
272
|
+
// An owning handler supplies this identity; it is a reuse boundary, not an
|
|
273
|
+
// authority proof. Omitting it preserves the historical SDLC input hash.
|
|
274
|
+
function validateCheckEvidenceContext(value) {
|
|
275
|
+
if (value !== undefined && (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,255}$/.test(value)))
|
|
276
|
+
throw new AppError("check_evidence_context_invalid", "Check evidence context must be a bounded opaque identifier", 2, { effect: "no_effect" });
|
|
277
|
+
}
|
|
278
|
+
// Additive NULL columns must not invalidate accepted legacy SQL digests.
|
|
279
|
+
const scopedReceiptColumns = new Set(["fingerprint_scope", "input_manifest_json", "execution_failures_json", "execution_contract_hash"]);
|
|
280
|
+
const evidenceDigest = (value) => crypto.createHash("sha256").update(JSON.stringify(value, (key, item) => scopedReceiptColumns.has(key) && item === null ? undefined : item)).digest("hex");
|
|
281
|
+
/** Prepares current physical evidence outside a writer and retains an exact
|
|
282
|
+
* SQL-only fence. Declarations must come from the owning handler/policy, never
|
|
283
|
+
* from the result whose acceptance is being decided. This does not supply
|
|
284
|
+
* semantic acceptance or choose a memory verification policy.
|
|
285
|
+
*/
|
|
286
|
+
export class PreparedCheckEvidence {
|
|
287
|
+
#input;
|
|
288
|
+
#snapshot;
|
|
289
|
+
#generation;
|
|
290
|
+
#references;
|
|
291
|
+
#artifacts;
|
|
292
|
+
#completions = [];
|
|
293
|
+
#passed;
|
|
294
|
+
constructor(input, snapshot, generation, references, artifacts, passed) {
|
|
295
|
+
this.#input = structuredClone({ projectId: input.projectId, runId: input.runId, runHome: input.runHome, workspaceRoot: input.workspaceRoot, scope: input.scope, checks: input.checks,
|
|
296
|
+
...(input.mode === undefined ? {} : { mode: input.mode }), ...(input.workId === undefined ? {} : { workId: input.workId }), ...(input.expectedProfileHash === undefined ? {} : { expectedProfileHash: input.expectedProfileHash }), ...(input.evidenceContext === undefined ? {} : { evidenceContext: input.evidenceContext }), ...(input.checkEnvironment === undefined ? {} : { checkEnvironment: input.checkEnvironment }) });
|
|
297
|
+
this.#snapshot = snapshot;
|
|
298
|
+
this.#generation = generation;
|
|
299
|
+
this.#references = references;
|
|
300
|
+
this.#artifacts = artifacts;
|
|
301
|
+
this.#passed = passed;
|
|
302
|
+
Object.freeze(this);
|
|
303
|
+
}
|
|
304
|
+
static prepare(context, input) {
|
|
305
|
+
const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
|
|
306
|
+
if (checkEnvironment)
|
|
307
|
+
input = { ...input, checkEnvironment };
|
|
308
|
+
validateCheckEvidenceContext(input.evidenceContext);
|
|
309
|
+
const fail = () => { throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, { effect: "no_effect" }); };
|
|
310
|
+
if (!input.checks.length || (input.scope === "work") !== Boolean(input.workId) || input.mode !== undefined && input.mode !== "diagnostic")
|
|
311
|
+
fail();
|
|
312
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
313
|
+
const snapshot = checkEvidenceSqlSnapshot(context, input);
|
|
314
|
+
const selected = JSON.parse(snapshot);
|
|
315
|
+
if (!selected.run || fs.realpathSync.native(selected.run.workspace_root) !== fs.realpathSync.native(input.workspaceRoot)
|
|
316
|
+
|| fs.realpathSync.native(selected.run.run_root ?? selected.run.run_dir) !== fs.realpathSync.native(input.runHome))
|
|
317
|
+
fail();
|
|
318
|
+
const rows = selected.receipts;
|
|
319
|
+
const root = fs.realpathSync.native(input.runHome);
|
|
320
|
+
for (const row of rows) {
|
|
321
|
+
const relative = path.relative(path.resolve(input.runHome), row.receipt_path);
|
|
322
|
+
if (!path.isAbsolute(row.receipt_path) || relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative))
|
|
323
|
+
fail();
|
|
324
|
+
if (fs.existsSync(row.receipt_path)) {
|
|
325
|
+
const physical = path.relative(root, fs.realpathSync.native(row.receipt_path));
|
|
326
|
+
if (physical.startsWith(`..${path.sep}`) || physical === ".." || path.isAbsolute(physical))
|
|
327
|
+
fail();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const check of input.checks)
|
|
331
|
+
validateCheckDeclaration(input.workspaceRoot, check);
|
|
332
|
+
const { profile } = readCodeCheckProfile(input.workspaceRoot), profileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
|
|
333
|
+
if (input.expectedProfileHash !== undefined && profileHash !== input.expectedProfileHash)
|
|
334
|
+
fail();
|
|
335
|
+
const checks = input.checks.map(check => {
|
|
336
|
+
const runner = profile?.runner_evidence_by_alias?.[check.command] ?? check.runner_evidence;
|
|
337
|
+
return { ...check, ports: check.ports ?? profile?.ports_by_alias?.[check.command] ?? [], inputs: check.inputs ?? profile?.inputs_by_alias?.[check.command] ?? [],
|
|
338
|
+
...(runner ? { runner_evidence: runner, required_artifacts: [...new Set([...(check.required_artifacts ?? []), runner.path])] } : {}) };
|
|
339
|
+
});
|
|
340
|
+
const commands = resolveCodeCheckCommands(input.workspaceRoot, input.runId, checks.map(check => check.command));
|
|
341
|
+
assertConsistentDeclarations(checks, commands);
|
|
342
|
+
const state = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
343
|
+
const receipts = rows.map(projectCheckReceiptRow);
|
|
344
|
+
const artifacts = [], completions = [];
|
|
345
|
+
let passed = true;
|
|
346
|
+
const references = deduplicate(checks, commands).map(group => {
|
|
347
|
+
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, workspaceInputFingerprint(input.workspaceRoot, state.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment);
|
|
348
|
+
const receipt = receipts.filter(item => item.input_hash === inputHash && item.profile_hash === profileHash && item.scope === input.scope && item.work_id === (input.workId ?? null)).at(-1);
|
|
349
|
+
if (!receipt)
|
|
350
|
+
throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, {
|
|
351
|
+
effect: "no_effect", reason: "input_identity_mismatch", expected_input_hash: inputHash,
|
|
352
|
+
candidates: receipts.filter(item => item.scope === input.scope && item.work_id === (input.workId ?? null))
|
|
353
|
+
.slice(-16).map(item => ({ id: item.id, input_hash: item.input_hash, profile_hash: item.profile_hash }))
|
|
354
|
+
});
|
|
355
|
+
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
|
|
356
|
+
fail();
|
|
357
|
+
passed = passed && receipt.status === "passed";
|
|
358
|
+
for (const artifact of receipt.artifacts)
|
|
359
|
+
artifacts.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), "artifacts", artifact.path)).split(path.sep).join("/"), sha256: artifact.sha256 });
|
|
360
|
+
const bytes = fs.readFileSync(receipt.receipt_path);
|
|
361
|
+
completions.push({ root: "run", path: path.relative(input.runHome, receipt.completion_path).split(path.sep).join("/"), sha256: crypto.createHash("sha256").update(fs.readFileSync(receipt.completion_path)).digest("hex") });
|
|
362
|
+
if (receipt.input_manifest)
|
|
363
|
+
completions.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path)).split(path.sep).join("/"), sha256: receipt.input_manifest.sha256 });
|
|
364
|
+
return { id: receipt.id, path: receipt.receipt_path, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
|
|
365
|
+
});
|
|
366
|
+
const prepared = new PreparedCheckEvidence(input, snapshot, generation, references, artifacts, passed);
|
|
367
|
+
prepared.#completions = completions;
|
|
368
|
+
prepared.assertCustody(context);
|
|
369
|
+
return prepared;
|
|
370
|
+
}
|
|
371
|
+
references() { return structuredClone(this.#references); }
|
|
372
|
+
artifactReferences() { return structuredClone(this.#artifacts); }
|
|
373
|
+
sourceReferences() { return [...this.artifactReferences(), ...structuredClone(this.#completions), ...this.resultReferences().map(reference => ({ root: "run", ...reference }))]; }
|
|
374
|
+
qualityPassed() { return this.#passed; }
|
|
375
|
+
resultReferences() {
|
|
376
|
+
return this.#references.map(reference => ({ path: path.relative(this.#input.runHome, reference.path).split(path.sep).join("/"), sha256: reference.sha256 }));
|
|
377
|
+
}
|
|
378
|
+
/** For an owning acceptance writer to retain independently of agent bytes.
|
|
379
|
+
* The row digest pins historical execution, not later workspace policy.
|
|
380
|
+
*/
|
|
381
|
+
retainedReferences(context) {
|
|
382
|
+
return context.db.readSnapshot(() => {
|
|
383
|
+
this.assertCustody(context);
|
|
384
|
+
return this.#references.map(reference => ({ id: reference.id, path: path.relative(this.#input.runHome, reference.path).split(path.sep).join("/"), sha256: reference.sha256,
|
|
385
|
+
sql_sha256: evidenceDigest(context.db.get("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? AND id=?", [this.#input.projectId, this.#input.runId, reference.id])) }));
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
assertOwner(scope) {
|
|
389
|
+
if (scope.projectId !== this.#input.projectId || scope.runId !== this.#input.runId || scope.scope !== undefined && scope.scope !== this.#input.scope)
|
|
390
|
+
throw new AppError("check_receipt_changed", "Prepared checks belong to another RUN or scope", 2, { effect: "no_effect" });
|
|
391
|
+
}
|
|
392
|
+
assertCustody(context) {
|
|
393
|
+
assertRunMutationAllowed(context, this.#input.projectId, this.#input.runId, this.#generation);
|
|
394
|
+
if (checkEvidenceSqlSnapshot(context, this.#input) !== this.#snapshot)
|
|
395
|
+
throw new AppError("check_receipt_changed", "Prepared check SQL custody changed", 2, { effect: "no_effect" });
|
|
396
|
+
}
|
|
397
|
+
revalidate(context) {
|
|
398
|
+
const current = PreparedCheckEvidence.prepare(context, this.#input);
|
|
399
|
+
if (!isDeepStrictEqual(current.sourceReferences(), this.sourceReferences()) || !isDeepStrictEqual(current.#references, this.#references) || current.#snapshot !== this.#snapshot)
|
|
400
|
+
throw new AppError("check_evidence_stale", "Prepared check evidence changed", 2, { effect: "no_effect" });
|
|
401
|
+
this.assertCustody(context);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/** Historical aggregate execution proof. Selectors must come from an owning
|
|
405
|
+
* committed acceptance receipt, not a proposed result. This reader does not
|
|
406
|
+
* select current declarations, inspect the current workspace or execute checks.
|
|
407
|
+
* Relocation of SQL path fields still requires explicit receipt projection.
|
|
408
|
+
*/
|
|
409
|
+
export class PreparedRetainedCheckEvidence {
|
|
410
|
+
#input;
|
|
411
|
+
#snapshot;
|
|
412
|
+
#generation;
|
|
413
|
+
constructor(input, snapshot, generation) {
|
|
414
|
+
this.#input = structuredClone(input);
|
|
415
|
+
this.#snapshot = snapshot;
|
|
416
|
+
this.#generation = generation;
|
|
417
|
+
Object.freeze(this);
|
|
418
|
+
}
|
|
419
|
+
static prepare(context, input) {
|
|
420
|
+
const fail = () => { throw new AppError("retained_check_evidence_invalid", "Accepted checks lack exact historical execution proof", 2, { effect: "no_effect" }); };
|
|
421
|
+
if (!Array.isArray(input.references) || !input.references.length || input.references.length > 4096
|
|
422
|
+
|| new Set(input.references.map(reference => reference?.id)).size !== input.references.length
|
|
423
|
+
|| input.references.some(reference => !reference || typeof reference.id !== "string" || !reference.id
|
|
424
|
+
|| typeof reference.path !== "string" || !/^[a-f0-9]{64}$/.test(reference.sha256) || !/^[a-f0-9]{64}$/.test(reference.sql_sha256)))
|
|
425
|
+
fail();
|
|
426
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId), snapshot = retainedCheckSqlSnapshot(context, input);
|
|
427
|
+
const selected = JSON.parse(snapshot);
|
|
428
|
+
if (!selected.run || selected.receipts.some(row => !row))
|
|
429
|
+
fail();
|
|
430
|
+
const runRoot = selected.run.run_root ?? selected.run.run_dir;
|
|
431
|
+
if (!path.isAbsolute(runRoot) || fs.realpathSync.native(runRoot) !== path.resolve(runRoot))
|
|
432
|
+
fail();
|
|
433
|
+
for (let index = 0; index < input.references.length; index++) {
|
|
434
|
+
const reference = input.references[index], row = selected.receipts[index];
|
|
435
|
+
if (row.scope !== "aggregate" || row.work_id !== null || evidenceDigest(row) !== reference.sql_sha256
|
|
436
|
+
|| row.receipt_path !== path.join(runRoot, reference.path))
|
|
437
|
+
fail();
|
|
438
|
+
// Admit contained regular receipt/completion files before any helper
|
|
439
|
+
// reads them; escaping paths and symlink parents are rejected.
|
|
440
|
+
const bytes = readRetainedCheckFile(runRoot, reference.path);
|
|
441
|
+
readRetainedCheckFile(runRoot, path.posix.join(path.posix.dirname(reference.path), "completion.json"));
|
|
442
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== reference.sha256 || !checkReceiptEvidenceValidForMode(projectCheckReceiptRow(row), input.mode))
|
|
443
|
+
fail();
|
|
444
|
+
}
|
|
445
|
+
const prepared = new PreparedRetainedCheckEvidence(input, snapshot, generation);
|
|
446
|
+
prepared.assertCustody(context);
|
|
447
|
+
return prepared;
|
|
448
|
+
}
|
|
449
|
+
assertCustody(context) {
|
|
450
|
+
assertRunMutationAllowed(context, this.#input.projectId, this.#input.runId, this.#generation);
|
|
451
|
+
if (retainedCheckSqlSnapshot(context, this.#input) !== this.#snapshot)
|
|
452
|
+
throw new AppError("retained_check_evidence_invalid", "Accepted check SQL custody changed", 2, { effect: "no_effect" });
|
|
453
|
+
}
|
|
454
|
+
revalidate(context) {
|
|
455
|
+
const current = PreparedRetainedCheckEvidence.prepare(context, this.#input);
|
|
456
|
+
if (current.#snapshot !== this.#snapshot)
|
|
457
|
+
throw new AppError("retained_check_evidence_invalid", "Accepted check proof changed", 2, { effect: "no_effect" });
|
|
458
|
+
this.assertCustody(context);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function readRetainedCheckFile(root, relative) {
|
|
462
|
+
const fail = () => { throw new AppError("retained_check_evidence_invalid", "Accepted check file is not a contained regular file", 2, { effect: "no_effect" }); };
|
|
463
|
+
if (!relative || path.posix.isAbsolute(relative) || path.win32.isAbsolute(relative) || relative.includes("\\") || relative.includes("\0")
|
|
464
|
+
|| relative.split("/").some(segment => !segment || segment === "." || segment === ".."))
|
|
465
|
+
fail();
|
|
466
|
+
try {
|
|
467
|
+
let parent = root;
|
|
468
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
469
|
+
fail();
|
|
470
|
+
for (const segment of relative.split("/").slice(0, -1)) {
|
|
471
|
+
parent = path.join(parent, segment);
|
|
472
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
473
|
+
fail();
|
|
474
|
+
}
|
|
475
|
+
return readBoundedReleaseFile(path.join(root, relative), 16 * 1024 * 1024);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return fail();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function retainedCheckSqlSnapshot(context, input) {
|
|
482
|
+
return context.db.readSnapshot(() => JSON.stringify({
|
|
483
|
+
run: context.db.get("SELECT project_id,id,workspace_root,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]) ?? null,
|
|
484
|
+
receipts: input.references.map(reference => context.db.get("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? AND id=?", [input.projectId, input.runId, reference.id]) ?? null)
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
function checkEvidenceSqlSnapshot(context, input) {
|
|
488
|
+
return context.db.readSnapshot(() => JSON.stringify({
|
|
489
|
+
run: context.db.get("SELECT project_id,id,workspace_root,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]),
|
|
490
|
+
receipts: context.db.all("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? ORDER BY reservation_sequence,rowid", [input.projectId, input.runId])
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
80
493
|
export async function runCodeChecks(context, input) {
|
|
81
|
-
|
|
494
|
+
const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
|
|
495
|
+
if (checkEnvironment)
|
|
496
|
+
input = { ...input, checkEnvironment };
|
|
497
|
+
validateCheckEvidenceContext(input.evidenceContext);
|
|
498
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
499
|
+
let gate;
|
|
500
|
+
for (;;) {
|
|
501
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
502
|
+
reconcileUnfinishedChecks(context, input);
|
|
503
|
+
reconcileWorkspaceOwnership(context, input.workspaceRoot);
|
|
504
|
+
try {
|
|
505
|
+
gate = acquireWorkspaceGate(context, input);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
const sameRunGate = managedProcessStatus(context).find(item => item.kind === "workspace-gate" && item.operation_id === workspaceOperationId(input.workspaceRoot) && ["starting", "running", "stopping"].includes(item.state) && item.project_id === input.projectId && item.run_id === input.runId);
|
|
510
|
+
if (input.scope !== "work" || !input.workId || !(error instanceof AppError) || !["check_in_progress", "managed_operation_in_progress"].includes(error.code) || !sameRunGate)
|
|
511
|
+
throw error;
|
|
512
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
let receipts = await runCodeChecksOwned(context, input, gate.id, generation);
|
|
517
|
+
// One bounded episode, not a product-repair turn. The second execution
|
|
518
|
+
// remains guarded by its own before/after snapshot and SQL reservation.
|
|
519
|
+
const stale = receipts.filter(staleCheckRetryEligible);
|
|
520
|
+
if (!input.retryReceiptId && !input.retryReceiptIds?.length && stale.length) {
|
|
521
|
+
const otherSession = context.db.get("SELECT 1 FROM work_sessions s JOIN works w ON w.work_id = s.work_id WHERE w.project_id = ? AND w.run_id = ? AND w.work_id != ? AND s.status = 'running' AND s.completed_at IS NULL LIMIT 1", [input.projectId, input.runId, input.workId ?? ""]);
|
|
522
|
+
if (!otherSession) {
|
|
523
|
+
for (const predecessor of stale) {
|
|
524
|
+
const selected = input.checks.filter(check => predecessor.check_refs.includes(checkRef(check)));
|
|
525
|
+
if (!selected.length || selected.some(check => check.reuse !== "deterministic"))
|
|
526
|
+
continue;
|
|
527
|
+
const retried = await runCodeChecksOwned(context, { ...input, retryReceiptId: predecessor.id, retryReason: "check_inputs_changed" }, gate.id, generation, new Set(predecessor.check_refs));
|
|
528
|
+
receipts = receipts.filter(receipt => receipt.id !== predecessor.id).concat(retried);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return receipts;
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
finishManagedProcess(context, { id: gate.id, leaseToken: gate.lease_token, state: "stopped", reason: "workspace_gate_settled" });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async function runCodeChecksOwned(context, input, workspaceGateId, generation, selectedRefs) {
|
|
539
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
82
540
|
for (const check of input.checks)
|
|
83
541
|
validateCheckDeclaration(input.workspaceRoot, check);
|
|
84
|
-
const {
|
|
85
|
-
const
|
|
542
|
+
const { profile } = readCodeCheckProfile(input.workspaceRoot);
|
|
543
|
+
const profileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
|
|
544
|
+
if (input.expectedProfileHash !== undefined && input.expectedProfileHash !== profileHash)
|
|
545
|
+
throw new AppError("check_profile_drift", "The frozen check profile differs from the workspace profile", 1, { expected_profile_hash: input.expectedProfileHash, actual_profile_hash: profileHash });
|
|
546
|
+
const checks = input.checks.map(check => {
|
|
547
|
+
const ports = profile?.ports_by_alias?.[check.command], inputs = profile?.inputs_by_alias?.[check.command];
|
|
548
|
+
const runner = profile?.runner_evidence_by_alias?.[check.command] ?? check.runner_evidence;
|
|
549
|
+
if (runner && !runnerContractsValid({ "@check/declaration": runner }))
|
|
550
|
+
throw new AppError("invalid_code_check_profile", "Check runner evidence contract is invalid", 2, { check_id: check.id });
|
|
551
|
+
return { ...check, ...(check.ports || !ports ? {} : { ports }), ...(check.inputs || !inputs ? {} : { inputs }), ...(runner ? { runner_evidence: runner, required_artifacts: [...new Set([...(check.required_artifacts ?? []), runner.path])] } : {}) };
|
|
552
|
+
});
|
|
86
553
|
const resolved = resolveCodeCheckCommands(input.workspaceRoot, input.runId, checks.map((check) => check.command));
|
|
87
|
-
|
|
88
|
-
|
|
554
|
+
assertConsistentDeclarations(checks, resolved);
|
|
555
|
+
// Retain all declarations for the planned-alias profile hash; filter only
|
|
556
|
+
// execution, otherwise a targeted retry changes its own frozen contract.
|
|
557
|
+
const groups = deduplicate(checks, resolved).filter(group => !selectedRefs || group.declarations.some(check => selectedRefs.has(checkRef(check)))), receipts = [];
|
|
558
|
+
const retryIds = [...new Set([...(input.retryReceiptId ? [input.retryReceiptId] : []), ...(input.retryReceiptIds ?? [])])];
|
|
559
|
+
const retries = new Map();
|
|
560
|
+
for (const receiptId of retryIds) {
|
|
561
|
+
const predecessor = validateFailedCheckRetry(context, { projectId: input.projectId, runId: input.runId, receiptId, declarations: checks, reason: input.retryReason });
|
|
562
|
+
const snapshot = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
563
|
+
const group = groups.find(candidate => candidate.declarations.some(check => check.run_at === predecessor.gate)
|
|
564
|
+
&& (checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) === predecessor.input_hash
|
|
565
|
+
|| sourceRepairContractMatches(predecessor, candidate, profileHash)
|
|
566
|
+
|| input.retryReason === "check_inputs_changed" && staleRetryContractMatches(predecessor, candidate, profileHash, input)));
|
|
567
|
+
if (!group || retries.has(group))
|
|
568
|
+
throw new AppError("retry_check_invalid", "Retry must match exactly one current execution contract", 2, { check_receipt_id: receiptId });
|
|
569
|
+
// Preserve the identity of a pre-deduplication failed execution when an
|
|
570
|
+
// upgraded RUN retries it; the new receipt still proves every grouped ref.
|
|
571
|
+
group.gate = predecessor.gate;
|
|
572
|
+
retries.set(group, receiptId);
|
|
573
|
+
}
|
|
89
574
|
for (let index = 0; index < groups.length; index += 1) {
|
|
90
575
|
const group = groups[index];
|
|
91
576
|
const before = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
92
577
|
const startedAt = context.now();
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
578
|
+
const inputFiles = selectedInputFiles(input.workspaceRoot, before.files, group.inputs, [input.runHome]);
|
|
579
|
+
const inputFingerprint = inputFilesFingerprint(inputFiles);
|
|
580
|
+
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint, input.evidenceContext, input.checkEnvironment);
|
|
581
|
+
const epoch = crypto.createHash("sha256").update(`${group.gate}\0${inputFingerprint}`).digest("hex");
|
|
582
|
+
const executionContractHash = checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment);
|
|
583
|
+
const reservationInput = { ...input, retryReceiptIds: retries.has(group) ? [retries.get(group)] : [], generation, group, before, inputFiles, inputFingerprint, executionContractHash, profileHash, inputHash, epoch, startedAt };
|
|
584
|
+
delete reservationInput.retryReceiptId;
|
|
585
|
+
const decision = reserveOrReuseReceipt(context, reservationInput);
|
|
586
|
+
if (decision.receipt) {
|
|
587
|
+
receipts.push(bindReceipt(decision.receipt, group));
|
|
588
|
+
input.progress?.(`check ${index + 1}/${groups.length} ${decision.receipt.status === "passed" ? "reused" : "requires repair"}: ${group.command}`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const reserved = decision.reserved;
|
|
96
592
|
const { id, evidenceDir, stdoutPath, stderrPath, receiptPath, completionPath } = reserved;
|
|
97
593
|
let resources = {};
|
|
98
594
|
let result = { exitCode: null, error: "check did not start", aborted: true };
|
|
99
595
|
try {
|
|
100
596
|
fs.mkdirSync(evidenceDir, { recursive: true });
|
|
101
|
-
|
|
102
|
-
const
|
|
597
|
+
retainInputManifest(context, reserved);
|
|
598
|
+
const startingReceipt = receiptFor({ ...reserved, resources, result, status: "running", after: before, mutationPaths: [], artifacts: [] });
|
|
599
|
+
if (group.runnerEvidence)
|
|
600
|
+
startingReceipt.runner_evidence_required = group.runnerEvidence;
|
|
601
|
+
writeReceipt(receiptPath, startingReceipt);
|
|
602
|
+
const budget = runtimeBudgetForRun(context, input.projectId, input.runId);
|
|
603
|
+
if (group.runnerEvidence) {
|
|
604
|
+
// Reject configuration before acquiring a check lease/ports, so there
|
|
605
|
+
// is no unconfirmed managed child to strand on a validation error.
|
|
606
|
+
runnerObservationDurations(process.env);
|
|
607
|
+
}
|
|
608
|
+
const managed = registerManagedProcess(context, { kind: "check", ownerPid: process.pid, ownerId: `check:${id}`, operationId: workspaceOperationId(input.workspaceRoot), projectId: input.projectId, runId: input.runId, workId: input.workId ?? null, checkId: id, stdoutPath, stderrPath, metadata: { command: group.command, role: "check", workspace_root: input.workspaceRoot, workspace_gate_id: workspaceGateId, budget } });
|
|
103
609
|
const allocation = await reservePorts(context, { ownerId: managed.owner_id, processId: managed.id, names: group.ports });
|
|
104
610
|
resources = allocation.ports;
|
|
105
|
-
|
|
611
|
+
try {
|
|
612
|
+
input.progress?.(`check ${index + 1}/${groups.length} preparing: ${group.command}`);
|
|
613
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
allocation.release();
|
|
617
|
+
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: "failed", reason: "dispatch_fenced_before_spawn" });
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
106
620
|
const stdout = fs.openSync(stdoutPath, "a");
|
|
107
621
|
const stderr = fs.openSync(stderrPath, "a");
|
|
622
|
+
let confirmedExpiry = null;
|
|
108
623
|
try {
|
|
109
|
-
|
|
624
|
+
const runnerEnvironment = group.runnerEvidence ? { DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH: path.join(evidenceDir, group.runnerEvidence.path), DD_FLOW_CHECK_RUNNER_PROGRESS_PATH: path.join(evidenceDir, "runner-progress.jsonl"), DD_FLOW_CHECK_INVOCATION_ID: id, DD_FLOW_CHECK_ID: id, DD_FLOW_CHECK_INPUT_HASH: inputHash, DD_FLOW_CHECK_VERIFICATION_EPOCH: epoch } : {};
|
|
625
|
+
if (group.runnerEvidence)
|
|
626
|
+
fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
|
|
627
|
+
result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), ...browserCheckCapabilityEnvironment(context, input.runId, input.runHome), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
|
|
628
|
+
confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
|
|
629
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
630
|
+
if (budget)
|
|
631
|
+
admitRuntimeCheck(context, managed.id, managed.lease_token);
|
|
632
|
+
}, attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: managed.id, leaseToken: managed.lease_token }), { leaseExpiresAt: () => confirmedExpiry });
|
|
110
633
|
}
|
|
111
634
|
finally {
|
|
112
635
|
fs.closeSync(stdout);
|
|
@@ -116,7 +639,8 @@ export async function runCodeChecks(context, input) {
|
|
|
116
639
|
if (processTreeIsAlive(current))
|
|
117
640
|
throw new AppError("check_in_progress", "The check shell exited but its process tree is still live; retain this receipt and its resources until settlement", 2, { process_id: managed.id, check_receipt_id: id });
|
|
118
641
|
allocation.release();
|
|
119
|
-
|
|
642
|
+
if (!result.cleanupUnconfirmed)
|
|
643
|
+
finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
|
|
120
644
|
}
|
|
121
645
|
catch (error) {
|
|
122
646
|
if (error instanceof AppError && error.code === "check_in_progress")
|
|
@@ -124,23 +648,63 @@ export async function runCodeChecks(context, input) {
|
|
|
124
648
|
result = { exitCode: null, error: error instanceof Error ? error.message : String(error), aborted: true };
|
|
125
649
|
}
|
|
126
650
|
const after = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
127
|
-
const
|
|
128
|
-
const
|
|
651
|
+
const afterFiles = selectedInputFiles(input.workspaceRoot, after.files, group.inputs, [input.runHome]);
|
|
652
|
+
const mutationPaths = changedPaths(reserved.inputFiles, afterFiles);
|
|
653
|
+
let artifacts = collectCheckArtifacts(evidenceDir, group.requiredArtifacts);
|
|
129
654
|
if (fs.existsSync(stderrPath)) {
|
|
130
655
|
if (!artifacts.complete)
|
|
131
656
|
fs.appendFileSync(stderrPath, `\nmissing required evidence artifacts: ${artifacts.missing.join(", ")}\n`);
|
|
132
657
|
if (result.error)
|
|
133
658
|
fs.appendFileSync(stderrPath, `\n${result.error}\n`);
|
|
134
659
|
if (mutationPaths.length)
|
|
135
|
-
fs.appendFileSync(stderrPath, `\ncheck
|
|
660
|
+
fs.appendFileSync(stderrPath, `\ncheck inputs changed while running: ${mutationPaths.join(", ")}\n`);
|
|
661
|
+
}
|
|
662
|
+
const runnerRead = group.runnerEvidence ? readRunnerEvidenceDetailed(evidenceDir, group.runnerEvidence, { invocation_id: id, check_id: id, command: group.command, input_hash: inputHash, verification_epoch: epoch }) : undefined;
|
|
663
|
+
const runnerEvidence = runnerRead?.evidence;
|
|
664
|
+
const physicalFailures = executionFailures({ fingerprintScope: "check_inputs", exitCode: result.exitCode, launcherError: result.error ?? undefined, cleanupUnconfirmed: result.cleanupUnconfirmed, missing: artifacts.missing, mutations: mutationPaths, invalidRunner: Boolean(group.runnerEvidence && !runnerEvidence) });
|
|
665
|
+
if (result.failure)
|
|
666
|
+
physicalFailures.push(result.failure);
|
|
667
|
+
const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
668
|
+
const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
669
|
+
if (group.runnerEvidence)
|
|
670
|
+
retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
|
|
671
|
+
const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(runnerEvidence), ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
|
|
672
|
+
artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
|
|
673
|
+
const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
674
|
+
const receipt = receiptFor({ ...reserved, resources, result, status, after: { ...after, fingerprint: inputFilesFingerprint(afterFiles) }, mutationPaths, artifacts: artifacts.items });
|
|
675
|
+
receipt.required_artifacts = requiredArtifacts;
|
|
676
|
+
receipt.candidate_root = input.workspaceRoot;
|
|
677
|
+
receipt.candidate_files = candidateFiles;
|
|
678
|
+
receipt.candidate_absences = candidateAbsences;
|
|
679
|
+
receipt.execution_failures = physicalFailures;
|
|
680
|
+
context.db.run("UPDATE check_receipts SET execution_failures_json = ? WHERE project_id = ? AND id = ? AND status = 'running'", [JSON.stringify(physicalFailures), input.projectId, id]);
|
|
681
|
+
const completion = readCompletion(completionPath);
|
|
682
|
+
if (completion)
|
|
683
|
+
writeCompletionSnapshot(completionPath, completion, receipt);
|
|
684
|
+
if (group.runnerEvidence) {
|
|
685
|
+
receipt.runner_evidence_required = group.runnerEvidence;
|
|
686
|
+
receipt.runner_evidence = runnerEvidence ?? null;
|
|
687
|
+
receipt.runner_evidence_diagnostics = runnerRead.diagnostics;
|
|
688
|
+
if (runnerRead?.diagnostics_truncated)
|
|
689
|
+
receipt.runner_evidence_diagnostics_truncated = true;
|
|
136
690
|
}
|
|
137
|
-
const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 ? "passed" : "failed";
|
|
138
|
-
const receipt = receiptFor({ ...reserved, resources, result, status, after, mutationPaths, artifacts: artifacts.items });
|
|
139
691
|
context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [receipt.status, receipt.exit_code, receipt.after_fingerprint, JSON.stringify(receipt.mutation_paths), JSON.stringify(receipt.artifacts), receipt.finished_at, id, input.projectId]);
|
|
140
692
|
writeReceipt(receiptPath, receipt);
|
|
693
|
+
if (status === "failed")
|
|
694
|
+
receipt.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(receipt, "diagnostic");
|
|
141
695
|
receipts.push(receipt);
|
|
142
696
|
input.progress?.(`check ${index + 1}/${groups.length} ${status}: ${group.command}`);
|
|
143
697
|
}
|
|
698
|
+
if (receipts.every(receipt => receipt.status === "passed")) {
|
|
699
|
+
const current = workspaceState(input.workspaceRoot, [input.runHome]), currentProfileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
|
|
700
|
+
for (const receipt of receipts) {
|
|
701
|
+
const group = groups.find(candidate => candidate.declarations.some(check => receipt.check_refs.includes(checkRef(check))));
|
|
702
|
+
const currentHash = group ? checkInputHash(input.workspaceRoot, group, currentProfileHash, workspaceInputFingerprint(input.workspaceRoot, current.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) : null;
|
|
703
|
+
if (!group || !checkReceiptEvidenceValid(receipt) || receipt.input_hash !== currentHash)
|
|
704
|
+
throw new AppError("check_evidence_stale", "Check evidence changed before the gate could be accepted", 1, { check_receipt_id: receipt.id });
|
|
705
|
+
}
|
|
706
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
707
|
+
}
|
|
144
708
|
return receipts;
|
|
145
709
|
}
|
|
146
710
|
/**
|
|
@@ -149,62 +713,218 @@ export async function runCodeChecks(context, input) {
|
|
|
149
713
|
* a dead/terminal process becomes an explicit aborted receipt that can retry.
|
|
150
714
|
*/
|
|
151
715
|
export function reconcileUnfinishedChecks(context, input) {
|
|
152
|
-
|
|
716
|
+
let where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ? AND status = 'running'" : "project_id = ? AND run_id = ? AND status = 'running'";
|
|
153
717
|
const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
|
|
718
|
+
if (input.receiptId) {
|
|
719
|
+
where += " AND id = ?";
|
|
720
|
+
params.push(input.receiptId);
|
|
721
|
+
}
|
|
154
722
|
const pending = context.db.all(`SELECT id, receipt_path, started_at FROM check_receipts WHERE ${where}`, params);
|
|
155
723
|
if (pending.length === 0)
|
|
156
724
|
return [];
|
|
157
725
|
const processes = managedProcessStatus(context);
|
|
158
726
|
const reconciled = [];
|
|
159
727
|
for (const pendingReceipt of pending) {
|
|
160
|
-
const process = processes.filter((item) => item.check_id === pendingReceipt.id).at(-1);
|
|
728
|
+
const process = processes.filter((item) => item.check_id === pendingReceipt.id && item.project_id === input.projectId && item.run_id === input.runId).at(-1);
|
|
161
729
|
const receipt = readReceipt(pendingReceipt.receipt_path);
|
|
730
|
+
const authority = context.db.get("SELECT * FROM check_receipts WHERE project_id = ? AND id = ?", [input.projectId, pendingReceipt.id]);
|
|
731
|
+
if ((receipt?.fingerprint_scope ?? null) !== (authority?.fingerprint_scope ?? null)
|
|
732
|
+
|| authority?.fingerprint_scope === "check_inputs" && JSON.stringify(receipt?.input_manifest ?? null) !== authority.input_manifest_json)
|
|
733
|
+
throw new AppError("check_receipt_changed", "Detached receipt scope or input manifest differs from SQL custody", 2, { check_receipt_id: pendingReceipt.id });
|
|
162
734
|
const completion = receipt ? readCompletion(receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json")) : null;
|
|
163
735
|
// A stale lease after host sleep is not permission to duplicate a living
|
|
164
736
|
// check. Its durable completion marker or explicit reconciliation decides
|
|
165
737
|
// the outcome before another invocation may start.
|
|
166
|
-
|
|
738
|
+
// A confirmed PID plus its birth time is sufficient to prove that this
|
|
739
|
+
// exact check writer is gone. Do not retain a dead receipt merely because
|
|
740
|
+
// older registrations lack the optional parent-owner identity.
|
|
741
|
+
const processProvenDead = Boolean(process?.pid && process.pid_started_at && !processTreeIsAlive(process));
|
|
742
|
+
if (process && !processProvenDead && (processTreeIsAlive(process) || (["starting", "running", "stopping", "orphaned"].includes(process.state) && managedOwnerIsAlive(process) !== false))) {
|
|
167
743
|
throw new AppError("check_in_progress", "A previous check invocation is still running", 1, { check_receipt_id: pendingReceipt.id, process_id: process.id, pid: process.pid });
|
|
168
744
|
}
|
|
169
745
|
if (!process && !completion && Date.now() - Date.parse(pendingReceipt.started_at) < receiptStartingGraceMs) {
|
|
170
746
|
throw new AppError("check_in_progress", "A previous check invocation is still initializing", 1, { check_receipt_id: pendingReceipt.id, initializing: true });
|
|
171
747
|
}
|
|
172
748
|
const after = workspaceState(input.workspaceRoot, input.runHome ? [input.runHome] : []);
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
749
|
+
let artifacts = receipt ? collectCheckArtifacts(path.join(path.dirname(receipt.completion_path || pendingReceipt.receipt_path), "artifacts"), receipt.required_artifacts ?? []) : { complete: false, missing: [], items: [] };
|
|
750
|
+
const scoped = receipt?.fingerprint_scope === "check_inputs";
|
|
751
|
+
const completionSnapshot = scoped && completion?.fingerprint_scope === "check_inputs" && completion.id === receipt.id && completion.input_hash === receipt.input_hash
|
|
752
|
+
&& completion.verification_epoch === receipt.verification_epoch && completion.before_fingerprint === receipt.before_fingerprint
|
|
753
|
+
&& JSON.stringify(completion.input_manifest ?? null) === JSON.stringify(receipt.input_manifest ?? null) && typeof completion.after_fingerprint === "string";
|
|
754
|
+
const afterFingerprint = scoped ? completionSnapshot ? completion.after_fingerprint : workspaceInputFingerprint(input.workspaceRoot, after.files, receipt.inputs ?? [], input.runHome ? [input.runHome] : []) : after.fingerprint;
|
|
755
|
+
const workspaceUnchanged = receipt?.before_fingerprint === afterFingerprint;
|
|
756
|
+
const beforeFiles = scoped ? retainedInputFiles(receipt) : null;
|
|
757
|
+
// A completion digest describes that instant, not the later recovery tree.
|
|
758
|
+
// Without an after manifest, never attach today's delta to that old proof.
|
|
759
|
+
const mutationPaths = workspaceUnchanged ? [] : scoped && completionSnapshot ? ["<declared check inputs differed at completion; exact delta unavailable>"]
|
|
760
|
+
: scoped && beforeFiles ? changedPaths(beforeFiles, selectedInputFiles(input.workspaceRoot, after.files, receipt.inputs ?? [], input.runHome ? [input.runHome] : []))
|
|
761
|
+
: [scoped ? "<declared check inputs changed; exact manifest unavailable>" : "<workspace changed while detached check ran>"];
|
|
762
|
+
const runnerRead = receipt?.runner_evidence_required ? readRunnerEvidenceDetailed(path.join(path.dirname(pendingReceipt.receipt_path), "artifacts"), receipt.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch }) : undefined;
|
|
763
|
+
const runnerEvidence = runnerRead?.evidence;
|
|
176
764
|
const exitCode = completion?.exit_code ?? null;
|
|
177
765
|
const finishedAt = completion?.finished_at ?? context.now();
|
|
178
|
-
|
|
766
|
+
const physicalFailures = executionFailures({ fingerprintScope: scoped ? "check_inputs" : undefined, exitCode, missing: artifacts.missing.filter(file => file !== "engine-outcome.json"), mutations: mutationPaths, invalidRunner: Boolean(receipt?.runner_evidence_required && !runnerEvidence) });
|
|
767
|
+
if (scoped && !completionSnapshot)
|
|
768
|
+
physicalFailures.push({ code: "completion_snapshot_unknown", message: "Detached check has no bound scoped completion snapshot; current inputs cannot prove inputs at command completion" });
|
|
769
|
+
let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
770
|
+
let candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
771
|
+
if (receipt?.runner_evidence_required) {
|
|
772
|
+
const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
|
|
773
|
+
if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
|
|
774
|
+
// A completed live writer's bound proof is already authoritative. Never erase its blockers during reconciliation.
|
|
775
|
+
const retained = context.db.get("SELECT artifacts_json FROM check_receipts WHERE id = ?", [pendingReceipt.id]);
|
|
776
|
+
const proven = resolvedReceiptArtifacts(pendingReceipt.receipt_path, retained ? JSON.parse(retained.artifacts_json) : [], receipt);
|
|
777
|
+
if (proven.candidate_root) {
|
|
778
|
+
physicalFailures.push(...(proven.execution_failures ?? []));
|
|
779
|
+
candidateFiles = proven.candidate_files ?? candidateFiles;
|
|
780
|
+
candidateAbsences = proven.candidate_absences ?? [];
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
|
|
784
|
+
fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
|
|
788
|
+
receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), ...runnerProofArtifacts(runnerEvidence), "engine-outcome.json"])];
|
|
789
|
+
artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
|
|
790
|
+
}
|
|
791
|
+
const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete && physicalFailures.length === 0 && (!receipt?.runner_evidence_required || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
792
|
+
context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [status, exitCode, afterFingerprint, JSON.stringify(mutationPaths), JSON.stringify(artifacts.items), finishedAt, pendingReceipt.id, input.projectId]);
|
|
793
|
+
context.db.run("UPDATE check_receipts SET execution_failures_json = ? WHERE project_id = ? AND id = ?", [JSON.stringify(physicalFailures), input.projectId, pendingReceipt.id]);
|
|
179
794
|
if (process)
|
|
180
795
|
finishManagedProcess(context, { id: process.id, leaseToken: process.lease_token, state: status === "passed" ? "stopped" : "failed", reason: status === "passed" ? "reconciled_completion" : completion === null ? "completion_marker_missing" : "reconciled_check_failed" });
|
|
181
796
|
if (receipt) {
|
|
182
|
-
const repaired = { ...receipt, completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], status
|
|
797
|
+
const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null, runner_evidence_diagnostics: runnerRead.diagnostics, ...(runnerRead?.diagnostics_truncated ? { runner_evidence_diagnostics_truncated: true } : {}) } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: afterFingerprint, mutation_paths: mutationPaths, artifacts: artifacts.items, finished_at: finishedAt };
|
|
798
|
+
repaired.candidate_root = input.workspaceRoot;
|
|
799
|
+
repaired.candidate_files = candidateFiles;
|
|
800
|
+
repaired.candidate_absences = candidateAbsences;
|
|
801
|
+
repaired.execution_failures = physicalFailures;
|
|
183
802
|
writeReceipt(pendingReceipt.receipt_path, repaired);
|
|
803
|
+
if (status === "failed")
|
|
804
|
+
repaired.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(repaired, "diagnostic");
|
|
184
805
|
reconciled.push(repaired);
|
|
185
806
|
}
|
|
186
807
|
}
|
|
187
808
|
return reconciled;
|
|
188
809
|
}
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
810
|
+
function reserveOrReuseReceipt(context, input) {
|
|
811
|
+
// Receipt files and artifact hashes are prepared before reserving SQLite.
|
|
812
|
+
// The compact SQL snapshot below fences races with another check settlement.
|
|
813
|
+
const snapshotSql = "SELECT * FROM check_receipts WHERE project_id = ? AND run_id = ? ORDER BY reservation_sequence, rowid";
|
|
814
|
+
const snapshot = JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId]));
|
|
815
|
+
const receipts = checkReceipts(context, { projectId: input.projectId, runId: input.runId });
|
|
816
|
+
if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
|
|
817
|
+
throw new AppError("check_receipt_changed", "Check receipts changed during evidence preparation", 1);
|
|
818
|
+
const prior = receipts.filter((receipt) => receipt.fingerprint_scope === "check_inputs" && receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash).at(-1) ?? null;
|
|
819
|
+
const reusablePrior = prior?.status === "passed" && checkReceiptEvidenceValid(prior);
|
|
820
|
+
const retryIds = new Set([...(input.retryReceiptId ? [input.retryReceiptId] : []), ...(input.retryReceiptIds ?? [])]);
|
|
821
|
+
const successor = retryIds.size === 1 ? receipts.find((receipt) => receipt.retry_of_receipt_id === [...retryIds][0] && receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash) : null;
|
|
822
|
+
const reusableSuccessor = successor?.status === "passed" && checkReceiptEvidenceValid(successor);
|
|
823
|
+
const retryPredecessor = receipts.find(receipt => retryIds.has(receipt.id));
|
|
824
|
+
const sourceCorrection = Boolean(retryPredecessor && (sourceRepairContractMatches(retryPredecessor, input.group, input.profileHash)
|
|
825
|
+
|| input.retryReason === "check_inputs_changed" && staleRetryContractMatches(retryPredecessor, input.group, input.profileHash, input)));
|
|
826
|
+
const latestContract = receipts.filter(receipt => receipt.command === input.group.command && receipt.profile_hash === input.profileHash && receipt.gate === input.group.gate && receipt.scope === input.scope && receipt.work_id === (input.workId ?? null)).at(-1);
|
|
827
|
+
return context.db.writeTransaction(() => {
|
|
828
|
+
assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
|
|
829
|
+
if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
|
|
830
|
+
throw new AppError("check_receipt_changed", "Check receipts changed before reservation", 1);
|
|
831
|
+
if (prior?.status === "running")
|
|
832
|
+
throw new AppError("check_in_progress", "A matching check invocation is still running", 1, { check_receipt_id: prior.id });
|
|
833
|
+
if (retryIds.size > 1)
|
|
834
|
+
throw new AppError("retry_check_invalid", "A check execution accepts exactly one retry predecessor", 2);
|
|
835
|
+
const retryId = [...retryIds][0];
|
|
836
|
+
if (retryId) {
|
|
837
|
+
if (input.retryReason === "check_inputs_changed" && retryPredecessor) {
|
|
838
|
+
const allSuccessors = receipts.filter(receipt => receipt.retry_of_receipt_id === retryId);
|
|
839
|
+
if (allSuccessors.length === 1 && reusableSuccessor)
|
|
840
|
+
return { receipt: successor };
|
|
841
|
+
if (retryPredecessor.retry_reason === "check_inputs_changed" || allSuccessors.length)
|
|
842
|
+
throw new AppError("retry_check_invalid", "Scoped input retry episode already consumed its one successor", 2, { check_receipt_id: retryId });
|
|
843
|
+
}
|
|
844
|
+
if (!input.retryReason?.trim())
|
|
845
|
+
throw new AppError("retry_check_invalid", "A check retry requires the restored-condition reason", 2, { check_receipt_id: retryId });
|
|
846
|
+
const predecessor = receipts.find(receipt => receipt.id === retryId);
|
|
847
|
+
if (!predecessor || !["failed", "aborted"].includes(predecessor.status) || predecessor.input_hash !== input.inputHash && !sourceCorrection || predecessor.profile_hash !== input.profileHash || predecessor.command !== input.group.command || predecessor.gate !== input.group.gate)
|
|
848
|
+
throw new AppError("retry_check_invalid", "Retry predecessor does not match this exact check contract", 2, { check_receipt_id: retryId });
|
|
849
|
+
if ((sourceCorrection ? latestContract?.id : prior?.id) !== (successor?.id ?? predecessor.id))
|
|
850
|
+
throw new AppError("retry_check_invalid", "This retry was superseded by a newer check outcome; inspect the latest receipt", 2, { check_receipt_id: retryId, latest_check_receipt_id: prior?.id ?? null, latest_status: prior?.status ?? null });
|
|
851
|
+
if (successor?.status === "running")
|
|
852
|
+
throw new AppError("check_in_progress", "The requested retry is already running", 1, { check_receipt_id: successor.id, retry_of_receipt_id: retryId });
|
|
853
|
+
if (successor) {
|
|
854
|
+
if (reusableSuccessor)
|
|
855
|
+
return { receipt: successor };
|
|
856
|
+
throw new AppError("retry_check_invalid", "The requested retry already has a terminal outcome; choose its failed receipt for another retry", 2, { check_receipt_id: successor.id, retry_of_receipt_id: retryId, status: successor.status });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// An unchanged failed contract needs a concrete restored-condition retry,
|
|
860
|
+
// not repeated coordinator finish until it happens to become green.
|
|
861
|
+
if (!retryId && latestContract?.fingerprint_scope === "check_inputs" && latestContract.status === "failed"
|
|
862
|
+
&& latestContract.execution_failures?.some(failure => failure.code === "check_inputs_changed") && latestContract.execution_contract_hash === input.executionContractHash)
|
|
863
|
+
return { receipt: latestContract };
|
|
864
|
+
if (prior && ["failed", "aborted"].includes(prior.status) && !retryId)
|
|
865
|
+
return { receipt: prior };
|
|
866
|
+
if (prior?.status === "passed" && !retryIds.has(prior.id) && input.group.reusable && reusablePrior)
|
|
867
|
+
return { receipt: prior };
|
|
868
|
+
let ordinal = (context.db.get("SELECT COALESCE(MAX(reservation_sequence), 0) + 1 AS sequence FROM check_receipts WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId])?.sequence ?? 1);
|
|
869
|
+
let localId = `RCP-${String(ordinal).padStart(3, "0")}`, id = `${input.workId ?? input.runId}/${localId}`;
|
|
870
|
+
while (context.db.get("SELECT 1 FROM check_receipts WHERE id = ?", [id])) {
|
|
871
|
+
ordinal += 1;
|
|
872
|
+
localId = `RCP-${String(ordinal).padStart(3, "0")}`;
|
|
873
|
+
id = `${input.workId ?? input.runId}/${localId}`;
|
|
874
|
+
}
|
|
196
875
|
const directory = path.join(input.runHome, input.artifactDir ?? "05-code", "checks", localId);
|
|
197
|
-
const reserved = { id, localId, directory, evidenceDir: path.join(directory, "artifacts"), stdoutPath: path.join(directory, "stdout.log"), stderrPath: path.join(directory, "stderr.log"), receiptPath: path.join(directory, "receipt.json"), completionPath: path.join(directory, "completion.json"), group: input.group, before: input.before, profileHash: input.profileHash, inputHash: input.inputHash, epoch: input.epoch, startedAt: input.startedAt, scope: input.scope, ...(input.workId ? { workId: input.workId } : {}), projectId: input.projectId, runId: input.runId };
|
|
198
|
-
context.db.run("INSERT INTO check_receipts (id, project_id, run_id, work_id, scope, declaration_id, check_refs_json, gate, command, input_hash, verification_epoch, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, ?, ?, ?, ?, ?, ?, ?, '[]', '[]', ?, ?)", [id, input.projectId, input.runId, input.workId ?? null, input.scope, input.group.declarations[0].id, JSON.stringify(input.group.declarations.map(checkRef)), input.group.gate, input.group.command, input.inputHash, input.epoch, reserved.stdoutPath, reserved.stderrPath, reserved.receiptPath, input.before.fingerprint, input.before.fingerprint, input.before.fingerprint, input.profileHash, input.startedAt, input.startedAt]);
|
|
199
|
-
context.db.
|
|
200
|
-
return reserved;
|
|
876
|
+
const reserved = { id, localId, directory, evidenceDir: path.join(directory, "artifacts"), stdoutPath: path.join(directory, "stdout.log"), stderrPath: path.join(directory, "stderr.log"), receiptPath: path.join(directory, "receipt.json"), completionPath: path.join(directory, "completion.json"), group: input.group, before: input.before, inputFiles: input.inputFiles, inputFingerprint: input.inputFingerprint, executionContractHash: input.executionContractHash, profileHash: input.profileHash, inputHash: input.inputHash, epoch: input.epoch, startedAt: input.startedAt, scope: input.scope, ...(retryId ? { retryOf: retryId, retryReason: input.retryReason } : {}), ...(input.workId ? { workId: input.workId } : {}), projectId: input.projectId, runId: input.runId, workspaceRoot: input.workspaceRoot, runHome: input.runHome };
|
|
877
|
+
context.db.run("INSERT INTO check_receipts (id, project_id, run_id, work_id, scope, declaration_id, check_refs_json, gate, command, input_hash, verification_epoch, reservation_sequence, retry_of_receipt_id, retry_reason, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, ?, ?, ?, ?, ?, ?, ?, '[]', '[]', ?, ?)", [id, input.projectId, input.runId, input.workId ?? null, input.scope, input.group.declarations[0].id, JSON.stringify(input.group.declarations.map(checkRef).sort()), input.group.gate, input.group.command, input.inputHash, input.epoch, ordinal, reserved.retryOf ?? null, reserved.retryReason ?? null, reserved.stdoutPath, reserved.stderrPath, reserved.receiptPath, input.before.fingerprint, input.before.fingerprint, input.before.fingerprint, input.profileHash, input.startedAt, input.startedAt]);
|
|
878
|
+
context.db.run("UPDATE check_receipts SET fingerprint_scope = ?, before_fingerprint = ?, after_fingerprint = ?, execution_contract_hash = ? WHERE id = ? AND project_id = ?", ["check_inputs", input.inputFingerprint, input.inputFingerprint, input.executionContractHash, id, input.projectId]);
|
|
879
|
+
return { reserved };
|
|
880
|
+
}, "checks.reserveOrReuseReceipt");
|
|
881
|
+
}
|
|
882
|
+
function sourceRepairContractMatches(receipt, group, profileHash) {
|
|
883
|
+
return checkRepairDisposition(receipt) === "source_repair" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
|
|
884
|
+
&& isDeepStrictEqual(derivedReceiptArtifacts(group, receipt).sort(), [...receipt.required_artifacts].sort()) && isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
|
|
885
|
+
}
|
|
886
|
+
function staleCheckRetryEligible(receipt) {
|
|
887
|
+
if (receipt.fingerprint_scope !== "check_inputs" || receipt.status !== "failed" || receipt.exit_code !== 0 || receipt.retry_reason === "check_inputs_changed"
|
|
888
|
+
|| !receipt.execution_failures?.length || receipt.execution_failures.some(failure => failure.code !== "check_inputs_changed")
|
|
889
|
+
|| receipt.runner_evidence_required && !runnerEvidencePass(receipt.runner_evidence))
|
|
890
|
+
return false;
|
|
891
|
+
return checkReceiptEvidenceValidForMode(receipt, "diagnostic");
|
|
892
|
+
}
|
|
893
|
+
function staleRetryContractMatches(receipt, group, profileHash, input) {
|
|
894
|
+
return group.reusable && staleCheckRetryEligible(receipt) && isDeepStrictEqual(group.runnerEvidence ?? null, receipt.runner_evidence_required ?? null)
|
|
895
|
+
&& receipt.execution_contract_hash === checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment);
|
|
896
|
+
}
|
|
897
|
+
function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, ...(input.status === "aborted" ? { abort_reason: input.result.error } : {}), exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, fingerprint_scope: "check_inputs", input_manifest: input.inputManifest ?? null, execution_contract_hash: input.executionContractHash, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.inputFingerprint, after_fingerprint: input.status === "running" ? input.inputFingerprint : input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
|
|
898
|
+
function retainInputManifest(context, reserved) {
|
|
899
|
+
const full = JSON.stringify({ fingerprint_scope: "check_inputs", before_fingerprint: reserved.inputFingerprint, files: [...reserved.inputFiles].sort(([left], [right]) => left.localeCompare(right)) });
|
|
900
|
+
const truncated = Buffer.byteLength(full) > 1024 * 1024;
|
|
901
|
+
const bytes = truncated ? JSON.stringify({ fingerprint_scope: "check_inputs", before_fingerprint: reserved.inputFingerprint, truncated: true }) : full;
|
|
902
|
+
const relative = "inputs-before.json", file = path.join(reserved.directory, relative);
|
|
903
|
+
fs.writeFileSync(file, bytes, { flag: "wx", mode: 0o600 });
|
|
904
|
+
reserved.inputManifest = { path: relative, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), truncated };
|
|
905
|
+
context.db.run("UPDATE check_receipts SET input_manifest_json = ? WHERE project_id = ? AND id = ? AND status = 'running'", [JSON.stringify(reserved.inputManifest), reserved.projectId, reserved.id]);
|
|
906
|
+
}
|
|
907
|
+
function retainedInputFiles(receipt) {
|
|
908
|
+
if (!receipt.input_manifest || receipt.input_manifest.truncated || receipt.input_manifest.path !== "inputs-before.json")
|
|
909
|
+
return null;
|
|
910
|
+
try {
|
|
911
|
+
const bytes = readBoundedReleaseFile(path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path), 1024 * 1024);
|
|
912
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== receipt.input_manifest.sha256)
|
|
913
|
+
return null;
|
|
914
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
915
|
+
if (value.before_fingerprint !== receipt.before_fingerprint || !Array.isArray(value.files) || value.files.some((entry) => !Array.isArray(entry) || entry.length !== 2 || entry.some(item => typeof item !== "string")))
|
|
916
|
+
return null;
|
|
917
|
+
return new Map(value.files);
|
|
201
918
|
}
|
|
202
|
-
catch
|
|
203
|
-
|
|
204
|
-
throw error;
|
|
919
|
+
catch {
|
|
920
|
+
return null;
|
|
205
921
|
}
|
|
206
922
|
}
|
|
207
|
-
function
|
|
923
|
+
function writeCompletionSnapshot(file, completion, receipt) {
|
|
924
|
+
const temporary = file + ".tmp-" + crypto.randomUUID();
|
|
925
|
+
fs.writeFileSync(temporary, JSON.stringify({ ...completion, fingerprint_scope: receipt.fingerprint_scope, id: receipt.id, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch, before_fingerprint: receipt.before_fingerprint, after_fingerprint: receipt.after_fingerprint, input_manifest: receipt.input_manifest ?? null }));
|
|
926
|
+
fs.renameSync(temporary, file);
|
|
927
|
+
}
|
|
208
928
|
function writeReceipt(file, receipt) { const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`); fs.renameSync(temporary, file); }
|
|
209
929
|
function readReceipt(file) { try {
|
|
210
930
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -215,35 +935,444 @@ catch {
|
|
|
215
935
|
function readCompletion(file) { try {
|
|
216
936
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
217
937
|
const done = value;
|
|
218
|
-
return value && typeof value === "object" && Number.isInteger(done.exit_code) ? { exit_code: Number(done.exit_code), finished_at: typeof done.finished_at === "string" && Number.isFinite(Date.parse(done.finished_at)) ? done.finished_at : null } : null;
|
|
938
|
+
return value && typeof value === "object" && Number.isInteger(done.exit_code) ? { ...done, exit_code: Number(done.exit_code), finished_at: typeof done.finished_at === "string" && Number.isFinite(Date.parse(done.finished_at)) ? done.finished_at : null } : null;
|
|
219
939
|
}
|
|
220
940
|
catch {
|
|
221
941
|
return null;
|
|
222
942
|
} }
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
943
|
+
/** Validate an explicit retry; the caller still executes a fresh real gate and retains a new receipt. */
|
|
944
|
+
export function validateFailedCheckRetry(context, input) {
|
|
945
|
+
if (!input.reason?.trim())
|
|
946
|
+
throw new AppError("retry_check_invalid", "--retry-check requires --reason describing the restored environment", 2);
|
|
947
|
+
const receipt = checkReceipts(context, { projectId: input.projectId, runId: input.runId }).find((item) => item.id === input.receiptId);
|
|
948
|
+
if (!receipt || !["failed", "aborted"].includes(receipt.status))
|
|
949
|
+
throw new AppError("retry_check_invalid", "--retry-check must name a failed or aborted receipt in this RUN", 2, { check_receipt_id: input.receiptId });
|
|
950
|
+
const declared = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)]));
|
|
951
|
+
if (!receipt.check_refs.some((ref) => declared.has(ref)))
|
|
952
|
+
throw new AppError("retry_check_invalid", "--retry-check does not belong to this stage gate", 2, { check_receipt_id: input.receiptId, check_refs: receipt.check_refs });
|
|
953
|
+
if (receipt.fingerprint_scope === "check_inputs" && receipt.execution_failures?.some(failure => failure.code === "check_inputs_changed") && input.reason !== "check_inputs_changed")
|
|
954
|
+
throw new AppError("retry_check_invalid", "Scoped drift uses its bounded stale transition, not an environment/source retry", 2, { check_receipt_id: receipt.id });
|
|
955
|
+
if (input.reason === "check_inputs_changed" && (!staleCheckRetryEligible(receipt) || receipt.retry_reason === "check_inputs_changed"))
|
|
956
|
+
throw new AppError("retry_check_invalid", "Scoped input retry requires a bound, safely completed stale predecessor and one unconsumed episode", 2, { check_receipt_id: receipt.id });
|
|
957
|
+
return receipt;
|
|
958
|
+
}
|
|
226
959
|
export function checkReceipts(context, input) {
|
|
227
960
|
const where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ?" : "project_id = ? AND run_id = ?";
|
|
228
961
|
const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
|
|
229
|
-
return context.db.all(`SELECT
|
|
962
|
+
return context.db.all(`SELECT * FROM check_receipts WHERE ${where} ORDER BY reservation_sequence, rowid`, params).map(projectCheckReceiptRow);
|
|
963
|
+
}
|
|
964
|
+
function projectCheckReceiptRow(row) {
|
|
965
|
+
const inputManifest = row.input_manifest_json ? JSON.parse(row.input_manifest_json) : null;
|
|
966
|
+
const receipt = { ...row, input_manifest: inputManifest, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []), resources: readReceiptResources(row.receipt_path), ...resolvedReceiptArtifacts(row.receipt_path, JSON.parse(row.artifacts_json), { ...row, input_manifest: inputManifest }), ...(row.execution_failures_json ? { execution_failures: JSON.parse(row.execution_failures_json) } : {}) };
|
|
967
|
+
if (receipt.status === "failed")
|
|
968
|
+
receipt.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(receipt, "diagnostic");
|
|
969
|
+
return receipt;
|
|
970
|
+
}
|
|
971
|
+
/** File bytes remain immutable; only the reader projects their current location. */
|
|
972
|
+
function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
973
|
+
let original = {};
|
|
974
|
+
try {
|
|
975
|
+
const value = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
|
|
976
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
977
|
+
original = value;
|
|
978
|
+
}
|
|
979
|
+
catch { /* Historical missing receipts remain visibly incomplete. */ }
|
|
980
|
+
const currentRoot = path.dirname(receiptPath);
|
|
981
|
+
const oldRoot = typeof original.receipt_path === "string" ? path.dirname(original.receipt_path) : currentRoot;
|
|
982
|
+
const relocate = (file) => {
|
|
983
|
+
const relative = path.relative(oldRoot, file);
|
|
984
|
+
return !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) ? path.join(currentRoot, relative) : file;
|
|
985
|
+
};
|
|
986
|
+
let evidence = null;
|
|
987
|
+
let diagnostics = [];
|
|
988
|
+
let diagnosticsTruncated = false;
|
|
989
|
+
if (original.runner_evidence_required && original.id && original.command && original.input_hash && original.verification_epoch) {
|
|
990
|
+
const root = path.join(currentRoot, "artifacts");
|
|
991
|
+
const retained = collectCheckArtifacts(root, [original.runner_evidence_required.path]);
|
|
992
|
+
if (retained.complete && retained.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
|
|
993
|
+
const read = readRunnerEvidenceDetailed(root, original.runner_evidence_required, { invocation_id: binding.id, check_id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch });
|
|
994
|
+
evidence = read.evidence;
|
|
995
|
+
diagnostics = read.diagnostics;
|
|
996
|
+
diagnosticsTruncated = Boolean(read.diagnostics_truncated);
|
|
997
|
+
}
|
|
998
|
+
else
|
|
999
|
+
diagnostics = [{ reason: retained.complete ? "artifact_mismatch" : "missing", report: original.runner_evidence_required.path }];
|
|
1000
|
+
}
|
|
1001
|
+
let outcome = {};
|
|
1002
|
+
const outcomePath = path.join(currentRoot, "artifacts", "engine-outcome.json");
|
|
1003
|
+
const retainedOutcome = collectCheckArtifacts(path.join(currentRoot, "artifacts"), ["engine-outcome.json"]);
|
|
1004
|
+
if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
|
|
1005
|
+
try {
|
|
1006
|
+
const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
|
|
1007
|
+
if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && (value.candidate_absences === undefined || candidateAbsencesValid(value.candidate_absences)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
|
|
1008
|
+
outcome = value;
|
|
1009
|
+
}
|
|
1010
|
+
catch { /* Missing physical proof cannot grant attribution. */ }
|
|
1011
|
+
}
|
|
1012
|
+
return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence, runner_evidence_diagnostics: diagnostics, ...(diagnosticsTruncated ? { runner_evidence_diagnostics_truncated: true } : {}) } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}), ...(outcome.candidate_absences ? { candidate_absences: outcome.candidate_absences } : {}) };
|
|
1013
|
+
}
|
|
1014
|
+
function executionFailures(input) {
|
|
1015
|
+
const failures = [];
|
|
1016
|
+
if (input.exitCode === null)
|
|
1017
|
+
failures.push({ code: "completion_unknown", message: "Check completion is unknown" });
|
|
1018
|
+
if (input.launcherError)
|
|
1019
|
+
failures.push({ code: "launcher_error", message: input.launcherError });
|
|
1020
|
+
if (input.cleanupUnconfirmed)
|
|
1021
|
+
failures.push({ code: "cleanup_unconfirmed", message: "Check process cleanup is not confirmed" });
|
|
1022
|
+
if (input.missing.length)
|
|
1023
|
+
failures.push({ code: "missing_required_artifact", message: input.missing.join(", ") });
|
|
1024
|
+
if (input.mutations.length)
|
|
1025
|
+
failures.push({ code: input.fingerprintScope === "check_inputs" ? "check_inputs_changed" : "check_input_mutated", message: input.mutations.join(", ") });
|
|
1026
|
+
if (input.invalidRunner)
|
|
1027
|
+
failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
|
|
1028
|
+
return failures;
|
|
1029
|
+
}
|
|
1030
|
+
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = []) {
|
|
1031
|
+
const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
|
|
1032
|
+
if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
|
|
1033
|
+
throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
|
|
1034
|
+
fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, candidate_absences: candidateAbsences, execution_failures: failures }), { flag: "wx", mode: 0o600 });
|
|
1035
|
+
fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
|
|
230
1036
|
}
|
|
231
1037
|
export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
1038
|
+
/** One NUL-aware source of truth for paths differing from HEAD. Rename/copy
|
|
1039
|
+
* records carry a second path; retain both because the old side is a deletion. */
|
|
1040
|
+
export function workspaceChangedPaths(root) {
|
|
1041
|
+
const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
1042
|
+
// A workspace can be intentionally gitless. Keep the same conservative
|
|
1043
|
+
// file inventory used for fingerprints instead of making result validation
|
|
1044
|
+
// fail before the work contract can report its actual error.
|
|
1045
|
+
if (isGitlessWorkspace(root, status))
|
|
1046
|
+
return listFiles(root);
|
|
1047
|
+
if (status.status !== 0 || status.error)
|
|
1048
|
+
throw new AppError("workspace_status_failed", "Could not determine changed workspace paths", 1, { workspace_root: root, stderr: status.stderr || null, cause: status.error?.message ?? null });
|
|
1049
|
+
const records = status.stdout.split("\0");
|
|
1050
|
+
const paths = [];
|
|
1051
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
1052
|
+
const record = records[index];
|
|
1053
|
+
if (!record)
|
|
1054
|
+
continue;
|
|
1055
|
+
if (record.length < 4 || record[2] !== " ")
|
|
1056
|
+
throw new AppError("workspace_status_invalid", "Git returned an invalid porcelain-v1 record", 1, { workspace_root: root, record });
|
|
1057
|
+
paths.push(normalizeWorkspacePath(root, record.slice(3)));
|
|
1058
|
+
if (record[0] === "R" || record[0] === "C" || record[1] === "R" || record[1] === "C") {
|
|
1059
|
+
const prior = records[++index];
|
|
1060
|
+
if (!prior)
|
|
1061
|
+
throw new AppError("workspace_status_invalid", "Git rename/copy record is missing its second path", 1, { workspace_root: root, record });
|
|
1062
|
+
paths.push(normalizeWorkspacePath(root, prior));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
const ownedHook = paths.includes(".agents/hooks.json") ? ownedWorkspaceHookPath(root) : null;
|
|
1066
|
+
return [...new Set(paths)].filter(file => file !== ownedHook).sort();
|
|
1067
|
+
}
|
|
1068
|
+
/** Ignored machine state cannot be reported as a product change. Tracked paths
|
|
1069
|
+
* remain eligible regardless of their directory name. */
|
|
1070
|
+
export function ignoredWorkspacePaths(root, paths) {
|
|
1071
|
+
const normalized = [...new Set(paths.map((item) => normalizeWorkspacePath(root, item)))];
|
|
1072
|
+
if (!normalized.length)
|
|
1073
|
+
return [];
|
|
1074
|
+
const status = spawnSync("git", ["check-ignore", "-z", "--stdin"], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", input: `${normalized.join("\0")}\0`, maxBuffer: 64 * 1024 * 1024 });
|
|
1075
|
+
// The canonical owned hook has one meaning across Git status, check cache
|
|
1076
|
+
// and MERGE, without changing a sibling worktree's shared Git excludes.
|
|
1077
|
+
const gitless = isGitlessWorkspace(root, status);
|
|
1078
|
+
if (!gitless && (![0, 1].includes(status.status ?? -1) || status.error))
|
|
1079
|
+
throw new AppError("workspace_ignore_check_failed", "Could not classify reported workspace paths", 1, { workspace_root: root, stderr: status.stderr || null, cause: status.error?.message ?? null });
|
|
1080
|
+
const ownedHook = normalized.includes(".agents/hooks.json") ? ownedWorkspaceHookPath(root, gitless ? [] : undefined) : null;
|
|
1081
|
+
if (gitless)
|
|
1082
|
+
return ownedHook ? [ownedHook] : [];
|
|
1083
|
+
return [...new Set([...status.stdout.split("\0").filter(Boolean).map((item) => normalizeWorkspacePath(root, item)), ...(ownedHook ? [ownedHook] : [])])].sort();
|
|
1084
|
+
}
|
|
1085
|
+
export function normalizeWorkspacePath(root, value) {
|
|
1086
|
+
if (typeof value !== "string" || !value || value.includes("\0") || path.isAbsolute(value))
|
|
1087
|
+
throw new AppError("workspace_path_invalid", "Workspace path must be a non-empty relative path", 2, { path: value });
|
|
1088
|
+
const resolvedRoot = path.resolve(root);
|
|
1089
|
+
const resolved = path.resolve(resolvedRoot, value);
|
|
1090
|
+
const relative = path.relative(resolvedRoot, resolved);
|
|
1091
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
1092
|
+
throw new AppError("workspace_path_invalid", "Workspace path escapes its root", 2, { path: value, workspace_root: root });
|
|
1093
|
+
return relative.split(path.sep).join("/");
|
|
1094
|
+
}
|
|
1095
|
+
function deduplicate(checks, commands) {
|
|
1096
|
+
const groups = new Map();
|
|
1097
|
+
checks.forEach((check, index) => {
|
|
1098
|
+
const artifacts = [...(check.required_artifacts ?? [])].sort(), ports = [...(check.ports ?? [])].sort(), inputs = normalizedInputs(check.inputs ?? []);
|
|
1099
|
+
const reusable = check.reuse === "deterministic";
|
|
1100
|
+
// Stage labels identify obligations, not different processes in this gate.
|
|
1101
|
+
// Cross-invocation reuse still requires the explicit deterministic contract.
|
|
1102
|
+
const environmentInputs = normalizedEnvironmentInputs(check.environment_inputs);
|
|
1103
|
+
const key = JSON.stringify([commands[index], artifacts, ports, inputs, reusable, environmentInputs, check.runner_evidence ?? null]);
|
|
1104
|
+
const current = groups.get(key);
|
|
1105
|
+
if (current)
|
|
1106
|
+
current.declarations.push(check);
|
|
1107
|
+
else
|
|
1108
|
+
groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports, inputs, reusable, ...(environmentInputs === undefined ? {} : { environmentInputs }), ...(check.runner_evidence ? { runnerEvidence: check.runner_evidence } : {}) });
|
|
1109
|
+
});
|
|
1110
|
+
return [...groups.values()].map(group => ({ ...group, gate: [...group.declarations].map(check => check.run_at).sort()[0], declarations: [...group.declarations].sort((left, right) => checkRef(left).localeCompare(checkRef(right))) }));
|
|
1111
|
+
}
|
|
1112
|
+
function workspaceState(root, ignoredRoots = []) {
|
|
1113
|
+
const git = workspaceGitFiles(root, ["-co", "--exclude-standard"]);
|
|
1114
|
+
const ignored = ignoredRoots.map(entry => path.resolve(entry));
|
|
1115
|
+
const listing = [...new Set(git ?? listFiles(root))].sort();
|
|
1116
|
+
const ownedHook = listing.includes(".agents/hooks.json") ? ownedWorkspaceHookPath(root, git ? undefined : []) : null;
|
|
1117
|
+
const files = new Map(), hash = crypto.createHash("sha256");
|
|
1118
|
+
for (const relative of listing) {
|
|
1119
|
+
if (relative === ownedHook)
|
|
1120
|
+
continue;
|
|
1121
|
+
const file = path.resolve(root, relative);
|
|
1122
|
+
if (ignored.some(ignoredRoot => file === ignoredRoot || file.startsWith(`${ignoredRoot}${path.sep}`)))
|
|
1123
|
+
continue;
|
|
1124
|
+
// Deleted tracked paths are absent content, both before and after commit.
|
|
1125
|
+
// Do not mask other IO errors as an unchanged workspace.
|
|
1126
|
+
let stat;
|
|
1127
|
+
try {
|
|
1128
|
+
stat = fs.lstatSync(file);
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
if (error.code === "ENOENT")
|
|
1132
|
+
continue;
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
const content = stat.isSymbolicLink() ? Buffer.from(`symlink\0${fs.readlinkSync(file)}`) : stat.isFile() ? fs.readFileSync(file) : Buffer.from("<directory>");
|
|
1136
|
+
const digest = crypto.createHash("sha256").update(content).digest("hex");
|
|
1137
|
+
files.set(relative, digest);
|
|
1138
|
+
hash.update(relative).update("\0").update(digest).update("\0");
|
|
1139
|
+
}
|
|
1140
|
+
return { fingerprint: hash.digest("hex"), files };
|
|
1141
|
+
}
|
|
1142
|
+
function selectedInputFiles(root, files, inputs, ignoredRoots) {
|
|
1143
|
+
const selected = new Map(inputs.length === 0 || inputs.includes(".") ? files : []);
|
|
1144
|
+
for (const [file, digest] of explicitInputFiles(root, inputs.filter(input => input !== "."), ignoredRoots, files.keys()))
|
|
1145
|
+
selected.set(file, digest);
|
|
1146
|
+
return selected;
|
|
1147
|
+
}
|
|
1148
|
+
function workspaceInputFingerprint(root, files, inputs, ignoredRoots) {
|
|
1149
|
+
return inputFilesFingerprint(selectedInputFiles(root, files, inputs, ignoredRoots));
|
|
1150
|
+
}
|
|
1151
|
+
function inputFilesFingerprint(selected) {
|
|
1152
|
+
const hash = crypto.createHash("sha256");
|
|
1153
|
+
for (const [file, digest] of [...selected].sort(([left], [right]) => left.localeCompare(right)))
|
|
1154
|
+
hash.update(file).update("\0").update(digest).update("\0");
|
|
1155
|
+
return hash.digest("hex");
|
|
1156
|
+
}
|
|
245
1157
|
function changedPaths(before, after) { return [...new Set([...before.keys(), ...after.keys()])].filter((key) => before.get(key) !== after.get(key)).sort(); }
|
|
246
1158
|
function checkRef(check) { return check.canonical_ref ?? check.id; }
|
|
1159
|
+
function assertConsistentDeclarations(checks, commands) {
|
|
1160
|
+
const seen = new Map();
|
|
1161
|
+
for (const [index, check] of checks.entries()) {
|
|
1162
|
+
const contract = JSON.stringify([check.run_at, commands[index], [...(check.required_artifacts ?? [])].sort(), [...(check.ports ?? [])].sort(), normalizedInputs(check.inputs ?? []), check.reuse ?? null, normalizedEnvironmentInputs(check.environment_inputs), check.runner_evidence ?? null]);
|
|
1163
|
+
const ref = checkRef(check), prior = seen.get(ref);
|
|
1164
|
+
if (prior && prior !== contract)
|
|
1165
|
+
throw new AppError("check_declaration_conflict", "The same check reference has conflicting execution definitions", 2, { check_ref: ref });
|
|
1166
|
+
seen.set(ref, contract);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function normalizedEnvironmentInputs(value) {
|
|
1170
|
+
if (value === undefined)
|
|
1171
|
+
return undefined;
|
|
1172
|
+
if (!Array.isArray(value) || !value.length || value.length > 64 || value.some(name => typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*\*?$/.test(name)))
|
|
1173
|
+
throw new AppError("invalid_check_environment_inputs", "Internal check environment inputs must be bounded names or prefixes", 2, { effect: "no_effect" });
|
|
1174
|
+
return [...new Set(value)].sort();
|
|
1175
|
+
}
|
|
1176
|
+
function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
|
|
1177
|
+
const environment = codeExecutionEnvironment(workspaceRoot, checkEnvironment);
|
|
1178
|
+
// Invocation authority is checked by its own owners, not an application check
|
|
1179
|
+
// input. Preserve arbitrary application variables (including DD_FLOW_PORT_*)
|
|
1180
|
+
// and tool selection; omit only known process/control bookkeeping.
|
|
1181
|
+
const bookkeeping = new Set(["PWD", "OLDPWD", "SHLVL", "_", "DD_FLOW_DAEMON_ID",
|
|
1182
|
+
"DD_FLOW_CURRENT_INVOCATION", "DD_FLOW_INVOCATION_READONLY", "DD_FLOW_INVOCATION_SCOPE", "DD_FLOW_CODEX_STATE_DIR",
|
|
1183
|
+
"DD_FLOW_HOOK_DEADLINE_AT", // Per-call transport budget, not a project check input.
|
|
1184
|
+
"DD_FLOW_RUNTIME_OWNER", "DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN",
|
|
1185
|
+
"DD_FLOW_BIN", "DD_FLOW_ENGINE_HOME", "DD_FLOW_RUNTIME_BUDGET", "DD_FLOW_WORK_ID", "DD_FLOW_SESSION_ID", "CODEX_THREAD_ID", "DD_EVAL_OPERATION_ID",
|
|
1186
|
+
"CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION", "CODEX_TOOL_CALL_ID"]);
|
|
1187
|
+
return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
|
|
1188
|
+
.filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
|
|
1189
|
+
&& (name === "PLAYWRIGHT_BROWSERS_PATH" || inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
|
|
1190
|
+
.sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
|
|
1191
|
+
}
|
|
1192
|
+
function acquireWorkspaceGate(context, input) {
|
|
1193
|
+
const operationId = workspaceOperationId(input.workspaceRoot);
|
|
1194
|
+
const active = managedProcessStatus(context).find(item => item.operation_id === operationId && ["starting", "running", "stopping"].includes(item.state));
|
|
1195
|
+
if (active)
|
|
1196
|
+
throw new AppError("check_in_progress", "The workspace is owned by another bootstrap or check; observe it before starting a new check", 1, { process_id: active.id, operation_id: operationId });
|
|
1197
|
+
return registerManagedProcess(context, { kind: "workspace-gate", ownerPid: process.pid, ownerId: `workspace-gate:${crypto.randomUUID()}`, operationId, uniqueActiveOperation: true, projectId: input.projectId, runId: input.runId, metadata: { workspace_root: input.workspaceRoot, role: "check_gate" } });
|
|
1198
|
+
}
|
|
1199
|
+
export function reconcileWorkspaceOwnership(context, workspaceRoot) {
|
|
1200
|
+
const operationId = workspaceOperationId(workspaceRoot);
|
|
1201
|
+
const active = managedProcessStatus(context).filter(item => item.operation_id === operationId && ["starting", "running", "stopping", "orphaned"].includes(item.state));
|
|
1202
|
+
if (active.some(item => processTreeIsAlive(item) || managedOwnerIsAlive(item) !== false))
|
|
1203
|
+
return;
|
|
1204
|
+
// Registry IDs are host-wide, whereas receipts belong to one runtime DB.
|
|
1205
|
+
// An unknown/foreign home must not be settled against this client's rows.
|
|
1206
|
+
if (active.some(item => managedProcessRuntimeHome(item, context) !== context.ddFlowHome))
|
|
1207
|
+
return;
|
|
1208
|
+
for (const child of active.filter(item => item.kind !== "workspace-gate")) {
|
|
1209
|
+
if (child.kind !== "check" || !child.project_id || !child.run_id || !child.check_id)
|
|
1210
|
+
return;
|
|
1211
|
+
const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [child.project_id, child.run_id]);
|
|
1212
|
+
if (!run?.run_root || !context.db.get("SELECT 1 FROM check_receipts WHERE id = ? AND project_id = ? AND run_id = ?", [child.check_id, child.project_id, child.run_id]))
|
|
1213
|
+
return;
|
|
1214
|
+
reconcileUnfinishedChecks(context, { projectId: child.project_id, runId: child.run_id, workspaceRoot, runHome: run.run_root, receiptId: child.check_id });
|
|
1215
|
+
// A crash may occur after receipt settlement but before registry settlement.
|
|
1216
|
+
const current = managedProcessStatus(context).find(item => item.id === child.id);
|
|
1217
|
+
if (!current || current.lease_token !== child.lease_token)
|
|
1218
|
+
return;
|
|
1219
|
+
if (!["stopped", "failed"].includes(current.state) && !finishManagedProcess(context, { id: child.id, leaseToken: child.lease_token, state: "stopped", reason: "workspace_check_owner_reconciled" }))
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
for (const gate of active.filter(item => item.kind === "workspace-gate"))
|
|
1223
|
+
finishManagedProcess(context, { id: gate.id, leaseToken: gate.lease_token, state: "stopped", reason: "workspace_gate_owner_reconciled" });
|
|
1224
|
+
}
|
|
1225
|
+
function normalizedInputs(inputs) { for (const input of inputs)
|
|
1226
|
+
validateCheckInputSyntax(input); return [...new Set(inputs.map((input) => { const normalized = path.posix.normalize(input.replaceAll("\\", "/")).replace(/^\.\/$/, ""); return normalized === "" || normalized === "." ? "." : normalized.replace(/\/$/, ""); }))].sort(); }
|
|
1227
|
+
function validateCheckInputs(workspaceRoot, inputs) { const root = fs.realpathSync.native(workspaceRoot); const allowed = sourceInputPathFilter(workspaceRoot, inputs); for (const input of normalizedInputs(inputs)) {
|
|
1228
|
+
if (input.split("/").some(part => [".git", "node_modules"].includes(part)))
|
|
1229
|
+
throw new AppError("invalid_check_input", "Check inputs cannot include Git internals or installed dependencies; declare lock/config inputs", 2, { input });
|
|
1230
|
+
const candidate = path.resolve(workspaceRoot, input);
|
|
1231
|
+
if (candidate !== workspaceRoot && !candidate.startsWith(`${workspaceRoot}${path.sep}`))
|
|
1232
|
+
throw new AppError("invalid_check_input", "Check inputs must stay within the workspace", 2, { input });
|
|
1233
|
+
if (fs.existsSync(candidate))
|
|
1234
|
+
validateInputTree(root, candidate, input, new Set(), allowed);
|
|
1235
|
+
else
|
|
1236
|
+
validateMissingInputParent(root, candidate, input);
|
|
1237
|
+
} }
|
|
1238
|
+
/** Directory scopes select source inventory, not generated Git-ignored output.
|
|
1239
|
+
* Direct ignored inputs (e.g. a real config) remain explicit obligations. */
|
|
1240
|
+
function sourceInputPathFilter(root, inputs, sourceFiles) {
|
|
1241
|
+
const listing = sourceFiles ? [...sourceFiles] : workspaceGitFiles(root, ["-co", "--exclude-standard"]);
|
|
1242
|
+
const retained = new Set([...(listing ?? []), ...inputs]), branches = new Set();
|
|
1243
|
+
for (const file of retained)
|
|
1244
|
+
for (let parent = path.posix.dirname(file); parent !== "." && parent !== "/"; parent = path.posix.dirname(parent))
|
|
1245
|
+
branches.add(parent);
|
|
1246
|
+
const explicitTrees = inputs.filter(input => !listing?.some(file => file === input || file.startsWith(`${input}/`)));
|
|
1247
|
+
const serviceAllowed = projectServicePathFilter(root, inputs, listing ?? []);
|
|
1248
|
+
return file => {
|
|
1249
|
+
const relative = path.relative(root, file).split(path.sep).join("/");
|
|
1250
|
+
if (relative.split("/").some(part => [".git", "node_modules"].includes(part)) || !serviceAllowed(file))
|
|
1251
|
+
return false;
|
|
1252
|
+
return listing === null || retained.has(relative) || branches.has(relative) || explicitTrees.some(input => relative.startsWith(`${input}/`));
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function validateCheckInputSyntax(input) { if (typeof input !== "string" || !input.trim() || path.isAbsolute(input) || input.split(/[\\/]/).includes(".."))
|
|
1256
|
+
throw new AppError("invalid_check_input", "Check inputs must be non-empty relative workspace paths", 2, { input }); }
|
|
1257
|
+
function validateMissingInputParent(root, candidate, input) { let parent = candidate; while (!fs.existsSync(parent))
|
|
1258
|
+
parent = path.dirname(parent); const resolved = fs.realpathSync.native(parent); if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
|
|
1259
|
+
throw new AppError("invalid_check_input", "Check input parent symlink escapes the workspace", 2, { input }); }
|
|
1260
|
+
function validateInputTree(root, candidate, input, seen, allowed) {
|
|
1261
|
+
const resolved = fs.realpathSync.native(candidate);
|
|
1262
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
|
|
1263
|
+
throw new AppError("invalid_check_input", "Check input symlink escapes the workspace", 2, { input });
|
|
1264
|
+
if (seen.has(resolved))
|
|
1265
|
+
throw new AppError("invalid_check_input", "Check input contains a symbolic-link cycle", 2, { input });
|
|
1266
|
+
seen.add(resolved);
|
|
1267
|
+
const stat = fs.statSync(candidate);
|
|
1268
|
+
if (stat.isDirectory() && fs.lstatSync(candidate).isSymbolicLink())
|
|
1269
|
+
throw new AppError("invalid_check_input", "Directory symlinks cannot prove scoped source inputs; declare their contained real source path instead", 2, { input });
|
|
1270
|
+
if (stat.isDirectory())
|
|
1271
|
+
for (const entry of fs.readdirSync(candidate)) {
|
|
1272
|
+
const file = path.join(candidate, entry);
|
|
1273
|
+
if (allowed(file))
|
|
1274
|
+
validateInputTree(root, file, input, seen, allowed);
|
|
1275
|
+
}
|
|
1276
|
+
seen.delete(resolved);
|
|
1277
|
+
}
|
|
1278
|
+
function explicitInputFiles(root, inputs, ignoredRoots, sourceFiles) {
|
|
1279
|
+
const rootReal = fs.realpathSync.native(root), ignored = ignoredRoots.map(entry => path.resolve(entry)), result = new Map();
|
|
1280
|
+
const allowed = sourceInputPathFilter(root, inputs, sourceFiles);
|
|
1281
|
+
const visit = (file, relative, seen) => {
|
|
1282
|
+
if (ignored.some(entry => file === entry || file.startsWith(`${entry}${path.sep}`)))
|
|
1283
|
+
return;
|
|
1284
|
+
if (!fs.existsSync(file)) {
|
|
1285
|
+
result.set(relative, "<missing>");
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const resolved = fs.realpathSync.native(file);
|
|
1289
|
+
if (resolved !== rootReal && !resolved.startsWith(`${rootReal}${path.sep}`))
|
|
1290
|
+
throw new AppError("invalid_check_input", "Check input symlink escapes the workspace", 2, { input: relative });
|
|
1291
|
+
if (seen.has(resolved))
|
|
1292
|
+
throw new AppError("invalid_check_input", "Check input contains a symbolic-link cycle", 2, { input: relative });
|
|
1293
|
+
const symbolic = fs.lstatSync(file).isSymbolicLink();
|
|
1294
|
+
const stat = fs.statSync(file);
|
|
1295
|
+
if (symbolic && stat.isDirectory())
|
|
1296
|
+
throw new AppError("invalid_check_input", "Directory symlinks cannot prove scoped source inputs; declare their contained real source path instead", 2, { input: relative });
|
|
1297
|
+
if (stat.isDirectory()) {
|
|
1298
|
+
seen.add(resolved);
|
|
1299
|
+
for (const entry of fs.readdirSync(file).sort()) {
|
|
1300
|
+
const child = path.join(file, entry);
|
|
1301
|
+
if (allowed(child))
|
|
1302
|
+
visit(child, path.posix.join(relative, entry), seen);
|
|
1303
|
+
}
|
|
1304
|
+
seen.delete(resolved);
|
|
1305
|
+
}
|
|
1306
|
+
else if (stat.isFile())
|
|
1307
|
+
result.set(relative, crypto.createHash("sha256").update(symbolic ? `symlink\0${fs.readlinkSync(file)}\0` : "").update(fs.readFileSync(file)).digest("hex"));
|
|
1308
|
+
};
|
|
1309
|
+
for (const input of inputs)
|
|
1310
|
+
visit(path.resolve(root, input), input, new Set());
|
|
1311
|
+
return [...result.entries()];
|
|
1312
|
+
}
|
|
1313
|
+
function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment), input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
|
|
1314
|
+
function derivedReceiptArtifacts(group, receipt) { return [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(receipt.runner_evidence), ...(receipt.required_artifacts.includes("engine-outcome.json") ? ["engine-outcome.json"] : [])])]; }
|
|
1315
|
+
function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: derivedReceiptArtifacts(group, receipt), inputs: group.inputs }; }
|
|
1316
|
+
export function checkReceiptEvidenceValid(receipt) { return checkReceiptEvidenceValidForMode(receipt); }
|
|
1317
|
+
function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
1318
|
+
try {
|
|
1319
|
+
const staleDiagnostic = mode === "diagnostic" && receipt.fingerprint_scope === "check_inputs" && receipt.status === "failed" && Number.isInteger(receipt.exit_code)
|
|
1320
|
+
&& receipt.before_fingerprint !== receipt.after_fingerprint && receipt.mutation_paths.length > 0 && receipt.execution_failures?.some(failure => failure.code === "check_inputs_changed");
|
|
1321
|
+
if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
|
|
1322
|
+
return false;
|
|
1323
|
+
if (receipt.fingerprint_scope === "check_inputs") {
|
|
1324
|
+
const completion = readCompletion(receipt.completion_path);
|
|
1325
|
+
if (!completion || completion.fingerprint_scope !== "check_inputs" || completion.id !== receipt.id || completion.input_hash !== receipt.input_hash
|
|
1326
|
+
|| completion.verification_epoch !== receipt.verification_epoch || completion.before_fingerprint !== receipt.before_fingerprint || completion.after_fingerprint !== receipt.after_fingerprint
|
|
1327
|
+
|| JSON.stringify(completion.input_manifest ?? null) !== JSON.stringify(receipt.input_manifest ?? null))
|
|
1328
|
+
return false;
|
|
1329
|
+
if (!receipt.input_manifest || receipt.input_manifest.path !== "inputs-before.json")
|
|
1330
|
+
return false;
|
|
1331
|
+
const manifestBytes = readBoundedReleaseFile(path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path), 1024 * 1024);
|
|
1332
|
+
if (crypto.createHash("sha256").update(manifestBytes).digest("hex") !== receipt.input_manifest.sha256)
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
if (!(receipt.status === "passed" && receipt.exit_code === 0 || mode === "diagnostic" && receipt.status === "failed" && typeof receipt.exit_code === "number" && receipt.exit_code !== 0 || staleDiagnostic) || !staleDiagnostic && receipt.before_fingerprint !== receipt.after_fingerprint
|
|
1336
|
+
|| !staleDiagnostic && receipt.mutation_paths.length !== 0 || receipt.completion_path !== path.join(path.dirname(receipt.receipt_path), "completion.json")
|
|
1337
|
+
|| readCompletion(receipt.completion_path)?.exit_code !== receipt.exit_code)
|
|
1338
|
+
return false;
|
|
1339
|
+
const stored = readReceipt(receipt.receipt_path);
|
|
1340
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored)
|
|
1341
|
+
|| typeof stored.receipt_path !== "string" || !path.isAbsolute(stored.receipt_path)
|
|
1342
|
+
|| path.basename(stored.receipt_path) !== "receipt.json"
|
|
1343
|
+
|| stored.completion_path !== path.join(path.dirname(stored.receipt_path), "completion.json"))
|
|
1344
|
+
return false;
|
|
1345
|
+
// Gate/ref labels may be rebound for explicit deterministic reuse. Physical
|
|
1346
|
+
// execution identity and settled SQL evidence must never be rebound from a
|
|
1347
|
+
// mutable receipt file, including after recovery relocates the RUN home.
|
|
1348
|
+
const fields = ["fingerprint_scope", "execution_contract_hash", "id", "scope", "work_id", "command", "input_hash", "verification_epoch", "status", "exit_code",
|
|
1349
|
+
"workspace_fingerprint", "before_fingerprint", "after_fingerprint", "profile_hash", "started_at", "finished_at"];
|
|
1350
|
+
if (JSON.stringify(stored.input_manifest ?? null) !== JSON.stringify(receipt.input_manifest ?? null))
|
|
1351
|
+
return false;
|
|
1352
|
+
if (fields.some(field => (stored[field] ?? null) !== (receipt[field] ?? null)) || !Array.isArray(stored.artifacts)
|
|
1353
|
+
|| !Array.isArray(stored.mutation_paths) || !isDeepStrictEqual(stored.mutation_paths, receipt.mutation_paths)
|
|
1354
|
+
|| JSON.stringify(stored.required_artifacts) !== JSON.stringify(receipt.required_artifacts)
|
|
1355
|
+
|| JSON.stringify(stored.inputs) !== JSON.stringify(receipt.inputs))
|
|
1356
|
+
return false;
|
|
1357
|
+
const retainedProof = resolvedReceiptArtifacts(receipt.receipt_path, stored.artifacts, receipt);
|
|
1358
|
+
const retained = retainedProof.artifacts;
|
|
1359
|
+
if (JSON.stringify(retained) !== JSON.stringify(receipt.artifacts))
|
|
1360
|
+
return false;
|
|
1361
|
+
// Receipt annotations are not execution authority. Compare the SQL-owned
|
|
1362
|
+
// failures with the independently hash/binding-checked engine sidecar.
|
|
1363
|
+
const hasEngineOutcome = receipt.required_artifacts.includes("engine-outcome.json") || receipt.artifacts.some(artifact => artifact.path === "engine-outcome.json" || artifact.path === path.join(path.dirname(receipt.receipt_path), "artifacts", "engine-outcome.json"));
|
|
1364
|
+
if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? [])))
|
|
1365
|
+
return false;
|
|
1366
|
+
const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
|
|
1367
|
+
if (stored.runner_evidence_required && mode !== "diagnostic" && !runnerEvidencePass(readRunnerEvidence(root, stored.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch })))
|
|
1368
|
+
return false;
|
|
1369
|
+
const artifacts = collectCheckArtifacts(root, receipt.required_artifacts);
|
|
1370
|
+
return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(retained);
|
|
1371
|
+
}
|
|
1372
|
+
catch {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
247
1376
|
function parseArray(value, fallback) { try {
|
|
248
1377
|
const parsed = JSON.parse(value);
|
|
249
1378
|
return Array.isArray(parsed) && parsed.every((item) => typeof item === "string") ? parsed : fallback;
|
|
@@ -252,8 +1381,14 @@ catch {
|
|
|
252
1381
|
return fallback;
|
|
253
1382
|
} }
|
|
254
1383
|
function isStringRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((item) => typeof item === "string" && item.trim().length > 0); }
|
|
255
|
-
function isGateRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([gate, aliases]) => ["work", "code", "readiness", "merge", "release"
|
|
1384
|
+
function isGateRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([gate, aliases]) => ["work", "code", "readiness", "merge", "release"].includes(gate) && Array.isArray(aliases) && aliases.every((item) => typeof item === "string" && item.startsWith("@check/"))); }
|
|
256
1385
|
function isPortAliasRecord(value) { return value === undefined || (Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([alias, ports]) => alias.startsWith("@check/") && Array.isArray(ports) && ports.length > 0 && new Set(ports).size === ports.length && ports.every((port) => typeof port === "string" && /^[A-Za-z][A-Za-z0-9_]*$/.test(port)))); }
|
|
1386
|
+
function isInputAliasRecord(workspaceRoot, value) { try {
|
|
1387
|
+
return value === undefined || (Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([alias, inputs]) => alias.startsWith("@check/") && Array.isArray(inputs) && (validateCheckInputs(workspaceRoot, inputs), true)));
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
return false;
|
|
1391
|
+
} }
|
|
257
1392
|
function portEnvironment(ports) { return Object.fromEntries(Object.entries(ports).map(([name, value]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(value)])); }
|
|
258
1393
|
function readReceiptResources(file) { try {
|
|
259
1394
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -262,88 +1397,235 @@ function readReceiptResources(file) { try {
|
|
|
262
1397
|
catch {
|
|
263
1398
|
return { ports: {} };
|
|
264
1399
|
} }
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
1400
|
+
function collectCheckArtifacts(root, required) {
|
|
1401
|
+
for (const relative of required) {
|
|
1402
|
+
if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))
|
|
1403
|
+
throw new AppError("invalid_evidence_path", "Required evidence paths must be relative to DD_FLOW_EVIDENCE_DIR", 2, { path: relative });
|
|
1404
|
+
}
|
|
1405
|
+
// Required paths are minimum obligations; retain all explicit check evidence.
|
|
1406
|
+
// Never follow symlinks, including a replaced evidence root.
|
|
1407
|
+
const items = [];
|
|
1408
|
+
const visit = (directory) => {
|
|
1409
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
1410
|
+
const file = path.join(directory, entry.name);
|
|
1411
|
+
if (entry.isDirectory())
|
|
1412
|
+
visit(file);
|
|
1413
|
+
else if (entry.isFile())
|
|
1414
|
+
items.push({ path: path.relative(root, file), sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") });
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
if (fs.existsSync(root) && fs.lstatSync(root).isDirectory())
|
|
1418
|
+
visit(root);
|
|
1419
|
+
items.sort((a, b) => a.path.localeCompare(b.path));
|
|
1420
|
+
const retained = new Set(items.map((item) => item.path));
|
|
1421
|
+
const missing = required.filter((relative) => !retained.has(path.normalize(relative)));
|
|
1422
|
+
return { complete: missing.length === 0, missing, items };
|
|
1423
|
+
}
|
|
1424
|
+
function listFiles(root) {
|
|
1425
|
+
const allowed = projectServicePathFilter(root, [], []);
|
|
1426
|
+
const ownedHook = ownedWorkspaceHookPath(root, []);
|
|
1427
|
+
const visit = (current) => fs.readdirSync(current, { withFileTypes: true }).flatMap(entry => {
|
|
1428
|
+
const absolute = path.join(current, entry.name);
|
|
1429
|
+
if ([".git", "node_modules"].includes(entry.name) || !allowed(absolute))
|
|
1430
|
+
return [];
|
|
1431
|
+
return entry.isDirectory() ? visit(absolute) : [path.relative(root, absolute)];
|
|
1432
|
+
});
|
|
1433
|
+
return visit(root).filter(file => file.split(path.sep).join("/") !== ownedHook).sort();
|
|
1434
|
+
}
|
|
1435
|
+
export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, onSpawn, renewLease, observation = {}) {
|
|
1436
|
+
const inactivityTimeoutMs = validateObservationDuration(observation.inactivityTimeoutMs ?? 30 * 60_000);
|
|
1437
|
+
const { startupMs: runnerStartupMs, executionMs: runnerExecutionMs } = runnerObservationDurations(environment.DD_FLOW_CHECK_RUNNER_PROGRESS_PATH ? environment : {});
|
|
278
1438
|
return new Promise((resolve) => {
|
|
279
1439
|
const started = Date.now();
|
|
280
|
-
const child = spawn("/bin/sh", ["-
|
|
1440
|
+
const child = spawn("/bin/sh", ["-c", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
281
1441
|
let error = null;
|
|
1442
|
+
let failure;
|
|
282
1443
|
let stopping = false;
|
|
1444
|
+
let closed = false;
|
|
1445
|
+
let admitted = false;
|
|
283
1446
|
let progress;
|
|
284
|
-
let
|
|
1447
|
+
let cleanup;
|
|
1448
|
+
let cleanupUnconfirmed = false;
|
|
1449
|
+
const renewal = createRenewalState();
|
|
1450
|
+
let renewalTimer;
|
|
1451
|
+
const clock = new ObservationClock({ timeoutMs: inactivityTimeoutMs });
|
|
1452
|
+
let outputCursor = 0;
|
|
1453
|
+
let runnerSequence = 0;
|
|
1454
|
+
let runnerPhase = "startup";
|
|
1455
|
+
const runnerProgress = environment.DD_FLOW_CHECK_RUNNER_PROGRESS_PATH;
|
|
1456
|
+
const runnerClock = runnerProgress ? new ObservationClock({ timeoutMs: runnerStartupMs }) : null;
|
|
1457
|
+
const outputs = [fs.createWriteStream("", { fd: stdout, autoClose: false }), fs.createWriteStream("", { fd: stderr, autoClose: false })];
|
|
1458
|
+
const flushed = outputs.map(output => new Promise(resolve => { output.once("finish", resolve); output.once("error", () => resolve()); }));
|
|
285
1459
|
const stop = (cause) => {
|
|
286
1460
|
if (stopping)
|
|
287
1461
|
return;
|
|
288
1462
|
stopping = true;
|
|
289
1463
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
1464
|
+
if (cause instanceof AppError)
|
|
1465
|
+
failure = { code: cause.code, message: cause.message };
|
|
290
1466
|
if (progress) {
|
|
291
1467
|
clearInterval(progress);
|
|
292
1468
|
progress = undefined;
|
|
293
1469
|
}
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1470
|
+
if (renewalTimer)
|
|
1471
|
+
clearTimeout(renewalTimer);
|
|
1472
|
+
if (observationTimer)
|
|
1473
|
+
clearInterval(observationTimer);
|
|
1474
|
+
cleanup = stopProcessGroup(child, 5_000).catch(cause => {
|
|
1475
|
+
cleanupUnconfirmed = true;
|
|
1476
|
+
error = `${error}; cleanup: ${JSON.stringify(errorRecord(cause))}`;
|
|
1477
|
+
resolve({ exitCode: null, error, aborted: true, cleanupUnconfirmed, ...(failure ? { failure } : {}) });
|
|
1478
|
+
});
|
|
298
1479
|
};
|
|
1480
|
+
const observationTimer = setInterval(() => {
|
|
1481
|
+
if (stopping || closed || !admitted || observation.phase?.() === "serving")
|
|
1482
|
+
return;
|
|
1483
|
+
if (runnerClock && runnerProgress) {
|
|
1484
|
+
try {
|
|
1485
|
+
for (const event of readRunnerProgress(runnerProgress, environment.DD_FLOW_CHECK_INVOCATION_ID ?? "", runnerSequence)) {
|
|
1486
|
+
runnerSequence = event.sequence;
|
|
1487
|
+
if (runnerPhase !== "execution")
|
|
1488
|
+
runnerPhase = event.phase;
|
|
1489
|
+
runnerClock.sample(runnerSequence, Date.parse(event.at));
|
|
1490
|
+
}
|
|
1491
|
+
// The outer guard includes the runner's bounded cleanup tail.
|
|
1492
|
+
runnerClock.timeoutMs = runnerPhase === "execution" ? runnerExecutionMs : runnerStartupMs;
|
|
1493
|
+
if (runnerClock.sample(runnerSequence))
|
|
1494
|
+
stop(new AppError("operation_observation_lost", "Structured test runner stopped making progress", 1, { phase: runnerPhase, sequence: runnerSequence }));
|
|
1495
|
+
}
|
|
1496
|
+
catch (cause) {
|
|
1497
|
+
stop(cause);
|
|
1498
|
+
}
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (clock.sample(outputCursor))
|
|
1502
|
+
stop(new AppError("operation_observation_lost", "Owned command has no confirmed progress within its inactivity window", 1, { phase: observation.phase?.() ?? "finite", inactivity_timeout_ms: inactivityTimeoutMs }));
|
|
1503
|
+
}, Math.min(1000, inactivityTimeoutMs));
|
|
299
1504
|
// Register handlers before checking pid or calling persistence callbacks.
|
|
300
1505
|
// Failure is not completion until the spawned process has actually closed.
|
|
301
1506
|
child.once("error", (value) => { error = value.message; });
|
|
302
|
-
child.
|
|
1507
|
+
for (const [index, stream] of [child.stdout, child.stderr].entries()) {
|
|
1508
|
+
outputs[index].once("error", stop);
|
|
1509
|
+
stream.on("data", () => { if (!runnerClock) {
|
|
1510
|
+
outputCursor++;
|
|
1511
|
+
clock.sample(outputCursor);
|
|
1512
|
+
} });
|
|
1513
|
+
stream.pipe(outputs[index]);
|
|
1514
|
+
}
|
|
1515
|
+
child.stdin.on("error", stop);
|
|
1516
|
+
child.once("close", async (exitCode, signal) => {
|
|
1517
|
+
closed = true;
|
|
303
1518
|
if (progress)
|
|
304
1519
|
clearInterval(progress);
|
|
305
|
-
if (
|
|
306
|
-
clearTimeout(
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
1520
|
+
if (renewalTimer)
|
|
1521
|
+
clearTimeout(renewalTimer);
|
|
1522
|
+
if (observationTimer)
|
|
1523
|
+
clearInterval(observationTimer);
|
|
1524
|
+
await cleanup;
|
|
1525
|
+
await Promise.all(flushed);
|
|
1526
|
+
resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error), ...(cleanupUnconfirmed ? { cleanupUnconfirmed } : {}), ...(failure ? { failure } : {}) });
|
|
310
1527
|
});
|
|
311
1528
|
if (!child.pid) {
|
|
312
1529
|
error = "check process did not provide a PID";
|
|
313
1530
|
return;
|
|
314
1531
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
progress = setInterval(() => {
|
|
1532
|
+
const renew = () => {
|
|
1533
|
+
if (stopping || closed)
|
|
1534
|
+
return;
|
|
1535
|
+
if (renewalTimer) {
|
|
1536
|
+
clearTimeout(renewalTimer);
|
|
1537
|
+
renewalTimer = undefined;
|
|
1538
|
+
}
|
|
323
1539
|
try {
|
|
324
|
-
|
|
1540
|
+
renewal.uncertainSince ??= performance.now();
|
|
1541
|
+
const remaining = renewalRemaining(renewal);
|
|
1542
|
+
if (!remaining) {
|
|
1543
|
+
stop("managed process renewal budget exhausted");
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
const receipt = renewLease(Math.min(RENEWAL_POLICY.attemptMs, remaining));
|
|
1547
|
+
if (receipt === false || typeof receipt === "object" && !receipt.owned) {
|
|
325
1548
|
stop("managed process lease lost");
|
|
326
1549
|
return;
|
|
327
1550
|
}
|
|
1551
|
+
if (typeof receipt === "object" && receipt.owned) {
|
|
1552
|
+
const expiry = Date.parse(receipt.lease_expires_at);
|
|
1553
|
+
if (!Number.isFinite(expiry))
|
|
1554
|
+
throw new AppError("managed_lease_receipt_invalid", "Check renewal returned invalid expiry", 1);
|
|
1555
|
+
if (expiry <= Date.now())
|
|
1556
|
+
throw Object.assign(new Error("Check renewal receipt expired before acknowledgement"), { code: "process_maintenance_timeout" });
|
|
1557
|
+
}
|
|
1558
|
+
if (!renewalConfirmed(renewal, performance.now(), typeof receipt === "object" && receipt.owned ? receipt.lease_expires_at : null)) {
|
|
1559
|
+
stop("managed process renewal budget exhausted");
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
catch (cause) {
|
|
1564
|
+
if (!maintenanceRetryable(cause) || !renewalFailure(renewal)) {
|
|
1565
|
+
stop(cause);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
renewalTimer = setTimeout(renew, Math.min(renewalDelay(renewal), renewalRemaining(renewal)));
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
// A progress callback error is not a retryable renewal failure.
|
|
1572
|
+
try {
|
|
328
1573
|
heartbeat(Math.floor((Date.now() - started) / 1000));
|
|
329
1574
|
}
|
|
330
1575
|
catch (cause) {
|
|
331
1576
|
stop(cause);
|
|
332
1577
|
}
|
|
333
|
-
}
|
|
1578
|
+
};
|
|
1579
|
+
// The shell is held on stdin: a no-effect identity read may be retried,
|
|
1580
|
+
// but never the command or an admission callback with unknown effects.
|
|
1581
|
+
const confirmation = createRenewalState();
|
|
1582
|
+
confirmation.uncertainSince = performance.now();
|
|
1583
|
+
let confirmationError;
|
|
1584
|
+
const admit = () => {
|
|
1585
|
+
renewalTimer = undefined;
|
|
1586
|
+
if (stopping || closed)
|
|
1587
|
+
return;
|
|
1588
|
+
if (confirmationError && !renewalRemaining(confirmation)) {
|
|
1589
|
+
stop(confirmationError);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
try {
|
|
1593
|
+
onSpawn(child.pid);
|
|
1594
|
+
}
|
|
1595
|
+
catch (cause) {
|
|
1596
|
+
if (cause instanceof AppError && cause.code === "process_ownership_unknown"
|
|
1597
|
+
&& cause.details.reason === "physical_identity_unavailable" && maintenanceRetryable(cause)
|
|
1598
|
+
&& renewalFailure(confirmation)) {
|
|
1599
|
+
confirmationError = cause;
|
|
1600
|
+
renewalTimer = setTimeout(admit, Math.min(renewalDelay(confirmation), renewalRemaining(confirmation)));
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
stop(cause);
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (observation.leaseExpiresAt) {
|
|
1607
|
+
const expiry = observation.leaseExpiresAt();
|
|
1608
|
+
if (!expiry || !renewalConfirmed(renewal, performance.now(), expiry)) {
|
|
1609
|
+
stop("check confirmation has no current authority");
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
admitted = true;
|
|
1614
|
+
child.stdin.end("admitted\n");
|
|
1615
|
+
clock.sample(++outputCursor);
|
|
1616
|
+
progress = setInterval(renew, 15_000);
|
|
1617
|
+
};
|
|
1618
|
+
admit();
|
|
334
1619
|
});
|
|
335
1620
|
}
|
|
336
1621
|
function checkShellScript() {
|
|
337
1622
|
return [
|
|
1623
|
+
// The actual command cannot run before PID ownership and admission persist.
|
|
1624
|
+
'IFS= read -r admission && [ "$admission" = admitted ] || exit 125',
|
|
338
1625
|
'completion="${DD_FLOW_CHECK_COMPLETION_FILE:-}"',
|
|
339
|
-
'/bin/sh -
|
|
1626
|
+
'/bin/sh -c "$DD_FLOW_CHECK_COMMAND"',
|
|
340
1627
|
"code=$?",
|
|
341
1628
|
'if [ -n "$completion" ]; then temporary="$completion.tmp.$$"; printf \'{"exit_code":%s,"finished_at":"%s"}\\n\' "$code" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$temporary" && mv "$temporary" "$completion"; fi',
|
|
342
1629
|
'exit "$code"'
|
|
343
1630
|
].join("\n");
|
|
344
1631
|
}
|
|
345
|
-
function terminateProcessGroup(pid, signal) { if (!pid)
|
|
346
|
-
return; try {
|
|
347
|
-
process.kill(process.platform === "win32" ? pid : -pid, signal);
|
|
348
|
-
}
|
|
349
|
-
catch { /* already exited */ } }
|