@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138
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 +12 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +56 -3
- package/dist/cli/command-option-descriptions.js +5 -0
- package/dist/cli/executor-onboarding.js +8 -3
- package/dist/cli/flows.js +29 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/response-file.js +2 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +9 -7
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- 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/opencode-plugin.mjs +5 -1
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -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 +22 -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/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 +49 -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/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +79 -13
- package/dist/services/dashboard.js +8 -2
- 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 +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +31 -3
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +7 -4
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -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 +98 -0
- package/dist/services/memory-init-start.js +175 -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 +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -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 +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -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 +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -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 +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +184 -48
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +76 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +136 -10
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/vnext-protocolize.js +60 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations } from "./check-runner-evidence.js";
|
|
2
|
+
import { candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations } from "./check-runner-evidence.js";
|
|
3
3
|
export { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
|
|
4
4
|
import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -15,6 +15,7 @@ import { createRenewalState, maintenanceRetryable, renewalConfirmed, renewalDela
|
|
|
15
15
|
import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, managedProcessRuntimeHome, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
|
|
16
16
|
import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
|
|
17
17
|
import { getResourceDatabase } from "../storage/database.js";
|
|
18
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
18
19
|
const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
|
|
19
20
|
// Receipt reservation precedes child spawn so an interrupted CLI leaves an
|
|
20
21
|
// auditable attempt. A second caller must not mistake that small handoff window
|
|
@@ -59,7 +60,11 @@ export function terminalCheckSettled(context, record) {
|
|
|
59
60
|
resource.close?.();
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
export function codeExecutionEnvironment(workspaceRoot) {
|
|
63
|
+
export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
|
|
64
|
+
if (checkEnvironment !== undefined && (!checkEnvironment || typeof checkEnvironment !== "object" || Array.isArray(checkEnvironment)
|
|
65
|
+
|| Object.keys(checkEnvironment).length !== 1 || typeof checkEnvironment.CODEX_HOME !== "string"
|
|
66
|
+
|| checkEnvironment.CODEX_HOME.length > 4096 || checkEnvironment.CODEX_HOME.includes("\0") || !path.isAbsolute(checkEnvironment.CODEX_HOME)))
|
|
67
|
+
throw new AppError("invalid_check_environment", "Owned check environment must name one absolute Codex home", 2, { effect: "no_effect" });
|
|
63
68
|
const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
|
|
64
69
|
// The managed CLI's Node must win over a provider app's PATH: a Rosetta
|
|
65
70
|
// provider can otherwise run x64 checks against arm64-installed modules.
|
|
@@ -67,6 +72,8 @@ export function codeExecutionEnvironment(workspaceRoot) {
|
|
|
67
72
|
delete environment.NODE_PATH;
|
|
68
73
|
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"])
|
|
69
74
|
delete environment[key];
|
|
75
|
+
if (checkEnvironment)
|
|
76
|
+
environment.CODEX_HOME = checkEnvironment.CODEX_HOME;
|
|
70
77
|
return environment;
|
|
71
78
|
}
|
|
72
79
|
export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
|
|
@@ -172,6 +179,10 @@ export function resolveCodeCheckCommands(workspaceRoot, runId, commands) {
|
|
|
172
179
|
}
|
|
173
180
|
export function projectPolicyCheckDeclarations(workspaceRoot, gate) {
|
|
174
181
|
const { profile } = readCodeCheckProfile(workspaceRoot);
|
|
182
|
+
return projectPolicyCheckDeclarationsFromProfile(profile, gate);
|
|
183
|
+
}
|
|
184
|
+
/** The caller may already have independently admitted exact profile bytes. */
|
|
185
|
+
export function projectPolicyCheckDeclarationsFromProfile(profile, gate) {
|
|
175
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] } : {}) }; });
|
|
176
187
|
}
|
|
177
188
|
export function effectiveCheckDeclarations(workspaceRoot, declared, gates) { return gates.flatMap((gate) => [...declared.filter((check) => check.run_at === gate), ...projectPolicyCheckDeclarations(workspaceRoot, gate)]); }
|
|
@@ -182,6 +193,8 @@ export function validateCheckPlacement(workspaceRoot, checks) { const { profile
|
|
|
182
193
|
if (check.run_at === "work" && aggregateCommands.has(check.command))
|
|
183
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 }); }
|
|
184
195
|
export function validateCheckDeclaration(workspaceRoot, check) {
|
|
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 });
|
|
185
198
|
if (!["work", "code", "readiness", "merge", "release"].includes(check.run_at))
|
|
186
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 });
|
|
187
200
|
if (check.runner_evidence !== undefined && !runnerContractsValid({ "@check/declaration": check.runner_evidence }))
|
|
@@ -256,7 +269,216 @@ function assertDirectCheckExecutable(workspaceRoot, command, checkId) {
|
|
|
256
269
|
if (probe.status !== 0)
|
|
257
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 });
|
|
258
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
|
+
const evidenceDigest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
279
|
+
/** Prepares current physical evidence outside a writer and retains an exact
|
|
280
|
+
* SQL-only fence. Declarations must come from the owning handler/policy, never
|
|
281
|
+
* from the result whose acceptance is being decided. This does not supply
|
|
282
|
+
* semantic acceptance or choose a memory verification policy.
|
|
283
|
+
*/
|
|
284
|
+
export class PreparedCheckEvidence {
|
|
285
|
+
#input;
|
|
286
|
+
#snapshot;
|
|
287
|
+
#generation;
|
|
288
|
+
#references;
|
|
289
|
+
#artifacts;
|
|
290
|
+
#completions = [];
|
|
291
|
+
#passed;
|
|
292
|
+
constructor(input, snapshot, generation, references, artifacts, passed) {
|
|
293
|
+
this.#input = structuredClone({ projectId: input.projectId, runId: input.runId, runHome: input.runHome, workspaceRoot: input.workspaceRoot, scope: input.scope, checks: input.checks,
|
|
294
|
+
...(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 }) });
|
|
295
|
+
this.#snapshot = snapshot;
|
|
296
|
+
this.#generation = generation;
|
|
297
|
+
this.#references = references;
|
|
298
|
+
this.#artifacts = artifacts;
|
|
299
|
+
this.#passed = passed;
|
|
300
|
+
Object.freeze(this);
|
|
301
|
+
}
|
|
302
|
+
static prepare(context, input) {
|
|
303
|
+
validateCheckEvidenceContext(input.evidenceContext);
|
|
304
|
+
const fail = () => { throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, { effect: "no_effect" }); };
|
|
305
|
+
if (!input.checks.length || (input.scope === "work") !== Boolean(input.workId) || input.mode !== undefined && input.mode !== "diagnostic")
|
|
306
|
+
fail();
|
|
307
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
308
|
+
const snapshot = checkEvidenceSqlSnapshot(context, input);
|
|
309
|
+
const selected = JSON.parse(snapshot);
|
|
310
|
+
if (!selected.run || fs.realpathSync.native(selected.run.workspace_root) !== fs.realpathSync.native(input.workspaceRoot)
|
|
311
|
+
|| fs.realpathSync.native(selected.run.run_root ?? selected.run.run_dir) !== fs.realpathSync.native(input.runHome))
|
|
312
|
+
fail();
|
|
313
|
+
const rows = selected.receipts;
|
|
314
|
+
const root = fs.realpathSync.native(input.runHome);
|
|
315
|
+
for (const row of rows) {
|
|
316
|
+
const relative = path.relative(path.resolve(input.runHome), row.receipt_path);
|
|
317
|
+
if (!path.isAbsolute(row.receipt_path) || relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative))
|
|
318
|
+
fail();
|
|
319
|
+
if (fs.existsSync(row.receipt_path)) {
|
|
320
|
+
const physical = path.relative(root, fs.realpathSync.native(row.receipt_path));
|
|
321
|
+
if (physical.startsWith(`..${path.sep}`) || physical === ".." || path.isAbsolute(physical))
|
|
322
|
+
fail();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const check of input.checks)
|
|
326
|
+
validateCheckDeclaration(input.workspaceRoot, check);
|
|
327
|
+
const { profile } = readCodeCheckProfile(input.workspaceRoot), profileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
|
|
328
|
+
if (input.expectedProfileHash !== undefined && profileHash !== input.expectedProfileHash)
|
|
329
|
+
fail();
|
|
330
|
+
const checks = input.checks.map(check => {
|
|
331
|
+
const runner = profile?.runner_evidence_by_alias?.[check.command] ?? check.runner_evidence;
|
|
332
|
+
return { ...check, ports: check.ports ?? profile?.ports_by_alias?.[check.command] ?? [], inputs: check.inputs ?? profile?.inputs_by_alias?.[check.command] ?? [],
|
|
333
|
+
...(runner ? { runner_evidence: runner, required_artifacts: [...new Set([...(check.required_artifacts ?? []), runner.path])] } : {}) };
|
|
334
|
+
});
|
|
335
|
+
const commands = resolveCodeCheckCommands(input.workspaceRoot, input.runId, checks.map(check => check.command));
|
|
336
|
+
assertConsistentDeclarations(checks, commands);
|
|
337
|
+
const state = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
338
|
+
const receipts = rows.map(projectCheckReceiptRow);
|
|
339
|
+
const artifacts = [], completions = [];
|
|
340
|
+
let passed = true;
|
|
341
|
+
const references = deduplicate(checks, commands).map(group => {
|
|
342
|
+
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, workspaceInputFingerprint(input.workspaceRoot, state.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment);
|
|
343
|
+
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);
|
|
344
|
+
if (!receipt || !checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
|
|
345
|
+
fail();
|
|
346
|
+
passed = passed && receipt.status === "passed";
|
|
347
|
+
for (const artifact of receipt.artifacts)
|
|
348
|
+
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 });
|
|
349
|
+
const bytes = fs.readFileSync(receipt.receipt_path);
|
|
350
|
+
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") });
|
|
351
|
+
return { id: receipt.id, path: receipt.receipt_path, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
|
|
352
|
+
});
|
|
353
|
+
const prepared = new PreparedCheckEvidence(input, snapshot, generation, references, artifacts, passed);
|
|
354
|
+
prepared.#completions = completions;
|
|
355
|
+
prepared.assertCustody(context);
|
|
356
|
+
return prepared;
|
|
357
|
+
}
|
|
358
|
+
references() { return structuredClone(this.#references); }
|
|
359
|
+
artifactReferences() { return structuredClone(this.#artifacts); }
|
|
360
|
+
sourceReferences() { return [...this.artifactReferences(), ...structuredClone(this.#completions), ...this.resultReferences().map(reference => ({ root: "run", ...reference }))]; }
|
|
361
|
+
qualityPassed() { return this.#passed; }
|
|
362
|
+
resultReferences() {
|
|
363
|
+
return this.#references.map(reference => ({ path: path.relative(this.#input.runHome, reference.path).split(path.sep).join("/"), sha256: reference.sha256 }));
|
|
364
|
+
}
|
|
365
|
+
/** For an owning acceptance writer to retain independently of agent bytes.
|
|
366
|
+
* The row digest pins historical execution, not later workspace policy.
|
|
367
|
+
*/
|
|
368
|
+
retainedReferences(context) {
|
|
369
|
+
return context.db.readSnapshot(() => {
|
|
370
|
+
this.assertCustody(context);
|
|
371
|
+
return this.#references.map(reference => ({ id: reference.id, path: path.relative(this.#input.runHome, reference.path).split(path.sep).join("/"), sha256: reference.sha256,
|
|
372
|
+
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])) }));
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
assertOwner(scope) {
|
|
376
|
+
if (scope.projectId !== this.#input.projectId || scope.runId !== this.#input.runId || scope.scope !== undefined && scope.scope !== this.#input.scope)
|
|
377
|
+
throw new AppError("check_receipt_changed", "Prepared checks belong to another RUN or scope", 2, { effect: "no_effect" });
|
|
378
|
+
}
|
|
379
|
+
assertCustody(context) {
|
|
380
|
+
assertRunMutationAllowed(context, this.#input.projectId, this.#input.runId, this.#generation);
|
|
381
|
+
if (checkEvidenceSqlSnapshot(context, this.#input) !== this.#snapshot)
|
|
382
|
+
throw new AppError("check_receipt_changed", "Prepared check SQL custody changed", 2, { effect: "no_effect" });
|
|
383
|
+
}
|
|
384
|
+
revalidate(context) {
|
|
385
|
+
const current = PreparedCheckEvidence.prepare(context, this.#input);
|
|
386
|
+
if (!isDeepStrictEqual(current.sourceReferences(), this.sourceReferences()) || !isDeepStrictEqual(current.#references, this.#references) || current.#snapshot !== this.#snapshot)
|
|
387
|
+
throw new AppError("check_evidence_stale", "Prepared check evidence changed", 2, { effect: "no_effect" });
|
|
388
|
+
this.assertCustody(context);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** Historical aggregate execution proof. Selectors must come from an owning
|
|
392
|
+
* committed acceptance receipt, not a proposed result. This reader does not
|
|
393
|
+
* select current declarations, inspect the current workspace or execute checks.
|
|
394
|
+
* Relocation of SQL path fields still requires explicit receipt projection.
|
|
395
|
+
*/
|
|
396
|
+
export class PreparedRetainedCheckEvidence {
|
|
397
|
+
#input;
|
|
398
|
+
#snapshot;
|
|
399
|
+
#generation;
|
|
400
|
+
constructor(input, snapshot, generation) {
|
|
401
|
+
this.#input = structuredClone(input);
|
|
402
|
+
this.#snapshot = snapshot;
|
|
403
|
+
this.#generation = generation;
|
|
404
|
+
Object.freeze(this);
|
|
405
|
+
}
|
|
406
|
+
static prepare(context, input) {
|
|
407
|
+
const fail = () => { throw new AppError("retained_check_evidence_invalid", "Accepted checks lack exact historical execution proof", 2, { effect: "no_effect" }); };
|
|
408
|
+
if (!Array.isArray(input.references) || !input.references.length || input.references.length > 4096
|
|
409
|
+
|| new Set(input.references.map(reference => reference?.id)).size !== input.references.length
|
|
410
|
+
|| input.references.some(reference => !reference || typeof reference.id !== "string" || !reference.id
|
|
411
|
+
|| typeof reference.path !== "string" || !/^[a-f0-9]{64}$/.test(reference.sha256) || !/^[a-f0-9]{64}$/.test(reference.sql_sha256)))
|
|
412
|
+
fail();
|
|
413
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId), snapshot = retainedCheckSqlSnapshot(context, input);
|
|
414
|
+
const selected = JSON.parse(snapshot);
|
|
415
|
+
if (!selected.run || selected.receipts.some(row => !row))
|
|
416
|
+
fail();
|
|
417
|
+
const runRoot = selected.run.run_root ?? selected.run.run_dir;
|
|
418
|
+
if (!path.isAbsolute(runRoot) || fs.realpathSync.native(runRoot) !== path.resolve(runRoot))
|
|
419
|
+
fail();
|
|
420
|
+
for (let index = 0; index < input.references.length; index++) {
|
|
421
|
+
const reference = input.references[index], row = selected.receipts[index];
|
|
422
|
+
if (row.scope !== "aggregate" || row.work_id !== null || evidenceDigest(row) !== reference.sql_sha256
|
|
423
|
+
|| row.receipt_path !== path.join(runRoot, reference.path))
|
|
424
|
+
fail();
|
|
425
|
+
// Admit contained regular receipt/completion files before any helper
|
|
426
|
+
// reads them; escaping paths and symlink parents are rejected.
|
|
427
|
+
const bytes = readRetainedCheckFile(runRoot, reference.path);
|
|
428
|
+
readRetainedCheckFile(runRoot, path.posix.join(path.posix.dirname(reference.path), "completion.json"));
|
|
429
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== reference.sha256 || !checkReceiptEvidenceValidForMode(projectCheckReceiptRow(row), input.mode))
|
|
430
|
+
fail();
|
|
431
|
+
}
|
|
432
|
+
const prepared = new PreparedRetainedCheckEvidence(input, snapshot, generation);
|
|
433
|
+
prepared.assertCustody(context);
|
|
434
|
+
return prepared;
|
|
435
|
+
}
|
|
436
|
+
assertCustody(context) {
|
|
437
|
+
assertRunMutationAllowed(context, this.#input.projectId, this.#input.runId, this.#generation);
|
|
438
|
+
if (retainedCheckSqlSnapshot(context, this.#input) !== this.#snapshot)
|
|
439
|
+
throw new AppError("retained_check_evidence_invalid", "Accepted check SQL custody changed", 2, { effect: "no_effect" });
|
|
440
|
+
}
|
|
441
|
+
revalidate(context) {
|
|
442
|
+
const current = PreparedRetainedCheckEvidence.prepare(context, this.#input);
|
|
443
|
+
if (current.#snapshot !== this.#snapshot)
|
|
444
|
+
throw new AppError("retained_check_evidence_invalid", "Accepted check proof changed", 2, { effect: "no_effect" });
|
|
445
|
+
this.assertCustody(context);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function readRetainedCheckFile(root, relative) {
|
|
449
|
+
const fail = () => { throw new AppError("retained_check_evidence_invalid", "Accepted check file is not a contained regular file", 2, { effect: "no_effect" }); };
|
|
450
|
+
if (!relative || path.posix.isAbsolute(relative) || path.win32.isAbsolute(relative) || relative.includes("\\") || relative.includes("\0")
|
|
451
|
+
|| relative.split("/").some(segment => !segment || segment === "." || segment === ".."))
|
|
452
|
+
fail();
|
|
453
|
+
try {
|
|
454
|
+
let parent = root;
|
|
455
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
456
|
+
fail();
|
|
457
|
+
for (const segment of relative.split("/").slice(0, -1)) {
|
|
458
|
+
parent = path.join(parent, segment);
|
|
459
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
460
|
+
fail();
|
|
461
|
+
}
|
|
462
|
+
return readBoundedReleaseFile(path.join(root, relative), 16 * 1024 * 1024);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return fail();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function retainedCheckSqlSnapshot(context, input) {
|
|
469
|
+
return context.db.readSnapshot(() => JSON.stringify({
|
|
470
|
+
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,
|
|
471
|
+
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)
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
function checkEvidenceSqlSnapshot(context, input) {
|
|
475
|
+
return context.db.readSnapshot(() => JSON.stringify({
|
|
476
|
+
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]),
|
|
477
|
+
receipts: context.db.all("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? ORDER BY reservation_sequence,rowid", [input.projectId, input.runId])
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
259
480
|
export async function runCodeChecks(context, input) {
|
|
481
|
+
validateCheckEvidenceContext(input.evidenceContext);
|
|
260
482
|
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
261
483
|
let gate;
|
|
262
484
|
for (;;) {
|
|
@@ -305,7 +527,8 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
|
|
|
305
527
|
const predecessor = validateFailedCheckRetry(context, { projectId: input.projectId, runId: input.runId, receiptId, declarations: checks, reason: input.retryReason });
|
|
306
528
|
const snapshot = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
307
529
|
const group = groups.find(candidate => candidate.declarations.some(check => check.run_at === predecessor.gate)
|
|
308
|
-
&& checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome])) === predecessor.input_hash
|
|
530
|
+
&& (checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) === predecessor.input_hash
|
|
531
|
+
|| sourceRepairContractMatches(predecessor, candidate, profileHash)));
|
|
309
532
|
if (!group || retries.has(group))
|
|
310
533
|
throw new AppError("retry_check_invalid", "Retry must match exactly one current execution contract", 2, { check_receipt_id: receiptId });
|
|
311
534
|
// Preserve the identity of a pre-deduplication failed execution when an
|
|
@@ -318,7 +541,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
|
|
|
318
541
|
const before = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
319
542
|
const startedAt = context.now();
|
|
320
543
|
const inputFingerprint = workspaceInputFingerprint(input.workspaceRoot, before.files, group.inputs, [input.runHome]);
|
|
321
|
-
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint);
|
|
544
|
+
const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint, input.evidenceContext, input.checkEnvironment);
|
|
322
545
|
const epoch = crypto.createHash("sha256").update(`${group.gate}\0${before.fingerprint}`).digest("hex");
|
|
323
546
|
const reservationInput = { ...input, retryReceiptIds: retries.has(group) ? [retries.get(group)] : [], generation, group, before, profileHash, inputHash, epoch, startedAt };
|
|
324
547
|
delete reservationInput.retryReceiptId;
|
|
@@ -363,7 +586,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
|
|
|
363
586
|
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 } : {};
|
|
364
587
|
if (group.runnerEvidence)
|
|
365
588
|
fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
|
|
366
|
-
result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), 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) => {
|
|
589
|
+
result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), 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) => {
|
|
367
590
|
confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
|
|
368
591
|
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
369
592
|
if (budget)
|
|
@@ -388,7 +611,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
|
|
|
388
611
|
}
|
|
389
612
|
const after = workspaceState(input.workspaceRoot, [input.runHome]);
|
|
390
613
|
const mutationPaths = changedPaths(before.files, after.files);
|
|
391
|
-
|
|
614
|
+
let artifacts = collectCheckArtifacts(evidenceDir, group.requiredArtifacts);
|
|
392
615
|
if (fs.existsSync(stderrPath)) {
|
|
393
616
|
if (!artifacts.complete)
|
|
394
617
|
fs.appendFileSync(stderrPath, `\nmissing required evidence artifacts: ${artifacts.missing.join(", ")}\n`);
|
|
@@ -398,14 +621,26 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
|
|
|
398
621
|
fs.appendFileSync(stderrPath, `\ncheck mutated project workspace: ${mutationPaths.join(", ")}\n`);
|
|
399
622
|
}
|
|
400
623
|
const runnerEvidence = group.runnerEvidence ? readRunnerEvidence(evidenceDir, group.runnerEvidence, { invocation_id: id, check_id: id, command: group.command, input_hash: inputHash, verification_epoch: epoch }) : undefined;
|
|
624
|
+
const physicalFailures = executionFailures({ exitCode: result.exitCode, launcherError: result.error ?? undefined, cleanupUnconfirmed: result.cleanupUnconfirmed, missing: artifacts.missing, mutations: mutationPaths, invalidRunner: Boolean(group.runnerEvidence && !runnerEvidence) });
|
|
625
|
+
const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
626
|
+
if (group.runnerEvidence)
|
|
627
|
+
retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch }, input.workspaceRoot, physicalFailures, candidateFiles);
|
|
628
|
+
const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
|
|
629
|
+
artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
|
|
401
630
|
const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
402
631
|
const receipt = receiptFor({ ...reserved, resources, result, status, after, mutationPaths, artifacts: artifacts.items });
|
|
632
|
+
receipt.required_artifacts = requiredArtifacts;
|
|
633
|
+
receipt.candidate_root = input.workspaceRoot;
|
|
634
|
+
receipt.candidate_files = candidateFiles;
|
|
635
|
+
receipt.execution_failures = physicalFailures;
|
|
403
636
|
if (group.runnerEvidence) {
|
|
404
637
|
receipt.runner_evidence_required = group.runnerEvidence;
|
|
405
638
|
receipt.runner_evidence = runnerEvidence ?? null;
|
|
406
639
|
}
|
|
407
640
|
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]);
|
|
408
641
|
writeReceipt(receiptPath, receipt);
|
|
642
|
+
if (status === "failed")
|
|
643
|
+
receipt.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(receipt, "diagnostic");
|
|
409
644
|
receipts.push(receipt);
|
|
410
645
|
input.progress?.(`check ${index + 1}/${groups.length} ${status}: ${group.command}`);
|
|
411
646
|
}
|
|
@@ -413,7 +648,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
|
|
|
413
648
|
const current = workspaceState(input.workspaceRoot, [input.runHome]), currentProfileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
|
|
414
649
|
for (const receipt of receipts) {
|
|
415
650
|
const group = groups.find(candidate => candidate.declarations.some(check => receipt.check_refs.includes(checkRef(check))));
|
|
416
|
-
const currentHash = group ? checkInputHash(input.workspaceRoot, group, currentProfileHash, workspaceInputFingerprint(input.workspaceRoot, current.files, group.inputs, [input.runHome])) : null;
|
|
651
|
+
const currentHash = group ? checkInputHash(input.workspaceRoot, group, currentProfileHash, workspaceInputFingerprint(input.workspaceRoot, current.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) : null;
|
|
417
652
|
if (!group || !checkReceiptEvidenceValid(receipt) || receipt.input_hash !== currentHash)
|
|
418
653
|
throw new AppError("check_evidence_stale", "Check evidence changed before the gate could be accepted", 1, { check_receipt_id: receipt.id });
|
|
419
654
|
}
|
|
@@ -456,18 +691,44 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
456
691
|
throw new AppError("check_in_progress", "A previous check invocation is still initializing", 1, { check_receipt_id: pendingReceipt.id, initializing: true });
|
|
457
692
|
}
|
|
458
693
|
const after = workspaceState(input.workspaceRoot, input.runHome ? [input.runHome] : []);
|
|
459
|
-
|
|
694
|
+
let artifacts = receipt ? collectCheckArtifacts(path.join(path.dirname(receipt.completion_path || pendingReceipt.receipt_path), "artifacts"), receipt.required_artifacts ?? []) : { complete: false, missing: [], items: [] };
|
|
460
695
|
const workspaceUnchanged = receipt?.before_fingerprint === after.fingerprint;
|
|
461
696
|
const runnerEvidence = receipt?.runner_evidence_required ? readRunnerEvidence(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;
|
|
462
|
-
const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete && (!receipt?.runner_evidence_required || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
463
697
|
const exitCode = completion?.exit_code ?? null;
|
|
464
698
|
const finishedAt = completion?.finished_at ?? context.now();
|
|
699
|
+
const physicalFailures = executionFailures({ exitCode, missing: artifacts.missing.filter(file => file !== "engine-outcome.json"), mutations: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], invalidRunner: Boolean(receipt?.runner_evidence_required && !runnerEvidence) });
|
|
700
|
+
let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
701
|
+
if (receipt?.runner_evidence_required) {
|
|
702
|
+
const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
|
|
703
|
+
if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
|
|
704
|
+
// A completed live writer's bound proof is already authoritative. Never erase its blockers during reconciliation.
|
|
705
|
+
const retained = context.db.get("SELECT artifacts_json FROM check_receipts WHERE id = ?", [pendingReceipt.id]);
|
|
706
|
+
const proven = resolvedReceiptArtifacts(pendingReceipt.receipt_path, retained ? JSON.parse(retained.artifacts_json) : [], receipt);
|
|
707
|
+
if (proven.candidate_root) {
|
|
708
|
+
physicalFailures.push(...(proven.execution_failures ?? []));
|
|
709
|
+
candidateFiles = proven.candidate_files ?? candidateFiles;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
|
|
713
|
+
fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles);
|
|
717
|
+
receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), "engine-outcome.json"])];
|
|
718
|
+
artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
|
|
719
|
+
}
|
|
720
|
+
const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete && physicalFailures.length === 0 && (!receipt?.runner_evidence_required || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
465
721
|
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, after.fingerprint, JSON.stringify(workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"]), JSON.stringify(artifacts.items), finishedAt, pendingReceipt.id, input.projectId]);
|
|
466
722
|
if (process)
|
|
467
723
|
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" });
|
|
468
724
|
if (receipt) {
|
|
469
725
|
const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null } : {}), 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: after.fingerprint, mutation_paths: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], artifacts: artifacts.items, finished_at: finishedAt };
|
|
726
|
+
repaired.candidate_root = input.workspaceRoot;
|
|
727
|
+
repaired.candidate_files = candidateFiles;
|
|
728
|
+
repaired.execution_failures = physicalFailures;
|
|
470
729
|
writeReceipt(pendingReceipt.receipt_path, repaired);
|
|
730
|
+
if (status === "failed")
|
|
731
|
+
repaired.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(repaired, "diagnostic");
|
|
471
732
|
reconciled.push(repaired);
|
|
472
733
|
}
|
|
473
734
|
}
|
|
@@ -486,6 +747,9 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
486
747
|
const retryIds = new Set([...(input.retryReceiptId ? [input.retryReceiptId] : []), ...(input.retryReceiptIds ?? [])]);
|
|
487
748
|
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;
|
|
488
749
|
const reusableSuccessor = successor?.status === "passed" && checkReceiptEvidenceValid(successor);
|
|
750
|
+
const retryPredecessor = receipts.find(receipt => retryIds.has(receipt.id));
|
|
751
|
+
const sourceCorrection = Boolean(retryPredecessor && sourceRepairContractMatches(retryPredecessor, input.group, input.profileHash));
|
|
752
|
+
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);
|
|
489
753
|
return context.db.writeTransaction(() => {
|
|
490
754
|
assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
|
|
491
755
|
if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
|
|
@@ -499,9 +763,9 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
499
763
|
if (!input.retryReason?.trim())
|
|
500
764
|
throw new AppError("retry_check_invalid", "A check retry requires the restored-condition reason", 2, { check_receipt_id: retryId });
|
|
501
765
|
const predecessor = receipts.find(receipt => receipt.id === retryId);
|
|
502
|
-
if (!predecessor || !["failed", "aborted"].includes(predecessor.status) || predecessor.input_hash !== input.inputHash || predecessor.profile_hash !== input.profileHash || predecessor.command !== input.group.command || predecessor.gate !== input.group.gate)
|
|
766
|
+
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)
|
|
503
767
|
throw new AppError("retry_check_invalid", "Retry predecessor does not match this exact check contract", 2, { check_receipt_id: retryId });
|
|
504
|
-
if (prior?.id !== (successor?.id ?? predecessor.id))
|
|
768
|
+
if ((sourceCorrection ? latestContract?.id : prior?.id) !== (successor?.id ?? predecessor.id))
|
|
505
769
|
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 });
|
|
506
770
|
if (successor?.status === "running")
|
|
507
771
|
throw new AppError("check_in_progress", "The requested retry is already running", 1, { check_receipt_id: successor.id, retry_of_receipt_id: retryId });
|
|
@@ -530,6 +794,10 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
530
794
|
return { reserved };
|
|
531
795
|
}, "checks.reserveOrReuseReceipt");
|
|
532
796
|
}
|
|
797
|
+
function sourceRepairContractMatches(receipt, group, profileHash) {
|
|
798
|
+
return checkRepairDisposition(receipt) === "source_repair" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
|
|
799
|
+
&& isDeepStrictEqual([...group.requiredArtifacts].sort(), receipt.required_artifacts.filter(file => file !== "engine-outcome.json").sort()) && isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
|
|
800
|
+
}
|
|
533
801
|
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, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.before.fingerprint, after_fingerprint: 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() }; }
|
|
534
802
|
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); }
|
|
535
803
|
function readReceipt(file) { try {
|
|
@@ -561,17 +829,25 @@ export function validateFailedCheckRetry(context, input) {
|
|
|
561
829
|
export function checkReceipts(context, input) {
|
|
562
830
|
const where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ?" : "project_id = ? AND run_id = ?";
|
|
563
831
|
const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
|
|
564
|
-
return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, 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 FROM check_receipts WHERE ${where} ORDER BY reservation_sequence, rowid`, params).map(
|
|
832
|
+
return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, 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 FROM check_receipts WHERE ${where} ORDER BY reservation_sequence, rowid`, params).map(projectCheckReceiptRow);
|
|
833
|
+
}
|
|
834
|
+
function projectCheckReceiptRow(row) {
|
|
835
|
+
const receipt = { ...row, 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) };
|
|
836
|
+
if (receipt.status === "failed")
|
|
837
|
+
receipt.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(receipt, "diagnostic");
|
|
838
|
+
return receipt;
|
|
565
839
|
}
|
|
566
840
|
/** File bytes remain immutable; only the reader projects their current location. */
|
|
567
841
|
function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
568
842
|
let original = {};
|
|
569
843
|
try {
|
|
570
|
-
|
|
844
|
+
const value = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
|
|
845
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
846
|
+
original = value;
|
|
571
847
|
}
|
|
572
848
|
catch { /* Historical missing receipts remain visibly incomplete. */ }
|
|
573
849
|
const currentRoot = path.dirname(receiptPath);
|
|
574
|
-
const oldRoot = original.receipt_path ? path.dirname(original.receipt_path) : currentRoot;
|
|
850
|
+
const oldRoot = typeof original.receipt_path === "string" ? path.dirname(original.receipt_path) : currentRoot;
|
|
575
851
|
const relocate = (file) => {
|
|
576
852
|
const relative = path.relative(oldRoot, file);
|
|
577
853
|
return !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) ? path.join(currentRoot, relative) : file;
|
|
@@ -580,10 +856,44 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
|
580
856
|
if (original.runner_evidence_required && original.id && original.command && original.input_hash && original.verification_epoch) {
|
|
581
857
|
const root = path.join(currentRoot, "artifacts");
|
|
582
858
|
const retained = collectCheckArtifacts(root, [original.runner_evidence_required.path]);
|
|
583
|
-
if (retained.complete &&
|
|
859
|
+
if (retained.complete && retained.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256)))
|
|
584
860
|
evidence = readRunnerEvidence(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 });
|
|
585
861
|
}
|
|
586
|
-
|
|
862
|
+
let outcome = {};
|
|
863
|
+
const outcomePath = path.join(currentRoot, "artifacts", "engine-outcome.json");
|
|
864
|
+
const retainedOutcome = collectCheckArtifacts(path.join(currentRoot, "artifacts"), ["engine-outcome.json"]);
|
|
865
|
+
if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
|
|
866
|
+
try {
|
|
867
|
+
const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
|
|
868
|
+
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 && 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)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
|
|
869
|
+
outcome = value;
|
|
870
|
+
}
|
|
871
|
+
catch { /* Missing physical proof cannot grant attribution. */ }
|
|
872
|
+
}
|
|
873
|
+
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 } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}) };
|
|
874
|
+
}
|
|
875
|
+
function executionFailures(input) {
|
|
876
|
+
const failures = [];
|
|
877
|
+
if (input.exitCode === null)
|
|
878
|
+
failures.push({ code: "completion_unknown", message: "Check completion is unknown" });
|
|
879
|
+
if (input.launcherError)
|
|
880
|
+
failures.push({ code: "launcher_error", message: input.launcherError });
|
|
881
|
+
if (input.cleanupUnconfirmed)
|
|
882
|
+
failures.push({ code: "cleanup_unconfirmed", message: "Check process cleanup is not confirmed" });
|
|
883
|
+
if (input.missing.length)
|
|
884
|
+
failures.push({ code: "missing_required_artifact", message: input.missing.join(", ") });
|
|
885
|
+
if (input.mutations.length)
|
|
886
|
+
failures.push({ code: "check_input_mutated", message: input.mutations.join(", ") });
|
|
887
|
+
if (input.invalidRunner)
|
|
888
|
+
failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
|
|
889
|
+
return failures;
|
|
890
|
+
}
|
|
891
|
+
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles) {
|
|
892
|
+
const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
|
|
893
|
+
if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
|
|
894
|
+
throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
|
|
895
|
+
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, candidate_root: candidateRoot, candidate_files: candidateFiles, execution_failures: failures }), { flag: "wx", mode: 0o600 });
|
|
896
|
+
fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
|
|
587
897
|
}
|
|
588
898
|
export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
|
|
589
899
|
/** One NUL-aware source of truth for paths differing from HEAD. Rename/copy
|
|
@@ -650,12 +960,13 @@ function deduplicate(checks, commands) {
|
|
|
650
960
|
const reusable = check.reuse === "deterministic";
|
|
651
961
|
// Stage labels identify obligations, not different processes in this gate.
|
|
652
962
|
// Cross-invocation reuse still requires the explicit deterministic contract.
|
|
653
|
-
const
|
|
963
|
+
const environmentInputs = normalizedEnvironmentInputs(check.environment_inputs);
|
|
964
|
+
const key = JSON.stringify([commands[index], artifacts, ports, inputs, reusable, environmentInputs, check.runner_evidence ?? null]);
|
|
654
965
|
const current = groups.get(key);
|
|
655
966
|
if (current)
|
|
656
967
|
current.declarations.push(check);
|
|
657
968
|
else
|
|
658
|
-
groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports, inputs, reusable, ...(check.runner_evidence ? { runnerEvidence: check.runner_evidence } : {}) });
|
|
969
|
+
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 } : {}) });
|
|
659
970
|
});
|
|
660
971
|
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))) }));
|
|
661
972
|
}
|
|
@@ -703,16 +1014,34 @@ function checkRef(check) { return check.canonical_ref ?? check.id; }
|
|
|
703
1014
|
function assertConsistentDeclarations(checks, commands) {
|
|
704
1015
|
const seen = new Map();
|
|
705
1016
|
for (const [index, check] of checks.entries()) {
|
|
706
|
-
const contract = JSON.stringify([check.run_at, commands[index], [...(check.required_artifacts ?? [])].sort(), [...(check.ports ?? [])].sort(), normalizedInputs(check.inputs ?? []), check.reuse ?? null, check.runner_evidence ?? null]);
|
|
1017
|
+
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]);
|
|
707
1018
|
const ref = checkRef(check), prior = seen.get(ref);
|
|
708
1019
|
if (prior && prior !== contract)
|
|
709
1020
|
throw new AppError("check_declaration_conflict", "The same check reference has conflicting execution definitions", 2, { check_ref: ref });
|
|
710
1021
|
seen.set(ref, contract);
|
|
711
1022
|
}
|
|
712
1023
|
}
|
|
713
|
-
function
|
|
714
|
-
|
|
715
|
-
|
|
1024
|
+
function normalizedEnvironmentInputs(value) {
|
|
1025
|
+
if (value === undefined)
|
|
1026
|
+
return undefined;
|
|
1027
|
+
if (!Array.isArray(value) || !value.length || value.length > 64 || value.some(name => typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*\*?$/.test(name)))
|
|
1028
|
+
throw new AppError("invalid_check_environment_inputs", "Internal check environment inputs must be bounded names or prefixes", 2, { effect: "no_effect" });
|
|
1029
|
+
return [...new Set(value)].sort();
|
|
1030
|
+
}
|
|
1031
|
+
function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
|
|
1032
|
+
const environment = codeExecutionEnvironment(workspaceRoot, checkEnvironment);
|
|
1033
|
+
// Invocation authority is checked by its own owners, not an application check
|
|
1034
|
+
// input. Preserve arbitrary application variables (including DD_FLOW_PORT_*)
|
|
1035
|
+
// and tool selection; omit only known process/control bookkeeping.
|
|
1036
|
+
const bookkeeping = new Set(["PWD", "OLDPWD", "SHLVL", "_", "DD_FLOW_DAEMON_ID",
|
|
1037
|
+
"DD_FLOW_CURRENT_INVOCATION", "DD_FLOW_INVOCATION_READONLY", "DD_FLOW_INVOCATION_SCOPE", "DD_FLOW_CODEX_STATE_DIR",
|
|
1038
|
+
"DD_FLOW_RUNTIME_OWNER", "DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN",
|
|
1039
|
+
"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",
|
|
1040
|
+
"CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION"]);
|
|
1041
|
+
return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
|
|
1042
|
+
.filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
|
|
1043
|
+
&& (inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
|
|
1044
|
+
.sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
|
|
716
1045
|
}
|
|
717
1046
|
function acquireWorkspaceGate(context, input) {
|
|
718
1047
|
const operationId = workspaceOperationId(input.workspaceRoot);
|
|
@@ -806,19 +1135,43 @@ function explicitInputFiles(root, inputs, ignoredRoots) {
|
|
|
806
1135
|
visit(path.resolve(root, input), input, new Set());
|
|
807
1136
|
return [...result.entries()];
|
|
808
1137
|
}
|
|
809
|
-
function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint) { 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), input_fingerprint: inputFingerprint })).digest("hex"); }
|
|
810
|
-
function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: group.requiredArtifacts, inputs: group.inputs }; }
|
|
811
|
-
export function checkReceiptEvidenceValid(receipt) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1138
|
+
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"); }
|
|
1139
|
+
function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: [...new Set([...group.requiredArtifacts, ...(receipt.required_artifacts.includes("engine-outcome.json") ? ["engine-outcome.json"] : [])])], inputs: group.inputs }; }
|
|
1140
|
+
export function checkReceiptEvidenceValid(receipt) { return checkReceiptEvidenceValidForMode(receipt); }
|
|
1141
|
+
function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
1142
|
+
try {
|
|
1143
|
+
if (!(receipt.status === "passed" && receipt.exit_code === 0 || mode === "diagnostic" && receipt.status === "failed" && typeof receipt.exit_code === "number" && receipt.exit_code !== 0) || receipt.before_fingerprint !== receipt.after_fingerprint
|
|
1144
|
+
|| receipt.mutation_paths.length !== 0 || receipt.completion_path !== path.join(path.dirname(receipt.receipt_path), "completion.json")
|
|
1145
|
+
|| readCompletion(receipt.completion_path)?.exit_code !== receipt.exit_code)
|
|
1146
|
+
return false;
|
|
1147
|
+
const stored = readReceipt(receipt.receipt_path);
|
|
1148
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored)
|
|
1149
|
+
|| typeof stored.receipt_path !== "string" || !path.isAbsolute(stored.receipt_path)
|
|
1150
|
+
|| path.basename(stored.receipt_path) !== "receipt.json"
|
|
1151
|
+
|| stored.completion_path !== path.join(path.dirname(stored.receipt_path), "completion.json"))
|
|
1152
|
+
return false;
|
|
1153
|
+
// Gate/ref labels may be rebound for explicit deterministic reuse. Physical
|
|
1154
|
+
// execution identity and settled SQL evidence must never be rebound from a
|
|
1155
|
+
// mutable receipt file, including after recovery relocates the RUN home.
|
|
1156
|
+
const fields = ["id", "scope", "work_id", "command", "input_hash", "verification_epoch", "status", "exit_code",
|
|
1157
|
+
"workspace_fingerprint", "before_fingerprint", "after_fingerprint", "profile_hash", "started_at", "finished_at"];
|
|
1158
|
+
if (fields.some(field => stored[field] !== receipt[field]) || !Array.isArray(stored.artifacts)
|
|
1159
|
+
|| !Array.isArray(stored.mutation_paths) || stored.mutation_paths.length !== 0
|
|
1160
|
+
|| JSON.stringify(stored.required_artifacts) !== JSON.stringify(receipt.required_artifacts)
|
|
1161
|
+
|| JSON.stringify(stored.inputs) !== JSON.stringify(receipt.inputs))
|
|
1162
|
+
return false;
|
|
1163
|
+
const retained = resolvedReceiptArtifacts(receipt.receipt_path, stored.artifacts, receipt).artifacts;
|
|
1164
|
+
if (JSON.stringify(retained) !== JSON.stringify(receipt.artifacts))
|
|
1165
|
+
return false;
|
|
1166
|
+
const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
|
|
1167
|
+
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 })))
|
|
1168
|
+
return false;
|
|
1169
|
+
const artifacts = collectCheckArtifacts(root, receipt.required_artifacts);
|
|
1170
|
+
return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(retained);
|
|
1171
|
+
}
|
|
1172
|
+
catch {
|
|
819
1173
|
return false;
|
|
820
|
-
|
|
821
|
-
return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(stored.artifacts ?? []);
|
|
1174
|
+
}
|
|
822
1175
|
}
|
|
823
1176
|
function parseArray(value, fallback) { try {
|
|
824
1177
|
const parsed = JSON.parse(value);
|