@adhdev/daemon-core 0.9.82-rc.16 → 0.9.82-rc.161

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 (209) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/chat/source-machine.d.ts +166 -0
  3. package/dist/chat/source-resolver.d.ts +104 -0
  4. package/dist/chat/subscription-updates.d.ts +1 -0
  5. package/dist/cli-adapter-types.d.ts +19 -2
  6. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  7. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  8. package/dist/cli-adapters/provider-cli-adapter.d.ts +104 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +27 -0
  11. package/dist/commands/handler.d.ts +66 -0
  12. package/dist/commands/mesh-coordinator.d.ts +72 -1
  13. package/dist/commands/router.d.ts +22 -0
  14. package/dist/config/chat-history.d.ts +7 -0
  15. package/dist/config/config.d.ts +5 -0
  16. package/dist/config/mesh-config.d.ts +71 -1
  17. package/dist/git/git-commands.d.ts +5 -1
  18. package/dist/index.d.ts +31 -6
  19. package/dist/index.js +17210 -3575
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +17127 -3551
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/installer.d.ts +1 -4
  24. package/dist/ipc/local-ipc-server.d.ts +91 -0
  25. package/dist/launch.d.ts +1 -1
  26. package/dist/logging/async-batch-writer.d.ts +10 -0
  27. package/dist/mesh/beads-db.d.ts +72 -0
  28. package/dist/mesh/contracts.d.ts +164 -0
  29. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  30. package/dist/mesh/coordinator-registry.d.ts +59 -0
  31. package/dist/mesh/mesh-active-work.d.ts +90 -0
  32. package/dist/mesh/mesh-events.d.ts +77 -5
  33. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  34. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  35. package/dist/mesh/mesh-ledger.d.ts +58 -1
  36. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  37. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  38. package/dist/mesh/preview-freshness.d.ts +18 -0
  39. package/dist/mesh/refine-config.d.ts +193 -0
  40. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  41. package/dist/providers/approval-utils.d.ts +9 -0
  42. package/dist/providers/cli-provider-instance.d.ts +7 -2
  43. package/dist/providers/contracts.d.ts +98 -5
  44. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  45. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  46. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  47. package/dist/providers/native-history/dispatcher.d.ts +24 -0
  48. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  49. package/dist/providers/native-history/index.d.ts +13 -0
  50. package/dist/providers/provider-loader.d.ts +19 -1
  51. package/dist/providers/read-chat-contract.d.ts +29 -0
  52. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  53. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  54. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  55. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
  56. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  57. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  58. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  59. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  60. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  61. package/dist/providers/sdk/v1/index.d.ts +30 -0
  62. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  63. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  64. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  65. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  66. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  67. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  68. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  69. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  70. package/dist/providers/spec/adapter.d.ts +56 -0
  71. package/dist/providers/spec/cli-adapter.d.ts +76 -0
  72. package/dist/providers/spec/driver.d.ts +148 -0
  73. package/dist/providers/spec/evaluator.d.ts +47 -0
  74. package/dist/providers/spec/loader.d.ts +14 -0
  75. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  76. package/dist/providers/spec/route.d.ts +4 -0
  77. package/dist/providers/spec/schema.gen.d.ts +507 -0
  78. package/dist/providers/spec/types.d.ts +211 -0
  79. package/dist/providers/transcript-v2.d.ts +176 -0
  80. package/dist/repo-mesh-types.d.ts +100 -1
  81. package/dist/sessions/registry.d.ts +3 -0
  82. package/dist/shared-types.d.ts +12 -0
  83. package/dist/status/reporter.d.ts +2 -0
  84. package/dist/status/snapshot.d.ts +1 -0
  85. package/dist/types.d.ts +5 -0
  86. package/package.json +7 -2
  87. package/src/boot/daemon-lifecycle.ts +17 -10
  88. package/src/boot/process-hardening.ts +89 -0
  89. package/src/chat/source-machine.ts +534 -0
  90. package/src/chat/source-resolver.ts +0 -0
  91. package/src/chat/subscription-updates.ts +14 -1
  92. package/src/cli-adapter-types.d.ts +1 -0
  93. package/src/cli-adapter-types.ts +18 -2
  94. package/src/cli-adapters/cli-script-runner.ts +421 -0
  95. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  96. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  97. package/src/cli-adapters/provider-cli-adapter.ts +664 -1137
  98. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  99. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  100. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  101. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  102. package/src/cli-adapters/provider-cli-shared.ts +57 -11
  103. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  104. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  105. package/src/commands/chat-commands.ts +1597 -60
  106. package/src/commands/cli-manager.ts +195 -3
  107. package/src/commands/handler.ts +547 -2
  108. package/src/commands/mesh-coordinator.ts +231 -127
  109. package/src/commands/router.ts +3547 -502
  110. package/src/config/chat-history.ts +87 -24
  111. package/src/config/config.ts +12 -0
  112. package/src/config/mesh-config.ts +264 -2
  113. package/src/config/recent-activity.ts +8 -2
  114. package/src/daemon/dev-cli-debug.ts +10 -1
  115. package/src/detection/ide-detector.ts +26 -16
  116. package/src/git/git-commands.ts +17 -5
  117. package/src/git/git-worktree.ts +8 -1
  118. package/src/index.ts +91 -5
  119. package/src/installer.d.ts +1 -1
  120. package/src/installer.ts +8 -6
  121. package/src/ipc/local-ipc-server.ts +278 -0
  122. package/src/launch.d.ts +1 -1
  123. package/src/launch.ts +37 -28
  124. package/src/logging/async-batch-writer.ts +55 -0
  125. package/src/logging/logger.ts +2 -1
  126. package/src/mesh/beads-db.ts +479 -0
  127. package/src/mesh/contracts.ts +329 -0
  128. package/src/mesh/coordinator-prompt.ts +204 -30
  129. package/src/mesh/coordinator-registry.ts +121 -0
  130. package/src/mesh/mesh-active-work.ts +437 -0
  131. package/src/mesh/mesh-events.ts +820 -61
  132. package/src/mesh/mesh-fast-forward.ts +430 -0
  133. package/src/mesh/mesh-host-ownership.ts +73 -0
  134. package/src/mesh/mesh-ledger.ts +457 -104
  135. package/src/mesh/mesh-refine-status.ts +144 -0
  136. package/src/mesh/mesh-work-queue.ts +216 -158
  137. package/src/mesh/preview-freshness.ts +118 -0
  138. package/src/mesh/refine-config.ts +366 -0
  139. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  140. package/src/providers/approval-utils.ts +39 -5
  141. package/src/providers/chat-message-normalization.ts +4 -11
  142. package/src/providers/cli-provider-instance.ts +388 -47
  143. package/src/providers/contracts.ts +105 -5
  144. package/src/providers/ide-provider-instance.ts +17 -3
  145. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  146. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  147. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  148. package/src/providers/native-history/dispatcher.ts +227 -0
  149. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  150. package/src/providers/native-history/index.ts +30 -0
  151. package/src/providers/provider-loader.ts +420 -31
  152. package/src/providers/provider-schema.ts +31 -13
  153. package/src/providers/read-chat-contract.ts +76 -16
  154. package/src/providers/sdk/README.md +49 -0
  155. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  156. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  157. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  158. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
  159. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  160. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  161. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  162. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  163. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  164. package/src/providers/sdk/v1/index.ts +152 -0
  165. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  166. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  167. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  168. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +457 -0
  169. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  170. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  171. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  172. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  173. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  174. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  175. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  176. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  177. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
  178. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  179. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  180. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  181. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  182. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  183. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  185. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  186. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  187. package/src/providers/sdk/v1/validators/index.ts +19 -0
  188. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  189. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  190. package/src/providers/spec/adapter.ts +153 -0
  191. package/src/providers/spec/cli-adapter.ts +318 -0
  192. package/src/providers/spec/driver.ts +498 -0
  193. package/src/providers/spec/evaluator.ts +268 -0
  194. package/src/providers/spec/loader.ts +130 -0
  195. package/src/providers/spec/native-history-executor.ts +612 -0
  196. package/src/providers/spec/route.ts +51 -0
  197. package/src/providers/spec/schema.gen.ts +507 -0
  198. package/src/providers/spec/schema.json +210 -0
  199. package/src/providers/spec/types.ts +230 -0
  200. package/src/providers/transcript-v2.ts +567 -0
  201. package/src/providers/version-archive.ts +38 -20
  202. package/src/repo-mesh-types.ts +110 -1
  203. package/src/sessions/registry.ts +3 -0
  204. package/src/shared-types.ts +9 -0
  205. package/src/status/builders.ts +23 -6
  206. package/src/status/reporter.ts +15 -0
  207. package/src/status/snapshot.ts +35 -11
  208. package/src/system/host-memory.ts +29 -12
  209. package/src/types.ts +5 -0
