@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
package/dist/cli/setup.js DELETED
@@ -1,147 +0,0 @@
1
- import { prepareCommandRoute } from "./command-inputs.js";
2
- import path from "node:path";
3
- import { parseOutputOptions } from "./output-options.js";
4
- import { resolveDdFlowHome } from "../storage/paths.js";
5
- import { readSetupStatus, installAuthenticatedRelease, readRetainedSetupExecutor, hasDeferredSetupOnboarding, readSetupOnboarding, commitSetupOnboarding } from "../services/setup.js";
6
- import { readTrustedReleaseProducers, readReleaseBundle } from "../services/release-source.js";
7
- import { createReleaseCompatibility, installedCatalogVersions } from "../services/release-compatibility.js";
8
- import { getCliVersionReport } from "../services/build-info.js";
9
- import { readOperatorProfiles, readProfileCreationIntent } from "../services/operator-profiles.js";
10
- import { onboardExecutor, selectOnboardingExecutor } from "../services/executor-onboarding.js";
11
- import { askExecutorChoice } from "./executor-onboarding.js";
12
- import { requiredOption, optionalOption } from "./argument-values.js";
13
- import { AppError, errorRecord } from "../shared/errors.js";
14
- import { hybridDigest } from "../services/hybrid-contracts.js";
15
- import { prepareResponseDestination, writeResponseFile } from "./response-file.js";
16
- import { readRegistryRelease } from "../services/release-registry.js";
17
- import { writePreparationError } from "./error-output.js";
18
- /** Lightweight global diagnostic ingress; never creates a runtime context. */
19
- export async function runSetupCli(args, env, io = { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin }) {
20
- let json = false;
21
- try {
22
- const output = parseOutputOptions(args);
23
- json = output.json;
24
- if (output.responseFile)
25
- prepareResponseDestination(output.responseFile);
26
- const route = prepareCommandRoute(output.args);
27
- const humanProgress = !output.json && !output.progressJsonl;
28
- const reportHumanProgress = (message) => {
29
- // Diagnostic delivery cannot turn a completed installation into failure.
30
- try {
31
- io.stderr.write(`setup: ${message}\n`);
32
- }
33
- catch { /* Best-effort human progress only. */ }
34
- };
35
- const home = resolveDdFlowHome(env);
36
- let result;
37
- if (route.key === "setup status")
38
- result = { ok: true, action: "setup_status", observed_cli: getCliVersionReport().cli, ...readSetupStatus(home) };
39
- else if (route.key === "setup" || route.key === "setup update") {
40
- const parsed = route.parsed;
41
- const source = optionalOption(parsed, "canon-source"), version = optionalOption(parsed, "canon-version");
42
- const registryFile = optionalOption(parsed, "release-registry");
43
- if (source && (version || registryFile))
44
- throw new AppError("usage", "--canon-source is exclusive with --canon-version and --release-registry; see dd-flow setup --help", 2, { effect: "no_effect" });
45
- const channel = (optionalOption(parsed, "channel") ?? "stable");
46
- const selection = source ? null : readRegistryRelease(registryFile ?? env.DD_FLOW_RELEASE_REGISTRY ?? path.join(home, "release-registry.json"), channel, version);
47
- const profile = optionalOption(parsed, "profile"), skip = parsed.options.has("skip-agent-setup");
48
- const intentFile = optionalOption(parsed, "save-profile-intent");
49
- if (intentFile && skip)
50
- throw new AppError("usage", "--save-profile-intent conflicts with --skip-agent-setup", 2, { effect: "no_effect" });
51
- let profileCreation = intentFile ? readProfileCreationIntent(intentFile) : undefined;
52
- const stdin = io.stdin ?? process.stdin;
53
- const nonInteractive = parsed.options.has("non-interactive") || output.json || output.progressJsonl
54
- || !stdin.isTTY || !io.stderr.isTTY;
55
- const profileFile = optionalOption(parsed, "profile-file");
56
- const requestId = requiredOption(parsed, "request-id");
57
- const retained = profile === undefined && profileFile === undefined && !intentFile && !skip ? readRetainedSetupExecutor(home, requestId) : null;
58
- if (retained?.profileCreation)
59
- profileCreation = retained.profileCreation;
60
- const choice = { ...(profile !== undefined ? { profileId: profile } : {}),
61
- ...(profileFile !== undefined ? { profileFile } : {}),
62
- ...(retained?.executor ? { profile: retained.executor } : profileCreation && profile === undefined && profileFile === undefined ? { profile: profileCreation.profile } : {}),
63
- skip: skip || retained !== null && retained.executor === null };
64
- let deferred = hasDeferredSetupOnboarding(home, requestId);
65
- let selected;
66
- if (!deferred) {
67
- try {
68
- selected = selectOnboardingExecutor(choice, () => readOperatorProfiles(home), { allowSkip: true });
69
- }
70
- catch (error) {
71
- if (!(error instanceof AppError) || error.code !== "agent_profile_missing" || nonInteractive
72
- || profile !== undefined || profileFile !== undefined || intentFile || skip)
73
- throw error;
74
- deferred = true;
75
- }
76
- }
77
- else if (profile !== undefined || profileFile !== undefined || intentFile || skip) {
78
- // Explicit continuation inputs still fail before downloading or replaying.
79
- selected = selectOnboardingExecutor(choice, () => readOperatorProfiles(home), { allowSkip: true });
80
- }
81
- const observedCli = getCliVersionReport().cli;
82
- const compatibility = createReleaseCompatibility({ channel,
83
- catalogVersions: installedCatalogVersions, routerVersion: observedCli.version });
84
- const producers = readTrustedReleaseProducers(requiredOption(parsed, "trust-file"));
85
- const bundle = await readReleaseBundle(source ?? selection.envelope_url, producers, compatibility, {
86
- ...(selection ? { expectedVersion: selection.version } : {}),
87
- ...((output.progressJsonl || humanProgress) ? { onProgress: (progress) => {
88
- if (output.progressJsonl)
89
- io.stdout.write(`${JSON.stringify({ type: "setup_download_progress", request_id: requestId, ...progress })}\n`);
90
- else if (progress.received_bytes === progress.expected_bytes)
91
- reportHumanProgress(`${progress.artifact} archive received (${progress.received_bytes} bytes); integrity validation follows`);
92
- } } : {})
93
- });
94
- if (selection && bundle.authenticated.manifest.version !== selection.version)
95
- throw new AppError("release_registry", "Authenticated release differs from selected registry version", 1, { reason: "selected_version_mismatch", phase: "release_selection", effect: "no_effect", help: "setup" });
96
- const installed = installAuthenticatedRelease(home, { requestId, action: route.key === "setup update" ? "update" : "install",
97
- envelope: bundle.envelope, trustedProducers: producers, compatibility, canonArchive: bundle.canonArchive, engineArchive: bundle.engineArchive,
98
- ...(deferred ? { deferredOnboarding: true } : { executor: selected.profile, ...(profileCreation ? { profileCreation } : {}) }),
99
- ...((output.progressJsonl || humanProgress) ? { onCheckpoint: phase => {
100
- if (output.progressJsonl)
101
- io.stdout.write(`${JSON.stringify({ type: "setup_progress", request_id: requestId, phase })}\n`);
102
- else
103
- reportHumanProgress({ prepared: "verified release request retained", canon_cached: "canon cache verified", engine_cached: "engine cache verified",
104
- profiles_saved: "profile settings saved", committed: "installation committed; runtime qualification remains unknown" }[phase]);
105
- } } : {}) });
106
- if (humanProgress && installed.replayed)
107
- reportHumanProgress("exact request replay verified; retained selection was not reactivated");
108
- if (deferred) {
109
- const saved = readSetupOnboarding(home, requestId);
110
- const explicit = profile !== undefined || profileFile !== undefined || intentFile !== undefined || skip;
111
- const deferredChoice = saved && !explicit ? { ...(saved.executor ? { profile: saved.executor } : { skip: true }),
112
- ...(saved.profileCreation ? { profileCreation: saved.profileCreation } : {}) } : choice;
113
- try {
114
- selected ??= await onboardExecutor(deferredChoice, () => readOperatorProfiles(home), { allowSkip: true, nonInteractive }, settings => askExecutorChoice(settings, { stdin, stderr: io.stderr }));
115
- const consent = commitSetupOnboarding(home, requestId, { executor: selected.profile,
116
- ...(profileCreation ?? selected.profileCreation ? { profileCreation: profileCreation ?? selected.profileCreation } : {}) });
117
- installed.executor = consent.executor;
118
- if (humanProgress)
119
- reportHumanProgress("executor onboarding committed after installation");
120
- }
121
- catch (error) {
122
- if (error instanceof AppError) {
123
- error.details.installation = "committed";
124
- error.details.request_id = requestId;
125
- error.details.next_action = "Replay the same setup request to resume executor onboarding; installation remains retained";
126
- }
127
- throw error;
128
- }
129
- }
130
- result = { ...installed, action: route.key === "setup update" ? "setup_update" : "setup_install", observed_cli: observedCli,
131
- executor: installed.executor ? { profile_id: installed.executor.id, revision: hybridDigest(installed.executor), readiness: "unknown_requires_qualification" } : { profile_id: null, readiness: "not_configured" },
132
- ...readSetupStatus(home) };
133
- }
134
- else
135
- throw new AppError("usage", "Unknown setup command; see dd-flow setup --help", 2, { effect: "no_effect" });
136
- io.stdout.write(`${JSON.stringify(writeResponseFile(result, output), null, json ? undefined : 2)}\n`);
137
- return 0;
138
- }
139
- catch (error) {
140
- const record = errorRecord(error);
141
- writePreparationError(io, args, record);
142
- if (!json && error instanceof AppError && error.details.installation === "committed") {
143
- io.stderr.write("setup: installation remains committed; replay the same request to resume executor onboarding\n");
144
- }
145
- return record.exitCode;
146
- }
147
- }
@@ -1,27 +0,0 @@
1
- import { parseCommandArgs } from "./lifecycle-command.js";
2
- /** Syntax eligibility only. Storage/project/RUN authority is checked separately. */
3
- export function isUpgradeAllowlisted(args) {
4
- const parsed = parseCommandArgs(args), [family, command] = parsed.positional;
5
- const option = (name) => {
6
- const value = parsed.options.get(name)?.at(-1);
7
- return value && value !== "__dd_flow_flag__" ? value : null;
8
- };
9
- if (family === "status")
10
- return true;
11
- if (family === "engine" && command === "resolve")
12
- return true;
13
- if (family === "migration" && ["impact", "plan", "report", "verify", "apply"].includes(command ?? ""))
14
- return true;
15
- if (family === "memory" && command === "permissions" && parsed.positional[2] === "preflight")
16
- return true;
17
- if (family === "run" && command === "start")
18
- return option("flow-kind") === "mb-upgrade";
19
- const bound = Boolean(option("run") || parsed.positional.some(value => value.startsWith("RUN-")));
20
- if (family === "stage" && ["start", "pause", "resume", "finish"].includes(command ?? ""))
21
- return bound;
22
- if (family === "session" && ["status", "register", "stop", "usage"].includes(command ?? ""))
23
- return bound;
24
- if (family === "dashboard" && ["data", "render", "refresh", "open"].includes(command ?? ""))
25
- return true;
26
- return false;
27
- }
@@ -1,2 +0,0 @@
1
- import { discoverExecutable } from "./static.js";
2
- export const discoverAgy = (input) => discoverExecutable("agy", ["agy"], input);
@@ -1,2 +0,0 @@
1
- import { discoverExecutable } from "./static.js";
2
- export const discoverCodex = (input) => discoverExecutable("codex", ["codex"], input);
@@ -1,2 +0,0 @@
1
- import { discoverExecutable } from "./static.js";
2
- export const discoverDroid = (input) => discoverExecutable("droid", ["droid"], input);
@@ -1,2 +0,0 @@
1
- import { discoverExecutable } from "./static.js";
2
- export const discoverGrok = (input) => discoverExecutable("grok", ["grok"], input);
@@ -1,2 +0,0 @@
1
- import { discoverExecutable } from "./static.js";
2
- export const discoverOpenCode = (input) => discoverExecutable("opencode", ["opencode"], input);
@@ -1,31 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { AppError } from "../../shared/errors.js";
4
- /** Presence only: no child process, config/credential read, network or generative probe. */
5
- export function discoverExecutable(harness, names, input) {
6
- const platform = input.platform ?? process.platform;
7
- if (input.executable && !path.isAbsolute(input.executable))
8
- throw new AppError("usage", "Explicit executable must be an absolute path", 2, { effect: "no_effect" });
9
- const suffixes = platform === "win32" ? (input.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(suffix => /^\.[A-Za-z0-9]+$/.test(suffix)) : [""];
10
- const paths = input.executable ? [input.executable] : (input.env.PATH ?? "").split(platform === "win32" ? ";" : path.delimiter)
11
- .filter(directory => path.isAbsolute(directory)).flatMap(directory => names.flatMap(name => suffixes.map(suffix => path.join(directory, `${name}${suffix}`))));
12
- const candidates = new Map();
13
- let unreadable = false;
14
- for (const file of paths) {
15
- try {
16
- if (!fs.statSync(file).isFile())
17
- continue;
18
- fs.accessSync(file, platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
19
- candidates.set(fs.realpathSync(file), file);
20
- }
21
- catch (error) {
22
- if (!["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
23
- unreadable = true;
24
- }
25
- }
26
- const selected = candidates.size === 1;
27
- return { harness, candidates: [...candidates.keys()].map(file => ({ path: file, version: null, selected })),
28
- installed: selected ? "yes" : candidates.size > 1 || unreadable ? "unknown" : "no",
29
- compatible: "unknown", protocol: "unknown", auth: "unknown", qualified: "unknown", evidence: [],
30
- reason: selected ? "executable_present_no_qualification_probe" : candidates.size > 1 ? "multiple_installations_require_explicit_selection" : unreadable ? "filesystem_observation_incomplete" : "executable_not_found" };
31
- }
@@ -1,2 +0,0 @@
1
- import { discoverExecutable } from "./static.js";
2
- export const discoverZcode = (input) => discoverExecutable("zcode", ["zcode"], input);
@@ -1,56 +0,0 @@
1
- import { AppError } from "../shared/errors.js";
2
- import { hybridDigest } from "./hybrid-contracts.js";
3
- import { prepareOperatorProfile, readOperatorProfileInput, resolveOperatorProfile, validateProfileCreationIntent } from "./operator-profiles.js";
4
- function refuse(code, message, exitCode = 2) {
5
- throw new AppError(code, message, exitCode, { phase: "executor_onboarding", effect: "no_effect", help: "profiles" });
6
- }
7
- /** Shared setup/first-flow admission; settings are read lazily, never written. */
8
- export function selectOnboardingExecutor(choice, readSettings, policy) {
9
- if (!choice || typeof choice !== "object" || Array.isArray(choice)
10
- || Object.keys(choice).some(key => !["profileId", "profile", "profileFile", "skip", "profileCreation"].includes(key))
11
- || choice.profileFile !== undefined && (typeof choice.profileFile !== "string" || !choice.profileFile.trim())
12
- || choice.skip !== undefined && typeof choice.skip !== "boolean"
13
- || choice.profileId !== undefined && (typeof choice.profileId !== "string" || !choice.profileId.trim())) {
14
- refuse("usage", "Invalid executor selection");
15
- }
16
- if ([choice.profileId !== undefined, choice.profile !== undefined, choice.profileFile !== undefined, choice.skip === true].filter(Boolean).length > 1) {
17
- refuse("usage", "Executor profile, profile ID and skip are mutually exclusive");
18
- }
19
- if (choice.profileCreation !== undefined) {
20
- validateProfileCreationIntent(choice.profileCreation);
21
- if (choice.profile === undefined || hybridDigest(choice.profile) !== hybridDigest(choice.profileCreation.profile))
22
- refuse("usage", "Profile save intent requires its exact explicit profile");
23
- }
24
- if (choice.skip) {
25
- if (!policy.allowSkip)
26
- refuse("agent_profile_missing", "This operation requires an executor profile", 1);
27
- return { profile: null, revision: null, source: "skip" };
28
- }
29
- const explicit = choice.profile !== undefined || choice.profileFile !== undefined || choice.profileId !== undefined;
30
- if (!explicit && policy.projectProfileId !== undefined && (typeof policy.projectProfileId !== "string" || !policy.projectProfileId.trim())) {
31
- refuse("usage", "Invalid project executor profile selection");
32
- }
33
- const profile = prepareOperatorProfile(choice.profile !== undefined ? choice.profile : choice.profileFile !== undefined
34
- ? readOperatorProfileInput(choice.profileFile) : resolveOperatorProfile(readSettings(), choice.profileId ?? policy.projectProfileId));
35
- return { profile: structuredClone(profile), revision: hybridDigest(profile),
36
- ...(choice.profileCreation ? { profileCreation: structuredClone(choice.profileCreation) } : {}),
37
- source: choice.profile !== undefined ? "explicit-profile" : choice.profileFile !== undefined ? "explicit-file" : choice.profileId !== undefined ? "explicit-id" : policy.projectProfileId !== undefined ? "project-default" : "default" };
38
- }
39
- /** Interactive decisions use exactly the same admission as headless choices. */
40
- export async function onboardExecutor(choice, readSettings, policy, ask) {
41
- try {
42
- return selectOnboardingExecutor(choice, readSettings, policy);
43
- }
44
- catch (error) {
45
- if (!(error instanceof AppError) || error.code !== "agent_profile_missing"
46
- || choice.profileId !== undefined || choice.profile !== undefined || choice.profileFile !== undefined || choice.skip === true
47
- || policy.projectProfileId !== undefined || policy.nonInteractive || !ask)
48
- throw error;
49
- // No speculative defaults or writes: the human supplies a concrete choice.
50
- const decision = await ask(structuredClone(readSettings()));
51
- if (decision.profileId === undefined && decision.profile === undefined && decision.profileFile === undefined && decision.skip !== true) {
52
- refuse("agent_profile_missing", "Onboarding requires an explicit choice", 1);
53
- }
54
- return selectOnboardingExecutor(decision, readSettings, policy);
55
- }
56
- }
@@ -1,29 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { validateHybridContract } from "./hybrid-contracts.js";
3
- import { AppError } from "../shared/errors.js";
4
- import { discoverCodex } from "./discovery/codex-discovery.js";
5
- import { discoverZcode } from "./discovery/zcode-discovery.js";
6
- import { discoverAgy } from "./discovery/agy-discovery.js";
7
- import { discoverOpenCode } from "./discovery/opencode-discovery.js";
8
- import { discoverGrok } from "./discovery/grok-discovery.js";
9
- import { discoverDroid } from "./discovery/droid-discovery.js";
10
- import { readOnlyProbeScope, runScopedProbe, staticDiscoveryEffects } from "./harness-probe-effects.js";
11
- const modules = { codex: discoverCodex, zcode: discoverZcode, agy: discoverAgy, opencode: discoverOpenCode, grok: discoverGrok, droid: discoverDroid };
12
- export const harnessCatalog = {
13
- schema_id: "dd-flow/harness-catalog@1", version: "1.0.0",
14
- entries: Object.keys(modules).map(id => ({ id, executable_candidates: [id], probe_module: `${id}-discovery`,
15
- probe_timeout_ms: 3000, version_range: "unknown_requires_review", protocol_version: "unknown_requires_protocol_probe" }))
16
- };
17
- export function discoverHarnesses(input) {
18
- if (input.harness && !Object.hasOwn(modules, input.harness))
19
- throw new AppError("usage", "Unknown harness family", 2, { effect: "no_effect" });
20
- if (input.executable && !input.harness)
21
- throw new AppError("usage", "Explicit executable requires one harness family", 2, { effect: "no_effect" });
22
- const families = input.harness ? [input.harness] : Object.keys(modules);
23
- const result = { schema_id: "dd-flow/discovery-result@1", request_id: input.requestId ?? randomUUID(), observed_at: new Date().toISOString(),
24
- probe_scope: "static", live_authorization_receipt: null, entries: families.map(family => runScopedProbe(staticDiscoveryEffects, readOnlyProbeScope, () => modules[family](input))) };
25
- if (validateHybridContract("harness-catalog", harnessCatalog).length || validateHybridContract("discovery-result", result).length) {
26
- throw new AppError("discovery_contract", "Discovery owner produced invalid facts", 1, { effect: "no_effect" });
27
- }
28
- return result;
29
- }
@@ -1,75 +0,0 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { parse } from "semver";
4
- import { AppError } from "../shared/errors.js";
5
- import { legacyDoctorEffects, runScopedProbe } from "./harness-probe-effects.js";
6
- import { readBoundedReleaseFile } from "./release-source.js";
7
- import { discoverExecutable } from "./discovery/static.js";
8
- const binOptions = {
9
- codex: "codex-bin", zcode: "zcode-acp-bin", agy: "agy-bin", opencode: "opencode-bin", grok: "grok-bin", droid: "droid-bin"
10
- };
11
- function refuse(code, message, exitCode = 1) {
12
- throw new AppError(code, message, exitCode, { phase: "harness_doctor", help: "harnesses doctor" });
13
- }
14
- /** Existing non-generative adapter doctor, bounded by its owner transport.
15
- * Reported compatibility is not qualification or verified account access.
16
- * OpenCode observes an explicitly bound existing daemon; it never starts one.
17
- */
18
- export async function probeHarnessDoctor(input) {
19
- const timeoutMs = input.timeoutMs ?? 30_000;
20
- if (!Object.hasOwn(binOptions, input.harness) || !path.isAbsolute(input.executable)
21
- || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000
22
- || input.stateDir !== undefined && !path.isAbsolute(input.stateDir)
23
- || input.nativeExecutable !== undefined && (!path.isAbsolute(input.nativeExecutable) || input.harness !== "zcode")
24
- || input.stateDir !== undefined && input.harness !== "opencode")
25
- refuse("usage", "Doctor requires explicit contained diagnostic inputs", 2);
26
- return runScopedProbe(legacyDoctorEffects[input.harness], input.scope, async () => {
27
- if (input.harness === "droid" && discoverExecutable("system-ps", ["ps"], { env: { ...process.env, ...input.env } }).candidates.length === 0) {
28
- throw new AppError("doctor_prerequisite_missing", "Droid doctor requires the OS ps utility for physical process settlement", 1, { phase: "harness_doctor", effect: "no_effect", prerequisite: "ps", help: "harnesses doctor" });
29
- }
30
- const args = [fileURLToPath(new URL(`../harness-runtime/bin/dd-${input.harness}.mjs`, import.meta.url)), "doctor",
31
- `--${binOptions[input.harness]}`, input.executable, "--timeout-ms", String(timeoutMs), "--json"];
32
- if (input.harness === "zcode") {
33
- if (!input.nativeExecutable)
34
- refuse("harness_selection_required", "ZCode doctor also requires --native-executable for its native runtime");
35
- args.push("--zcode-path", input.nativeExecutable);
36
- }
37
- if (input.harness === "opencode") {
38
- if (!input.stateDir)
39
- refuse("harness_selection_required", "OpenCode doctor requires --state-dir for an existing owned daemon; no service is started");
40
- let state;
41
- try {
42
- state = JSON.parse(readBoundedReleaseFile(path.join(input.stateDir, "daemon.json"), 1024 * 1024).toString("utf8"));
43
- }
44
- catch {
45
- return refuse("doctor_state_unavailable", "Selected daemon identity cannot be read");
46
- }
47
- const selected = state;
48
- if (selected?.config?.bin !== input.executable)
49
- refuse("doctor_state_mismatch", "Selected daemon does not bind the discovered executable");
50
- args.push("--state-dir", input.stateDir);
51
- }
52
- const transport = await import(new URL("../harness-runtime/lib/process-json.mjs", import.meta.url).href);
53
- let report;
54
- try {
55
- report = await transport.commandJson(process.execPath, args, { env: input.env, timeoutMs, signal: AbortSignal.timeout(timeoutMs), phase: "native-doctor" });
56
- }
57
- catch {
58
- return refuse("harness_doctor_failed", "Adapter doctor failed or could not confirm its observation; no model Turn was requested");
59
- }
60
- if (!report || typeof report !== "object" || Array.isArray(report) || report.ok !== true)
61
- refuse("harness_doctor_failed", "Adapter doctor did not return an accepted diagnostic");
62
- const observed = report.observed_runtime && typeof report.observed_runtime === "object" ? report.observed_runtime : {};
63
- const versionField = input.harness === "codex" ? "codex_cli" : input.harness === "zcode" ? "zcode_acp" : input.harness;
64
- const versions = report.versions && typeof report.versions === "object" && !Array.isArray(report.versions) ? report.versions : {};
65
- // Existing OpenCode doctor reports versions separately from observed_runtime.
66
- const versionValue = input.harness === "opencode" ? versions.opencode : observed[versionField];
67
- const version = typeof versionValue === "string" ? parse(versionValue)?.version ?? null : null;
68
- const protocol = observed.dd_harness_contract;
69
- const models = Array.isArray(report.models) ? report.models.filter((model) => typeof model === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,255}$/.test(model)).slice(0, 10_000) : [];
70
- return { harness: input.harness, effects: legacyDoctorEffects[input.harness], reported_version: version,
71
- reported_protocol: typeof protocol === "string" && /^dd-[a-z0-9-]+@[0-9]+$/.test(protocol) ? protocol : null,
72
- reported_models: [...new Set(models)].sort(), models_complete: false, reported_compatible: typeof report.compatible === "boolean" ? report.compatible : null,
73
- account_access: "unknown", qualified: "unknown", model_turn: "not_requested" };
74
- });
75
- }
@@ -1,51 +0,0 @@
1
- import { AppError } from "../shared/errors.js";
2
- export const readOnlyProbeScope = Object.freeze({
3
- processes: false, services: false, credentials: false, writes: false
4
- });
5
- /** Source-grounded effects of existing adapters; this is not permission to invoke them. */
6
- export const legacyDoctorEffects = {
7
- codex: { child_process: true, service_process: true, credential_access: "possible", filesystem: "temporary_or_runtime_writes", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-codex.mjs:doctor/withBridge" },
8
- zcode: { child_process: true, service_process: false, credential_access: "possible", filesystem: "read_only", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-zcode.mjs:doctor" },
9
- agy: { child_process: true, service_process: false, credential_access: "possible", filesystem: "temporary_or_runtime_writes", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-agy.mjs:doctor" },
10
- opencode: { child_process: true, service_process: true, credential_access: "possible", filesystem: "temporary_or_runtime_writes", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-opencode-daemon.mjs:daemon.doctor/startDaemon" },
11
- grok: { child_process: true, service_process: false, credential_access: "possible", filesystem: "read_only", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-grok.mjs:doctor" },
12
- droid: { child_process: true, service_process: true, credential_access: "possible", filesystem: "temporary_or_runtime_writes", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-droid-daemon.mjs:runtime.start" }
13
- };
14
- for (const effects of Object.values(legacyDoctorEffects))
15
- Object.freeze(effects);
16
- Object.freeze(legacyDoctorEffects);
17
- export const staticDiscoveryEffects = Object.freeze({
18
- child_process: false, service_process: false, credential_access: "none", filesystem: "read_only", model_turn: "not_requested",
19
- source_owner: "services/discovery/static.ts:discoverExecutable"
20
- });
21
- /** Enforce effect ceilings before invoking a probe. A non-generative label alone is insufficient. */
22
- export function assertProbeWithinScope(effects, scope) {
23
- const scopeKeys = ["processes", "services", "credentials", "writes"];
24
- const effectKeys = ["child_process", "service_process", "credential_access", "filesystem", "model_turn", "source_owner"];
25
- if (!scope || typeof scope !== "object" || Array.isArray(scope)
26
- || Object.keys(scope).some(key => !scopeKeys.includes(key))
27
- || scopeKeys.some(key => !Object.hasOwn(scope, key) || typeof scope[key] !== "boolean")
28
- || !effects || typeof effects !== "object" || Array.isArray(effects)
29
- || effectKeys.some(key => !Object.hasOwn(effects, key)) || Object.keys(effects).some(key => !effectKeys.includes(key))
30
- || typeof effects.child_process !== "boolean" || typeof effects.service_process !== "boolean"
31
- || !["none", "possible"].includes(effects.credential_access)
32
- || !["read_only", "temporary_or_runtime_writes"].includes(effects.filesystem)
33
- || !["not_requested", "possible"].includes(effects.model_turn)
34
- || typeof effects.source_owner !== "string" || !effects.source_owner.trim()) {
35
- throw new AppError("probe_scope_invalid", "Probe effects and permissions require an explicit typed scope", 2, { phase: "probe_admission", effect: "no_effect", help: "harnesses" });
36
- }
37
- const blocked = [
38
- effects.child_process && !scope.processes ? "child_process" : null,
39
- effects.service_process && !scope.services ? "service_process" : null,
40
- effects.credential_access !== "none" && !scope.credentials ? "credential_access" : null,
41
- effects.filesystem !== "read_only" && !scope.writes ? "filesystem_writes" : null,
42
- effects.model_turn !== "not_requested" ? "live_authorization_required" : null
43
- ].filter(Boolean);
44
- if (blocked.length)
45
- throw new AppError("probe_effects_not_authorized", "Probe exceeds the selected effect scope", 1, { phase: "probe_admission", effect: "no_effect", blocked_effects: blocked, help: "harnesses" });
46
- }
47
- /** The callback is not entered, including synchronous effects, until admission succeeds. */
48
- export function runScopedProbe(effects, scope, probe) {
49
- assertProbeWithinScope(effects, scope);
50
- return probe();
51
- }
@@ -1,100 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import path from "node:path";
3
- import { parse } from "semver";
4
- import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
5
- import { AppError } from "../shared/errors.js";
6
- import { runScopedProbe } from "./harness-probe-effects.js";
7
- // Even --version runs operator-installed code. Do not infer credential/write
8
- // isolation from an argument spelling or execute it during static discovery.
9
- export const versionProbeEffects = Object.freeze({
10
- child_process: true, service_process: false, credential_access: "possible",
11
- filesystem: "temporary_or_runtime_writes", model_turn: "not_requested",
12
- source_owner: "services/harness-version-probe.ts:probeHarnessVersion"
13
- });
14
- /** Recognized version fields only; unrelated diagnostic numbers are not facts. */
15
- export function parseHarnessVersion(harness, stdout) {
16
- const normalize = (value) => {
17
- const version = parse(value);
18
- return version ? `${version.version}${version.build.length ? `+${version.build.join(".")}` : ""}` : null;
19
- };
20
- if (harness === "grok") {
21
- try {
22
- const value = JSON.parse(stdout);
23
- if (!value || typeof value !== "object" || Array.isArray(value))
24
- return null;
25
- const reported = value.currentVersion;
26
- return typeof reported === "string" ? normalize(reported.trim().split(/\s+/)[0]) : null;
27
- }
28
- catch {
29
- return null;
30
- }
31
- }
32
- const labels = {
33
- codex: "codex(?:-cli)?", zcode: "zcode", agy: "(?:agy|antigravity)", opencode: "opencode", droid: "droid"
34
- };
35
- const pattern = new RegExp(`^(?:${labels[harness]}\\s+)?(v?[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?(?:\\+[A-Za-z0-9.-]+)?)$`, "i");
36
- const versions = new Set(stdout.split(/\r?\n/).flatMap(line => {
37
- const matched = line.trim().match(pattern)?.[1];
38
- const version = matched ? normalize(matched) : null;
39
- return version ? [version] : [];
40
- }));
41
- return versions.size === 1 ? [...versions][0] : null;
42
- }
43
- /** Diagnostic only: a version is not protocol, account or flow qualification. */
44
- export function probeHarnessVersion(input) {
45
- const timeoutMs = input.timeoutMs ?? 3000;
46
- if (!["codex", "zcode", "agy", "opencode", "grok", "droid"].includes(input.harness)) {
47
- throw new AppError("usage", "Unknown harness family", 2, { effect: "no_effect" });
48
- }
49
- if (!path.isAbsolute(input.executable) || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000) {
50
- throw new AppError("usage", "Version probe requires an absolute executable and a timeout of 1–60000 ms", 2, { effect: "no_effect" });
51
- }
52
- return runScopedProbe(versionProbeEffects, input.scope, () => new Promise((resolve, reject) => {
53
- const args = input.harness === "grok" ? ["version", "--json"] : ["--version"];
54
- const detached = process.platform !== "win32";
55
- const child = spawn(input.executable, args, { env: input.env, detached, stdio: ["ignore", "pipe", "pipe"] });
56
- let stdout = "";
57
- let bytes = 0;
58
- let failure;
59
- let cleanup;
60
- const stop = (code, message) => {
61
- failure ??= new AppError(code, message, 1, { phase: "version_probe", effect: "process_dispatched" });
62
- cleanup ??= stopProcessGroup(child, 1000, { detached });
63
- // Observe immediately; close may arrive after asynchronous cleanup failure.
64
- void cleanup.catch(() => { });
65
- };
66
- const timer = setTimeout(() => stop("probe_timeout", "Version probe exceeded its deadline"), timeoutMs);
67
- child.stdout.setEncoding("utf8").on("data", (chunk) => {
68
- bytes += Buffer.byteLength(chunk);
69
- if (bytes > 64_000)
70
- stop("probe_output_limit", "Version probe exceeded its output limit");
71
- else
72
- stdout += chunk;
73
- });
74
- child.stderr.on("data", (chunk) => {
75
- bytes += chunk.length;
76
- if (bytes > 64_000)
77
- stop("probe_output_limit", "Version probe exceeded its output limit");
78
- });
79
- child.on("error", () => { failure ??= new AppError("probe_launch_failed", "Version probe could not start", 1); });
80
- child.on("close", (code, signal) => {
81
- clearTimeout(timer);
82
- void (async () => {
83
- try {
84
- await cleanup;
85
- if (failure)
86
- throw failure;
87
- if (code !== 0 || signal)
88
- throw new AppError("probe_failed", "Version probe did not exit successfully", 1);
89
- // Never expose arbitrary subprocess output: it can contain secrets.
90
- const version = parseHarnessVersion(input.harness, stdout);
91
- resolve({ harness: input.harness, version, status: version ? "observed" : "unrecognized",
92
- compatible: "unknown", protocol: "unknown", auth: "unknown", qualified: "unknown" });
93
- }
94
- catch (error) {
95
- reject(error instanceof AppError ? error : new AppError("probe_cleanup_unconfirmed", "Version probe cleanup could not be confirmed", 1));
96
- }
97
- })();
98
- });
99
- }));
100
- }