@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.137

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/build-info.json +3 -3
  3. package/dist/harness-runtime/DD-ZCODE.md +13 -0
  4. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
  5. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +14 -1
  6. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +36 -1
  7. package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
  8. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -1
  9. package/dist/services/controller-child-observer.js +99 -0
  10. package/dist/services/controller-fanout.js +9 -2
  11. package/dist/services/run-controller.js +83 -8
  12. package/dist/services/stage-lifecycle.js +2 -1
  13. package/dist/services/stage-report-renderer.js +13 -0
  14. package/dist/services/vnext-fanout.js +66 -1
  15. package/dist/services/vnext-protocolize.js +60 -2
  16. package/package.json +1 -1
  17. package/dist/cli/command-metadata.js +0 -144
  18. package/dist/cli/command-option-descriptions.js +0 -186
  19. package/dist/cli/completion-cli.js +0 -70
  20. package/dist/cli/completion.js +0 -57
  21. package/dist/cli/error-output.js +0 -52
  22. package/dist/cli/executor-onboarding.js +0 -71
  23. package/dist/cli/harnesses.js +0 -84
  24. package/dist/cli/models.js +0 -39
  25. package/dist/cli/operator-profiles.js +0 -61
  26. package/dist/cli/output-options.js +0 -35
  27. package/dist/cli/response-file.js +0 -69
  28. package/dist/cli/setup.js +0 -147
  29. package/dist/services/cli-upgrade-allowlist.js +0 -27
  30. package/dist/services/discovery/agy-discovery.js +0 -2
  31. package/dist/services/discovery/codex-discovery.js +0 -2
  32. package/dist/services/discovery/droid-discovery.js +0 -2
  33. package/dist/services/discovery/grok-discovery.js +0 -2
  34. package/dist/services/discovery/opencode-discovery.js +0 -2
  35. package/dist/services/discovery/static.js +0 -31
  36. package/dist/services/discovery/zcode-discovery.js +0 -2
  37. package/dist/services/executor-onboarding.js +0 -56
  38. package/dist/services/harness-discovery.js +0 -29
  39. package/dist/services/harness-doctor.js +0 -75
  40. package/dist/services/harness-probe-effects.js +0 -51
  41. package/dist/services/harness-version-probe.js +0 -100
  42. package/dist/services/hybrid-contracts.js +0 -455
  43. package/dist/services/installation-lock.js +0 -92
  44. package/dist/services/model-catalog.js +0 -109
  45. package/dist/services/model-discovery.js +0 -46
  46. package/dist/services/operator-profiles.js +0 -236
  47. package/dist/services/release-archive-producer.js +0 -52
  48. package/dist/services/release-archive.js +0 -143
  49. package/dist/services/release-cache.js +0 -142
  50. package/dist/services/release-compatibility.js +0 -18
  51. package/dist/services/release-download.js +0 -67
  52. package/dist/services/release-envelope.js +0 -104
  53. package/dist/services/release-manifest.js +0 -45
  54. package/dist/services/release-payload.js +0 -114
  55. package/dist/services/release-producer.js +0 -63
  56. package/dist/services/release-provenance.js +0 -160
  57. package/dist/services/release-registry.js +0 -66
  58. package/dist/services/release-source.js +0 -194
  59. package/dist/services/setup.js +0 -377
