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

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 (161) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +33 -0
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +1 -0
  5. package/dist/cli/command-metadata.js +144 -0
  6. package/dist/cli/command-option-descriptions.js +186 -0
  7. package/dist/cli/completion-cli.js +70 -0
  8. package/dist/cli/completion.js +57 -0
  9. package/dist/cli/error-output.js +52 -0
  10. package/dist/cli/executor-onboarding.js +71 -0
  11. package/dist/cli/harnesses.js +84 -0
  12. package/dist/cli/models.js +39 -0
  13. package/dist/cli/operator-profiles.js +61 -0
  14. package/dist/cli/output-options.js +35 -0
  15. package/dist/cli/response-file.js +69 -0
  16. package/dist/cli/run-cli.js +5 -0
  17. package/dist/cli/setup.js +147 -0
  18. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  19. package/dist/harness-runtime/bin/dd-opencode.mjs +1 -1
  20. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +13 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +193 -39
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +295 -100
  23. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
  24. package/dist/harness-runtime/lib/managed-daemon.mjs +1 -1
  25. package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
  26. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  27. package/dist/harness-runtime/lib/opencode-plugin.mjs +60 -0
  28. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  29. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  30. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  31. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  32. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  33. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  34. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  35. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  36. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  37. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  38. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  39. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  40. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  41. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  42. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  43. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  44. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  45. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  46. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  47. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  48. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  49. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  50. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  51. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  52. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  53. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  54. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  55. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  56. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  57. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  58. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  59. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  60. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  61. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  62. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  63. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  64. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  65. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  66. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  67. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  68. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  69. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  70. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  71. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  72. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  73. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  74. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  75. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  76. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  77. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  78. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  79. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  80. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  81. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  82. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  83. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  84. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  85. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  86. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  87. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  88. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  89. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  90. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  91. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  92. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  93. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  94. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  95. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  96. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  97. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  98. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  99. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  100. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  101. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  102. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  103. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  104. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  105. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  106. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  107. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  108. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  109. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  110. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  111. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  112. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  113. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  114. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  115. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  116. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  117. package/dist/services/cli-operation-classifier.js +4 -0
  118. package/dist/services/cli-upgrade-allowlist.js +27 -0
  119. package/dist/services/discovery/agy-discovery.js +2 -0
  120. package/dist/services/discovery/codex-discovery.js +2 -0
  121. package/dist/services/discovery/droid-discovery.js +2 -0
  122. package/dist/services/discovery/grok-discovery.js +2 -0
  123. package/dist/services/discovery/opencode-discovery.js +2 -0
  124. package/dist/services/discovery/static.js +31 -0
  125. package/dist/services/discovery/zcode-discovery.js +2 -0
  126. package/dist/services/engines.js +23 -25
  127. package/dist/services/eval-snapshots.js +1 -1
  128. package/dist/services/executor-onboarding.js +56 -0
  129. package/dist/services/external-work-launch.js +1 -1
  130. package/dist/services/harness-discovery.js +29 -0
  131. package/dist/services/harness-doctor.js +75 -0
  132. package/dist/services/harness-probe-effects.js +51 -0
  133. package/dist/services/harness-version-probe.js +100 -0
  134. package/dist/services/hooks.js +1 -1
  135. package/dist/services/hybrid-contracts.js +455 -0
  136. package/dist/services/installation-lock.js +92 -0
  137. package/dist/services/managed-processes.js +30 -0
  138. package/dist/services/merge-server.js +1 -1
  139. package/dist/services/model-catalog.js +109 -0
  140. package/dist/services/model-discovery.js +46 -0
  141. package/dist/services/operator-profiles.js +236 -0
  142. package/dist/services/release-archive-producer.js +52 -0
  143. package/dist/services/release-archive.js +143 -0
  144. package/dist/services/release-cache.js +142 -0
  145. package/dist/services/release-compatibility.js +18 -0
  146. package/dist/services/release-download.js +67 -0
  147. package/dist/services/release-envelope.js +104 -0
  148. package/dist/services/release-manifest.js +45 -0
  149. package/dist/services/release-payload.js +114 -0
  150. package/dist/services/release-producer.js +63 -0
  151. package/dist/services/release-provenance.js +160 -0
  152. package/dist/services/release-registry.js +66 -0
  153. package/dist/services/release-source.js +194 -0
  154. package/dist/services/run-controller-recovery.js +2 -2
  155. package/dist/services/run-controller.js +3 -2
  156. package/dist/services/run-health.js +149 -0
  157. package/dist/services/runs.js +61 -13
  158. package/dist/services/setup.js +377 -0
  159. package/dist/storage/database.js +18 -3
  160. package/dist/storage/writer-contract.js +8 -0
  161. package/package.json +4 -1
@@ -0,0 +1,56 @@
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
+ }
@@ -123,7 +123,7 @@ export async function launchExternalWork(context, input) {
123
123
  prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null);
124
124
  }
125
125
  catch (error) {
126
- if (runtimeHarness(resolved.worker) !== "codex-desktop")
126
+ if (!["codex-desktop", "opencode-server"].includes(runtimeHarness(resolved.worker)))
127
127
  throw error;
128
128
  const retained = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
129
129
  const capacityIntents = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_intents ?? [] : [];
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,75 @@
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
+ }
@@ -0,0 +1,51 @@
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
+ }
@@ -0,0 +1,100 @@
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
+ }
@@ -843,7 +843,7 @@ function handleControlledToolEvent(context, input, event, harness) {
843
843
  });
844
844
  return { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}) };
845
845
  }
846
- if (!command || !(droid ? ["Execute"] : ["bash", "Bash", "run_command", "run_terminal_command", "RunTerminalCommand"]).includes(toolName)) {
846
+ if (!command || !(droid ? ["Execute"] : ["bash", "Bash", "shell", "run_command", "run_terminal_command", "RunTerminalCommand"]).includes(toolName)) {
847
847
  return { ok: true, observed: false, reason: command ? "non_bash_tool" : "event_not_participating" };
848
848
  }
849
849
  const lifecycle = lifecycleFacts(command);