@@ -33,6 +33,26 @@ import { validateProviderDefinition } from './provider-schema.js';
33
33
  import type { ProviderSourceMode } from '../config/config.js';
34
34
  import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
35
35
 
36
+ /**
37
+ * Adds a provider-script root to the require whitelist. Wrapped in a
38
+ * try/catch + null check so a loader hot-path can't crash on a path
39
+ * that doesn't exist yet or one the whitelist hook rejects.
40
+ *
41
+ * The require-whitelist module is loaded lazily on first call. Eagerly
42
+ * top-level importing it pulls `node:fs.realpathSync.native` into
43
+ * module evaluation, which breaks unit tests that partially mock `fs`
44
+ * (e.g. test/commands/get-logs-incremental.test.ts mocks only
45
+ * existsSync + readFileSync). Lazy load keeps that mock surface valid.
46
+ */
47
+ function registerProviderScriptRootSafely(root: string | null | undefined): void {
48
+ if (!root || typeof root !== 'string') return;
49
+ try {
50
+ const { registerProviderScriptRoot } =
51
+ require('./sdk/v1/sandbox/require-whitelist.js') as typeof import('./sdk/v1/sandbox/require-whitelist.js');
52
+ registerProviderScriptRoot(root);
53
+ } catch { /* boot-time only — swallow */ }
54
+ }
55
+
36
56
  interface ProviderAvailabilityState {
37
57
  installed: boolean;
38
58
  detectedPath: string | null;
@@ -92,7 +112,9 @@ export class ProviderLoader {
92
112
  }
93
113
 
94
114
  private static readonly GITHUB_TARBALL_URL = 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
115
+ private static readonly REGISTRY_BASE_URL = 'https://api.adhf.dev/api/v1/registry';
95
116
  private static readonly META_FILE = '.meta.json';
117
+ private static readonly REGISTRY_META_FILE = '.registry-meta.json';
96
118
  private static readonly REPO_PROVIDER_DIRNAME = 'adhdev-providers';
97
119
  private static readonly SIBLING_MARKER_FILE = '.adhdev-provider-root';
98
120
  private static readonly SIBLING_ENV_VAR = 'ADHDEV_USE_SIBLING_PROVIDERS';
@@ -224,7 +246,12 @@ export class ProviderLoader {
224
246
  * Highest-priority editable overrides come first.
225
247
  */
226
248
  getProviderRoots(): string[] {
227
- return [this.userDir, this.upstreamDir];
249
+ // Order matters: user customs > marketplace installs > upstream auto-sync.
250
+ // findProviderDirInternal walks this list in order to locate the provider
251
+ // dir containing the scripts/, so marketplace must be included here even
252
+ // though loadAll() also reads it directly.
253
+ const marketplaceDir = path.join(os.homedir(), '.adhdev', 'marketplace');
254
+ return [this.userDir, marketplaceDir, this.upstreamDir];
228
255
  }
229
256
 
230
257
  getSourceConfig(): ProviderSourceConfigSnapshot {
@@ -336,7 +363,19 @@ export class ProviderLoader {
336
363
  this.log('Upstream loading disabled (sourceMode=no-upstream)');
337
364
  }
338
365
 
339
- // 2. Load user custom (excluding .upstream — highest priority, never auto-updated)
366
+ // 2. Load marketplace installs from ~/.adhdev/marketplace/ (overrides upstream,
367
+ // but is itself overridden by user customs in step 3). These are providers the
368
+ // user explicitly installed via the Marketplace UI. They are NOT touched by
369
+ // upstream sync.
370
+ const marketplaceDir = path.join(os.homedir(), '.adhdev', 'marketplace');
371
+ if (fs.existsSync(marketplaceDir)) {
372
+ const marketplaceCount = this.loadDir(marketplaceDir);
373
+ if (marketplaceCount > 0) {
374
+ this.log(`Loaded ${marketplaceCount} marketplace-installed providers`);
375
+ }
376
+ }
377
+
378
+ // 3. Load user custom (excluding .upstream — highest priority, never auto-updated)
340
379
  if (fs.existsSync(this.userDir)) {
341
380
  const userCount = this.loadDir(this.userDir, ['.upstream']);
342
381
  if (userCount > 0) {
@@ -988,7 +1027,12 @@ export class ProviderLoader {
988
1027
  }
989
1028
 
990
1029
  // 3. Composite override (OS + version)
991
- if (base.overrides) {
1030
+ // Legacy shape: base.overrides is an Array<{ when: {os,version}, scripts }>.
1031
+ // v1 manifests (Phase 3-4) repurposed `overrides` as an object map of
1032
+ // capability overrides (e.g. { detectStatus: { path, schema } }), which is
1033
+ // consumed by the SDK builders, not by this resolver. Only iterate when
1034
+ // the field is in the legacy array shape.
1035
+ if (Array.isArray(base.overrides)) {
992
1036
  for (const override of base.overrides) {
993
1037
  const osMatch = !override.when.os || override.when.os === currentOs;
994
1038
  const verMatch = !override.when.version || (currentVersion && this.matchesVersion(currentVersion, override.when.version));
@@ -996,6 +1040,40 @@ export class ProviderLoader {
996
1040
  resolved.scripts = { ...resolved.scripts, ...override.scripts };
997
1041
  }
998
1042
  }
1043
+ } else if (base.overrides && typeof base.overrides === 'object') {
1044
+ // v1 manifest shape: { detectStatus: { path }, parseSession: { path }, ... }
1045
+ // Each script name maps to a path inside the provider directory. We load
1046
+ // the file and merge its export(s) into resolved.scripts. Lets a
1047
+ // provider override a single primitive (e.g. just detectStatus) while
1048
+ // letting the SDK synthesize the rest from the tui block.
1049
+ const providerDir = this.findProviderDirInternal(base.type);
1050
+ if (providerDir) {
1051
+ for (const [scriptName, override] of Object.entries(base.overrides as Record<string, any>)) {
1052
+ if (!override || typeof override.path !== 'string') continue;
1053
+ const fullPath = path.join(providerDir, override.path);
1054
+ if (!fs.existsSync(fullPath)) {
1055
+ this.log(` [overrides] ${base.type}: ${scriptName} path not found: ${fullPath}`);
1056
+ continue;
1057
+ }
1058
+ try {
1059
+ // Override scripts go through the same whitelist gate as the
1060
+ // main scripts dir. Use the provider parent root so a v1
1061
+ // override can still require ../_shared helpers.
1062
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1063
+ delete require.cache[require.resolve(fullPath)];
1064
+ const fn = require(fullPath);
1065
+ const target = typeof fn === 'function' ? fn : (fn && fn[scriptName]);
1066
+ if (typeof target === 'function') {
1067
+ resolved.scripts = { ...resolved.scripts, [scriptName]: target } as any;
1068
+ this.log(` [overrides] ${base.type}: ${scriptName} loaded from ${override.path}`);
1069
+ } else {
1070
+ this.log(` [overrides] ${base.type}: ${scriptName} export missing in ${override.path}`);
1071
+ }
1072
+ } catch (e: any) {
1073
+ this.log(` [overrides] ${base.type}: ${scriptName} require failed: ${e?.message || e}`);
1074
+ }
1075
+ }
1076
+ }
999
1077
  }
1000
1078
 
1001
1079
  if ((resolved.category === 'cli' || resolved.category === 'acp') && resolved.spawn?.command) {
@@ -1006,6 +1084,125 @@ export class ProviderLoader {
1006
1084
  };
1007
1085
  }
1008
1086
 
1087
+ // (spec migration) Late-binding spec.json native-history hook. Runs
1088
+ // *after* every script-loading path (compatibility / defaultScriptDir /
1089
+ // overrides) so it deterministically wins over a legacy v1 scripts.js
1090
+ // export. Three modes, picked by spec.json's native_history block:
1091
+ // 1. source — declarative jsonl/sqlite executor (new-provider path,
1092
+ // no daemon change needed for new on-disk formats)
1093
+ // 2. override_path — provider-supplied reader file (escape hatch for
1094
+ // exotic formats); module default-exports a reader fn
1095
+ // 3. reader — built-in reader id (claude-cli / codex-cli /
1096
+ // antigravity-cli / hermes-cli), kept for backwards
1097
+ // compatibility with the four shipped providers
1098
+ if (providerDir) {
1099
+ try {
1100
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1101
+ const fs = require('node:fs');
1102
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1103
+ const path = require('node:path');
1104
+ // Pick the right spec file for the detected CLI version. Resolution
1105
+ // order:
1106
+ // 1. compatibility[i].spec where ideVersion matches currentVersion
1107
+ // (lets a provider ship specs/2.0.json, specs/2.1.json, etc.
1108
+ // alongside the matching scriptDir)
1109
+ // 2. specs/default.json — explicit fallback
1110
+ // 3. spec.json — legacy single-spec layout
1111
+ // Missing files fall through silently to the next candidate.
1112
+ const candidates: string[] = [];
1113
+ if (Array.isArray((base as any).compatibility)) {
1114
+ for (const entry of (base as any).compatibility) {
1115
+ if (typeof entry?.spec !== 'string') continue;
1116
+ // If currentVersion is unknown (cli-manager hasn't probed yet)
1117
+ // we still let compatibility entries that don't pin a version
1118
+ // through, plus any entry whose pin matches.
1119
+ const matches = !entry.ideVersion
1120
+ || (currentVersion && this.matchesVersion(currentVersion, entry.ideVersion))
1121
+ || !currentVersion;
1122
+ if (matches) candidates.push(path.join(providerDir, entry.spec));
1123
+ }
1124
+ }
1125
+ candidates.push(path.join(providerDir, 'specs', 'default.json'));
1126
+ candidates.push(path.join(providerDir, 'spec.json'));
1127
+ const specPath = candidates.find((p: string) => fs.existsSync(p));
1128
+ if (specPath) {
1129
+ // Hand the resolved spec path off to route.ts via a hidden field
1130
+ // so the routing layer doesn't have to repeat the candidate walk.
1131
+ (resolved as any)._resolvedSpecPath = specPath;
1132
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1133
+ const { loadSpec } = require('./spec/loader.js');
1134
+ const r = loadSpec(specPath);
1135
+ // Stub each control_bar entry as a provider.scripts.<id>. The
1136
+ // upstream invoke_provider_script gate checks that the script
1137
+ // name exists on provider.scripts before calling adapter.invokeScript;
1138
+ // for spec providers the *actual* dispatch happens inside
1139
+ // SpecCliAdapter.invokeScript which maps the name to control_bar.
1140
+ // The stub is just a presence marker so the gate doesn't reject.
1141
+ if (r.ok) {
1142
+ const controls = r.spec.control_bar ?? [];
1143
+ if (controls.length > 0) {
1144
+ resolved.scripts = { ...(resolved.scripts || {}) };
1145
+ for (const ctl of controls) {
1146
+ if (!(resolved.scripts as any)[ctl.id]) {
1147
+ (resolved.scripts as any)[ctl.id] = (..._args: unknown[]) => ({
1148
+ __spec_control: true,
1149
+ controlId: ctl.id,
1150
+ actionType: ctl.action.type,
1151
+ });
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+ const nh = r.ok ? r.spec.native_history : undefined;
1157
+ if (nh) {
1158
+ let reader: ((input: any) => any) | null = null;
1159
+ let format = 'spec';
1160
+
1161
+ if (nh.source) {
1162
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1163
+ const { executeNativeHistory } = require('./spec/native-history-executor.js');
1164
+ format = `spec-${nh.source.kind}`;
1165
+ reader = (input: any) => executeNativeHistory(nh, input);
1166
+ } else if (nh.override_path) {
1167
+ const overrideFile = path.resolve(providerDir, nh.override_path);
1168
+ if (fs.existsSync(overrideFile)) {
1169
+ try {
1170
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1171
+ delete require.cache[require.resolve(overrideFile)];
1172
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1173
+ const mod = require(overrideFile);
1174
+ const fn = typeof mod === 'function' ? mod : (mod && typeof mod.default === 'function' ? mod.default : null);
1175
+ if (fn) {
1176
+ format = 'spec-override';
1177
+ reader = (input: any) => fn(input);
1178
+ }
1179
+ } catch { /* fall through — leave native unavailable */ }
1180
+ }
1181
+ } else if (nh.reader) {
1182
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1183
+ const { createNativeHistoryDispatcher } = require('./native-history/dispatcher.js');
1184
+ const dispatch = createNativeHistoryDispatcher(nh.reader);
1185
+ format = nh.reader;
1186
+ reader = (input: any) => dispatch(input);
1187
+ }
1188
+
1189
+ if (reader) {
1190
+ resolved.scripts = { ...(resolved.scripts || {}) };
1191
+ (resolved.scripts as any).readNativeHistory = reader;
1192
+ (resolved as any).nativeHistory = {
1193
+ format,
1194
+ watchPath: undefined,
1195
+ scripts: { readSession: 'readNativeHistory' },
1196
+ mode: 'native-source',
1197
+ };
1198
+ }
1199
+ }
1200
+ }
1201
+ } catch {
1202
+ // Best-effort — spec wiring failure must not break legacy providers.
1203
+ }
1204
+ }
1205
+
1009
1206
  return resolved;
1010
1207
  }
1011
1208
 
@@ -1016,16 +1213,27 @@ export class ProviderLoader {
1016
1213
  private loadScriptsFromDir(type: string, scriptDir: string): Partial<ProviderScripts> | null {
1017
1214
  const providerDir = this.findProviderDirInternal(type);
1018
1215
  if (!providerDir) {
1019
- this.log(` [loadScriptsFromDir] ${type}: providerDir not found`);
1216
+ // No provider dir for this type a spec-only provider with no
1217
+ // legacy scripts/v1 layout is a normal configuration, not a
1218
+ // problem to surface at INFO. resolve() calls this on every
1219
+ // request; INFO spam every 200ms drowns out the real signal.
1220
+ this.debugLog(`[loadScriptsFromDir] ${type}: providerDir not found`);
1020
1221
  return null;
1021
1222
  }
1022
1223
 
1023
1224
  const dir = path.join(providerDir, scriptDir);
1024
1225
  if (!fs.existsSync(dir)) {
1025
- this.log(` [loadScriptsFromDir] ${type}: dir not found: ${dir}`);
1226
+ this.debugLog(`[loadScriptsFromDir] ${type}: dir not found: ${dir}`);
1026
1227
  return null;
1027
1228
  }
1028
1229
 
1230
+ // Register the provider's *parent root* (e.g. .../adhdev-providers/) so
1231
+ // the require whitelist gates every script + every _shared helper this
1232
+ // provider may reach. Picking the grandparent (one above the category
1233
+ // dir `cli/`) lets sibling helpers in `_shared` resolve while still
1234
+ // blocking `../../etc/...` escapes. Idempotent.
1235
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1236
+
1029
1237
  // Return cached scripts if available (cleared on reload/watch)
1030
1238
  const cached = this.scriptsCache.get(dir);
1031
1239
  if (cached) return cached;
@@ -1036,7 +1244,7 @@ export class ProviderLoader {
1036
1244
  try {
1037
1245
  delete require.cache[require.resolve(scriptsJs)];
1038
1246
  const loaded = require(scriptsJs);
1039
- this.log(` [loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
1247
+ this.debugLog(`[loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
1040
1248
  this.scriptsCache.set(dir, loaded);
1041
1249
  return loaded;
1042
1250
  } catch (e) {
@@ -1067,13 +1275,17 @@ export class ProviderLoader {
1067
1275
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
1068
1276
  });
1069
1277
 
1278
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
1070
1279
  const handleChange = (filePath: string) => {
1071
1280
  if (/[\/\\]fixtures[\/\\]/.test(filePath)) {
1072
1281
  return;
1073
1282
  }
1074
1283
  if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
1075
- this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1076
- this.reload();
1284
+ if (reloadTimer) clearTimeout(reloadTimer);
1285
+ reloadTimer = setTimeout(() => {
1286
+ this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1287
+ this.reload();
1288
+ }, 300);
1077
1289
  }
1078
1290
  };
1079
1291
 
@@ -1124,13 +1336,118 @@ export class ProviderLoader {
1124
1336
  *
1125
1337
  * @returns Whether an update occurred
1126
1338
  */
1339
+ /**
1340
+ * Sync providers from the ADHDev registry (registry.adhf.dev).
1341
+ *
1342
+ * Downloads only providers whose server checksum differs from the locally
1343
+ * cached checksum. Falls back gracefully to the GitHub tarball path if the
1344
+ * registry is unreachable or returns an unexpected response.
1345
+ *
1346
+ * Returns `{ updated: true }` when at least one provider file changed on disk,
1347
+ * `{ updated: false }` when everything is already current, or
1348
+ * `{ updated: false, error }` when the registry couldn't be reached and we
1349
+ * should proceed to the GitHub tarball fallback.
1350
+ */
1351
+ async fetchFromRegistry(): Promise<{ updated: boolean; error?: string }> {
1352
+ if (this.disableUpstream) {
1353
+ this.log('Registry sync skipped (sourceMode=no-upstream)');
1354
+ return { updated: false };
1355
+ }
1356
+ this.log(`Registry sync starting (${ProviderLoader.REGISTRY_BASE_URL})...`);
1357
+
1358
+ const https = require('https') as typeof import('https');
1359
+ const regMetaPath = path.join(this.upstreamDir, ProviderLoader.REGISTRY_META_FILE);
1360
+
1361
+ // Load cached checksums
1362
+ let cachedChecksums: Record<string, string> = {};
1363
+ try {
1364
+ if (fs.existsSync(regMetaPath)) {
1365
+ cachedChecksums = JSON.parse(fs.readFileSync(regMetaPath, 'utf-8')).checksums ?? {};
1366
+ }
1367
+ } catch { }
1368
+
1369
+ try {
1370
+ // 1. Fetch provider list
1371
+ const listUrl = `${ProviderLoader.REGISTRY_BASE_URL}/providers`;
1372
+ const listBody = await new Promise<string>((resolve, reject) => {
1373
+ const req = https.get(listUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 10000 }, (res) => {
1374
+ if (res.statusCode !== 200) { reject(new Error(`registry list HTTP ${res.statusCode}`)); return; }
1375
+ const chunks: Buffer[] = [];
1376
+ res.on('data', (c: Buffer) => chunks.push(c));
1377
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
1378
+ });
1379
+ req.on('error', reject);
1380
+ req.on('timeout', () => { req.destroy(); reject(new Error('registry list timeout')); });
1381
+ });
1382
+
1383
+ const list = JSON.parse(listBody) as { providers: Array<{ type: string; category: string; checksum: string; version: string }> };
1384
+ if (!Array.isArray(list.providers)) throw new Error('unexpected registry response shape');
1385
+
1386
+ let updatedCount = 0;
1387
+
1388
+ for (const entry of list.providers) {
1389
+ const { type, category, checksum, version } = entry;
1390
+ const cacheKey = `${category}/${type}`;
1391
+ if (cachedChecksums[cacheKey] === checksum) continue; // already current
1392
+
1393
+ // Download this provider's manifest
1394
+ const dlUrl = `${ProviderLoader.REGISTRY_BASE_URL}/providers/${type}/${version}/download`;
1395
+ const manifestBody = await new Promise<string>((resolve, reject) => {
1396
+ const req = https.get(dlUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 30000 }, (res) => {
1397
+ if (res.statusCode !== 200) { reject(new Error(`registry download HTTP ${res.statusCode} for ${type}@${version}`)); return; }
1398
+ const chunks: Buffer[] = [];
1399
+ res.on('data', (c: Buffer) => chunks.push(c));
1400
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
1401
+ });
1402
+ req.on('error', reject);
1403
+ req.on('timeout', () => { req.destroy(); reject(new Error(`download timeout for ${type}`)); });
1404
+ });
1405
+
1406
+ // Verify checksum
1407
+ const actualChecksum = await new Promise<string>((resolve) => {
1408
+ const crypto = require('crypto') as typeof import('crypto');
1409
+ resolve(crypto.createHash('sha256').update(manifestBody, 'utf-8').digest('hex'));
1410
+ });
1411
+ if (actualChecksum !== checksum) {
1412
+ this.log(`⚠ Registry checksum mismatch for ${type}@${version} — skipping`);
1413
+ continue;
1414
+ }
1415
+
1416
+ // Write to upstream dir
1417
+ const providerDir = path.join(this.upstreamDir, category, type);
1418
+ fs.mkdirSync(providerDir, { recursive: true });
1419
+ fs.writeFileSync(path.join(providerDir, 'provider.json'), manifestBody, 'utf-8');
1420
+
1421
+ cachedChecksums[cacheKey] = checksum;
1422
+ updatedCount++;
1423
+ this.log(`✓ Registry updated: ${category}/${type}@${version}`);
1424
+ }
1425
+
1426
+ // Persist updated checksums
1427
+ fs.mkdirSync(this.upstreamDir, { recursive: true });
1428
+ fs.writeFileSync(regMetaPath, JSON.stringify({
1429
+ checksums: cachedChecksums,
1430
+ syncedAt: new Date().toISOString(),
1431
+ providerCount: list.providers.length,
1432
+ }, null, 2));
1433
+
1434
+ this.log(`Registry sync complete: ${list.providers.length} providers, ${updatedCount} updated`);
1435
+ return { updated: updatedCount > 0 };
1436
+ } catch (e: any) {
1437
+ this.log(`⚠ Registry sync failed (falling back to GitHub tarball): ${e?.message}`);
1438
+ return { updated: false, error: e?.message };
1439
+ }
1440
+ }
1441
+
1127
1442
  async fetchLatest(): Promise<{ updated: boolean; error?: string }> {
1128
1443
  if (this.disableUpstream) {
1129
1444
  this.log('Upstream fetch skipped (sourceMode=no-upstream)');
1130
1445
  return { updated: false };
1131
1446
  }
1132
1447
  const https = require('https') as typeof import('https');
1133
- const { execSync } = require('child_process') as typeof import('child_process');
1448
+ const { exec } = require('child_process') as typeof import('child_process');
1449
+ const { promisify } = require('util');
1450
+ const execAsync = promisify(exec);
1134
1451
 
1135
1452
  const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
1136
1453
  let prevEtag = '';
@@ -1207,7 +1524,7 @@ export class ProviderLoader {
1207
1524
 
1208
1525
  // Extract
1209
1526
  fs.mkdirSync(tmpExtract, { recursive: true });
1210
- execSync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1527
+ await execAsync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1211
1528
 
1212
1529
  // Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
1213
1530
  const extracted = fs.readdirSync(tmpExtract);
@@ -1315,15 +1632,17 @@ export class ProviderLoader {
1315
1632
  } catch { }
1316
1633
  }
1317
1634
 
1318
- /** Count provider files (provider.js or provider.json) */
1635
+ /** Count provider files (provider.v1.json or provider.json — at most one per dir). */
1319
1636
  private countProviders(dir: string): number {
1320
1637
  if (!fs.existsSync(dir)) return 0;
1321
1638
  let count = 0;
1322
1639
  const scan = (d: string) => {
1323
1640
  try {
1324
- for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
1641
+ const entries = fs.readdirSync(d, { withFileTypes: true });
1642
+ const hasManifest = entries.some(e => e.name === 'provider.v1.json' || e.name === 'provider.json');
1643
+ if (hasManifest) count++;
1644
+ for (const entry of entries) {
1325
1645
  if (entry.isDirectory()) scan(path.join(d, entry.name));
1326
- else if (entry.name === 'provider.json') count++;
1327
1646
  }
1328
1647
  } catch { }
1329
1648
  };
@@ -1372,11 +1691,15 @@ export class ProviderLoader {
1372
1691
  return args ? args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(' ') : '';
1373
1692
  }
1374
1693
  const schemaDef = this.getSettingsSchema(providerType)[key];
1375
- const defaultVal = schemaDef
1376
- ? (key === 'autoApprove' && schemaDef.type === 'boolean'
1377
- ? true
1378
- : schemaDef.default)
1379
- : undefined;
1694
+ // (fix) Previously this hard-coded `autoApprove` boolean default to `true`,
1695
+ // overriding whatever schemaDef.default the provider.json declared. That
1696
+ // surfaced as soon as a provider added an `autoApprove` schema entry with
1697
+ // default=false: the user had never opted in but the daemon treated the
1698
+ // session as auto-approve, which then triggered recordAutoApproval every
1699
+ // time the CLI showed an approval modal — producing a flood of system
1700
+ // "Auto-approved: ..." messages and keeping the session pinned to
1701
+ // generating while modals cycled in and out. Trust the schemaDef.default.
1702
+ const defaultVal = schemaDef ? schemaDef.default : undefined;
1380
1703
 
1381
1704
  const config = this.readConfig();
1382
1705
  const userVal = config?.providerSettings?.[providerType]?.[key];
@@ -1490,10 +1813,17 @@ export class ProviderLoader {
1490
1813
  ...this.getSyntheticSettings(type, provider),
1491
1814
  ...(provider.settings || {}),
1492
1815
  };
1816
+ // (fix) Previously this clause forced `autoApprove.default = true` for any
1817
+ // boolean autoApprove schema, even when the provider.json explicitly set
1818
+ // `default: false`. Combined with the synthetic-settings fallback at
1819
+ // getSyntheticSettings (which also defaults autoApprove to true when the
1820
+ // provider doesn't supply one), that meant CLI providers silently turned on
1821
+ // auto-approval, producing a flood of "Auto-approved: ..." system messages
1822
+ // every time an approval modal appeared and pinning the session to
1823
+ // generating while modals cycled. Trust the provider's declared default.
1493
1824
  if (result.autoApprove?.type === 'boolean') {
1494
1825
  result.autoApprove = {
1495
1826
  ...result.autoApprove,
1496
- default: true,
1497
1827
  public: true,
1498
1828
  label: result.autoApprove.label || 'Auto Approve',
1499
1829
  description: result.autoApprove.description || 'Automatically approve actionable prompts without sending approval alerts.',
@@ -1518,7 +1848,10 @@ export class ProviderLoader {
1518
1848
  if (!provider.settings?.autoApprove) {
1519
1849
  result.autoApprove = {
1520
1850
  type: 'boolean',
1521
- default: true,
1851
+ // (fix) Safe default is *off*. Auto-approving every modal without the
1852
+ // user opting in produced silent-bash-execution surprises and the
1853
+ // "Auto-approved: ..." system-message flood seen on AGY/Codex.
1854
+ default: false,
1522
1855
  public: true,
1523
1856
  label: 'Auto Approve',
1524
1857
  description: 'Automatically approve actionable prompts without sending approval alerts.',
@@ -1581,23 +1914,32 @@ export class ProviderLoader {
1581
1914
  const cat = provider.category;
1582
1915
 
1583
1916
  const searchRoots = this.getProviderRoots();
1917
+ const hasManifest = (dir: string) =>
1918
+ fs.existsSync(path.join(dir, 'provider.v1.json')) || fs.existsSync(path.join(dir, 'provider.json'));
1919
+ const readManifestType = (dir: string): string | null => {
1920
+ for (const file of ['provider.v1.json', 'provider.json']) {
1921
+ const p = path.join(dir, file);
1922
+ if (!fs.existsSync(p)) continue;
1923
+ try {
1924
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
1925
+ if (typeof data?.type === 'string') return data.type;
1926
+ } catch { /* skip */ }
1927
+ }
1928
+ return null;
1929
+ };
1584
1930
  for (const root of searchRoots) {
1585
1931
  if (!fs.existsSync(root)) continue;
1586
1932
  const candidate = this.getProviderDir(root, cat, type);
1587
- if (fs.existsSync(path.join(candidate, 'provider.json'))) return candidate;
1933
+ if (hasManifest(candidate)) return candidate;
1588
1934
  // Scan category dir for type match
1589
1935
  const catDir = path.join(root, cat);
1590
1936
  if (fs.existsSync(catDir)) {
1591
1937
  try {
1592
1938
  for (const entry of fs.readdirSync(catDir, { withFileTypes: true })) {
1593
1939
  if (!entry.isDirectory()) continue;
1594
- const jsonPath = path.join(catDir, entry.name, 'provider.json');
1595
- if (fs.existsSync(jsonPath)) {
1596
- try {
1597
- const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
1598
- if (data.type === type) return path.join(catDir, entry.name);
1599
- } catch { /* skip */ }
1600
- }
1940
+ const entryDir = path.join(catDir, entry.name);
1941
+ const manifestType = readManifestType(entryDir);
1942
+ if (manifestType === type) return entryDir;
1601
1943
  }
1602
1944
  } catch { /* skip */ }
1603
1945
  }
@@ -1690,17 +2032,44 @@ export class ProviderLoader {
1690
2032
  return;
1691
2033
  }
1692
2034
 
1693
- // Check if this directory has provider.json
2035
+ // v1-first manifest selection. provider.v1.json (the SDK-shape
2036
+ // manifest with `overrides`, `tui`, `source`, `canonicalHistory`)
2037
+ // wins over provider.json (legacy). Without this branch the v1
2038
+ // file is silently ignored — that's how the codex-cli `overrides`
2039
+ // path and the tui-block builders went un-honored for the first
2040
+ // pass of SDK rollout.
2041
+ const hasV1 = entries.some(e => e.name === 'provider.v1.json');
1694
2042
  const hasJson = entries.some(e => e.name === 'provider.json');
1695
2043
 
1696
- if (hasJson) {
1697
- const jsonPath = path.join(d, 'provider.json');
2044
+ if (hasV1 || hasJson) {
2045
+ const manifestFile = hasV1 ? 'provider.v1.json' : 'provider.json';
2046
+ const jsonPath = path.join(d, manifestFile);
1698
2047
  try {
1699
2048
  const raw = fs.readFileSync(jsonPath, 'utf-8');
1700
2049
  const mod = JSON.parse(raw) as Omit<ProviderModule, 'extensionIdPattern'> & {
1701
2050
  extensionIdPattern?: RegExp | string;
1702
2051
  };
1703
2052
 
2053
+ // Validate v1 manifests against the SDK schema. Failures are
2054
+ // surfaced as a single warning line with all issues attached
2055
+ // so manifest authors don't need to guess which field is wrong.
2056
+ // Loading still proceeds — bricking the daemon on a single
2057
+ // bad field would be worse than running with a known warning.
2058
+ if (hasV1 && mod?.category === 'cli') {
2059
+ try {
2060
+ const { validateCliProviderManifest, formatManifestValidationIssues } =
2061
+ require('./sdk/v1/validators/manifest.js') as typeof import('./sdk/v1/validators/manifest.js');
2062
+ const validation = validateCliProviderManifest(mod);
2063
+ if (!validation.ok) {
2064
+ this.log(`⚠ ${jsonPath}: schema validation failed:\n${formatManifestValidationIssues(validation.issues)}`);
2065
+ }
2066
+ } catch (e: any) {
2067
+ // Validator load failed — log once and continue so a
2068
+ // broken validator can't take down provider loading.
2069
+ this.log(`⚠ ${jsonPath}: validator unavailable: ${e?.message || e}`);
2070
+ }
2071
+ }
2072
+
1704
2073
  // Restore RegExp fields from JSON (extensionIdPattern)
1705
2074
  if (typeof mod.extensionIdPattern === 'string') {
1706
2075
  const flags = mod.extensionIdPattern_flags || '';
@@ -1712,6 +2081,22 @@ export class ProviderLoader {
1712
2081
  ...(extensionIdPattern instanceof RegExp ? { extensionIdPattern } : {}),
1713
2082
  };
1714
2083
 
2084
+ // v1 manifests use `nativeHistory` as the canonical field name.
2085
+ // Legacy v0 manifests use `canonicalHistory`. The daemon's
2086
+ // runtime + downstream code reads `provider.nativeHistory`, so
2087
+ // for legacy manifests we copy `canonicalHistory` into
2088
+ // `nativeHistory` here. We also keep `canonicalHistory`
2089
+ // populated in both directions (deprecated alias) so any
2090
+ // external consumers still reading the old name keep working
2091
+ // during the one-release deprecation window.
2092
+ const nh = (normalizedProvider as any).nativeHistory;
2093
+ const ch = (normalizedProvider as any).canonicalHistory;
2094
+ if (nh && !ch) {
2095
+ (normalizedProvider as any).canonicalHistory = nh;
2096
+ } else if (ch && !nh) {
2097
+ (normalizedProvider as any).nativeHistory = ch;
2098
+ }
2099
+
1715
2100
  const validation = validateProviderDefinition(normalizedProvider);
1716
2101
  for (const warning of validation.warnings) {
1717
2102
  this.log(`⚠ ${jsonPath}: ${warning}`);
@@ -1725,6 +2110,10 @@ export class ProviderLoader {
1725
2110
  const scriptsPath = path.join(d, 'scripts.js');
1726
2111
  if (!hasCompatibility && fs.existsSync(scriptsPath)) {
1727
2112
  try {
2113
+ // Gate the IDE/extension scripts.js (legacy single-file
2114
+ // format) under the same whitelist. `d` here is the
2115
+ // provider dir; its grandparent contains _shared.
2116
+ registerProviderScriptRootSafely(path.dirname(path.dirname(d)));
1728
2117
  delete require.cache[require.resolve(scriptsPath)];
1729
2118
  const scripts = require(scriptsPath) as Partial<ProviderScripts>;
1730
2119
  normalizedProvider.scripts = scripts;