@@ -1,67 +0,0 @@
1
- import { AppError } from "../shared/errors.js";
2
- import { verifyReleaseArchive } from "./release-manifest.js";
3
- function refuse(reason) {
4
- throw new AppError("release_download", "Release artifact download failed", 1, { reason, phase: "artifact_download", effect: "no_effect", help: "setup" });
5
- }
6
- /** No provider login, credentials or redirect forwarding. Activation is a separate operation. */
7
- export async function downloadReleaseArchive(identity, options) {
8
- const maxBytes = options.maxBytes ?? 256 * 1024 * 1024;
9
- const timeoutMs = options.timeoutMs ?? 60_000;
10
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 ||
11
- !Number.isSafeInteger(identity.bytes) || identity.bytes < 1 || identity.bytes > maxBytes || !/^[a-f0-9]{64}$/.test(identity.sha256))
12
- refuse("invalid_download_limits_or_identity");
13
- let url;
14
- try {
15
- url = new URL(identity.url);
16
- }
17
- catch {
18
- return refuse("invalid_artifact_url");
19
- }
20
- if (url.protocol !== "https:" || url.username || url.password || url.hash || (url.port && url.port !== "443") || !options.allowedHosts.has(url.hostname))
21
- refuse("unapproved_artifact_origin");
22
- const fetcher = options.fetch ?? globalThis.fetch;
23
- let response;
24
- const signal = AbortSignal.timeout(timeoutMs);
25
- try {
26
- response = await fetcher(url, { redirect: "error", signal, credentials: "omit" });
27
- }
28
- catch {
29
- return refuse("network_unavailable_or_redirect");
30
- }
31
- if (!response.ok || !response.body) {
32
- await response.body?.cancel();
33
- return refuse("artifact_http_failure");
34
- }
35
- const length = response.headers.get("content-length");
36
- if (length !== null && (!/^[0-9]+$/.test(length) || Number(length) !== identity.bytes)) {
37
- await response.body.cancel();
38
- return refuse("content_length_mismatch");
39
- }
40
- const reader = response.body.getReader(), chunks = [];
41
- let received = 0;
42
- try {
43
- while (true) {
44
- const { done, value } = await reader.read();
45
- if (done)
46
- break;
47
- received += value.byteLength;
48
- if (received > identity.bytes || received > maxBytes) {
49
- await reader.cancel();
50
- return refuse("artifact_exceeds_declared_size");
51
- }
52
- chunks.push(Buffer.from(value));
53
- options.onProgress?.({ received_bytes: received, expected_bytes: identity.bytes });
54
- }
55
- }
56
- catch (error) {
57
- if (error instanceof AppError)
58
- throw error;
59
- return refuse("artifact_stream_interrupted");
60
- }
61
- finally {
62
- reader.releaseLock();
63
- }
64
- const bytes = Buffer.concat(chunks, received);
65
- verifyReleaseArchive(identity, bytes);
66
- return bytes;
67
- }
@@ -1,104 +0,0 @@
1
- import { createHash, createPublicKey, sign, verify } from "node:crypto";
2
- import { AppError } from "../shared/errors.js";
3
- import { admitReleaseManifest } from "./release-manifest.js";
4
- const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
5
- function refuse(reason) {
6
- throw new AppError("release_authentication", "Release producer cannot be authenticated", 1, { reason, effect: "no_effect", phase: "release_authentication", help: "setup" });
7
- }
8
- function object(value, keys) {
9
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== keys.length ||
10
- Object.keys(value).some(key => !keys.includes(key)))
11
- refuse("invalid_envelope_shape");
12
- return value;
13
- }
14
- function decoded(value, max = 16 * 1024 * 1024) {
15
- if (typeof value !== "string" || value.length > Math.ceil(max / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value))
16
- refuse("invalid_encoded_bytes");
17
- const bytes = Buffer.from(value, "base64");
18
- if (bytes.length > max || bytes.toString("base64") !== value)
19
- refuse("invalid_encoded_bytes");
20
- return bytes;
21
- }
22
- function json(bytes) {
23
- try {
24
- return JSON.parse(bytes.toString("utf8"));
25
- }
26
- catch {
27
- return refuse("invalid_signed_json");
28
- }
29
- }
30
- export function releaseProducerKeyId(publicKey) {
31
- if (publicKey.type !== "public" || publicKey.asymmetricKeyType !== "ed25519")
32
- refuse("unsupported_signing_key");
33
- return digest(publicKey.export({ type: "spki", format: "der" }));
34
- }
35
- function admission(payload, compatibility, producer) {
36
- const data = object(payload, ["schema_id", "manifest_base64", "source", "qualifications", "evidence"]);
37
- if (data.schema_id !== "dd-flow/release-payload@1" || !Array.isArray(data.qualifications) || !Array.isArray(data.evidence) || data.qualifications.length > 10_000 || data.evidence.length > 10_000)
38
- refuse("invalid_payload_shape");
39
- const source = object(data.source, ["kind", "repository", "commit", "tag"]);
40
- if (source.repository !== producer.repository || typeof source.commit !== "string" || !/^[a-f0-9]{40}$/.test(source.commit) ||
41
- !["published_release", "source_checkout"].includes(String(source.kind)))
42
- refuse("source_outside_producer_scope");
43
- if (!producer.channels.has(compatibility.channel))
44
- refuse("channel_outside_producer_scope");
45
- const receipts = new Map();
46
- const qualifiedRoutes = new Map(), evidence = new Map();
47
- for (const input of data.qualifications) {
48
- const fact = object(input, ["route", "sha256", "request_id", "project_root", "workspace", "canon_sha256", "engine_sha256"]);
49
- if (typeof fact.route !== "string" || !/^(darwin-arm64|darwin-x64|linux-arm64|linux-x64|win32-x64)\/(codex|zcode|agy|opencode|grok|droid)\/[a-z][a-z0-9-]{0,47}\/(external|native)$/.test(fact.route) ||
50
- [fact.sha256, fact.canon_sha256, fact.engine_sha256].some(value => typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) ||
51
- [fact.request_id, fact.project_root, fact.workspace].some(value => typeof value !== "string" || !value.trim()))
52
- refuse("invalid_qualification_fact");
53
- if (qualifiedRoutes.has(fact.route))
54
- refuse("duplicate_qualification_fact");
55
- qualifiedRoutes.set(fact.route, fact.sha256);
56
- receipts.set(fact.sha256, { kind: "route-qualification", requestId: fact.request_id, projectRoot: fact.project_root, workspace: fact.workspace });
57
- }
58
- for (const input of data.evidence) {
59
- const item = object(input, ["path", "bytes_base64"]);
60
- if (typeof item.path !== "string" || !/^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*(\/[a-zA-Z0-9_-][a-zA-Z0-9_.-]*)*$/.test(item.path) || evidence.has(item.path))
61
- refuse("invalid_evidence_path");
62
- evidence.set(item.path, decoded(item.bytes_base64));
63
- }
64
- const manifestBytes = decoded(data.manifest_base64, 1024 * 1024);
65
- const authority = { ...compatibility, receipts, qualifiedRoutes, evidence };
66
- const manifest = admitReleaseManifest(json(manifestBytes), authority);
67
- for (const input of data.qualifications) {
68
- if (input.canon_sha256 !== manifest.canon.archive.sha256 || input.engine_sha256 !== manifest.engine.archive.sha256)
69
- refuse("qualification_artifact_mismatch");
70
- }
71
- if (source.commit !== manifest.canon.commit)
72
- refuse("source_commit_mismatch");
73
- if (source.kind === "source_checkout") {
74
- if (manifest.channel !== "development" || source.tag !== null)
75
- refuse("source_cannot_claim_published_release");
76
- }
77
- else if (source.tag !== `v${manifest.version}` || manifest.channel === "development")
78
- refuse("published_release_identity_mismatch");
79
- return { manifest, manifestBytes, authority, source: source };
80
- }
81
- /** Producer receives qualification facts from its owner ledger, never manufactures them from manifest references. */
82
- export function produceReleaseEnvelope(payload, privateKey, compatibility) {
83
- if (privateKey.type !== "private" || privateKey.asymmetricKeyType !== "ed25519")
84
- refuse("unsupported_signing_key");
85
- admission(payload, compatibility, { repository: payload.source.repository, channels: new Set([compatibility.channel]) });
86
- const bytes = Buffer.from(JSON.stringify(payload));
87
- if (bytes.length > 16 * 1024 * 1024)
88
- refuse("oversized_payload");
89
- return { schema_id: "dd-flow/release-envelope@1", producer_key_id: releaseProducerKeyId(createPublicKey(privateKey)),
90
- payload_base64: bytes.toString("base64"), signature_base64: sign(null, bytes, privateKey).toString("base64") };
91
- }
92
- /** Trust keys and scope are selected independently; an envelope cannot nominate its own authority. */
93
- export function authenticateReleaseEnvelope(input, trusted, compatibility) {
94
- const envelope = object(input, ["schema_id", "producer_key_id", "payload_base64", "signature_base64"]);
95
- if (envelope.schema_id !== "dd-flow/release-envelope@1" || typeof envelope.producer_key_id !== "string")
96
- refuse("invalid_envelope_identity");
97
- const producer = trusted.get(envelope.producer_key_id);
98
- if (!producer || releaseProducerKeyId(producer.publicKey) !== envelope.producer_key_id)
99
- refuse("unknown_producer");
100
- const bytes = decoded(envelope.payload_base64), signature = decoded(envelope.signature_base64, 64);
101
- if (signature.length !== 64 || !verify(null, bytes, producer.publicKey, signature))
102
- refuse("signature_mismatch");
103
- return admission(json(bytes), compatibility, producer);
104
- }
@@ -1,45 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { AppError } from "../shared/errors.js";
3
- import { validateHybridContract } from "./hybrid-contracts.js";
4
- const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
5
- function refuse(reason) {
6
- throw new AppError("release_integrity", "Release manifest cannot be admitted", 1, { reason, phase: "release_admission", effect: "no_effect", help: "setup" });
7
- }
8
- /** Pure admission: no download, activation, filesystem writes or qualification probes. */
9
- export function admitReleaseManifest(input, authority) {
10
- const issues = validateHybridContract("release-manifest", input, { receipts: authority.receipts });
11
- if (issues.length)
12
- refuse(issues[0].code);
13
- const manifest = input;
14
- if (manifest.channel !== authority.channel)
15
- refuse("channel_mismatch");
16
- if (!authority.routerCompatible(manifest.min_router_version))
17
- refuse("router_incompatible");
18
- if (!authority.engineCompatible(manifest.engine.version, manifest.engine.compatible_range))
19
- refuse("engine_incompatible");
20
- for (const family of ["harness", "model", "flow"]) {
21
- if (manifest.catalog_versions[family] !== authority.catalogVersions[family])
22
- refuse("catalog_incompatible");
23
- }
24
- for (const route of manifest.qualified_routes) {
25
- const key = [route.platform, route.harness, route.flow, route.delegation].join("/");
26
- if (authority.qualifiedRoutes.get(key) !== route.evidence.sha256)
27
- refuse("route_qualification_mismatch");
28
- }
29
- for (const reference of [manifest.impact, ...manifest.qualified_routes.map(route => route.evidence)]) {
30
- const bytes = authority.evidence.get(reference.path);
31
- if (!bytes || sha256(bytes) !== reference.sha256)
32
- refuse("evidence_bytes_mismatch");
33
- }
34
- // Return a detached tuple: callers cannot mutate the submitted object after admission.
35
- return structuredClone(manifest);
36
- }
37
- /** Verify the actual transferred bytes before any archive parser or cache publication. */
38
- export function verifyReleaseArchive(archive, bytes) {
39
- if (!Number.isSafeInteger(archive.bytes) || archive.bytes < 1 || !/^[a-f0-9]{64}$/.test(archive.sha256))
40
- refuse("invalid_archive_identity");
41
- if (bytes.byteLength !== archive.bytes)
42
- refuse("archive_size_mismatch");
43
- if (sha256(bytes) !== archive.sha256)
44
- refuse("archive_digest_mismatch");
45
- }
@@ -1,114 +0,0 @@
1
- import { TextDecoder } from "node:util";
2
- import { valid } from "semver";
3
- import { Ajv2020 } from "ajv/dist/2020.js";
4
- import { AppError } from "../shared/errors.js";
5
- import { inspectReleaseArchive } from "./release-archive.js";
6
- const decoder = new TextDecoder("utf-8", { fatal: true });
7
- function refuse(reason) {
8
- throw new AppError("release_integrity", "Release payload cannot be admitted", 1, { reason, phase: "payload_validation", effect: "no_effect", help: "setup" });
9
- }
10
- function root(entries, kind) {
11
- const roots = new Set(entries.map(entry => entry.path.split("/")[0]));
12
- if (roots.size !== 1)
13
- refuse(`${kind}_payload_root_ambiguous`);
14
- const selected = [...roots][0];
15
- if (kind === "engine" && selected !== "package")
16
- refuse("engine_payload_root_invalid");
17
- if (entries.some(entry => entry.kind === "file" && entry.path === selected))
18
- refuse(`${kind}_payload_root_invalid`);
19
- return selected;
20
- }
21
- function text(entries, file) {
22
- const entry = entries.find(candidate => candidate.path === file);
23
- if (!entry || entry.kind !== "file")
24
- refuse("payload_metadata_missing");
25
- // Metadata is bounded separately from the expanded archive and never executed.
26
- if (entry.bytes.length > 1024 * 1024)
27
- refuse("payload_metadata_oversized");
28
- try {
29
- return decoder.decode(entry.bytes);
30
- }
31
- catch {
32
- return refuse("payload_metadata_invalid_utf8");
33
- }
34
- }
35
- function json(entries, file) {
36
- let value;
37
- try {
38
- value = JSON.parse(text(entries, file));
39
- }
40
- catch (error) {
41
- if (error instanceof AppError)
42
- throw error;
43
- return refuse("engine_payload_package_invalid");
44
- }
45
- if (!value || typeof value !== "object" || Array.isArray(value))
46
- refuse("engine_payload_package_invalid");
47
- return value;
48
- }
49
- function validateCanonLayout(entries, archiveRoot) {
50
- const markers = ["dd-flow/README.md", "mbb/index.md", "protocol/index.md"];
51
- const candidates = [archiveRoot, `${archiveRoot}/.memory-bank`];
52
- const present = (file) => entries.some(entry => entry.path === file && entry.kind === "file" && entry.bytes.length > 0);
53
- const layouts = candidates.filter(candidate => markers.every(marker => present(`${candidate}/${marker}`)));
54
- if (layouts.length !== 1)
55
- refuse(layouts.length ? "canon_payload_layout_ambiguous" : "canon_payload_layout_incomplete");
56
- const selected = layouts[0];
57
- // Even an incomplete second layout can shadow files during later resolution.
58
- if (candidates.some(candidate => candidate !== selected && markers.some(marker => present(`${candidate}/${marker}`))))
59
- refuse("canon_payload_layout_ambiguous");
60
- }
61
- /** Check package identity from transferred bytes before creating the installation home.
62
- * This is not catalog/schema or runtime qualification; those require their own evidence.
63
- */
64
- export function validateReleasePayloadIdentity(manifest, canon, engine) {
65
- const canonEntries = inspectReleaseArchive(manifest.canon.archive, canon);
66
- const engineEntries = inspectReleaseArchive(manifest.engine.archive, engine);
67
- const canonRoot = root(canonEntries, "canon"), engineRoot = root(engineEntries, "engine");
68
- if (text(canonEntries, `${canonRoot}/VERSION`).trim() !== manifest.canon.version)
69
- refuse("canon_payload_version_mismatch");
70
- validateCanonLayout(canonEntries, canonRoot);
71
- const metadata = json(engineEntries, `${engineRoot}/package.json`);
72
- if (metadata.name !== manifest.engine.package)
73
- refuse("engine_payload_package_mismatch");
74
- if (metadata.version !== manifest.engine.version)
75
- refuse("engine_payload_version_mismatch");
76
- if (metadata.type !== "module")
77
- refuse("engine_payload_module_type_invalid");
78
- const bins = metadata.bin;
79
- if (!bins || typeof bins !== "object" || Array.isArray(bins)
80
- || bins["dd-flow"] !== "dist/cli.js")
81
- refuse("engine_payload_bin_invalid");
82
- for (const target of Object.values(bins)) {
83
- if (typeof target !== "string" || !/^dist\/[A-Za-z0-9_./-]+$/.test(target)
84
- || target.split("/").some(part => !part || part === "." || part === ".."))
85
- refuse("engine_payload_bin_invalid");
86
- const executable = engineEntries.find(entry => entry.path === `${engineRoot}/${target}`);
87
- if (!executable || executable.kind !== "file" || executable.bytes.length === 0 || !(executable.mode & 0o111))
88
- refuse("engine_payload_bin_missing");
89
- }
90
- const build = json(engineEntries, `${engineRoot}/dist/build-info.json`);
91
- if (build.cli_package !== manifest.engine.package || build.cli_version !== manifest.engine.version
92
- || typeof build.cli_commit !== "string" || !/^[a-f0-9]{40}$/.test(build.cli_commit))
93
- refuse("engine_payload_build_identity_mismatch");
94
- const canonBuild = build.built_with_canon;
95
- // Build provenance is not the installed canon selection. One immutable engine
96
- // can serve multiple independently qualified canon tuples without being rebuilt.
97
- if (!canonBuild || typeof canonBuild !== "object" || Array.isArray(canonBuild)
98
- || typeof canonBuild.version !== "string" || !valid(canonBuild.version)
99
- || typeof canonBuild.commit !== "string" || !/^[a-f0-9]{40}$/.test(canonBuild.commit))
100
- refuse("engine_payload_canon_provenance_invalid");
101
- const schemaFiles = { descriptor: "flow-descriptor", execution: "execution-settings", bundle: "execution-bundle" };
102
- for (const family of ["descriptor", "execution", "bundle"]) {
103
- const schema = json(engineEntries, `${engineRoot}/dist/schemas/${schemaFiles[family]}.schema.json`);
104
- if (schema.$id !== manifest.schema_versions[family]
105
- || schema.$schema !== "https://json-schema.org/draft/2020-12/schema")
106
- refuse("engine_payload_schema_identity_mismatch");
107
- try {
108
- new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
109
- }
110
- catch {
111
- return refuse("engine_payload_schema_invalid");
112
- }
113
- }
114
- }
@@ -1,63 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { createPublicKey, randomUUID } from "node:crypto";
4
- import { AppError } from "../shared/errors.js";
5
- import { hybridDigest } from "./hybrid-contracts.js";
6
- import { acquireInstallationLock } from "./installation-lock.js";
7
- import { authenticateReleaseEnvelope, produceReleaseEnvelope, releaseProducerKeyId } from "./release-envelope.js";
8
- import { readLocalReleaseBundle } from "./release-source.js";
9
- import { validateReleasePayloadIdentity } from "./release-payload.js";
10
- function refuse(reason) {
11
- throw new AppError("release_producer", "Release bundle cannot be published", 1, { reason, phase: "release_production", effect: "no_effect" });
12
- }
13
- /** Local immutable delivery only. Qualification facts must already come from their
14
- * owner ledger; this producer never performs, synthesizes or upgrades qualification.
15
- */
16
- export function publishReleaseBundle(destination, input) {
17
- if (!input.payload || typeof input.payload !== "object" || !input.payload.source
18
- || typeof input.payload.source.repository !== "string")
19
- refuse("invalid_producer_payload");
20
- const envelope = produceReleaseEnvelope(input.payload, input.privateKey, input.compatibility);
21
- const publicKey = createPublicKey(input.privateKey), producers = new Map([[releaseProducerKeyId(publicKey), {
22
- publicKey, repository: input.payload.source.repository, channels: new Set([input.compatibility.channel])
23
- }]]);
24
- const admitted = authenticateReleaseEnvelope(envelope, producers, input.compatibility);
25
- validateReleasePayloadIdentity(admitted.manifest, input.canonArchive, input.engineArchive);
26
- const root = path.resolve(destination), parent = path.dirname(root);
27
- if (root === parent || !fs.existsSync(parent) || !fs.lstatSync(parent).isDirectory())
28
- refuse("invalid_output_parent");
29
- const envelopeBytes = Buffer.from(JSON.stringify(envelope));
30
- const lease = acquireInstallationLock(`${root}.lock`, hybridDigest({ envelope, canon: admitted.manifest.canon.archive, engine: admitted.manifest.engine.archive }));
31
- let staging = null;
32
- try {
33
- if (fs.existsSync(root)) {
34
- const retained = readLocalReleaseBundle(root, producers, input.compatibility);
35
- if (fs.readdirSync(root).sort().join("/") !== "canon.tgz/engine.tgz/release-envelope.json"
36
- || !fs.readFileSync(path.join(root, "release-envelope.json")).equals(envelopeBytes)
37
- || !retained.canonArchive.equals(input.canonArchive) || !retained.engineArchive.equals(input.engineArchive))
38
- refuse("immutable_bundle_conflict");
39
- return { root, replayed: true, producer_key_id: envelope.producer_key_id };
40
- }
41
- staging = path.join(parent, `.release-staging-${randomUUID()}`);
42
- fs.mkdirSync(staging, { mode: 0o700 });
43
- for (const [name, bytes] of [["release-envelope.json", envelopeBytes], ["canon.tgz", input.canonArchive], ["engine.tgz", input.engineArchive]]) {
44
- const fd = fs.openSync(path.join(staging, name), "wx", 0o600);
45
- try {
46
- fs.writeFileSync(fd, bytes);
47
- fs.fsyncSync(fd);
48
- }
49
- finally {
50
- fs.closeSync(fd);
51
- }
52
- }
53
- readLocalReleaseBundle(staging, producers, input.compatibility);
54
- fs.renameSync(staging, root);
55
- staging = null;
56
- return { root, replayed: false, producer_key_id: envelope.producer_key_id };
57
- }
58
- finally {
59
- if (staging)
60
- fs.rmSync(staging, { recursive: true, force: true });
61
- lease.release();
62
- }
63
- }
@@ -1,160 +0,0 @@
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 { readBoundedReleaseFile } from "./release-source.js";
7
- import { inspectReleaseArchive } from "./release-archive.js";
8
- import { produceReleaseArchive } from "./release-archive-producer.js";
9
- function refuse(reason) {
10
- throw new AppError("release_provenance", "Producer source provenance cannot be verified", 1, { reason, phase: "release_production", effect: "no_effect" });
11
- }
12
- function gitBytes(directory, args, maxBuffer = 1024 * 1024, input) {
13
- const env = { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null" };
14
- for (const name of Object.keys(env))
15
- if (name.startsWith("GIT_") && !["GIT_OPTIONAL_LOCKS", "GIT_CONFIG_NOSYSTEM", "GIT_CONFIG_GLOBAL"].includes(name))
16
- delete env[name];
17
- const result = spawnSync("git", ["-c", "core.fsmonitor=false", "-c", "core.untrackedCache=false", ...args], {
18
- cwd: directory, env, timeout: 10_000, maxBuffer, input
19
- });
20
- if (result.status !== 0 || result.error)
21
- refuse("git_source_unavailable");
22
- return result.stdout;
23
- }
24
- /** Read-only source/tag verification. No fetch, tag creation, commit or cleanup. */
25
- export function verifyProducerSource(root, source, version) {
26
- if (!source || !/^[a-f0-9]{40}$/.test(source.commit) || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(source.repository)
27
- || !/^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/.test(version)
28
- || (source.kind !== "source_checkout" && source.kind !== "published_release")
29
- || (source.kind === "source_checkout" ? source.tag !== null : source.tag !== `v${version}`))
30
- refuse("invalid_source_identity");
31
- let directory;
32
- try {
33
- directory = fs.realpathSync(root);
34
- }
35
- catch {
36
- return refuse("source_unavailable");
37
- }
38
- const git = (args) => gitBytes(directory, args).toString("utf8").trim();
39
- const top = git(["rev-parse", "--show-toplevel"]);
40
- try {
41
- if (fs.realpathSync(top) !== directory)
42
- refuse("source_root_mismatch");
43
- }
44
- catch (error) {
45
- if (error instanceof AppError)
46
- throw error;
47
- return refuse("source_root_mismatch");
48
- }
49
- const origin = git(["remote", "get-url", "origin"]);
50
- if (![`https://github.com/${source.repository}`, `https://github.com/${source.repository}.git`, `git@github.com:${source.repository}.git`, `ssh://git@github.com/${source.repository}.git`].includes(origin))
51
- refuse("source_repository_mismatch");
52
- if (git(["status", "--porcelain", "--untracked-files=all"]))
53
- refuse("source_not_clean");
54
- const commit = git(["rev-parse", "HEAD"]), tree = git(["rev-parse", "HEAD^{tree}"]);
55
- if (commit !== source.commit)
56
- refuse("source_commit_mismatch");
57
- if (readBoundedReleaseFile(path.join(directory, "VERSION"), 1024).toString("utf8").trim() !== version)
58
- refuse("source_version_mismatch");
59
- if (source.tag && git(["rev-parse", "--verify", `refs/tags/${source.tag}^{commit}`]) !== commit)
60
- refuse("source_tag_mismatch");
61
- return { commit, tree, version };
62
- }
63
- /** Exact committed release tree, with only the four canon directory aliases
64
- * materialized as regular files. No mutable worktree reads or export filters.
65
- */
66
- function committedBlobs(root, commit) {
67
- if (!/^[a-f0-9]{40}$/.test(commit))
68
- refuse("invalid_source_identity");
69
- const blobs = new Map();
70
- const links = [];
71
- const listing = gitBytes(root, ["ls-tree", "-rz", "--full-tree", commit], 16 * 1024 * 1024).toString("utf8");
72
- for (const record of listing.split("\0").filter(Boolean)) {
73
- const match = /^(100644|100755|120000) blob ([a-f0-9]{40})\t(.+)$/.exec(record);
74
- if (!match)
75
- refuse("unsupported_committed_tree_entry");
76
- const mode = match[1], hash = match[2], name = match[3];
77
- if (mode === "120000")
78
- links.push({ name, hash });
79
- else
80
- blobs.set(name, { hash, executable: mode === "100755" });
81
- }
82
- const aliases = { code: "mb-sdlc/code", plan: "mb-sdlc/plan", merge: "mb-sdlc/merge", specify: "mb-sdlc/specify" };
83
- for (const link of links) {
84
- const match = /^(\.memory-bank\/)?dd-flow\/(code|plan|merge|specify)$/.exec(link.name);
85
- if (!match)
86
- refuse("unreviewed_committed_alias");
87
- const target = gitBytes(root, ["cat-file", "blob", link.hash]).toString("utf8");
88
- if (target !== aliases[match[2]])
89
- refuse("unreviewed_committed_alias");
90
- const targetPrefix = `${path.posix.dirname(link.name)}/${target}/`;
91
- const children = [...blobs].filter(([name]) => name.startsWith(targetPrefix));
92
- if (!children.length)
93
- refuse("empty_committed_alias");
94
- for (const [name, blob] of children) {
95
- const alias = `${link.name}/${name.slice(targetPrefix.length)}`;
96
- if (blobs.has(alias))
97
- refuse("committed_alias_collision");
98
- blobs.set(alias, blob);
99
- }
100
- }
101
- return blobs;
102
- }
103
- export function verifyCanonArchiveSource(root, commit, identity, compressed) {
104
- const blobs = committedBlobs(root, commit), entries = inspectReleaseArchive(identity, compressed);
105
- const roots = new Set(entries.map(entry => entry.path.split("/")[0]));
106
- if (roots.size !== 1)
107
- refuse("canon_archive_root_ambiguous");
108
- const prefix = `${[...roots][0]}/`;
109
- const directories = new Set();
110
- for (const name of blobs.keys()) {
111
- const parts = name.split("/");
112
- for (let index = 1; index < parts.length; index++)
113
- directories.add(parts.slice(0, index).join("/"));
114
- }
115
- const seen = new Set();
116
- for (const entry of entries) {
117
- if (entry.path === prefix.slice(0, -1) && entry.kind === "directory")
118
- continue;
119
- const name = entry.path.slice(prefix.length);
120
- if (entry.kind === "directory") {
121
- if (!directories.has(name))
122
- refuse("canon_archive_extra_entry");
123
- continue;
124
- }
125
- const blob = blobs.get(name);
126
- if (!blob)
127
- refuse("canon_archive_extra_entry");
128
- const hash = createHash("sha1").update(`blob ${entry.bytes.length}\0`).update(entry.bytes).digest("hex");
129
- if (hash !== blob.hash || Boolean(entry.mode & 0o111) !== blob.executable)
130
- refuse("canon_archive_blob_mismatch");
131
- seen.add(name);
132
- }
133
- if (seen.size !== blobs.size)
134
- refuse("canon_archive_missing_entry");
135
- }
136
- /** Produce from immutable Git objects in one bounded batch, not worktree files. */
137
- export function produceCanonArchive(root, commit) {
138
- const blobs = committedBlobs(root, commit), hashes = [...new Set([...blobs.values()].map(blob => blob.hash))];
139
- const batch = gitBytes(root, ["cat-file", "--batch"], 512 * 1024 * 1024, Buffer.from(hashes.join("\n") + "\n"));
140
- const contents = new Map();
141
- let offset = 0;
142
- for (const hash of hashes) {
143
- const newline = batch.indexOf(10, offset);
144
- if (newline < 0)
145
- refuse("invalid_git_blob_batch");
146
- const match = /^([a-f0-9]{40}) blob ([0-9]+)$/.exec(batch.subarray(offset, newline).toString("ascii"));
147
- if (!match || match[1] !== hash)
148
- refuse("invalid_git_blob_batch");
149
- const size = Number(match[2]), end = newline + 1 + size;
150
- if (!Number.isSafeInteger(size) || end >= batch.length || batch[end] !== 10)
151
- refuse("invalid_git_blob_batch");
152
- contents.set(hash, batch.subarray(newline + 1, end));
153
- offset = end + 1;
154
- }
155
- if (offset !== batch.length)
156
- refuse("invalid_git_blob_batch");
157
- const bytes = produceReleaseArchive([...blobs].map(([name, blob]) => ({ path: `canon/${name}`, bytes: contents.get(blob.hash), executable: blob.executable })));
158
- verifyCanonArchiveSource(root, commit, { url: "https://producer.invalid/canon.tgz", bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }, bytes);
159
- return bytes;
160
- }
@@ -1,66 +0,0 @@
1
- import { valid, prerelease } from "semver";
2
- import { AppError } from "../shared/errors.js";
3
- import { readBoundedReleaseFile } from "./release-source.js";
4
- function refuse(reason) {
5
- throw new AppError("release_registry", "Release selection cannot be resolved", 1, { reason, phase: "release_selection", effect: "no_effect", help: "setup" });
6
- }
7
- function object(value, keys, required = keys) {
8
- if (!value || typeof value !== "object" || Array.isArray(value)
9
- || required.some(key => !Object.hasOwn(value, key))
10
- || Object.keys(value).some(key => !keys.includes(key)))
11
- refuse("invalid_registry_shape");
12
- return value;
13
- }
14
- /** Operator registry is a selector, not signing authority or qualification evidence. */
15
- export function selectRegistryRelease(input, channel, version) {
16
- if (!["stable", "preview", "development"].includes(channel))
17
- refuse("invalid_selected_channel");
18
- const registry = object(input, ["schema_id", "releases", "defaults"]);
19
- if (registry.schema_id !== "dd-flow/release-registry@1" || !Array.isArray(registry.releases)
20
- || registry.releases.length < 1 || registry.releases.length > 10_000)
21
- refuse("invalid_registry_shape");
22
- const defaults = object(registry.defaults, ["stable", "preview", "development"], []);
23
- const entries = new Map();
24
- for (const value of registry.releases) {
25
- const entry = object(value, ["channel", "version", "envelope_url"]);
26
- if (!["stable", "preview", "development"].includes(String(entry.channel))
27
- || typeof entry.version !== "string" || valid(entry.version) !== entry.version
28
- || (entry.channel === "stable" && prerelease(entry.version)) || typeof entry.envelope_url !== "string")
29
- refuse("invalid_registry_entry");
30
- let url;
31
- try {
32
- url = new URL(entry.envelope_url);
33
- }
34
- catch {
35
- return refuse("invalid_registry_url");
36
- }
37
- if (url.protocol !== "https:" || url.username || url.password || url.hash || (url.port && url.port !== "443"))
38
- refuse("invalid_registry_url");
39
- const key = `${entry.channel}/${entry.version}`;
40
- if (entries.has(key))
41
- refuse("duplicate_registry_selection");
42
- entries.set(key, { channel: entry.channel, version: entry.version, envelope_url: entry.envelope_url });
43
- }
44
- for (const [key, selected] of Object.entries(defaults)) {
45
- if (typeof selected !== "string" || !entries.has(`${key}/${selected}`))
46
- refuse("invalid_registry_default");
47
- }
48
- const selected = version ?? defaults[channel];
49
- if (typeof selected !== "string" || valid(selected) !== selected)
50
- refuse("missing_or_invalid_release_version");
51
- const entry = entries.get(`${channel}/${selected}`);
52
- if (!entry)
53
- refuse("release_not_in_selected_channel");
54
- return { ...entry };
55
- }
56
- export function readRegistryRelease(file, channel, version) {
57
- const bytes = readBoundedReleaseFile(file, 4 * 1024 * 1024);
58
- let input;
59
- try {
60
- input = JSON.parse(bytes.toString("utf8"));
61
- }
62
- catch {
63
- return refuse("invalid_registry_json");
64
- }
65
- return selectRegistryRelease(input, channel, version);
66
- }