@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +2 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +101 -40
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +70 -9
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createPublicKey } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { authenticateReleaseEnvelope, releaseProducerKeyId } from "./release-envelope.js";
|
|
6
|
+
import { verifyReleaseArchive } from "./release-manifest.js";
|
|
7
|
+
import { downloadReleaseArchive } from "./release-download.js";
|
|
8
|
+
function refuse(reason) {
|
|
9
|
+
throw new AppError("release_source", "Release source cannot be read", 1, { reason, phase: "release_source", effect: "no_effect", help: "setup" });
|
|
10
|
+
}
|
|
11
|
+
/** Bounded regular-file reads; paths, parser excerpts and key material never enter diagnostics. */
|
|
12
|
+
export function readBoundedReleaseFile(file, maxBytes) {
|
|
13
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
|
|
14
|
+
refuse("invalid_source_limit");
|
|
15
|
+
let fd;
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.lstatSync(file).isFile())
|
|
18
|
+
refuse("source_not_regular_file");
|
|
19
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof AppError)
|
|
23
|
+
throw error;
|
|
24
|
+
return refuse("source_unavailable");
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const stat = fs.fstatSync(fd);
|
|
28
|
+
if (!stat.isFile() || stat.size < 1 || stat.size > maxBytes)
|
|
29
|
+
refuse("source_size_outside_limit");
|
|
30
|
+
const bytes = Buffer.alloc(stat.size);
|
|
31
|
+
let offset = 0;
|
|
32
|
+
while (offset < bytes.length) {
|
|
33
|
+
const received = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
34
|
+
if (!received)
|
|
35
|
+
refuse("source_changed_during_read");
|
|
36
|
+
offset += received;
|
|
37
|
+
}
|
|
38
|
+
if (fs.readSync(fd, Buffer.alloc(1), 0, 1, offset) !== 0)
|
|
39
|
+
refuse("source_changed_during_read");
|
|
40
|
+
return bytes;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof AppError)
|
|
44
|
+
throw error;
|
|
45
|
+
return refuse("source_unavailable");
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
fs.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function json(bytes) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return refuse("invalid_source_json");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Explicit operator trust input is separate from the release bundle. No implicit key enrollment. */
|
|
60
|
+
export function readTrustedReleaseProducers(file) {
|
|
61
|
+
const input = json(readBoundedReleaseFile(file, 1024 * 1024));
|
|
62
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length !== 2 || Object.keys(input).some(key => !["producers", "schema_id"].includes(key)))
|
|
63
|
+
refuse("invalid_trust_shape");
|
|
64
|
+
const settings = input;
|
|
65
|
+
if (settings.schema_id !== "dd-flow/release-trust@1" || !Array.isArray(settings.producers) || settings.producers.length < 1 || settings.producers.length > 100)
|
|
66
|
+
refuse("invalid_trust_shape");
|
|
67
|
+
const result = new Map();
|
|
68
|
+
for (const input of settings.producers) {
|
|
69
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length !== 3 || Object.keys(input).some(key => !["channels", "public_key_pem", "repository"].includes(key)))
|
|
70
|
+
refuse("invalid_producer_shape");
|
|
71
|
+
const producer = input;
|
|
72
|
+
if (typeof producer.public_key_pem !== "string" || !/^-----BEGIN PUBLIC KEY-----\r?\n[A-Za-z0-9+/=\r\n]+-----END PUBLIC KEY-----\r?\n?$/.test(producer.public_key_pem) ||
|
|
73
|
+
typeof producer.repository !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(producer.repository) ||
|
|
74
|
+
!Array.isArray(producer.channels) || producer.channels.length < 1 || producer.channels.length > 3 ||
|
|
75
|
+
producer.channels.some(channel => !["stable", "preview", "development"].includes(channel)) || new Set(producer.channels).size !== producer.channels.length)
|
|
76
|
+
refuse("invalid_producer_scope");
|
|
77
|
+
try {
|
|
78
|
+
const publicKey = createPublicKey(producer.public_key_pem), id = releaseProducerKeyId(publicKey);
|
|
79
|
+
if (result.has(id))
|
|
80
|
+
refuse("duplicate_producer_key");
|
|
81
|
+
result.set(id, { publicKey, repository: producer.repository, channels: new Set(producer.channels) });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof AppError)
|
|
85
|
+
throw error;
|
|
86
|
+
return refuse("invalid_producer_key");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
/** Authenticate metadata before reading archives; all integrity checks precede installation writes. */
|
|
92
|
+
export function readLocalReleaseBundle(root, producers, compatibility) {
|
|
93
|
+
try {
|
|
94
|
+
if (!fs.lstatSync(root).isDirectory())
|
|
95
|
+
refuse("source_not_directory");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof AppError)
|
|
99
|
+
throw error;
|
|
100
|
+
return refuse("source_unavailable");
|
|
101
|
+
}
|
|
102
|
+
const envelope = json(readBoundedReleaseFile(path.join(root, "release-envelope.json"), 24 * 1024 * 1024));
|
|
103
|
+
const authenticated = authenticateReleaseEnvelope(envelope, producers, compatibility);
|
|
104
|
+
const canonArchive = readBoundedReleaseFile(path.join(root, "canon.tgz"), Math.min(authenticated.manifest.canon.archive.bytes, 256 * 1024 * 1024));
|
|
105
|
+
const engineArchive = readBoundedReleaseFile(path.join(root, "engine.tgz"), Math.min(authenticated.manifest.engine.archive.bytes, 256 * 1024 * 1024));
|
|
106
|
+
verifyReleaseArchive(authenticated.manifest.canon.archive, canonArchive);
|
|
107
|
+
verifyReleaseArchive(authenticated.manifest.engine.archive, engineArchive);
|
|
108
|
+
return { envelope, authenticated, canonArchive, engineArchive };
|
|
109
|
+
}
|
|
110
|
+
function remoteUrl(input) {
|
|
111
|
+
let url;
|
|
112
|
+
try {
|
|
113
|
+
url = new URL(input);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return refuse("invalid_remote_source");
|
|
117
|
+
}
|
|
118
|
+
// The explicit source selects a transport origin, never a signing key.
|
|
119
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash || (url.port && url.port !== "443"))
|
|
120
|
+
refuse("unsafe_remote_source");
|
|
121
|
+
return url;
|
|
122
|
+
}
|
|
123
|
+
/** Remote metadata is untrusted until signature verification; archives are fetched only afterward.
|
|
124
|
+
* Redirects, cross-origin artifacts and implicit credential forwarding are never admitted.
|
|
125
|
+
*/
|
|
126
|
+
export async function readRemoteReleaseBundle(source, producers, compatibility, options = {}) {
|
|
127
|
+
const url = remoteUrl(source), timeoutMs = options.timeoutMs ?? 60_000;
|
|
128
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000)
|
|
129
|
+
refuse("invalid_source_timeout");
|
|
130
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
131
|
+
let response;
|
|
132
|
+
try {
|
|
133
|
+
response = await fetcher(url, { redirect: "error", signal: AbortSignal.timeout(timeoutMs), credentials: "omit" });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return refuse("remote_source_unavailable_or_redirect");
|
|
137
|
+
}
|
|
138
|
+
if (!response.ok || !response.body) {
|
|
139
|
+
await response.body?.cancel();
|
|
140
|
+
return refuse("remote_source_http_failure");
|
|
141
|
+
}
|
|
142
|
+
const maxBytes = 24 * 1024 * 1024, length = response.headers.get("content-length");
|
|
143
|
+
if (length !== null && (!/^[0-9]+$/.test(length) || Number(length) < 1 || Number(length) > maxBytes)) {
|
|
144
|
+
await response.body.cancel();
|
|
145
|
+
return refuse("remote_source_size_outside_limit");
|
|
146
|
+
}
|
|
147
|
+
const reader = response.body.getReader(), chunks = [];
|
|
148
|
+
let received = 0;
|
|
149
|
+
try {
|
|
150
|
+
while (true) {
|
|
151
|
+
const { done, value } = await reader.read();
|
|
152
|
+
if (done)
|
|
153
|
+
break;
|
|
154
|
+
received += value.byteLength;
|
|
155
|
+
if (received > maxBytes) {
|
|
156
|
+
await reader.cancel();
|
|
157
|
+
return refuse("remote_source_size_outside_limit");
|
|
158
|
+
}
|
|
159
|
+
chunks.push(Buffer.from(value));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof AppError)
|
|
164
|
+
throw error;
|
|
165
|
+
return refuse("remote_source_interrupted");
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
reader.releaseLock();
|
|
169
|
+
}
|
|
170
|
+
if (!received || (length !== null && Number(length) !== received))
|
|
171
|
+
refuse("remote_source_length_mismatch");
|
|
172
|
+
const envelope = json(Buffer.concat(chunks, received));
|
|
173
|
+
const authenticated = authenticateReleaseEnvelope(envelope, producers, compatibility);
|
|
174
|
+
if (options.expectedVersion !== undefined && authenticated.manifest.version !== options.expectedVersion)
|
|
175
|
+
refuse("selected_version_mismatch");
|
|
176
|
+
for (const artifact of [authenticated.manifest.canon.archive, authenticated.manifest.engine.archive]) {
|
|
177
|
+
if (remoteUrl(artifact.url).origin !== url.origin)
|
|
178
|
+
refuse("cross_origin_artifact_refused");
|
|
179
|
+
}
|
|
180
|
+
const archives = [];
|
|
181
|
+
for (const artifact of ["canon", "engine"]) {
|
|
182
|
+
archives.push(await downloadReleaseArchive(authenticated.manifest[artifact].archive, {
|
|
183
|
+
allowedHosts: new Set([url.hostname]), timeoutMs, fetch: fetcher,
|
|
184
|
+
...(options.onProgress ? { onProgress: progress => options.onProgress({ artifact, ...progress }) } : {})
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
return { envelope, authenticated, canonArchive: archives[0], engineArchive: archives[1] };
|
|
188
|
+
}
|
|
189
|
+
/** One source selector for setup/update; local bundles remain fully offline. */
|
|
190
|
+
export async function readReleaseBundle(source, producers, compatibility, options = {}) {
|
|
191
|
+
if (/^https?:\/\//i.test(source))
|
|
192
|
+
return readRemoteReleaseBundle(source, producers, compatibility, options);
|
|
193
|
+
return readLocalReleaseBundle(source, producers, compatibility);
|
|
194
|
+
}
|
|
@@ -6,7 +6,7 @@ import { assertSourceRepairEligible } from "./code-checks.js";
|
|
|
6
6
|
/** Checks finish before entering this synchronous registration transaction. */
|
|
7
7
|
export function ensureAggregateRepair(context, input, create) {
|
|
8
8
|
if (!input.failures.length)
|
|
9
|
-
throw new AppError("repair_intent_invalid", "Repair requires
|
|
9
|
+
throw new AppError("repair_intent_invalid", "Repair requires candidate-source-backed failed receipts", 1);
|
|
10
10
|
assertSourceRepairEligible(input.failures);
|
|
11
11
|
const causes = input.failures.map(receipt => ({ id: receipt.id, epoch: receipt.verification_epoch, fingerprint: receipt.workspace_fingerprint })).sort((a, b) => a.id.localeCompare(b.id));
|
|
12
12
|
return ensureRepairIntent(context, { ...input, causes: { receipts: causes, origins: [...(input.origins ?? [])].sort() } }, create);
|
|
@@ -27,14 +27,22 @@ export function reviewerSourceInstruction(root, aspectIds) {
|
|
|
27
27
|
return ["Read the assigned canonical criterion files and the available project orientation below before independent review. Apply each criterion's scope; a small behavior change is not by itself a reason to omit testing or evidence. These starting inputs do not limit relevant project investigation. Do not edit them.", ...inputs.map(input => `- ${path.join(root, input.path)} (SHA256 ${input.sha256})`)].join("\n");
|
|
28
28
|
}
|
|
29
29
|
export function reviewEnvironmentAllowed(file) {
|
|
30
|
-
|
|
30
|
+
// Relative evidence names are scoped to the project: a private parent also
|
|
31
|
+
// makes its children private. Absolute filesystem traversal checks each
|
|
32
|
+
// directory separately, without classifying ancestors outside the workspace.
|
|
33
|
+
return path.isAbsolute(file) || path.win32.isAbsolute(file)
|
|
34
|
+
? localEnvironmentAllowed(file)
|
|
35
|
+
: file.split(/[\\/]/).every(part => localEnvironmentAllowed(part));
|
|
31
36
|
}
|
|
32
37
|
export function reviewSourcePaths(root, explicitInputs = [], base = "HEAD") {
|
|
33
38
|
const inventory = workspaceGitFiles(root, ["--cached", "--others", "--exclude-standard"]);
|
|
34
39
|
if (inventory === null)
|
|
35
40
|
return null;
|
|
36
41
|
const serviceAllowed = projectServicePathFilter(root, explicitInputs);
|
|
37
|
-
const
|
|
42
|
+
const hasHead = spawnSync("git", ["-C", root, "rev-parse", "--verify", "HEAD"], { encoding: "utf8" }).status === 0;
|
|
43
|
+
if (base === "unborn" && hasHead)
|
|
44
|
+
throw new AppError("review_git_evidence_failed", "Unborn review baseline no longer matches the workspace", 1, { workspace_root: root });
|
|
45
|
+
const hasBase = base === "unborn" ? false : base !== "HEAD" || hasHead;
|
|
38
46
|
const changes = hasBase ? spawnSync("git", ["-C", root, "diff", "--name-status", "-z", "--find-renames", base, "--"], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }) : null;
|
|
39
47
|
if (changes && (changes.status !== 0 || changes.error))
|
|
40
48
|
throw new AppError("review_git_evidence_failed", "Could not classify renamed review paths", 1, { workspace_root: root, cause: changes.error?.message ?? changes.stderr });
|