@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,195 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { Ajv } from "ajv/dist/ajv.js";
|
|
5
|
+
import semver from "semver";
|
|
6
|
+
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
|
|
8
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
9
|
+
import { isCanonicalOnlyMemoryPath } from "./memory-layer.js";
|
|
10
|
+
import { localEnvironmentAllowed } from "./workspace-files.js";
|
|
11
|
+
import { admitBundledFlow } from "./stage-handlers.js";
|
|
12
|
+
import { normalizeFlowContract } from "../domain/flow-contract.js";
|
|
13
|
+
const limit = 1024 * 1024, manifestPath = "project-pack/manifest.json";
|
|
14
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("project_flow_source_invalid", "Project-local flow requires its own valid curated pack; use the supported pack upgrade/repair route", 2, { reason, phase: "prepare", effect: "no_effect", next_action: "inspect_project_pack_and_upgrade_or_repair" });
|
|
17
|
+
}
|
|
18
|
+
function safe(name) {
|
|
19
|
+
return typeof name === "string" && /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(name)
|
|
20
|
+
&& name.split("/").every(part => ![".", "..", ".git", "node_modules", ".dd-flow"].includes(part.toLowerCase()) && localEnvironmentAllowed(part));
|
|
21
|
+
}
|
|
22
|
+
function schema(name) { return JSON.parse(fs.readFileSync(new URL(`../schemas/${name}.schema.json`, import.meta.url), "utf8")); }
|
|
23
|
+
function decoded(bytes) { try {
|
|
24
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return refuse("invalid_pack_json");
|
|
28
|
+
} }
|
|
29
|
+
function validate(scope, blobs) {
|
|
30
|
+
if (!scope || Object.keys(scope).sort().join() !== "flowId,projectRoot,workspace" || scope.flowId !== "interactive"
|
|
31
|
+
|| [scope.projectRoot, scope.workspace].some(value => typeof value !== "string" || !path.isAbsolute(value)))
|
|
32
|
+
refuse("invalid_project_scope");
|
|
33
|
+
const manifest = decoded(blobs.get(manifestPath) ?? Buffer.alloc(0));
|
|
34
|
+
if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("project-flow-pack-manifest"))(manifest)
|
|
35
|
+
|| manifest.schema_id !== "dd-flow/project-flow-pack-manifest@2" || !/^[a-f0-9]{40}$/.test(manifest.source_commit)
|
|
36
|
+
|| !semver.valid(manifest.canon_version_at_source_commit))
|
|
37
|
+
refuse("invalid_or_legacy_project_manifest");
|
|
38
|
+
if (manifest.included_files.length > 20_000 || manifest.included_files.some(name => !safe(name) || name === manifestPath
|
|
39
|
+
|| name.startsWith("project-pack/") || isCanonicalOnlyMemoryPath(`dd-flow/${name}`)))
|
|
40
|
+
refuse("invalid_curated_inventory");
|
|
41
|
+
const names = [manifestPath, ...manifest.included_files], folded = new Set();
|
|
42
|
+
let total = 0;
|
|
43
|
+
if (blobs.size !== names.length)
|
|
44
|
+
refuse("pack_inventory_changed");
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
if (folded.has(name.toLowerCase()))
|
|
47
|
+
refuse("aliased_pack_path");
|
|
48
|
+
folded.add(name.toLowerCase());
|
|
49
|
+
const bytes = blobs.get(name);
|
|
50
|
+
if (!bytes?.length || bytes.length > limit || (total += bytes.length) > 32 * limit)
|
|
51
|
+
refuse("missing_or_unbounded_pack_file");
|
|
52
|
+
if (name !== manifestPath && manifest.file_hashes?.[name] !== undefined && manifest.file_hashes[name] !== hash(bytes))
|
|
53
|
+
refuse("project_pack_file_changed");
|
|
54
|
+
}
|
|
55
|
+
const descriptorPath = `${scope.flowId}/flow-descriptor.json`, descriptor = decoded(blobs.get(descriptorPath) ?? Buffer.alloc(0));
|
|
56
|
+
if (validateHybridShape("flow-descriptor", descriptor).length || descriptor.id !== scope.flowId || descriptor.origin !== "project")
|
|
57
|
+
refuse("project_descriptor_required");
|
|
58
|
+
for (const stage of descriptor.stages)
|
|
59
|
+
for (const kind of ["instruction", "result_schema"]) {
|
|
60
|
+
const ref = stage[kind], bytes = blobs.get(ref.path);
|
|
61
|
+
if (!bytes || ref.path === manifestPath || !manifest.included_files.includes(ref.path) || hash(bytes) !== ref.sha256)
|
|
62
|
+
refuse("stage_source_not_in_project_pack");
|
|
63
|
+
try {
|
|
64
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
refuse("invalid_stage_source_encoding");
|
|
68
|
+
}
|
|
69
|
+
if (kind === "result_schema")
|
|
70
|
+
decoded(bytes);
|
|
71
|
+
}
|
|
72
|
+
const compatibility = decoded(blobs.get("compatibility.json") ?? Buffer.alloc(0));
|
|
73
|
+
if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("compatibility"))(compatibility))
|
|
74
|
+
refuse("invalid_project_compatibility");
|
|
75
|
+
const contract = normalizeFlowContract(decoded(blobs.get("flow-contract.json") ?? Buffer.alloc(0)));
|
|
76
|
+
if (contract.id !== manifest.flow_contract || compatibility.dd_flow_cli.flow_contract !== contract.id
|
|
77
|
+
|| compatibility.memory_bank_version !== manifest.canon_version_at_source_commit)
|
|
78
|
+
refuse("project_contract_mismatch");
|
|
79
|
+
return { manifest, descriptorPath, compatibility };
|
|
80
|
+
}
|
|
81
|
+
/** Project pack data, not engine/handler admission. Manifest canon paths are
|
|
82
|
+
* provenance hints only: never follow them or fall back to global instructions. */
|
|
83
|
+
export class PreparedProjectFlowSource {
|
|
84
|
+
sha256;
|
|
85
|
+
#scope;
|
|
86
|
+
#blobs;
|
|
87
|
+
#facts;
|
|
88
|
+
#fresh;
|
|
89
|
+
constructor(scope, blobs, fresh) {
|
|
90
|
+
this.#facts = validate(scope, blobs);
|
|
91
|
+
this.#scope = structuredClone(scope);
|
|
92
|
+
this.#blobs = new Map([...blobs].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, bytes]) => [name, Buffer.from(bytes)]));
|
|
93
|
+
this.sha256 = hybridDigest({ scope, inventory: [...this.#blobs].map(([name, bytes]) => ({ path: name, sha256: hash(bytes) })) });
|
|
94
|
+
this.#fresh = fresh;
|
|
95
|
+
Object.freeze(this);
|
|
96
|
+
}
|
|
97
|
+
static fromProject(scope) {
|
|
98
|
+
try {
|
|
99
|
+
for (const root of [scope.projectRoot, scope.workspace])
|
|
100
|
+
if (fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
|
|
101
|
+
refuse("noncanonical_project_root");
|
|
102
|
+
let root = scope.workspace;
|
|
103
|
+
for (const name of [".memory-bank", "dd-flow"]) {
|
|
104
|
+
root = path.join(root, name);
|
|
105
|
+
if (!fs.lstatSync(root).isDirectory())
|
|
106
|
+
refuse("linked_project_pack");
|
|
107
|
+
}
|
|
108
|
+
const read = (logical) => {
|
|
109
|
+
if (!safe(logical))
|
|
110
|
+
refuse("unsafe_pack_path");
|
|
111
|
+
let parent = root;
|
|
112
|
+
for (const part of logical.split("/").slice(0, -1)) {
|
|
113
|
+
parent = path.join(parent, part);
|
|
114
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
115
|
+
refuse("linked_project_pack");
|
|
116
|
+
}
|
|
117
|
+
return readBoundedReleaseFile(path.join(root, logical), limit);
|
|
118
|
+
};
|
|
119
|
+
const raw = read("manifest.json"), manifest = decoded(raw);
|
|
120
|
+
if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("project-flow-pack-manifest"))(manifest)
|
|
121
|
+
|| manifest.schema_id !== "dd-flow/project-flow-pack-manifest@2")
|
|
122
|
+
refuse("invalid_or_legacy_project_manifest");
|
|
123
|
+
const blobs = new Map([[manifestPath, raw]]);
|
|
124
|
+
if (manifest.included_files.length > 20_000)
|
|
125
|
+
refuse("unbounded_project_pack");
|
|
126
|
+
let total = raw.length;
|
|
127
|
+
for (const name of manifest.included_files) {
|
|
128
|
+
if (!safe(name) || isCanonicalOnlyMemoryPath(`dd-flow/${name}`) || name.startsWith("project-pack/"))
|
|
129
|
+
refuse("invalid_curated_inventory");
|
|
130
|
+
const bytes = read(name);
|
|
131
|
+
total += bytes.length;
|
|
132
|
+
if (total > 32 * limit)
|
|
133
|
+
refuse("unbounded_project_pack");
|
|
134
|
+
blobs.set(name, bytes);
|
|
135
|
+
}
|
|
136
|
+
if (!read("manifest.json").equals(raw))
|
|
137
|
+
refuse("manifest_changed_during_capture");
|
|
138
|
+
return new PreparedProjectFlowSource(scope, blobs, true);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error instanceof AppError && error.code === "project_flow_source_invalid")
|
|
142
|
+
throw error;
|
|
143
|
+
return refuse("project_pack_unavailable_or_changed");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
snapshot() { return { schema_id: "dd-flow/project-flow-source@1", scope: structuredClone(this.#scope), blobs: [...this.#blobs].map(([name, bytes]) => ({ path: name, base64: bytes.toString("base64") })) }; }
|
|
147
|
+
/** Data restoration only. expectedSha256 must come from independent request/
|
|
148
|
+
* RUN custody. Engine/handler admission is separate and never inferred here. */
|
|
149
|
+
static restoreRetained(value, expectedSha256) {
|
|
150
|
+
if (!value || Object.keys(value).sort().join() !== "blobs,schema_id,scope" || value.schema_id !== "dd-flow/project-flow-source@1"
|
|
151
|
+
|| !Array.isArray(value.blobs) || value.blobs.length > 20_001)
|
|
152
|
+
refuse("invalid_retained_project_source");
|
|
153
|
+
const blobs = new Map();
|
|
154
|
+
let total = 0;
|
|
155
|
+
for (const item of value.blobs) {
|
|
156
|
+
if (!item || Object.keys(item).sort().join() !== "base64,path" || !safe(item.path) || typeof item.base64 !== "string" || item.base64.length > 2 * limit || blobs.has(item.path))
|
|
157
|
+
refuse("invalid_retained_project_source");
|
|
158
|
+
const bytes = Buffer.from(item.base64, "base64");
|
|
159
|
+
total += bytes.length;
|
|
160
|
+
if (!bytes.length || bytes.length > limit || total > 32 * limit || bytes.toString("base64") !== item.base64)
|
|
161
|
+
refuse("invalid_retained_project_source");
|
|
162
|
+
blobs.set(item.path, bytes);
|
|
163
|
+
}
|
|
164
|
+
const result = new PreparedProjectFlowSource(value.scope, blobs, false);
|
|
165
|
+
if (result.sha256 !== expectedSha256)
|
|
166
|
+
refuse("retained_project_source_changed");
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
blobs() { return new Map([...this.#blobs].map(([name, bytes]) => [name, Buffer.from(bytes)])); }
|
|
170
|
+
scope() { return structuredClone(this.#scope); }
|
|
171
|
+
descriptorReference() { const name = this.#facts.descriptorPath; return { path: name, sha256: hash(this.#blobs.get(name)) }; }
|
|
172
|
+
projectPackProvenance() { return { sha256: hybridDigest([...this.#blobs].map(([name, bytes]) => ({ path: name === manifestPath ? "dd-flow/manifest.json" : `dd-flow/${name}`, sha256: hash(bytes) })).sort((a, b) => a.path.localeCompare(b.path, "en"))), source_commit: this.#facts.manifest.source_commit }; }
|
|
173
|
+
canonSource() { return { version: this.#facts.manifest.canon_version_at_source_commit, commit: this.#facts.manifest.source_commit }; }
|
|
174
|
+
/** Declared project base, not a claim that customized instructions are bytes
|
|
175
|
+
* from an installed canonical archive. Actual data identity is the pack hash. */
|
|
176
|
+
canonProvenance() { return { ...this.canonSource(), archive_sha256: null }; }
|
|
177
|
+
resolveRegisteredFlow() { const ref = this.descriptorReference(); return admitBundledFlow(this.#blobs.get(ref.path), ref.sha256, this.#blobs); }
|
|
178
|
+
assertEngineCompatibility(engine) {
|
|
179
|
+
const value = this.#facts.compatibility;
|
|
180
|
+
if (engine.package_name !== value.engine.package_name || engine.package_name !== value.dd_flow_cli.package_name
|
|
181
|
+
|| !semver.valid(engine.package_version) || !semver.valid(engine.engine_version)
|
|
182
|
+
|| !semver.validRange(value.engine.version_range) || !semver.valid(value.dd_flow_cli.min_version)
|
|
183
|
+
|| !semver.satisfies(engine.engine_version, value.engine.version_range, { includePrerelease: true })
|
|
184
|
+
|| !semver.gte(engine.package_version, value.dd_flow_cli.min_version))
|
|
185
|
+
refuse("project_pack_engine_incompatible");
|
|
186
|
+
}
|
|
187
|
+
revalidate() {
|
|
188
|
+
if (!this.#fresh) {
|
|
189
|
+
validate(this.#scope, this.#blobs);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (PreparedProjectFlowSource.fromProject(this.#scope).sha256 !== this.sha256)
|
|
193
|
+
refuse("project_source_changed_before_retention");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
|
+
import { interactiveWorkPathAllowed } from "./interactive-work-batch.js";
|
|
8
|
+
import { ObservedMemorySources } from "./memory-source-observations.js";
|
|
9
|
+
import { memorySourceKey } from "./memory-stage-results.js";
|
|
10
|
+
import { assertMemoryGitRemovalSources } from "./memory-git-integration.js";
|
|
11
|
+
function refuse(reason) {
|
|
12
|
+
throw new AppError("project_git_integration_invalid", "Reviewed project files lack exact local Git integration", 2, { reason, effect: "no_effect" });
|
|
13
|
+
}
|
|
14
|
+
export function assertReviewedProjectGitInventory(references, removals) {
|
|
15
|
+
const valid = (ref) => ref?.root === "workspace" && interactiveWorkPathAllowed(ref.path)
|
|
16
|
+
&& /^[a-f0-9]{64}$/.test(ref.sha256) && (Object.keys(ref).sort().join() === "path,root,sha256"
|
|
17
|
+
|| Object.keys(ref).sort().join() === "git_mode,path,root,sha256" && ["100644", "100755"].includes(ref.git_mode));
|
|
18
|
+
if (!Array.isArray(references) || !Array.isArray(removals) || !references.length && !removals.length
|
|
19
|
+
|| references.length + removals.length > 4096 || [...references, ...removals].some(ref => !valid(ref))
|
|
20
|
+
|| new Set([...references, ...removals].map(ref => ref.path)).size !== references.length + removals.length)
|
|
21
|
+
refuse("invalid_reviewed_project_inventory");
|
|
22
|
+
}
|
|
23
|
+
/** Physical selected-file readback only, not review, policy or commit authority.
|
|
24
|
+
* Unrelated dirty files/index entries are deliberately neither committed nor
|
|
25
|
+
* treated as failures. The terminal owner must admit the original dirty input. */
|
|
26
|
+
export function observeReviewedProjectGitIntegration(input, environment, references, removals = []) {
|
|
27
|
+
assertReviewedProjectGitInventory(references, removals);
|
|
28
|
+
for (const root of [input.projectRoot, input.workspaceRoot]) {
|
|
29
|
+
if (!path.isAbsolute(root) || fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
|
|
30
|
+
refuse("canonical_repository_root_required");
|
|
31
|
+
}
|
|
32
|
+
if (!path.isAbsolute(input.gitExecutable) || typeof input.integrationBranch !== "string" || !input.integrationBranch)
|
|
33
|
+
refuse("explicit_git_selection_required");
|
|
34
|
+
const env = { ...Object.fromEntries(Object.entries(environment).filter(([key]) => !key.startsWith("GIT_"))), GIT_OPTIONAL_LOCKS: "0", GIT_LITERAL_PATHSPECS: "1" };
|
|
35
|
+
const git = (root, args) => {
|
|
36
|
+
const result = spawnSync(input.gitExecutable, ["-c", "core.fsmonitor=false", "-C", root, ...args], { env, timeout: 10000, maxBuffer: 16 * 1024 * 1024, shell: false, windowsHide: true });
|
|
37
|
+
if (result.error || result.signal || result.status !== 0)
|
|
38
|
+
refuse("git_readback_failed");
|
|
39
|
+
return result.stdout;
|
|
40
|
+
};
|
|
41
|
+
const text = (root, args) => git(root, args).toString("utf8").trim();
|
|
42
|
+
const source = input.workspaceRoot, target = input.projectRoot, integrationRef = `refs/heads/${input.integrationBranch}`;
|
|
43
|
+
git(target, ["check-ref-format", integrationRef]);
|
|
44
|
+
const common = (root) => fs.realpathSync(path.resolve(root, text(root, ["rev-parse", "--git-common-dir"])));
|
|
45
|
+
const repository = common(source);
|
|
46
|
+
if (common(target) !== repository || [source, target].some(root => text(root, ["rev-parse", "--show-toplevel"]) !== root))
|
|
47
|
+
refuse("foreign_repository");
|
|
48
|
+
const workspaceBranch = text(source, ["symbolic-ref", "--short", "HEAD"]);
|
|
49
|
+
if (text(target, ["symbolic-ref", "--short", "HEAD"]) !== input.integrationBranch)
|
|
50
|
+
refuse("integration_branch_changed");
|
|
51
|
+
const head = text(source, ["rev-parse", "--verify", "HEAD^{commit}"]), integrated = text(target, ["rev-parse", "--verify", `${integrationRef}^{commit}`]);
|
|
52
|
+
git(target, ["merge-base", "--is-ancestor", head, integrated]);
|
|
53
|
+
const paths = [...references, ...removals].map(ref => ref.path);
|
|
54
|
+
const observations = [...new Set([source, target])].map(root => {
|
|
55
|
+
const visibility = git(root, ["ls-files", "-v", "-z", "--", ...paths]).toString("utf8");
|
|
56
|
+
if (visibility.split("\0").filter(Boolean).some(entry => !entry.startsWith("H ")))
|
|
57
|
+
refuse("selected_index_visibility_override");
|
|
58
|
+
git(root, ["diff", "--cached", "--exit-code", "--", ...paths]);
|
|
59
|
+
assertMemoryGitRemovalSources(root, removals);
|
|
60
|
+
const observed = ObservedMemorySources.capture({ workspace: root }, references.map(({ root, path }) => ({ root, path })));
|
|
61
|
+
const hashes = observed.hashes();
|
|
62
|
+
for (const ref of references) {
|
|
63
|
+
if (hashes.get(memorySourceKey("workspace", ref.path)) !== ref.sha256)
|
|
64
|
+
refuse("reviewed_workspace_bytes_changed");
|
|
65
|
+
if (process.platform !== "win32" && ((fs.lstatSync(path.join(root, ref.path)).mode & 0o111) !== 0) !== (ref.git_mode === "100755"))
|
|
66
|
+
refuse("reviewed_workspace_mode_changed");
|
|
67
|
+
}
|
|
68
|
+
return observed;
|
|
69
|
+
});
|
|
70
|
+
// ponytail: at most 4096 selected paths; batch Git reads if real interactive
|
|
71
|
+
// inventories make per-file subprocess latency material.
|
|
72
|
+
for (const [root, revision] of [[source, head], [target, integrated]]) {
|
|
73
|
+
for (const ref of references) {
|
|
74
|
+
const entry = git(root, ["ls-tree", "-z", revision, "--", ref.path]).toString("utf8");
|
|
75
|
+
if (!entry.startsWith(`${ref.git_mode ?? "100644"} blob `) || !entry.endsWith(`\t${ref.path}\0`))
|
|
76
|
+
refuse("reviewed_regular_git_blob_required");
|
|
77
|
+
if (createHash("sha256").update(git(root, ["cat-file", "blob", `${revision}:${ref.path}`])).digest("hex") !== ref.sha256)
|
|
78
|
+
refuse("reviewed_git_bytes_changed");
|
|
79
|
+
}
|
|
80
|
+
for (const ref of removals)
|
|
81
|
+
if (git(root, ["ls-tree", "-z", revision, "--", ref.path]).length)
|
|
82
|
+
refuse("removed_file_still_committed");
|
|
83
|
+
}
|
|
84
|
+
for (const observation of observations)
|
|
85
|
+
observation.revalidate();
|
|
86
|
+
if (text(source, ["rev-parse", "HEAD"]) !== head || text(target, ["rev-parse", integrationRef]) !== integrated)
|
|
87
|
+
refuse("integration_changed_during_observation");
|
|
88
|
+
return { schema_id: "dd-flow/project-git-integration-observation@1", project_root: target, workspace_root: source,
|
|
89
|
+
repository, workspace_branch: workspaceBranch, integration_ref: integrationRef, workspace_head: head, integration_head: integrated,
|
|
90
|
+
reviewed_files_sha256: hybridDigest([...references].sort((a, b) => a.path.localeCompare(b.path, "en"))),
|
|
91
|
+
removed_files_sha256: hybridDigest([...removals].sort((a, b) => a.path.localeCompare(b.path, "en"))) };
|
|
92
|
+
}
|
|
@@ -2,15 +2,10 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { stageAttempt, stageCycle, bindStageWork } from "./stage-work-graph.js";
|
|
3
3
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { assertCheckDiagnosisEligible } from "./code-checks.js";
|
|
6
6
|
/** Checks finish before entering this synchronous registration transaction. */
|
|
7
7
|
export function ensureAggregateRepair(context, input, create) {
|
|
8
|
-
|
|
9
|
-
throw new AppError("repair_intent_invalid", "Repair requires candidate-source-backed failed receipts", 1);
|
|
10
|
-
const workspace = context.db.get("SELECT workspace_root FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]);
|
|
11
|
-
if (!workspace?.workspace_root && input.failures.some(receipt => receipt.candidate_library_inputs?.length))
|
|
12
|
-
throw new AppError("repair_intent_invalid", "Library-backed repair requires the current RUN workspace", 1);
|
|
13
|
-
assertSourceRepairEligible(input.failures, workspace?.workspace_root);
|
|
8
|
+
assertCheckDiagnosisEligible(input.failures);
|
|
14
9
|
const causes = input.failures.map(receipt => ({ id: receipt.id, epoch: receipt.verification_epoch, fingerprint: receipt.fingerprint_scope === "check_inputs" ? receipt.before_fingerprint : receipt.workspace_fingerprint })).sort((a, b) => a.id.localeCompare(b.id));
|
|
15
10
|
return ensureRepairIntent(context, { ...input, causes: { receipts: causes, origins: [...(input.origins ?? [])].sort() } }, create);
|
|
16
11
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, readdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { canonicalPath } from "../storage/paths.js";
|
|
7
|
+
import { lifecycleCommandFromPayload } from "./lifecycle-command.js";
|
|
8
|
+
import { matchesIssuedNativeCommand, observedLifecycleInvocation } from "./lifecycle-invocations.js";
|
|
9
|
+
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
10
|
+
import { requireProjectByRoot } from "./project-registry.js";
|
|
11
|
+
import { handleAgyEvent, handleCodexHook, handleDroidEvent, handleOpenCodeEvent, handleGrokEvent, handleZcodeEvent } from "./hooks.js";
|
|
12
|
+
import { withDaemonProgressObserver } from "../harness-runtime/lib/daemon-observation.mjs";
|
|
13
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
14
|
+
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
15
|
+
const diagnostics = new WeakMap();
|
|
16
|
+
async function regularJson(file) {
|
|
17
|
+
try {
|
|
18
|
+
const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
19
|
+
try {
|
|
20
|
+
if (!(await handle.stat()).isFile())
|
|
21
|
+
return null;
|
|
22
|
+
return object(JSON.parse(await handle.readFile("utf8")));
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await handle.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (["ENOENT", "ELOOP", "ENOTDIR"].includes(error.code ?? "") || error instanceof SyntaxError)
|
|
30
|
+
return null;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function realDirectory(directory) {
|
|
35
|
+
try {
|
|
36
|
+
const info = await lstat(directory);
|
|
37
|
+
return info.isDirectory() && !info.isSymbolicLink();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code === "ENOENT")
|
|
41
|
+
return false;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function payloadCommand(binding) {
|
|
46
|
+
const payload = object(binding.native_payload);
|
|
47
|
+
if (!payload)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (binding.source_harness === "codex-desktop")
|
|
50
|
+
return lifecycleCommandFromPayload(payload) ?? undefined;
|
|
51
|
+
if (binding.source_harness === "grok-acp") {
|
|
52
|
+
const native = object(payload.tool_input ?? payload.toolInput);
|
|
53
|
+
const command = native?.command ?? native?.cmd ?? payload.command;
|
|
54
|
+
return typeof command === "string" ? command : undefined;
|
|
55
|
+
}
|
|
56
|
+
if (binding.source_harness === "zcode-acp") {
|
|
57
|
+
const update = object(object(payload.params)?.update), native = object(update?.rawInput);
|
|
58
|
+
const command = native?.command ?? native?.cmd;
|
|
59
|
+
return typeof command === "string" ? command : undefined;
|
|
60
|
+
}
|
|
61
|
+
const input = object(payload.input);
|
|
62
|
+
const command = input?.command ?? input?.cmd ?? input?.CommandLine;
|
|
63
|
+
return typeof command === "string" ? command : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** The directory is only a locator. The owning daemon must attest the exact
|
|
66
|
+
* binding from its verified native-delivery map before it becomes a receipt. */
|
|
67
|
+
export async function reconcileRetainedNativeHook(context, input) {
|
|
68
|
+
const { scope, invocationId } = input;
|
|
69
|
+
const diagnostic = (phase, error, binding) => {
|
|
70
|
+
if (!input.onDiagnostic)
|
|
71
|
+
return;
|
|
72
|
+
const supplied = error instanceof Error || object(error) ? error.code : undefined;
|
|
73
|
+
const code = typeof supplied === "string" && /^[A-Za-z][A-Za-z0-9_]{0,95}$/.test(supplied) ? supplied : "native_hook_verification_unavailable";
|
|
74
|
+
// Diagnostics never become execution authority or activity. Keep identifiers
|
|
75
|
+
// and a typed cause, not native messages/configs that can contain credentials.
|
|
76
|
+
const value = { phase, code, invocation_id: invocationId,
|
|
77
|
+
...(typeof binding?.request_id === "string" ? { request_id: binding.request_id } : {}),
|
|
78
|
+
...(typeof binding?.operation_id === "string" ? { operation_id: binding.operation_id } : {}) };
|
|
79
|
+
const seen = diagnostics.get(context) ?? new Set(), key = JSON.stringify(value);
|
|
80
|
+
if (seen.has(key))
|
|
81
|
+
return;
|
|
82
|
+
seen.add(key);
|
|
83
|
+
diagnostics.set(context, seen);
|
|
84
|
+
input.onDiagnostic(value);
|
|
85
|
+
};
|
|
86
|
+
if (!scope.runId || context.env.DD_FLOW_DAEMON_ID !== scope.daemonId)
|
|
87
|
+
return false;
|
|
88
|
+
let owner = null;
|
|
89
|
+
try {
|
|
90
|
+
owner = object(JSON.parse(context.env.DD_FLOW_RUNTIME_OWNER ?? "null"));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const stateDir = owner?.state_dir || context.env.DD_FLOW_CODEX_STATE_DIR || context.env.DD_AGY_STATE_DIR || context.env.DD_GROK_STATE_DIR || context.env.DD_ZCODE_STATE_DIR;
|
|
96
|
+
if (typeof stateDir !== "string" || !path.isAbsolute(stateDir) || !await realDirectory(stateDir))
|
|
97
|
+
return false;
|
|
98
|
+
// This is an optional additional observation source. A missing, retiring or
|
|
99
|
+
// unbound daemon must leave the existing hook path intact, never grant it
|
|
100
|
+
// authority and never turn an otherwise valid legacy delivery into a failure.
|
|
101
|
+
let owned;
|
|
102
|
+
let metadata;
|
|
103
|
+
try {
|
|
104
|
+
const state = readNativeDaemonState(stateDir);
|
|
105
|
+
if ((state.daemon_id ?? state.config?.daemonId) !== scope.daemonId) {
|
|
106
|
+
diagnostic("owner", { code: "daemon_owner_mismatch" });
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
110
|
+
owned = requireManagedDaemonBinding(context, { stateDir, projectId: project.id, runId: scope.runId });
|
|
111
|
+
metadata = JSON.parse(owned.process.metadata_json);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
diagnostic("owner", error);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (metadata.run_generation !== scope.generation) {
|
|
118
|
+
diagnostic("owner", { code: "run_generation_mismatch" });
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const operations = path.join(stateDir, "operations");
|
|
122
|
+
if (!await realDirectory(operations))
|
|
123
|
+
return false;
|
|
124
|
+
const candidates = [];
|
|
125
|
+
for (const entry of await readdir(operations, { withFileTypes: true })) {
|
|
126
|
+
if (!entry.isDirectory() || !/^[a-f0-9]{64}$/.test(entry.name))
|
|
127
|
+
continue;
|
|
128
|
+
const directory = path.join(operations, entry.name), hooks = path.join(directory, "hooks");
|
|
129
|
+
if (!await realDirectory(hooks))
|
|
130
|
+
continue;
|
|
131
|
+
const requested = await regularJson(path.join(directory, "requested.json"));
|
|
132
|
+
if (!requested || requested.schema_id !== "dd-flow/daemon-operation@1" || typeof requested.operation_id !== "string"
|
|
133
|
+
|| sha256(requested.operation_id) !== entry.name || requested.daemon_id !== scope.daemonId
|
|
134
|
+
|| requested.owner_id !== owned.process.owner_id || !["session.create", "session.start", "session.prompt"].includes(String(requested.operation))
|
|
135
|
+
|| requested.session_id && requested.session_id !== scope.rootSessionId)
|
|
136
|
+
continue;
|
|
137
|
+
for (const hook of await readdir(hooks, { withFileTypes: true })) {
|
|
138
|
+
if (!hook.isFile() || !/^[A-Za-z0-9-]{1,128}\.json$/.test(hook.name))
|
|
139
|
+
continue;
|
|
140
|
+
const binding = await regularJson(path.join(hooks, hook.name));
|
|
141
|
+
if (!binding || binding.schema_id !== "dd-flow/hook-request@1" || binding.request_id !== hook.name.slice(0, -5)
|
|
142
|
+
|| binding.daemon_id !== scope.daemonId || binding.operation_id !== requested.operation_id
|
|
143
|
+
|| binding.root_provider_session_id !== scope.rootSessionId || !["codex-desktop", "antigravity-cli", "grok-acp", "zcode-acp", "opencode-server", "droid-cli"].includes(String(binding.source_harness))
|
|
144
|
+
|| typeof binding.provider_session_id !== "string" || !binding.provider_session_id
|
|
145
|
+
|| !(binding.parent_provider_session_id === null || typeof binding.parent_provider_session_id === "string")
|
|
146
|
+
|| typeof binding.tool_call_id !== "string" || !binding.tool_call_id || binding.event !== "PreToolUse"
|
|
147
|
+
|| typeof binding.native_command !== "string" || binding.command_sha256 !== sha256(binding.native_command)
|
|
148
|
+
|| payloadCommand(binding) !== binding.native_command || !matchesIssuedNativeCommand(context, invocationId, binding.native_command))
|
|
149
|
+
continue;
|
|
150
|
+
candidates.push(binding);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const verified = [];
|
|
154
|
+
for (const binding of candidates) {
|
|
155
|
+
try {
|
|
156
|
+
const module = binding.source_harness === "codex-desktop"
|
|
157
|
+
? await import("../harness-runtime/lib/dd-codex-daemon.mjs")
|
|
158
|
+
: binding.source_harness === "grok-acp" ? await import("../harness-runtime/lib/dd-grok-daemon.mjs")
|
|
159
|
+
: binding.source_harness === "zcode-acp" ? await import("../harness-runtime/lib/dd-zcode-daemon.mjs")
|
|
160
|
+
// @ts-expect-error Executable adapter assets have no duplicate typed API.
|
|
161
|
+
: binding.source_harness === "opencode-server" ? await import("../harness-runtime/lib/dd-opencode-daemon.mjs")
|
|
162
|
+
// @ts-expect-error Executable adapter assets have no duplicate typed API.
|
|
163
|
+
: binding.source_harness === "droid-cli" ? await import("../harness-runtime/lib/dd-droid-daemon.mjs")
|
|
164
|
+
: await import("../harness-runtime/lib/dd-agy-daemon.mjs");
|
|
165
|
+
const proof = await withDaemonProgressObserver(progress => {
|
|
166
|
+
if (typeof progress.native_request_id === 'string' && typeof progress.observed_at === 'string')
|
|
167
|
+
input.onProgress?.({ cursor: `${binding.request_id}:${progress.native_request_id}`, observed_at: Date.parse(progress.observed_at) });
|
|
168
|
+
}, () => module.callDaemon(stateDir, "hook.verify", { daemonId: scope.daemonId, requestId: binding.request_id, binding }, 5_000));
|
|
169
|
+
if (binding.native_admission === "assigned_work_start") {
|
|
170
|
+
const resolved = object(proof.nativeHookBinding);
|
|
171
|
+
if (proof.verified !== true || JSON.stringify(proof.registeredHookBinding) !== JSON.stringify(binding) || !resolved
|
|
172
|
+
|| !["antigravity-cli", "opencode-server", "droid-cli"].includes(String(binding.source_harness))
|
|
173
|
+
|| resolved.native_ancestry !== "confirmed" || typeof resolved.native_cwd !== "string" || !path.isAbsolute(resolved.native_cwd)
|
|
174
|
+
|| !(resolved.parent_provider_session_id === null || typeof resolved.parent_provider_session_id === "string")
|
|
175
|
+
|| ["request_id", "operation_id", "daemon_id", "turn_generation", "source_harness", "root_provider_session_id", "provider_session_id",
|
|
176
|
+
"tool_call_id", "event", "native_command", "command_sha256", "native_creator", "native_admission"].some(key => resolved[key] !== binding[key])
|
|
177
|
+
|| payloadCommand(resolved) !== binding.native_command) {
|
|
178
|
+
diagnostic("verify", { code: "native_hook_unproven" }, binding);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
verified.push(resolved);
|
|
182
|
+
}
|
|
183
|
+
else if (proof.verified === true && JSON.stringify(proof.nativeHookBinding) === JSON.stringify(binding))
|
|
184
|
+
verified.push(binding);
|
|
185
|
+
else
|
|
186
|
+
diagnostic("verify", { code: "native_hook_unproven" }, binding);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
diagnostic("verify", error, binding);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Exact old tool delivery belongs to its immutable prior attempt. A public
|
|
193
|
+
// command may be identical in a fresh no-effect successor; do not repeatedly
|
|
194
|
+
// select its predecessor and starve the new native callback.
|
|
195
|
+
const eligible = verified.filter(binding => observedLifecycleInvocation(context, binding.native_command, scope, { identity: {
|
|
196
|
+
daemonId: scope.daemonId, rootSessionId: scope.rootSessionId, sessionId: binding.provider_session_id,
|
|
197
|
+
parentSessionId: binding.parent_provider_session_id, toolCallId: binding.tool_call_id
|
|
198
|
+
} })?.id === invocationId);
|
|
199
|
+
if (!eligible.length)
|
|
200
|
+
return false;
|
|
201
|
+
const identity = (binding) => JSON.stringify([binding.source_harness, binding.daemon_id, binding.root_provider_session_id,
|
|
202
|
+
binding.provider_session_id, binding.parent_provider_session_id, binding.native_cwd]);
|
|
203
|
+
if (new Set(eligible.map(identity)).size !== 1)
|
|
204
|
+
throw new AppError("invocation_ambiguous", "More than one native executor supplied this issued Work command", 1, { effect: "no_effect", invocation_id: invocationId });
|
|
205
|
+
const binding = eligible[0], payload = object(binding.native_payload);
|
|
206
|
+
if (binding.source_harness === "codex-desktop") {
|
|
207
|
+
const proof = object(binding.codex_proof);
|
|
208
|
+
if (!proof || proof.daemonId !== scope.daemonId || proof.rootSessionId !== scope.rootSessionId
|
|
209
|
+
|| proof.sessionId !== binding.provider_session_id || proof.parentSessionId !== binding.parent_provider_session_id
|
|
210
|
+
|| proof.toolCallId !== binding.tool_call_id || typeof proof.projectRoot !== "string"
|
|
211
|
+
|| canonicalPath(proof.projectRoot) !== canonicalPath(scope.projectRoot) || typeof proof.ddFlowHome !== "string"
|
|
212
|
+
|| canonicalPath(proof.ddFlowHome) !== canonicalPath(context.ddFlowHome))
|
|
213
|
+
return false;
|
|
214
|
+
const prior = context.codexHookProof;
|
|
215
|
+
try {
|
|
216
|
+
context.codexHookProof = { daemonId: scope.daemonId, rootSessionId: scope.rootSessionId,
|
|
217
|
+
sessionId: binding.provider_session_id, parentSessionId: binding.parent_provider_session_id,
|
|
218
|
+
toolCallId: binding.tool_call_id, projectRoot: proof.projectRoot, command: binding.native_command };
|
|
219
|
+
handleCodexHook(context, { event: "PreToolUse", projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
if (prior)
|
|
223
|
+
context.codexHookProof = prior;
|
|
224
|
+
else
|
|
225
|
+
delete context.codexHookProof;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (binding.source_harness === "grok-acp") {
|
|
229
|
+
const identity = object(object(payload._meta)?.ddGrok);
|
|
230
|
+
if (!identity || identity.daemonId !== scope.daemonId || identity.rootProviderSessionId !== scope.rootSessionId
|
|
231
|
+
|| identity.parentProviderSessionId !== binding.parent_provider_session_id
|
|
232
|
+
|| (payload.session_id ?? payload.sessionId) !== binding.provider_session_id
|
|
233
|
+
|| (payload.tool_use_id ?? payload.toolUseId ?? payload.tool_call_id) !== binding.tool_call_id)
|
|
234
|
+
return false;
|
|
235
|
+
handleGrokEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
|
|
236
|
+
}
|
|
237
|
+
else if (binding.source_harness === "zcode-acp") {
|
|
238
|
+
const controlled = object(object(payload._meta)?.ddZcode), update = object(object(payload.params)?.update), native = object(object(update?._meta)?.zcodeRuntime);
|
|
239
|
+
if (!controlled || controlled.daemonId !== scope.daemonId || controlled.rootProviderSessionId !== scope.rootSessionId
|
|
240
|
+
|| controlled.parentProviderSessionId !== binding.parent_provider_session_id || native?.source !== "subagent"
|
|
241
|
+
|| native.childSessionId !== binding.provider_session_id || native.childToolCallId !== binding.tool_call_id
|
|
242
|
+
|| typeof update?.toolCallId !== "string" || !update.toolCallId || typeof native.nativeTurnId !== "string" || !native.nativeTurnId)
|
|
243
|
+
return false;
|
|
244
|
+
handleZcodeEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
|
|
245
|
+
}
|
|
246
|
+
else if (binding.native_admission === "assigned_work_start") {
|
|
247
|
+
const harness = binding.source_harness, session = object(payload.session);
|
|
248
|
+
if (payload.daemon_id !== scope.daemonId || (harness === "antigravity-cli" ? payload.conversation_id !== binding.provider_session_id
|
|
249
|
+
|| (payload.parent_conversation_id ?? null) !== binding.parent_provider_session_id || payload.event_id !== binding.tool_call_id
|
|
250
|
+
: !session || session.provider_session_id !== binding.provider_session_id || (session.parent_provider_session_id ?? null) !== binding.parent_provider_session_id
|
|
251
|
+
|| payload.tool_call_id !== binding.tool_call_id))
|
|
252
|
+
return false;
|
|
253
|
+
const prior = context.nativeHookProof;
|
|
254
|
+
try {
|
|
255
|
+
context.nativeHookProof = { harness, daemonId: scope.daemonId, rootSessionId: scope.rootSessionId, sessionId: binding.provider_session_id,
|
|
256
|
+
toolCallId: binding.tool_call_id };
|
|
257
|
+
const handle = harness === "antigravity-cli" ? handleAgyEvent : harness === "droid-cli" ? handleDroidEvent : handleOpenCodeEvent;
|
|
258
|
+
handle(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
if (prior)
|
|
262
|
+
context.nativeHookProof = prior;
|
|
263
|
+
else
|
|
264
|
+
delete context.nativeHookProof;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
if (payload.daemon_id !== scope.daemonId || payload.conversation_id !== binding.provider_session_id
|
|
269
|
+
|| (payload.parent_conversation_id ?? null) !== binding.parent_provider_session_id || payload.event_id !== binding.tool_call_id)
|
|
270
|
+
return false;
|
|
271
|
+
handleAgyEvent(context, { projectRoot: scope.projectRoot, stdin: JSON.stringify(payload) });
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|