@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +274 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-child-repair.js +95 -0
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +19 -9
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +250 -33
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +274 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +25 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +257 -44
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +255 -69
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { nextAttempt } from "./runs.js";
|
|
2
|
+
import { stageAttempt, stageCycle, stageWorkIds } from "./stage-work-graph.js";
|
|
3
|
+
import { storageSessionId } from "./session-identity.js";
|
|
4
|
+
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
5
|
+
import { terminalProviderRecovery } from "../harness-runtime/lib/provider-overload-policy.mjs";
|
|
6
|
+
import { getVnextFanoutStatus } from "./vnext-fanout.js";
|
|
7
|
+
import { normalizeNativeChildren, nativeTreeReadiness } from "../harness-runtime/lib/native-children.mjs";
|
|
8
|
+
/** Disappearance from a truncated child list is not retirement evidence. */
|
|
9
|
+
export function acceptedChildrenRetired(expected, receipt, sessionId, operationId) {
|
|
10
|
+
const children = normalizeNativeChildren(receipt, sessionId);
|
|
11
|
+
const settledTree = nativeTreeReadiness(receipt, { sessionId, operationId }) === "ready"
|
|
12
|
+
&& Array.isArray(receipt.tree_session_ids) ? receipt.tree_session_ids : [];
|
|
13
|
+
return expected.every(id => {
|
|
14
|
+
const child = children.find(item => item.session_id === id);
|
|
15
|
+
return Boolean(child && ["completed", "failed", "cancelled"].includes(child.status)) || settledTree.includes(id);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Typed transport/provider failures only; integrity/ownership errors never become notes. */
|
|
19
|
+
export function lateNativeFailure(error, sessionId) {
|
|
20
|
+
const value = error;
|
|
21
|
+
return isObservationLoss(error) || isObservationLoss(value?.details?.cause)
|
|
22
|
+
|| value?.code === "settlement_recovery_required" || terminalProviderRecovery(error, sessionId);
|
|
23
|
+
}
|
|
24
|
+
/** A technical pause is an engine-recorded boundary, never an operator label. */
|
|
25
|
+
export function technicalDependencyControl(context, projectId, runId, controlId) {
|
|
26
|
+
if (!context.db.get("SELECT 1 FROM sqlite_schema WHERE name='run_controller_events'"))
|
|
27
|
+
return false;
|
|
28
|
+
return Boolean(context.db.get(`SELECT 1 FROM run_controller_events e JOIN run_controllers c ON c.controller_id=e.controller_id JOIN run_controls rc ON rc.project_id=c.project_id AND rc.run_id=c.run_id WHERE c.project_id=? AND c.run_id=? AND rc.control_id=? AND rc.action='pause' AND e.event_type='technical_dependency_paused' AND json_extract(e.data_json,'$.control_id')=rc.control_id AND json_extract(e.data_json,'$.schema_id')='dd-flow/controller-dependency@1'`, [projectId, runId, controlId]));
|
|
29
|
+
}
|
|
30
|
+
/** A technical pause may be resumed automatically only for its own accepted Work.
|
|
31
|
+
* This is a read-only predicate and is rechecked under the resume transaction. */
|
|
32
|
+
export function acceptedTechnicalWorkControl(context, row, controlId) {
|
|
33
|
+
try {
|
|
34
|
+
const control = context.db.get("SELECT request_id,recovery_id,status,action FROM run_controls WHERE project_id=? AND run_id=? AND control_id=?", [row.project_id, row.run_id, controlId]);
|
|
35
|
+
if (!control || control.action !== "pause" || control.status !== "settled")
|
|
36
|
+
return false;
|
|
37
|
+
const guard = context.db.get("SELECT recovery_id,generation,status,capture_path FROM run_recovery_guards WHERE project_id=? AND run_id=? ORDER BY generation DESC LIMIT 1", [row.project_id, row.run_id]);
|
|
38
|
+
if (!guard || guard.recovery_id !== control.recovery_id || guard.status !== "sealed" || !guard.capture_path || guard.generation <= row.generation)
|
|
39
|
+
return false;
|
|
40
|
+
const events = context.db.all("SELECT data_json FROM run_controller_events WHERE controller_id=? AND event_type='technical_dependency_paused' AND json_extract(data_json,'$.control_id')=?", [row.controller_id, controlId]);
|
|
41
|
+
if (events.length !== 1)
|
|
42
|
+
return false;
|
|
43
|
+
const event = JSON.parse(events[0].data_json);
|
|
44
|
+
if (event.schema_id !== "dd-flow/controller-dependency@1" || event.request_id !== control.request_id || event.generation !== row.generation || typeof event.source_operation_id !== "string")
|
|
45
|
+
return false;
|
|
46
|
+
const state = JSON.parse(row.state_json), session = state.current_session === null ? null : state.sessions?.[state.current_session];
|
|
47
|
+
if (!session?.id || !session.owner?.owner_id || event.state_dir !== session.state_dir)
|
|
48
|
+
return false;
|
|
49
|
+
const op = context.db.get("SELECT generation,status,input_json FROM run_controller_operations WHERE controller_id=? AND operation_id=?", [row.controller_id, event.source_operation_id]);
|
|
50
|
+
if (!op || op.generation !== row.generation || !["outcome_unknown", "completed"].includes(op.status))
|
|
51
|
+
return false;
|
|
52
|
+
const input = JSON.parse(op.input_json), expected = input.obligation;
|
|
53
|
+
if (input.owner !== session.owner.owner_id || input.state_dir !== session.state_dir || !Array.isArray(input.args) || input.args[0] !== "session" || input.args[1] !== "prompt" || input.args.indexOf("--session-id") < 0 || input.args[input.args.indexOf("--session-id") + 1] !== session.id || !expected || expected.action !== "work" || expected.stage !== state.stage || expected.generation !== row.generation || !Array.isArray(expected.work_ids) || !expected.work_ids.length || !Array.isArray(expected.work_session_ids))
|
|
54
|
+
return false;
|
|
55
|
+
const run = context.db.get("SELECT status,index_json FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
|
|
56
|
+
const stage = run && JSON.parse(run.index_json).stage_runs.find(item => item.stage === expected.stage && (item.attempt ?? "try-001") === expected.attempt);
|
|
57
|
+
if (!run || !["running", "paused"].includes(run.status) || stage?.status !== "running" || !controllerObligationAccepted(context, row, expected))
|
|
58
|
+
return false;
|
|
59
|
+
const stageWorks = stageWorkIds(context, row.project_id, row.run_id, expected.stage);
|
|
60
|
+
return expected.work_ids.every(id => stageWorks.has(id) && Boolean(context.db.get("SELECT 1 FROM work_sessions WHERE work_id=? AND status='completed'", [id])))
|
|
61
|
+
&& expected.work_session_ids.every(id => Boolean(context.db.get(`SELECT 1 FROM work_sessions WHERE id=? AND status='completed' AND work_id IN (${expected.work_ids.map(() => "?").join(",")})`, [id, ...expected.work_ids])));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Persist this snapshot with the exact dispatch, never infer success from one arbitrary Work. */
|
|
68
|
+
export function controllerObligation(context, row, stage, action, assignedWorkIds) {
|
|
69
|
+
const ids = [...stageWorkIds(context, row.project_id, row.run_id, stage)];
|
|
70
|
+
const works = ids.length ? context.db.all("SELECT work_id,status,parent_work_id FROM works WHERE project_id=? AND run_id=?", [row.project_id, row.run_id]).filter(work => ids.includes(work.work_id)) : [];
|
|
71
|
+
const targets = action === "work" ? works.filter(work => work.parent_work_id && ["created", "running"].includes(work.status) && (!assignedWorkIds || assignedWorkIds.includes(work.work_id))).map(work => work.work_id) : [];
|
|
72
|
+
const sessions = targets.length ? context.db.all("SELECT ws.id,ws.work_id FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id WHERE w.project_id=? AND w.run_id=? AND ws.status='running'", [row.project_id, row.run_id]).filter(session => targets.includes(session.work_id)).map(session => session.id) : [];
|
|
73
|
+
const run = context.db.get("SELECT index_json,project_root FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
|
|
74
|
+
const previous = run ? JSON.parse(run.index_json).stage_runs.find(item => item.stage === stage) : undefined;
|
|
75
|
+
return { stage, attempt: action === "entry" ? nextAttempt(previous) : stageAttempt(context, row.project_id, row.run_id, stage), cycle: stageCycle(context, row.project_id, row.run_id, stage), action, work_ids: targets, work_session_ids: sessions, generation: row.generation, ...(action === "resume" && previous?.pause ? { pause_id: previous.pause.id } : {}), ...(action === "graph" ? { graph_before: ids.sort() } : {}) };
|
|
76
|
+
}
|
|
77
|
+
export function controllerObligationAccepted(context, row, expected) {
|
|
78
|
+
if (expected.generation !== row.generation)
|
|
79
|
+
return false;
|
|
80
|
+
const run = context.db.get("SELECT index_json,project_root FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
|
|
81
|
+
if (!run)
|
|
82
|
+
return false;
|
|
83
|
+
const stages = JSON.parse(run.index_json).stage_runs;
|
|
84
|
+
const stage = [...stages].reverse().find(item => item.stage === expected.stage && (item.attempt ?? "try-001") === expected.attempt);
|
|
85
|
+
if (!stage || stageCycle(context, row.project_id, row.run_id, expected.stage) !== expected.cycle)
|
|
86
|
+
return false;
|
|
87
|
+
if (["done", "skipped"].includes(stage.status))
|
|
88
|
+
return true;
|
|
89
|
+
if (expected.action === "resume")
|
|
90
|
+
return Boolean(expected.pause_id && context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_resumed' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.pause_id')=?", [row.project_id, row.run_id, expected.stage, expected.pause_id]));
|
|
91
|
+
if (expected.action === "entry") {
|
|
92
|
+
// Entry-only is a boundary for fanout stages. A non-fanout entry Turn
|
|
93
|
+
// also owes semantic work: merely starting SPECIFY is not its result.
|
|
94
|
+
if (stage.status === "paused" && stage.pause)
|
|
95
|
+
return Boolean(context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND work_id=? AND status='paused'", [row.project_id, row.run_id, stage.pause.work_id]));
|
|
96
|
+
if (stage.status !== "running")
|
|
97
|
+
return false;
|
|
98
|
+
try {
|
|
99
|
+
return Boolean(getVnextFanoutStatus(context, { projectRoot: run.project_root, runId: row.run_id, stage: expected.stage }).orchestration);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (expected.action === "graph") {
|
|
106
|
+
const current = [...stageWorkIds(context, row.project_id, row.run_id, expected.stage)];
|
|
107
|
+
return Array.isArray(expected.graph_before) && current.length > expected.graph_before.length && expected.graph_before.every(id => current.includes(id));
|
|
108
|
+
}
|
|
109
|
+
if (expected.action !== "work" || !expected.work_ids.length)
|
|
110
|
+
return false;
|
|
111
|
+
return expected.work_ids.every(id => context.db.get("SELECT status FROM works WHERE project_id=? AND run_id=? AND work_id=?", [row.project_id, row.run_id, id])?.status === "completed")
|
|
112
|
+
&& expected.work_session_ids.every(id => context.db.get("SELECT status FROM work_sessions WHERE id=?", [id])?.status === "completed");
|
|
113
|
+
}
|
|
114
|
+
/** A native failure cannot revoke an accepted result; unknown association is not success. */
|
|
115
|
+
export function acceptedChildWork(context, row, harness, parent, child) {
|
|
116
|
+
const links = context.db.all(`SELECT ws.work_id,ws.status,w.status AS work_status FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id WHERE w.project_id=? AND w.run_id=? AND ws.session_id=? AND s.parent_session_id=? ORDER BY ws.created_at DESC,ws.rowid DESC`, [row.project_id, row.run_id, storageSessionId({ harness_id: harness, session_id: child }), storageSessionId({ harness_id: harness, session_id: parent })]);
|
|
117
|
+
const current = new Map();
|
|
118
|
+
for (const link of links)
|
|
119
|
+
if (!current.has(link.work_id))
|
|
120
|
+
current.set(link.work_id, link);
|
|
121
|
+
return current.size > 0 && [...current.values()].every(link => link.status === "completed" && link.work_status === "completed");
|
|
122
|
+
}
|
|
@@ -216,10 +216,11 @@ export function autoRefreshDashboards(context, input) {
|
|
|
216
216
|
if (projectRecoveryGuarded(context, project.id))
|
|
217
217
|
return { ok: true, skipped: true, reason: "run_recovery_guarded" };
|
|
218
218
|
const configured = readProjectConfig(context, project.id);
|
|
219
|
-
// Automatic observation must not change a memory
|
|
219
|
+
// Automatic observation must not change a memory/diagnostic baseline or
|
|
220
|
+
// invalidate accepted delivery checks by rewriting workspace projections.
|
|
220
221
|
// Keep projections in Home; do not hide runtime-created paths in Git or
|
|
221
222
|
// weaken source preservation by excluding user-owned .tasks directories.
|
|
222
|
-
const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind IN ('mb-init','mb-upgrade','mb-audit','project-review') AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
|
|
223
|
+
const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND (flow_kind IN ('mb-init','mb-upgrade','mb-fix','mb-audit','project-review','review-fix','interactive') OR flow_kind='mb-distill' AND json_extract(index_json,'$.execution_bundle.flow_id')='mb-distill' OR flow_kind IN ('release','deploy','publish') AND json_extract(index_json,'$.execution_bundle.flow_id')=flow_kind) AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
|
|
223
224
|
const config = initializing ? { ...configured, dashboard: { ...configured.dashboard,
|
|
224
225
|
markdown_path: path.join(projectDashboardDir(context.ddFlowHome, project.id), "project-dashboard.md") } } : configured;
|
|
225
226
|
if (!config.dashboard.auto_refresh) {
|
|
@@ -1307,6 +1308,7 @@ function normalizeFlowKind(flowKind) {
|
|
|
1307
1308
|
"mb-upgrade",
|
|
1308
1309
|
"mb-audit",
|
|
1309
1310
|
"mb-fix",
|
|
1311
|
+
"review-fix",
|
|
1310
1312
|
"mb-distill",
|
|
1311
1313
|
"mb-upgrade-review",
|
|
1312
1314
|
"mb-sdlc-review",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
import { resolveDeliveryBinding } from "./delivery-policy.js";
|
|
6
|
+
import { executeDeliveryStep } from "./delivery-execution.js";
|
|
7
|
+
const dimensions = ["identity", "authority", "target"];
|
|
8
|
+
const freshnessMs = 30000;
|
|
9
|
+
const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
function refuse(reason) {
|
|
11
|
+
throw new AppError("delivery_access_required", "Delivery access is not established for the selected operation; do not login or change context automatically", 2, { reason, effect: "no_effect" });
|
|
12
|
+
}
|
|
13
|
+
function identity(context, input) {
|
|
14
|
+
return hybridDigest({ home: context.ddFlowHome, project_id: input.projectId, run_id: input.runId,
|
|
15
|
+
operation_id: input.operationId, workspace: input.workspace, binding: input.binding });
|
|
16
|
+
}
|
|
17
|
+
/** Private same-process observation, not serialized publication authority.
|
|
18
|
+
* The owning Stage still establishes project-policy/readback safety and checks
|
|
19
|
+
* separate scoped approval, artifact custody and retained operation intent. */
|
|
20
|
+
export class PreparedDeliveryAccess {
|
|
21
|
+
#scope;
|
|
22
|
+
#pid = process.pid;
|
|
23
|
+
#expires;
|
|
24
|
+
#consumed = false;
|
|
25
|
+
#report;
|
|
26
|
+
constructor(context, input, observed, report) {
|
|
27
|
+
this.#scope = identity(context, input);
|
|
28
|
+
this.#expires = observed + freshnessMs;
|
|
29
|
+
this.#report = structuredClone(report);
|
|
30
|
+
}
|
|
31
|
+
static async observe(context, raw) {
|
|
32
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
33
|
+
refuse("observation_outside_writer_required");
|
|
34
|
+
const binding = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [raw.binding] }, raw.binding.selector);
|
|
35
|
+
const input = { ...raw, binding }, times = new Map(), processes = [];
|
|
36
|
+
const assertCurrent = () => {
|
|
37
|
+
const result = raw.assertCurrent();
|
|
38
|
+
if (types.isPromise(result)) {
|
|
39
|
+
void result.catch(() => { });
|
|
40
|
+
refuse("synchronous_owner_fence_required");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
assertCurrent();
|
|
44
|
+
if (binding.access.applicability === "required") {
|
|
45
|
+
const access = binding.access, procedure = binding.procedures.access_readback, deadline = Date.now() + procedure.timeout_ms;
|
|
46
|
+
// The project-owned procedure is the structured extractor. Each step
|
|
47
|
+
// emits only this public-context envelope (possibly partial/empty).
|
|
48
|
+
for (const step of procedure.procedure_steps) {
|
|
49
|
+
const remaining = deadline - Date.now();
|
|
50
|
+
if (remaining <= 0)
|
|
51
|
+
refuse("readback_failed");
|
|
52
|
+
let malformed = false;
|
|
53
|
+
try {
|
|
54
|
+
const result = await executeDeliveryStep(context, { projectId: input.projectId, runId: input.runId,
|
|
55
|
+
operationId: `access-${randomUUID()}`, stepId: step.id, workspace: input.workspace, procedure, deadlineAt: deadline,
|
|
56
|
+
assertCurrent, admit() { }, consumeOutput(bytes) {
|
|
57
|
+
let value;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
malformed = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!record(value) || value.schema_id !== "dd-flow/delivery-access-observation@1"
|
|
66
|
+
|| Object.keys(value).some(key => !["schema_id", ...dimensions].includes(key))) {
|
|
67
|
+
malformed = true;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const dimension of dimensions) {
|
|
71
|
+
if (!Object.hasOwn(value, dimension))
|
|
72
|
+
continue;
|
|
73
|
+
const expected = access[dimension], observed = value[dimension];
|
|
74
|
+
if (expected === "not_required") {
|
|
75
|
+
if (observed !== "not_required") {
|
|
76
|
+
malformed = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (!record(observed) || Object.keys(observed).sort().join() !== "id,kind"
|
|
81
|
+
|| observed.kind !== expected.kind || observed.id !== expected.id) {
|
|
82
|
+
malformed = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
times.set(dimension, Date.parse(context.now()));
|
|
86
|
+
}
|
|
87
|
+
} });
|
|
88
|
+
processes.push(result.process_id);
|
|
89
|
+
if (Date.now() >= deadline)
|
|
90
|
+
refuse("readback_failed");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
refuse("readback_failed");
|
|
94
|
+
}
|
|
95
|
+
if (malformed)
|
|
96
|
+
refuse("context_mismatch_or_unsafe_output");
|
|
97
|
+
}
|
|
98
|
+
if (!times.has("identity") || !times.has("target") || access.authority !== "not_required" && !times.has("authority"))
|
|
99
|
+
refuse("required_context_not_observable");
|
|
100
|
+
}
|
|
101
|
+
assertCurrent();
|
|
102
|
+
const now = Date.parse(context.now());
|
|
103
|
+
if (!Number.isFinite(now) || [...times.values()].some(time => !Number.isFinite(time) || time > now))
|
|
104
|
+
refuse("observation_clock_invalid");
|
|
105
|
+
const observed = times.size ? Math.min(...times.values()) : now;
|
|
106
|
+
const prepared = new PreparedDeliveryAccess(context, input, observed, { schema_id: "dd-flow/delivery-access@1",
|
|
107
|
+
operation_id: input.operationId, binding_id: binding.id, observed_at: new Date(observed).toISOString(),
|
|
108
|
+
applicability: binding.access.applicability === "required" ? "verified" : "not_required",
|
|
109
|
+
identity: binding.access.applicability === "required" ? "verified" : "not_required",
|
|
110
|
+
authority: binding.access.applicability === "required" && binding.access.authority !== "not_required" ? "verified" : "not_required",
|
|
111
|
+
target: binding.access.applicability === "required" ? "verified" : "not_required", expected: binding.access, processes });
|
|
112
|
+
prepared.assertFresh(context, input);
|
|
113
|
+
return prepared;
|
|
114
|
+
}
|
|
115
|
+
value() { return structuredClone(this.#report); }
|
|
116
|
+
assertFresh(context, input) {
|
|
117
|
+
const now = Date.parse(context.now());
|
|
118
|
+
if (this.#pid !== process.pid || this.#consumed || identity(context, input) !== this.#scope
|
|
119
|
+
|| !Number.isFinite(now) || now < Date.parse(this.#report.observed_at) || now >= this.#expires)
|
|
120
|
+
refuse("fresh_same_process_readback_required");
|
|
121
|
+
}
|
|
122
|
+
consume(context, input) { this.assertFresh(context, input); this.#consumed = true; }
|
|
123
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
3
|
+
import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
|
|
4
|
+
import { resolveDeliveryBinding } from "./delivery-policy.js";
|
|
5
|
+
const digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
6
|
+
const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
const keys = (value, names) => Object.keys(value).sort().join() === [...names].sort().join();
|
|
8
|
+
const text = (value) => typeof value === "string" && Boolean(value.trim()) && value.length <= 4096 && !value.includes("\0");
|
|
9
|
+
function refuse(reason) {
|
|
10
|
+
throw new AppError("delivery_approval_invalid", "Delivery needs current user approval for the exact selected operation", 2, { reason, effect: "no_effect" });
|
|
11
|
+
}
|
|
12
|
+
/** Public decision scope, not observed artifact bytes, account access or grant.
|
|
13
|
+
* The Stage owner supplies its retained artifact/target selection and freezes
|
|
14
|
+
* the same scope in the operation intent before execution. */
|
|
15
|
+
export function deliveryApprovalScope(scope, bundle, selection) {
|
|
16
|
+
const flow = bundle.manifest.flow_id;
|
|
17
|
+
if (flow !== "release" && flow !== "deploy" && flow !== "publish")
|
|
18
|
+
refuse("delivery_bundle_required");
|
|
19
|
+
const binding = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [selection.binding] }, selection.binding.selector);
|
|
20
|
+
if (binding.selector.operation !== flow || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(selection.operationId)
|
|
21
|
+
|| !record(selection.artifact) || !keys(selection.artifact, ["subject_ref", "version", "sha256"])
|
|
22
|
+
|| selection.artifact.subject_ref !== binding.selector.subject_ref || !text(selection.artifact.version) || !digest(selection.artifact.sha256))
|
|
23
|
+
refuse("exact_operation_scope_required");
|
|
24
|
+
const request = bundle.execution.snapshot().scope.requestId;
|
|
25
|
+
if (!text(request) || !digest(bundle.sha256))
|
|
26
|
+
refuse("original_request_required");
|
|
27
|
+
return { schema_id: "dd-flow/delivery-approval-scope@1", project_id: scope.projectId, run_id: scope.runId,
|
|
28
|
+
request_id: request, bundle_sha256: bundle.sha256, operation_id: selection.operationId, flow_id: flow,
|
|
29
|
+
binding_id: binding.id, binding_sha256: hybridDigest(binding), selector: binding.selector, artifact: structuredClone(selection.artifact) };
|
|
30
|
+
}
|
|
31
|
+
/** Custody and exact scope only: interpreting whether the cited user really
|
|
32
|
+
* approves this operation remains the owning agent/review's responsibility.
|
|
33
|
+
* Flow startup, a question or successful login never becomes approval here.
|
|
34
|
+
* This prepared value is not a publication token or dispatch authority. */
|
|
35
|
+
export function prepareDeliveryApproval(context, scope, bundle, selection, proposal) {
|
|
36
|
+
if (!record(proposal) || !keys(proposal, ["scope_sha256", "user_inputs_sha256", "basis", "expires_at", "reason"])
|
|
37
|
+
|| !digest(proposal.scope_sha256) || !digest(proposal.user_inputs_sha256) || !text(proposal.reason)
|
|
38
|
+
|| !text(proposal.expires_at) || !record(proposal.basis) || !keys(proposal.basis, ["role", "event_id", "sha256"])
|
|
39
|
+
|| !["request", "answer"].includes(proposal.basis.role) || !Number.isSafeInteger(proposal.basis.event_id)
|
|
40
|
+
|| proposal.basis.event_id < 0 || !digest(proposal.basis.sha256))
|
|
41
|
+
refuse("invalid_approval_proposal");
|
|
42
|
+
const selected = deliveryApprovalScope(scope, bundle, selection), flow = selected.flow_id;
|
|
43
|
+
const users = readPreparedMemoryFlowUserInputs(context, scope, flow, bundle), inputsHash = hybridDigest(users);
|
|
44
|
+
if (proposal.scope_sha256 !== hybridDigest(selected))
|
|
45
|
+
refuse("approval_scope_mismatch");
|
|
46
|
+
if (proposal.user_inputs_sha256 !== inputsHash)
|
|
47
|
+
refuse("current_user_input_required");
|
|
48
|
+
const source = users.sources.find(item => item.role === proposal.basis.role && item.event_id === proposal.basis.event_id && item.sha256 === proposal.basis.sha256);
|
|
49
|
+
if (!source || proposal.basis.role === "request" && proposal.basis.event_id !== 0)
|
|
50
|
+
refuse("delivered_approval_basis_required");
|
|
51
|
+
const expiry = Date.parse(proposal.expires_at), approvedAt = context.now();
|
|
52
|
+
if (!Number.isFinite(expiry) || !Number.isFinite(Date.parse(approvedAt)) || expiry <= Date.parse(approvedAt))
|
|
53
|
+
refuse("approval_expired");
|
|
54
|
+
const value = { schema_id: "dd-flow/delivery-approval@1", scope: selected, scope_sha256: hybridDigest(selected),
|
|
55
|
+
user_inputs_sha256: inputsHash, approver: "delivered_user", basis: structuredClone(proposal.basis),
|
|
56
|
+
approved_at: approvedAt, expires_at: new Date(expiry).toISOString(), interpretation: proposal.reason };
|
|
57
|
+
return {
|
|
58
|
+
value: () => structuredClone(value),
|
|
59
|
+
/** SQL/data-only fence. A later answer (including revocation) invalidates
|
|
60
|
+
* the interpretation until the owning Stage reassesses the full input set. */
|
|
61
|
+
assertCustody(writer) {
|
|
62
|
+
if (hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, flow, bundle)) !== inputsHash)
|
|
63
|
+
refuse("approval_inputs_changed");
|
|
64
|
+
if (!Number.isFinite(Date.parse(writer.now())) || Date.parse(writer.now()) >= expiry)
|
|
65
|
+
refuse("approval_expired");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
function refuse(reason) {
|
|
6
|
+
throw new AppError("delivery_artifact_invalid", "Delivery requires the exact accepted artifact bytes", 2, { reason, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
7
|
+
}
|
|
8
|
+
function observe(workspace, logicalPath) {
|
|
9
|
+
if (!path.isAbsolute(workspace) || typeof logicalPath !== "string" || !logicalPath || logicalPath.length > 4096
|
|
10
|
+
|| logicalPath.includes("\\") || logicalPath.includes("\0") || path.isAbsolute(logicalPath)
|
|
11
|
+
|| logicalPath.split("/").some(part => !part || part === "." || part === ".." || part === ".git"))
|
|
12
|
+
refuse("contained_artifact_required");
|
|
13
|
+
try {
|
|
14
|
+
if (fs.realpathSync(workspace) !== workspace || !fs.lstatSync(workspace).isDirectory())
|
|
15
|
+
refuse("canonical_workspace_required");
|
|
16
|
+
let file = workspace;
|
|
17
|
+
for (const part of logicalPath.split("/").slice(0, -1)) {
|
|
18
|
+
file = path.join(file, part);
|
|
19
|
+
if (!fs.lstatSync(file).isDirectory())
|
|
20
|
+
refuse("contained_artifact_required");
|
|
21
|
+
}
|
|
22
|
+
file = path.join(workspace, logicalPath);
|
|
23
|
+
if (!fs.lstatSync(file).isFile())
|
|
24
|
+
refuse("regular_artifact_required");
|
|
25
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
26
|
+
try {
|
|
27
|
+
const before = fs.fstatSync(fd, { bigint: true }), digest = createHash("sha256"), buffer = Buffer.allocUnsafe(256 * 1024);
|
|
28
|
+
if (!before.isFile())
|
|
29
|
+
refuse("regular_artifact_required");
|
|
30
|
+
let length = 0n;
|
|
31
|
+
for (;;) {
|
|
32
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
33
|
+
if (!count)
|
|
34
|
+
break;
|
|
35
|
+
digest.update(buffer.subarray(0, count));
|
|
36
|
+
length += BigInt(count);
|
|
37
|
+
}
|
|
38
|
+
const after = fs.fstatSync(fd, { bigint: true }), named = fs.lstatSync(file, { bigint: true });
|
|
39
|
+
if (length !== before.size || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs
|
|
40
|
+
|| !named.isFile() || named.dev !== before.dev || named.ino !== before.ino || fs.realpathSync(file) !== file)
|
|
41
|
+
refuse("artifact_changed_during_observation");
|
|
42
|
+
return { sha256: digest.digest("hex"), bytes: length.toString() };
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
fs.closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof AppError)
|
|
50
|
+
throw error;
|
|
51
|
+
return refuse("artifact_unavailable");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Exact file/archive bytes, or a project-owned manifest selecting immutable
|
|
55
|
+
* remote artifacts. A manifest digest alone does not prove its remote objects:
|
|
56
|
+
* the owning verify Stage must establish those references before approval.
|
|
57
|
+
* Observation is synchronous and outside SQL writers, immediately before the
|
|
58
|
+
* managed admission fence; it is not an OS lock against a hostile local writer. */
|
|
59
|
+
export class PreparedDeliveryArtifact {
|
|
60
|
+
#workspace;
|
|
61
|
+
#path;
|
|
62
|
+
#artifact;
|
|
63
|
+
#bytes;
|
|
64
|
+
constructor(workspace, logicalPath, artifact, bytes) {
|
|
65
|
+
this.#workspace = workspace;
|
|
66
|
+
this.#path = logicalPath;
|
|
67
|
+
this.#artifact = structuredClone(artifact);
|
|
68
|
+
this.#bytes = bytes;
|
|
69
|
+
}
|
|
70
|
+
static prepare(workspace, logicalPath, artifact) {
|
|
71
|
+
if (!artifact || Object.keys(artifact).sort().join() !== "sha256,subject_ref,version"
|
|
72
|
+
|| typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256)
|
|
73
|
+
|| [artifact.subject_ref, artifact.version].some(value => typeof value !== "string" || !value.trim() || value.length > 4096 || value.includes("\0")))
|
|
74
|
+
refuse("accepted_artifact_required");
|
|
75
|
+
const actual = observe(workspace, logicalPath);
|
|
76
|
+
if (actual.sha256 !== artifact.sha256)
|
|
77
|
+
refuse("accepted_artifact_changed");
|
|
78
|
+
return new PreparedDeliveryArtifact(workspace, logicalPath, artifact, actual.bytes);
|
|
79
|
+
}
|
|
80
|
+
value() { return { schema_id: "dd-flow/delivery-artifact@1", workspace: this.#workspace, path: this.#path, artifact: structuredClone(this.#artifact), bytes: this.#bytes }; }
|
|
81
|
+
assertCurrent() {
|
|
82
|
+
const actual = observe(this.#workspace, this.#path);
|
|
83
|
+
if (actual.sha256 !== this.#artifact.sha256 || actual.bytes !== this.#bytes)
|
|
84
|
+
refuse("accepted_artifact_changed");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { readDeliveryStageScope } from "./delivery-stage-entry.js";
|
|
3
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
4
|
+
import { readActiveDescriptorStageCoordinator, readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
5
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
6
|
+
import { prepareDeliveryProjectBinding } from "./delivery-policy.js";
|
|
7
|
+
import { PreparedDeliveryArtifact } from "./delivery-artifact.js";
|
|
8
|
+
import { deliveryApprovalScope } from "./delivery-approval.js";
|
|
9
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
10
|
+
import { codeCheckProfileHash, projectPolicyCheckDeclarations, runCodeChecks, PreparedCheckEvidence, PreparedRetainedCheckEvidence } from "./code-checks.js";
|
|
11
|
+
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
12
|
+
import { stageLifecycleMatchKey } from "./hooks.js";
|
|
13
|
+
import { assertDiagnosticSessionIndependence } from "./diagnostic-work-collection.js";
|
|
14
|
+
import { memoryCheckEnvironment } from "./memory-verification-checks.js";
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("delivery_checks_invalid", "Delivery verification needs current project checks for the exact selected artifact", 2, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
/** Compare actual provider Sessions, not model names or distinct logical IDs.
|
|
19
|
+
* Accepted generic settlement proves the author; current coordinator custody
|
|
20
|
+
* proves the verifier. Lifecycle match keys resolve paths before the writer. */
|
|
21
|
+
export function prepareDeliveryVerifierIndependence(context, input) {
|
|
22
|
+
if (input.stage !== "verify" || context.db.isOwnedWriteTransaction?.())
|
|
23
|
+
refuse("verification_stage_required");
|
|
24
|
+
const selected = readDeliveryStageScope(context, input), { scope, bundle } = selected;
|
|
25
|
+
const stages = selected.flowId === "deploy" ? ["prepare", "deploy"] : ["prepare"];
|
|
26
|
+
const sources = stages.map(stage => ({ stage, keys: entityReferenceVariants(input.runId)
|
|
27
|
+
.map(reference => stageLifecycleMatchKey("stage_finish", reference, stage, input.projectRoot)) }));
|
|
28
|
+
const observe = (reader) => {
|
|
29
|
+
selected.assertCustody(reader);
|
|
30
|
+
const physical = (sessionId) => {
|
|
31
|
+
const session = reader.db.get("SELECT harness,provider_session_id FROM sessions WHERE project_id=? AND run_id=? AND session_id=?", [scope.projectId, scope.runId, sessionId]);
|
|
32
|
+
if (!session?.harness || !session.provider_session_id)
|
|
33
|
+
refuse("verification_physical_owner_required");
|
|
34
|
+
return session;
|
|
35
|
+
};
|
|
36
|
+
const verifier = readActiveDescriptorStageCoordinator(reader, scope, bundle);
|
|
37
|
+
const authors = sources.map(source => readAcceptedDescriptorStageSettlement(reader, { ...scope, stage: source.stage }, bundle, source.keys));
|
|
38
|
+
const verifierSession = physical(verifier.session_id), authorSessions = authors.map(author => physical(author.binding.sessionId));
|
|
39
|
+
assertDiagnosticSessionIndependence([verifierSession], authorSessions);
|
|
40
|
+
return { verifier, authors, verifierSession, authorSessions };
|
|
41
|
+
};
|
|
42
|
+
const original = observe(context);
|
|
43
|
+
return (writer) => {
|
|
44
|
+
if (hybridDigest(observe(writer)) !== hybridDigest(original))
|
|
45
|
+
refuse("verification_physical_owner_changed");
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Downstream acceptance reuses the original accepted execution proof, not
|
|
49
|
+
* today's declarations, invented hashes or a new run of publication checks. */
|
|
50
|
+
export function prepareDeliveryHistoricalChecks(context, input) {
|
|
51
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
52
|
+
refuse("preparation_outside_writer_required");
|
|
53
|
+
const selected = readDeliveryStageScope(context, input), { scope, bundle } = selected;
|
|
54
|
+
if (input.sourceStage !== (selected.flowId === "deploy" && input.stage === "deploy" ? "prepare" : "verify"))
|
|
55
|
+
refuse("accepted_verification_stage_required");
|
|
56
|
+
const keys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.sourceStage, input.projectRoot));
|
|
57
|
+
const read = (reader) => {
|
|
58
|
+
const accepted = readAcceptedStageData(reader, { ...scope, stage: input.sourceStage });
|
|
59
|
+
const data = JSON.parse(accepted.bytes.toString("utf8"));
|
|
60
|
+
const rows = reader.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='delivery.stage_checks' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? LIMIT 2", [scope.projectId, scope.runId, input.sourceStage]);
|
|
61
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 1024 * 1024)
|
|
62
|
+
refuse("accepted_verification_required");
|
|
63
|
+
const value = JSON.parse(rows[0].payload_json), receipt = value.receipt;
|
|
64
|
+
if (data.schema_id !== "dd-flow/delivery-stage-result@1" || data.flow_id !== selected.flowId || data.run_id !== input.runId || data.stage !== input.sourceStage
|
|
65
|
+
|| !receipt || value.sha256 !== hybridDigest(receipt) || value.sha256 !== data.details?.verification_sha256
|
|
66
|
+
|| receipt.schema_id !== "dd-flow/delivery-checks@1" || receipt.run_id !== input.runId || receipt.stage !== input.sourceStage
|
|
67
|
+
|| receipt.bundle_sha256 !== bundle.sha256 || receipt.input_sha256 !== data.input_sha256
|
|
68
|
+
|| receipt.operation_scope_sha256 !== input.operationScopeSha256 || data.details.delivery_scope_sha256 !== input.operationScopeSha256
|
|
69
|
+
|| !Array.isArray(receipt.checks) || !receipt.checks.length)
|
|
70
|
+
refuse("accepted_verification_changed");
|
|
71
|
+
return { accepted, value, owner: readAcceptedDescriptorStageSettlement(reader, { ...scope, stage: input.sourceStage }, bundle, keys) };
|
|
72
|
+
};
|
|
73
|
+
const original = read(context);
|
|
74
|
+
const proof = PreparedRetainedCheckEvidence.prepare(context, { ...scope, references: original.value.receipt.checks });
|
|
75
|
+
return { sha256: original.value.sha256, receipt: structuredClone(original.value.receipt),
|
|
76
|
+
revalidate() { proof.revalidate(context); },
|
|
77
|
+
assertCustody(writer) {
|
|
78
|
+
selected.assertCustody(writer);
|
|
79
|
+
proof.assertCustody(writer);
|
|
80
|
+
if (hybridDigest(read(writer)) !== hybridDigest(original))
|
|
81
|
+
refuse("accepted_verification_changed");
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Immediately before an external effect, historical success is insufficient:
|
|
86
|
+
* the existing check owner must still match today's workspace and policy.
|
|
87
|
+
* This also catches an edited publish/deploy script with unchanged argv. */
|
|
88
|
+
export function prepareDeliveryCurrentAcceptedChecks(context, input) {
|
|
89
|
+
const historical = prepareDeliveryHistoricalChecks(context, input), selected = readDeliveryStageScope(context, input);
|
|
90
|
+
if (!selected.run.run_root)
|
|
91
|
+
refuse("run_root_required");
|
|
92
|
+
const workspace = selected.bundle.execution.snapshot().scope.workspace;
|
|
93
|
+
const checks = [...projectPolicyCheckDeclarations(workspace, "code"), ...projectPolicyCheckDeclarations(workspace, "readiness")];
|
|
94
|
+
const environment = memoryCheckEnvironment(context, selected.scope.projectId, input.runId);
|
|
95
|
+
const proof = PreparedCheckEvidence.prepare(context, { projectId: selected.scope.projectId, runId: input.runId,
|
|
96
|
+
runHome: selected.run.run_root, workspaceRoot: workspace, scope: "aggregate", checks,
|
|
97
|
+
expectedProfileHash: historical.receipt.profile_sha256, evidenceContext: historical.receipt.evidence_context,
|
|
98
|
+
...(environment ? { checkEnvironment: environment } : {}) });
|
|
99
|
+
if (!proof.qualityPassed() || hybridDigest(proof.retainedReferences(context)) !== hybridDigest(historical.receipt.checks))
|
|
100
|
+
refuse("accepted_verification_changed");
|
|
101
|
+
return {
|
|
102
|
+
revalidate() {
|
|
103
|
+
if (hybridDigest(memoryCheckEnvironment(context, selected.scope.projectId, input.runId) ?? null) !== hybridDigest(environment ?? null))
|
|
104
|
+
refuse("managed_check_home_changed");
|
|
105
|
+
historical.revalidate();
|
|
106
|
+
proof.revalidate(context);
|
|
107
|
+
},
|
|
108
|
+
assertCustody(writer) { historical.assertCustody(writer); proof.assertCustody(writer); }
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** The existing aggregate runner/check receipts, not model-reported tests.
|
|
112
|
+
* Declarations come from project policy, never the result being accepted.
|
|
113
|
+
* Semantic adequacy and independent verification remain Stage responsibilities. */
|
|
114
|
+
export function prepareDeliveryChecks(context, input) {
|
|
115
|
+
if (context.db.isOwnedWriteTransaction?.() || !["prepare", "verify"].includes(input.stage))
|
|
116
|
+
refuse("verification_stage_required");
|
|
117
|
+
if (!input.artifact || typeof input.artifact !== "object" || Array.isArray(input.artifact))
|
|
118
|
+
refuse("accepted_artifact_selection_required");
|
|
119
|
+
const selected = readDeliveryStageScope(context, input), { scope, bundle } = selected;
|
|
120
|
+
const independent = input.stage === "verify" ? prepareDeliveryVerifierIndependence(context, input) : undefined;
|
|
121
|
+
const packet = readDescriptorStageInput(context, scope, bundle);
|
|
122
|
+
if (!packet.input.predecessor || !selected.run.run_root)
|
|
123
|
+
refuse("accepted_predecessor_required");
|
|
124
|
+
const previousStage = packet.input.predecessor.stage;
|
|
125
|
+
const previous = readAcceptedStageData(context, { ...scope, stage: previousStage });
|
|
126
|
+
const prior = JSON.parse(previous.bytes.toString("utf8"));
|
|
127
|
+
const decision = prior.details.decision;
|
|
128
|
+
if (!decision || input.stage !== "prepare" && hybridDigest(prior.details.artifact) !== hybridDigest(input.artifact))
|
|
129
|
+
refuse("accepted_artifact_selection_required");
|
|
130
|
+
const workspace = bundle.execution.snapshot().scope.workspace;
|
|
131
|
+
const policy = prepareDeliveryProjectBinding(workspace, decision.policy, decision.binding.selector);
|
|
132
|
+
if (hybridDigest(policy.binding) !== hybridDigest(decision.binding))
|
|
133
|
+
refuse("accepted_policy_changed");
|
|
134
|
+
const artifact = PreparedDeliveryArtifact.prepare(workspace, input.artifact.path, { subject_ref: input.artifact.subject_ref, version: input.artifact.version, sha256: input.artifact.sha256 });
|
|
135
|
+
if (input.artifact.version !== decision.version)
|
|
136
|
+
refuse("accepted_version_required");
|
|
137
|
+
const operation = deliveryApprovalScope(scope, bundle, { operationId: decision.operation_id, binding: policy.binding,
|
|
138
|
+
artifact: { subject_ref: input.artifact.subject_ref, version: input.artifact.version, sha256: input.artifact.sha256 } });
|
|
139
|
+
const checks = [...projectPolicyCheckDeclarations(workspace, "code"), ...projectPolicyCheckDeclarations(workspace, "readiness")];
|
|
140
|
+
if (!checks.length)
|
|
141
|
+
refuse("project_verification_checks_required");
|
|
142
|
+
const owner = readActiveDescriptorStageCoordinator(context, scope, bundle), profile = codeCheckProfileHash(workspace, checks);
|
|
143
|
+
const environment = memoryCheckEnvironment(context, scope.projectId, input.runId);
|
|
144
|
+
const evidenceContext = `delivery:${hybridDigest({ operation, stage: input.stage, input: packet.inputSha256, users: selected.userInputsSha256 })}`;
|
|
145
|
+
const command = { projectId: scope.projectId, runId: scope.runId, runHome: selected.run.run_root, workspaceRoot: workspace,
|
|
146
|
+
scope: "aggregate", checks, expectedProfileHash: profile, evidenceContext,
|
|
147
|
+
...(environment ? { checkEnvironment: environment } : {}) };
|
|
148
|
+
const assertCustody = (writer) => {
|
|
149
|
+
selected.assertCustody(writer);
|
|
150
|
+
independent?.(writer);
|
|
151
|
+
if (hybridDigest(readDescriptorStageInput(writer, scope, bundle)) !== hybridDigest(packet)
|
|
152
|
+
|| hybridDigest(readAcceptedStageData(writer, { ...scope, stage: previousStage })) !== hybridDigest(previous)
|
|
153
|
+
|| hybridDigest(readActiveDescriptorStageCoordinator(writer, scope, bundle)) !== hybridDigest(owner))
|
|
154
|
+
refuse("verification_owner_or_inputs_changed");
|
|
155
|
+
};
|
|
156
|
+
const revalidate = () => {
|
|
157
|
+
assertCustody(context);
|
|
158
|
+
policy.assertCurrent();
|
|
159
|
+
artifact.assertCurrent();
|
|
160
|
+
if (hybridDigest(memoryCheckEnvironment(context, scope.projectId, input.runId) ?? null) !== hybridDigest(environment ?? null))
|
|
161
|
+
refuse("managed_check_home_changed");
|
|
162
|
+
if (codeCheckProfileHash(workspace, checks) !== profile)
|
|
163
|
+
refuse("verification_policy_changed");
|
|
164
|
+
};
|
|
165
|
+
revalidate();
|
|
166
|
+
return { operation: structuredClone(operation), artifact: structuredClone(input.artifact), assertCustody, revalidate,
|
|
167
|
+
async run() { revalidate(); const result = await runCodeChecks(context, command); revalidate(); return result; },
|
|
168
|
+
evidence() {
|
|
169
|
+
revalidate();
|
|
170
|
+
const proof = PreparedCheckEvidence.prepare(context, command);
|
|
171
|
+
if (!proof.qualityPassed())
|
|
172
|
+
refuse("verification_checks_failed");
|
|
173
|
+
return { proof, receipt: { schema_id: "dd-flow/delivery-checks@1", run_id: scope.runId, stage: input.stage,
|
|
174
|
+
bundle_sha256: bundle.sha256, input_sha256: packet.inputSha256, operation_scope_sha256: hybridDigest(operation),
|
|
175
|
+
evidence_context: evidenceContext, profile_sha256: profile, checks: proof.retainedReferences(context) } };
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|