@aiwg/cli 2026.10.5 → 2026.10.7

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 (263) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +7 -3
  6. package/dist/src/a2a/agent-card.js +11 -0
  7. package/dist/src/api/index.d.ts +1 -0
  8. package/dist/src/api/index.js +1 -0
  9. package/dist/src/artifacts/browser-export.js +1 -0
  10. package/dist/src/artifacts/citation-parser.js +8 -1
  11. package/dist/src/artifacts/cli.js +57 -6
  12. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  13. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  14. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  15. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  16. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  17. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  18. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  19. package/dist/src/artifacts/graph-query.js +18 -8
  20. package/dist/src/artifacts/index-adapters.js +339 -0
  21. package/dist/src/artifacts/index-builder.js +143 -14
  22. package/dist/src/artifacts/index-files.js +15 -4
  23. package/dist/src/artifacts/index-preparation.js +2 -0
  24. package/dist/src/artifacts/index-reader.js +44 -0
  25. package/dist/src/artifacts/index-refresh.js +114 -55
  26. package/dist/src/artifacts/index-status.js +20 -5
  27. package/dist/src/artifacts/memory-maintenance.js +503 -0
  28. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  29. package/dist/src/artifacts/query-engine.js +263 -108
  30. package/dist/src/artifacts/relations-builder.js +2 -2
  31. package/dist/src/artifacts/source-graph.js +2 -2
  32. package/dist/src/artifacts/types.js +9 -1
  33. package/dist/src/auth/client.js +5 -1
  34. package/dist/src/auth/credential-store.js +5 -1
  35. package/dist/src/auth/sensitive-input.js +151 -0
  36. package/dist/src/channel/manager.mjs +3 -0
  37. package/dist/src/cli/handlers/agentcard.js +47 -25
  38. package/dist/src/cli/handlers/auth.js +22 -2
  39. package/dist/src/cli/handlers/decision.js +22 -3
  40. package/dist/src/cli/handlers/effect.js +4 -84
  41. package/dist/src/cli/handlers/guided-setup.js +470 -0
  42. package/dist/src/cli/handlers/help.js +2 -2
  43. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  44. package/dist/src/cli/handlers/index.js +4 -2
  45. package/dist/src/cli/handlers/init.js +41 -6
  46. package/dist/src/cli/handlers/install.js +6 -0
  47. package/dist/src/cli/handlers/local-executor.js +5 -3
  48. package/dist/src/cli/handlers/marketplace.js +15 -4
  49. package/dist/src/cli/handlers/mc.js +509 -53
  50. package/dist/src/cli/handlers/models.js +32 -2
  51. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  52. package/dist/src/cli/handlers/rules.js +1 -1
  53. package/dist/src/cli/handlers/script-runner.js +73 -2
  54. package/dist/src/cli/handlers/serve.js +5 -3
  55. package/dist/src/cli/handlers/sessions.js +1 -0
  56. package/dist/src/cli/handlers/setup.js +432 -1
  57. package/dist/src/cli/handlers/skill-lint.js +1 -1
  58. package/dist/src/cli/handlers/subcommands.js +118 -8
  59. package/dist/src/cli/handlers/use.js +335 -107
  60. package/dist/src/cli/handlers/workspace.js +3 -1
  61. package/dist/src/cli/prompt-utils.js +81 -0
  62. package/dist/src/cli/router.js +16 -1
  63. package/dist/src/config/aiwg-config.js +42 -0
  64. package/dist/src/config/cli.js +7 -3
  65. package/dist/src/config/decision-config.js +14 -1
  66. package/dist/src/config/gitignore.js +4 -0
  67. package/dist/src/config/native-sandbox-warnings.js +25 -0
  68. package/dist/src/config/workspace.js +2 -1
  69. package/dist/src/contributors/validation.js +6 -5
  70. package/dist/src/decision/adapters/jev.js +20 -31
  71. package/dist/src/decision/admission.js +7 -3
  72. package/dist/src/decision/ask.js +6 -1
  73. package/dist/src/decision/benchmark.js +482 -0
  74. package/dist/src/decision/ledger.js +22 -3
  75. package/dist/src/decision/pricing.js +147 -0
  76. package/dist/src/decision/qualification/gate-evidence.js +23 -18
  77. package/dist/src/decision/qualification/load-staged.js +576 -0
  78. package/dist/src/decision/qualification/load.js +109 -33
  79. package/dist/src/decision/qualification/runner.js +12 -5
  80. package/dist/src/decision/review/service.js +6 -4
  81. package/dist/src/decision/review/validate.js +23 -2
  82. package/dist/src/decision/savings.js +8 -3
  83. package/dist/src/discovery/index.js +492 -0
  84. package/dist/src/discovery/interview.js +238 -0
  85. package/dist/src/discovery/plan-apply.js +567 -0
  86. package/dist/src/discovery/plan-store.js +171 -0
  87. package/dist/src/discovery/plan-types.js +2 -0
  88. package/dist/src/discovery/plan-validation.js +139 -0
  89. package/dist/src/discovery/types.js +2 -0
  90. package/dist/src/effects/host.js +91 -0
  91. package/dist/src/extensions/commands/definitions.js +49 -9
  92. package/dist/src/extensions/manifest.js +71 -44
  93. package/dist/src/extensions/project-local-discovery.js +1 -1
  94. package/dist/src/extensions/project-local-gitignore.js +2 -0
  95. package/dist/src/extensions/project-quickref.js +18 -17
  96. package/dist/src/extensions/validation.js +32 -29
  97. package/dist/src/housekeeper/daemon-config.js +148 -0
  98. package/dist/src/housekeeper/ipc.js +84 -0
  99. package/dist/src/housekeeper/lifecycle.js +148 -0
  100. package/dist/src/housekeeper/platform.js +35 -0
  101. package/dist/src/housekeeper/status.js +70 -0
  102. package/dist/src/housekeeper/units.js +114 -0
  103. package/dist/src/issues/cli.js +16 -5
  104. package/dist/src/issues/live.js +44 -12
  105. package/dist/src/jobs/flow.js +22 -21
  106. package/dist/src/lint/rule-activation.js +20 -5
  107. package/dist/src/lint/runner.js +1 -1
  108. package/dist/src/marketplace/exchange.js +18 -6
  109. package/dist/src/marketplace/provenance.js +3 -2
  110. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  111. package/dist/src/mcp/cli.mjs +22 -96
  112. package/dist/src/mcp/config-file.mjs +6 -2
  113. package/dist/src/mcp/config-layers.mjs +23 -7
  114. package/dist/src/mcp/grok-build-config.mjs +2 -1
  115. package/dist/src/mcp/muse-native-config.mjs +2 -1
  116. package/dist/src/mcp/profiles.js +18 -31
  117. package/dist/src/mcp/profiles.mjs +18 -32
  118. package/dist/src/mcp/registry.js +7 -27
  119. package/dist/src/mcp/registry.mjs +7 -28
  120. package/dist/src/mcp/server.mjs +34 -1
  121. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  122. package/dist/src/mcp/tools/command-run.mjs +3 -2
  123. package/dist/src/mcp/tools/decision.mjs +49 -48
  124. package/dist/src/mcp/tools/discovery.mjs +46 -21
  125. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  126. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  127. package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
  128. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  129. package/dist/src/memory/audit.js +719 -0
  130. package/dist/src/memory/canonical-context.js +392 -13
  131. package/dist/src/memory/canonical-maintenance.js +95 -0
  132. package/dist/src/memory/cli-result.js +22 -0
  133. package/dist/src/memory/cli.js +176 -0
  134. package/dist/src/memory/context-pack.js +57 -6
  135. package/dist/src/memory/cost.js +512 -0
  136. package/dist/src/memory/firewall-classification.d.mts +7 -0
  137. package/dist/src/memory/firewall-classification.mjs +32 -0
  138. package/dist/src/memory/index.js +8 -0
  139. package/dist/src/memory/line-maintenance.js +68 -0
  140. package/dist/src/memory/maintenance-cli.js +286 -0
  141. package/dist/src/memory/maintenance-contract.js +136 -0
  142. package/dist/src/memory/maintenance-index.js +18 -0
  143. package/dist/src/memory/maintenance-owner.js +2 -0
  144. package/dist/src/memory/maintenance-record.js +138 -0
  145. package/dist/src/memory/maintenance.js +254 -0
  146. package/dist/src/memory/project-registry.js +2 -1
  147. package/dist/src/memory/provider-memory-adapter.js +154 -0
  148. package/dist/src/memory/recommend-cli.js +83 -0
  149. package/dist/src/memory/recommend.js +269 -0
  150. package/dist/src/memory/scheduled.js +81 -0
  151. package/dist/src/memory/taxonomy.js +44 -0
  152. package/dist/src/mission-protocol/codecs.js +19 -9
  153. package/dist/src/mission-protocol/worker-status.js +91 -1
  154. package/dist/src/models/config-loader.js +11 -1
  155. package/dist/src/models/model-capabilities.v1.json +74 -50
  156. package/dist/src/models/provider-policy.js +125 -62
  157. package/dist/src/models/resolver.js +45 -8
  158. package/dist/src/models/worker-installation.d.mts +8 -1
  159. package/dist/src/models/worker-installation.mjs +231 -38
  160. package/dist/src/models/wrapper-route.js +1 -1
  161. package/dist/src/output-modes/registry.js +8 -7
  162. package/dist/src/packages/adapters/git.js +99 -23
  163. package/dist/src/packages/adapters/gitea.js +8 -27
  164. package/dist/src/packages/adapters/github.js +7 -25
  165. package/dist/src/packages/registry.js +4 -4
  166. package/dist/src/plugin/metadata-validator.js +1 -1
  167. package/dist/src/process/cli-command.d.mts +5 -0
  168. package/dist/src/process/cli-command.mjs +42 -0
  169. package/dist/src/providers/deploy-version.d.mts +1 -0
  170. package/dist/src/providers/deploy-version.mjs +17 -0
  171. package/dist/src/providers/provider-definitions.js +19 -16
  172. package/dist/src/providers/skill-ownership.d.mts +11 -2
  173. package/dist/src/providers/skill-ownership.mjs +35 -17
  174. package/dist/src/rules/activation.d.mts +11 -2
  175. package/dist/src/rules/activation.mjs +82 -6
  176. package/dist/src/rules/inventory.js +1 -1
  177. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  178. package/dist/src/schema/zod-validation.d.mts +6 -0
  179. package/dist/src/schema/zod-validation.mjs +50 -0
  180. package/dist/src/serve/executor-registry.js +297 -20
  181. package/dist/src/serve/executor-token.d.mts +1 -0
  182. package/dist/src/serve/executor-token.mjs +8 -0
  183. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  184. package/dist/src/serve/worker-invocation.js +124 -15
  185. package/dist/src/sessions/adapters/claude.js +19 -18
  186. package/dist/src/sessions/adapters/codex.js +10 -9
  187. package/dist/src/sessions/adapters/copilot.js +14 -13
  188. package/dist/src/sessions/adapters/cursor.js +19 -18
  189. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  190. package/dist/src/sessions/adapters/factory.js +11 -10
  191. package/dist/src/sessions/adapters/generic.js +32 -23
  192. package/dist/src/sessions/adapters/grokbot.js +35 -35
  193. package/dist/src/sessions/adapters/hermes.js +20 -19
  194. package/dist/src/sessions/adapters/muse.js +50 -49
  195. package/dist/src/sessions/adapters/omp.js +5 -4
  196. package/dist/src/sessions/adapters/openclaw.js +30 -29
  197. package/dist/src/sessions/adapters/opencode.js +26 -26
  198. package/dist/src/sessions/adapters/openhuman.js +24 -23
  199. package/dist/src/sessions/adapters/pi.js +5 -4
  200. package/dist/src/sessions/adapters/windsurf.js +8 -8
  201. package/dist/src/sessions/analytics.js +10 -9
  202. package/dist/src/sessions/candidates.js +7 -6
  203. package/dist/src/sessions/contracts.js +63 -53
  204. package/dist/src/sessions/output-registration.js +11 -10
  205. package/dist/src/sessions/workspace-discovery.js +3 -4
  206. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  207. package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
  208. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  209. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  210. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  211. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  212. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  213. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  214. package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
  215. package/dist/src/tracker/project-inputs.js +2 -0
  216. package/dist/src/update/checker.d.mts +4 -0
  217. package/dist/src/update/checker.mjs +14 -0
  218. package/dist/src/update/notifier.mjs +2 -4
  219. package/dist/src/writing/exemplar-selection.js +6 -6
  220. package/dist/src/writing/voice-evaluation.js +24 -23
  221. package/dist/src/writing/writer-migration.js +14 -13
  222. package/dist/src/writing/writer-profile.js +19 -18
  223. package/dist/src/writing/writing-brief.js +18 -17
  224. package/dist/src/writing/writing-receipt.js +47 -46
  225. package/package.json +5 -2
  226. package/schemas/decision/DecisionReview.schema.json +78 -1
  227. package/schemas/mission-v1.schema.json +3 -0
  228. package/tools/agents/deploy-agents.mjs +67 -39
  229. package/tools/agents/providers/antigravity.mjs +6 -1
  230. package/tools/agents/providers/base.mjs +24 -14
  231. package/tools/agents/providers/claude.mjs +6 -3
  232. package/tools/agents/providers/codex.mjs +54 -57
  233. package/tools/agents/providers/context-write.mjs +28 -0
  234. package/tools/agents/providers/copilot.mjs +13 -4
  235. package/tools/agents/providers/cursor.mjs +1 -1
  236. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  237. package/tools/agents/providers/factory.mjs +7 -1
  238. package/tools/agents/providers/grok-build.mjs +18 -2
  239. package/tools/agents/providers/grokbot.mjs +1 -1
  240. package/tools/agents/providers/hermes.mjs +12 -15
  241. package/tools/agents/providers/muse-hooks.mjs +21 -14
  242. package/tools/agents/providers/muse.mjs +7 -2
  243. package/tools/agents/providers/omp.mjs +31 -5
  244. package/tools/agents/providers/opencode.mjs +14 -5
  245. package/tools/agents/providers/openhuman.mjs +1 -1
  246. package/tools/agents/providers/pi.mjs +1 -1
  247. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  248. package/tools/agents/providers/rule-delivery.mjs +289 -80
  249. package/tools/agents/providers/warp.mjs +4 -1
  250. package/tools/agents/providers/windsurf.mjs +13 -3
  251. package/tools/cli/workspace-status.mjs +5 -0
  252. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  253. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  254. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  255. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  256. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  257. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  258. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  259. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  260. package/tools/ralph-external/process-monitor.mjs +49 -8
  261. package/tools/ralph-external/session-launcher.mjs +405 -35
  262. package/tools/skills/deploy-skills-codex.mjs +11 -7
  263. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -1,7 +1,7 @@
1
1
  /** Shared installed-worker qualification owner, also used by the Ralph compatibility facade. */
2
2
  import { spawn } from 'node:child_process';
3
3
  import { constants } from 'node:fs';
4
- import { open, realpath, lstat, mkdir, rename, writeFile } from 'node:fs/promises';
4
+ import { open, realpath, lstat, mkdir, rename, writeFile, unlink } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { homedir } from 'node:os';
7
7
  import { pathToFileURL } from 'node:url';
@@ -163,7 +163,7 @@ export async function qualifyWorkerInstallation(instance, options) {
163
163
  }
164
164
 
165
165
  export async function prepareInstalledWorkerInvocation(instance, invocation, qualification) {
166
- const { prepareWorkerInvocation, WorkerPathScopeError } = await import('../serve/worker-invocation.js');
166
+ const { prepareWorkerInvocation, WorkerPathScopeError, WorkerInvocationCheckError } = await import('../serve/worker-invocation.js');
167
167
  const { requiredWorkerControls, WorkerPolicyError, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
168
168
  if (!isOperatorQualifiedWorkerAdapter(qualification.adapter)) qualification = { ...qualification, requiredControls: requiredWorkerControls({
169
169
  ...qualification, effort: invocation.selected.effort.value }) };
@@ -187,7 +187,7 @@ export async function prepareInstalledWorkerInvocation(instance, invocation, qua
187
187
  throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker installation qualification changed');
188
188
  }
189
189
  if (await realpath(result.cwd) !== scope.cwd) {
190
- throw new WorkerPolicyError('OG_CONFIG_REFERENCE', 'Worker cwd differs from the qualified workspace');
190
+ throw new WorkerInvocationCheckError('OG_CONFIG_REFERENCE', 'qualified_workspace_mismatch');
191
191
  }
192
192
  for (const key of ['briefPath', 'stdoutPath', 'stderrPath', 'finalPath']) {
193
193
  const target = await canonicalFuturePath(result[key]);
@@ -443,6 +443,19 @@ export function boundedVersionProbe(executable, argv, environment, cwd, timeoutM
443
443
  || !Array.isArray(argv) || argv.some(arg => typeof arg !== 'string' || arg.includes('\0'))) {
444
444
  throw new Error('Invalid probe bounds');
445
445
  }
446
+ // ETXTBSY: the executable still has a writer somewhere, typically a sibling
447
+ // process forked while the file was being written or a CLI mid-update. It
448
+ // clears in milliseconds, so retry the spawn within the same overall bound.
449
+ const deadline = Date.now() + timeoutMs;
450
+ const attempt = delayMs => runBoundedVersionProbe(executable, argv, environment, cwd, timeoutMs, maxBytes, versionPattern)
451
+ .catch(error => {
452
+ if (error?.code !== 'ETXTBSY' || Date.now() + delayMs >= deadline) throw error;
453
+ return new Promise(resolveDelay => setTimeout(resolveDelay, delayMs)).then(() => attempt(Math.min(delayMs * 2, 200)));
454
+ });
455
+ return attempt(10);
456
+ }
457
+
458
+ function runBoundedVersionProbe(executable, argv, environment, cwd, timeoutMs, maxBytes, versionPattern) {
446
459
  return new Promise((resolve, reject) => {
447
460
  const child = spawn(executable, argv, { cwd, env: environment, stdio: 'pipe', detached: true, shell: false });
448
461
  let output = Buffer.alloc(0);
@@ -663,7 +676,9 @@ async function loadQualifiedModules({ registry, modules, workerWritableRoots, pl
663
676
  for (const id of entry.adapterIds) {
664
677
  const builtinConflict = canonicalIds.has(id) ? false : providerRegistry.has(id);
665
678
  if (ids.has(id) || ((builtinConflict || installedModules.has(inspectedModule.key))
666
- && installedModules.get(id) !== inspectedModule.key)) throw new Error(`Duplicate worker adapter ID: ${id}`);
679
+ && installedModules.get(id) !== inspectedModule.key)) {
680
+ throw Object.assign(new Error(`Duplicate worker adapter ID: ${id}`), { code: 'WORKER_ADAPTER_CONFLICT' });
681
+ }
667
682
  ids.add(id);
668
683
  }
669
684
  inspected.push(inspectedModule);
@@ -809,6 +824,40 @@ export async function prepareWorkerInstallationStore({ workerWritableRoots, appl
809
824
  return assertTrustedStore(installationStorePath(), workerWritableRoots, true, apply);
810
825
  }
811
826
 
827
+ // Use the existing delivery ledger's host-selected keyring, never public keys
828
+ // supplied by a registration. PAE separates this authority from effect records.
829
+ const qualificationPayloadType = 'application/vnd.aiwg.worker-installation-qualification+json';
830
+ const requalifyMessage = 'Re-run aiwg doctor --probe-workers --qualify. Existing unsigned approvals require deliberate one-time requalification.';
831
+ const keyRecoveryMessage = 'Worker qualification signing trust is unavailable. Check effects.keyProvider in aiwg.config, unlock or restore the existing effect-ledger key, and run aiwg effect keys init if its keyring is not initialized; then re-run aiwg doctor --probe-workers --qualify.';
832
+
833
+ async function qualificationTrust(cwd, scope) {
834
+ const { effectSession, openLedger } = await import('../effects/host.js');
835
+ const { openEffectLedger, readLedgerKeyring } = await import('../effects/ledger.js');
836
+ const ledger = scope ? openEffectLedger({ projectDir: cwd, scope, writer: 'cli' }) : openLedger(await effectSession(cwd));
837
+ const keyring = await readLedgerKeyring(ledger);
838
+ if (!keyring) throw new Error(keyRecoveryMessage);
839
+ return { ledger, keyring };
840
+ }
841
+
842
+ async function qualificationPayload(record, signedAt, projectDir, scope) {
843
+ const { canonicalJson } = await import('../security/artifact-trust.js');
844
+ const { signature: _signature, ...body } = record;
845
+ return Buffer.from(canonicalJson({ record: JSON.parse(JSON.stringify(body)), signedAt, projectDir, scope }), 'utf8');
846
+ }
847
+
848
+ async function qualificationSignatureFailure(record, trust) {
849
+ if (!record.signature) return 'signature-missing';
850
+ const { keyid, sig, signedAt, projectDir, scope } = record.signature;
851
+ if (typeof keyid !== 'string' || typeof sig !== 'string' || typeof signedAt !== 'string'
852
+ || typeof projectDir !== 'string' || !path.isAbsolute(projectDir) || !scope) return 'signature-invalid';
853
+ let keyring;
854
+ try { ({ keyring } = await trust(projectDir, scope)); } catch { return 'signature-trust-unavailable'; }
855
+ const { checkSignature } = await import('../effects/keyring.js');
856
+ const result = checkSignature(keyring, keyid, sig, qualificationPayloadType,
857
+ await qualificationPayload(record, signedAt, projectDir, scope), signedAt);
858
+ return result === 'ok' ? null : result === 'signature-invalid' ? result : `signature-${result}`;
859
+ }
860
+
812
861
  async function readInstallationStore(filename, expectedDigest) {
813
862
  try {
814
863
  const bytes = await readInstalledFile(filename);
@@ -825,6 +874,38 @@ async function readInstallationStore(filename, expectedDigest) {
825
874
  }
826
875
  }
827
876
 
877
+ /** Offline projection of recorded approvals only. Never imports drivers, reads auth, or probes CLIs.
878
+ * This is historical evidence, not current launch authority; loadRecordedWorkerInstallations
879
+ * performs binary/version/scope checks when dispatch actually requests a binding.
880
+ */
881
+ export async function inspectRecordedWorkerQualifications({ cwd, policy, parallelism }) {
882
+ const filename = installationStorePath();
883
+ await assertTrustedStore(filename, [cwd]);
884
+ const { stored } = await readInstallationStore(filename);
885
+ const workspace = await realpath(cwd);
886
+ const results = [];
887
+ const { qualificationDigest } = await import('./qualification-digest.mjs');
888
+ for (const record of stored.registrations) {
889
+ try {
890
+ const { cli, installation, adapter, binding } = record;
891
+ const configured = parallelism?.clis?.[cli];
892
+ if (!configured || configured.enabled === false || !/^[a-z][a-z0-9-]{0,62}$/.test(cli)
893
+ || await realpath(installation.cwd) !== workspace
894
+ || qualificationDigest(policy) !== installation.effectiveConfigDigest
895
+ || configured.adapter !== `providers:${adapter.provider}` && configured.adapter !== adapter.id
896
+ || configured.accountScope !== binding.accountScope
897
+ || binding.profile !== installation.profile || binding.cliVersion !== installation.cliVersion
898
+ || (configured.executable && await realpath(configured.executable) !== await realpath(installation.executable))) continue;
899
+ const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
900
+ if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) continue;
901
+ if (await qualificationSignatureFailure(record, () => qualificationTrust(cwd))) continue;
902
+ await assertWorkerInstallationEvidence({ ...installation, adapter, now: Date.now(), policy }, proof.scopeDigest);
903
+ results.push({ cli, provider: adapter.provider });
904
+ } catch { /* Malformed, expired or unbound evidence is not qualification. */ }
905
+ }
906
+ return results.filter(row => results.filter(other => row.cli === other.cli).length === 1);
907
+ }
908
+
828
909
  /** Explicit host operation: successful bounded qualification is required before
829
910
  * recording; credentials are reduced to names and are never written to disk. */
830
911
  export async function recordWorkerInstallation(instance, options) {
@@ -833,6 +914,15 @@ export async function recordWorkerInstallation(instance, options) {
833
914
  const installation = structuredClone(options.installation);
834
915
  const result = await qualifyWorkerInstallation(instance, { ...installation, now: Date.now() });
835
916
  if (result.status !== 'ENABLED') return result;
917
+ let signingKey, keyring, signingProjectDir;
918
+ try {
919
+ signingProjectDir = await realpath(options.projectDir ?? installation.cwd);
920
+ const trust = await qualificationTrust(signingProjectDir);
921
+ keyring = trust.keyring;
922
+ signingKey = await trust.ledger.signingKey();
923
+ const { activeKey } = await import('../effects/keyring.js');
924
+ if (activeKey(keyring).keyid !== signingKey.keyid) throw new Error('inactive key');
925
+ } catch { return { status: 'DISABLED', reason: keyRecoveryMessage }; }
836
926
  const adapter = structuredClone(installation.adapter);
837
927
  adapter.invocationProfiles[installation.profile].installationQualification = {
838
928
  scopeDigest: result.scopeDigest, fixtureDigest: result.fixtureDigest,
@@ -842,79 +932,169 @@ export async function recordWorkerInstallation(instance, options) {
842
932
  || binding.accountScope !== installation.accountScope) throw new Error('Installation binding differs from qualification');
843
933
  const filename = installationStorePath();
844
934
  await assertTrustedStore(filename, installation.workerWritableRoots);
845
- const { stored } = await readInstallationStore(filename);
846
- const environmentNames = Object.keys(installation.environment ?? {});
847
- delete installation.environment;
848
- delete installation.adapter;
849
- delete installation.now;
850
- const record = { cli, ...(options.modelObservations ? { modelObservations: structuredClone(options.modelObservations) } : {}), binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
851
- installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
852
- ...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
853
- ...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
854
- stored.registrations = stored.registrations.filter(entry => entry.cli !== cli);
855
- stored.registrations.push(record);
935
+ // Canonical workspace identity is an exact approval key, never a prefix.
936
+ installation.cwd = await realpath(installation.cwd);
856
937
  await mkdir(path.dirname(filename), { recursive: true, mode: 0o700 });
857
- await assertTrustedStore(filename, installation.workerWritableRoots);
858
- const temporary = `${filename}.${process.pid}.${createHash('sha256').update(String(Math.random())).digest('hex')}.tmp`;
859
- await writeFile(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
860
- await rename(temporary, filename);
861
- return result;
938
+ // Serialize read/modify/rename across qualifying CLI processes. A stale lock
939
+ // fails closed; qualification never steals another process's lock.
940
+ const lockPath = `${filename}.lock`;
941
+ let lock;
942
+ for (let attempt = 0; attempt < 100; attempt++) {
943
+ try { lock = await open(lockPath, 'wx', 0o600); break; }
944
+ catch (error) {
945
+ if (error.code !== 'EEXIST' || attempt === 99) throw error;
946
+ await new Promise(resolve => setTimeout(resolve, 20));
947
+ }
948
+ }
949
+ try {
950
+ await assertTrustedStore(filename, installation.workerWritableRoots);
951
+ const { stored } = await readInstallationStore(filename);
952
+ const environmentNames = Object.keys(installation.environment ?? {});
953
+ delete installation.environment;
954
+ delete installation.adapter;
955
+ delete installation.now;
956
+ const record = { cli, ...(options.modelObservations ? { modelObservations: structuredClone(options.modelObservations) } : {}), binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
957
+ installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
958
+ ...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
959
+ ...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
960
+ const signedAt = new Date().toISOString();
961
+ try {
962
+ const payload = await qualificationPayload(record, signedAt, signingProjectDir, keyring.scope);
963
+ const sig = signingKey.signPae(qualificationPayloadType, payload);
964
+ const { checkSignature } = await import('../effects/keyring.js');
965
+ if (checkSignature(keyring, signingKey.keyid, sig, qualificationPayloadType, payload, signedAt) !== 'ok') throw new Error('invalid signature');
966
+ record.signature = { keyid: signingKey.keyid, sig, signedAt, projectDir: signingProjectDir, scope: keyring.scope };
967
+ } catch { return { status: 'DISABLED', reason: keyRecoveryMessage }; }
968
+ const retained = [];
969
+ for (const entry of stored.registrations) {
970
+ const workspace = typeof entry.installation?.cwd === 'string'
971
+ ? await realpath(entry.installation.cwd).catch(() => entry.installation.cwd) : null;
972
+ if (entry.cli !== cli || workspace !== installation.cwd) retained.push(entry);
973
+ }
974
+ // Bound host authority without silently evicting another workspace approval.
975
+ if (retained.length >= 128) throw new Error('Worker installation store is full (128 workspace approvals); remove an obsolete approval before qualifying another workspace');
976
+ stored.registrations = retained;
977
+ stored.registrations.push(record);
978
+ await mkdir(path.dirname(filename), { recursive: true, mode: 0o700 });
979
+ await assertTrustedStore(filename, installation.workerWritableRoots);
980
+ const temporary = `${filename}.${process.pid}.${createHash('sha256').update(String(Math.random())).digest('hex')}.tmp`;
981
+ await writeFile(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
982
+ await rename(temporary, filename);
983
+ return result;
984
+ } finally {
985
+ await lock.close();
986
+ await unlink(lockPath);
987
+ }
862
988
  }
863
989
 
864
990
  /** Read recorded host authority, validate installation bytes and the current CLI
865
991
  * version, then load the same approved factory used by Ralph. */
866
- export async function loadRecordedWorkerInstallations({ registry, cwd, policy, parallelism, storePath, storeDigest }) {
992
+ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, parallelism, storePath, storeDigest, precheckPolicy }) {
867
993
  const { WorkerPolicyError, parseProviderModelCapabilities } = await import('./provider-policy.js');
868
994
  const { qualificationDigest } = await import('./qualification-digest.mjs');
869
- const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED', 'Trusted installation qualification is required');
870
- const filename = storePath ?? installationStorePath();
995
+ // Only fixed stage codes and bounded record indexes may leave this boundary.
996
+ // Underlying errors can contain probe output, environment values or paths.
997
+ const rejections = [];
998
+ const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED',
999
+ `Trusted installation qualification is required (${[...new Set(rejections.map(row => row.reason))].join(', ')})${rejections.some(row => row.reason.startsWith('signature-'))
1000
+ ? `. ${rejections.some(row => row.reason === 'signature-trust-unavailable') ? keyRecoveryMessage : requalifyMessage}` : ''}`,
1001
+ { rejections });
1002
+ let reason = 'store-binding';
1003
+ let filename;
871
1004
  let stored, digest;
872
1005
  try {
1006
+ filename = storePath ?? installationStorePath();
873
1007
  if (!path.isAbsolute(filename) || (storePath !== undefined && !/^sha256:[a-f0-9]{64}$/.test(storeDigest ?? ''))) throw fail();
1008
+ reason = 'store-untrusted';
874
1009
  await assertTrustedStore(filename, [cwd]);
1010
+ reason = 'store-record';
875
1011
  ({ stored, digest } = await readInstallationStore(filename, storeDigest));
876
1012
  }
877
- catch { throw fail(); }
1013
+ catch { rejections.push({ registration: null, reason }); throw fail(); }
1014
+ if (!stored.registrations.length) {
1015
+ rejections.push({ registration: null, reason: 'no-records' });
1016
+ throw fail();
1017
+ }
1018
+ // Request policy errors remain actionable before digest/adapter checks,
1019
+ // while an absent approval retains the qualification contract (#3070).
1020
+ precheckPolicy?.();
878
1021
  const installations = {};
879
1022
  const drivers = {};
880
1023
  const observations = {};
1024
+ const qualificationFailures = Object.create(null);
881
1025
  const resolvedRegistry = structuredClone(registry);
1026
+ const requestedWorkspace = await realpath(cwd);
1027
+ const workspaces = await Promise.all(stored.registrations.map(async record => {
1028
+ try { return await realpath(record?.installation?.cwd); } catch { return null; }
1029
+ }));
882
1030
  const counts = new Map();
883
- for (const record of stored.registrations) counts.set(record?.cli, (counts.get(record?.cli) ?? 0) + 1);
884
- for (const record of stored.registrations) {
1031
+ const selected = new Map();
1032
+ for (const [index, record] of stored.registrations.entries()) {
1033
+ const key = JSON.stringify([record?.cli, workspaces[index]]);
1034
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1035
+ // Prefer an exact approval. Retain the latest other-workspace record only
1036
+ // to preserve the typed workspace-mismatch diagnostic for unapproved dirs.
1037
+ const prior = selected.get(record?.cli);
1038
+ if (prior === undefined || workspaces[prior] !== requestedWorkspace) selected.set(record?.cli, index);
1039
+ }
1040
+ for (const [registration, record] of stored.registrations.entries()) {
1041
+ reason = 'record-identity';
885
1042
  try {
886
1043
  const { cli, binding, adapter } = record;
887
- if (!/^[a-z][a-z0-9-]{0,62}$/.test(cli) || counts.get(cli) !== 1) throw fail();
1044
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(cli)) throw fail();
1045
+ if (counts.get(JSON.stringify([cli, workspaces[registration]])) !== 1) throw fail();
1046
+ if (selected.get(cli) !== registration) continue;
1047
+ // A conductor-pinned store digest also binds the original config source.
1048
+ // Framing from a worker cwd must never read that cwd's configuration.
1049
+ reason = await qualificationSignatureFailure(record, (projectDir, scope) => storePath === undefined
1050
+ ? qualificationTrust(cwd) : qualificationTrust(projectDir, scope));
1051
+ if (reason) throw fail();
888
1052
  const installation = { ...record.installation, adapter, now: Date.now(), environment: {} };
1053
+ reason = 'environment-name';
889
1054
  if (record.installation.environment && Object.keys(record.installation.environment).length) throw fail();
890
1055
  for (const name of installation.environmentNames ?? []) {
891
1056
  if (typeof name !== 'string' || process.env[name] === undefined) throw fail();
892
1057
  installation.environment[name] = process.env[name];
893
1058
  }
1059
+ reason = 'binding-mismatch';
894
1060
  if (binding.profile !== installation.profile || binding.cliVersion !== installation.cliVersion
895
1061
  || binding.accountScope !== installation.accountScope
896
- || qualificationDigest(binding.boundary) !== qualificationDigest(installation.boundary)
897
- || qualificationDigest(policy) !== installation.effectiveConfigDigest) throw fail();
1062
+ || qualificationDigest(binding.boundary) !== qualificationDigest(installation.boundary)) throw fail();
1063
+ reason = 'effective-config-digest';
1064
+ if (qualificationDigest(policy) !== installation.effectiveConfigDigest) throw fail();
1065
+ reason = 'evidence';
898
1066
  const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
899
1067
  if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) throw fail();
900
1068
  // Reject expired/unbound negative observations before any factory import.
901
1069
  const { requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
902
1070
  if (!isOperatorQualifiedWorkerAdapter(adapter)) installation.requiredControls = requiredWorkerControls(installation);
903
1071
  await assertWorkerInstallationEvidence(installation, proof.scopeDigest);
1072
+ reason = 'store-untrusted';
904
1073
  await assertTrustedStore(filename, installation.workerWritableRoots);
1074
+ reason = 'adapter-metadata';
905
1075
  const candidateRegistry = structuredClone(resolvedRegistry);
906
1076
  if (candidateRegistry.providers[adapter.id]) candidateRegistry.providers[adapter.id].workerAdapter = adapter;
907
1077
  else { candidateRegistry.workerAdapters ??= {}; candidateRegistry.workerAdapters[adapter.id] = adapter; }
908
1078
  parseProviderModelCapabilities(candidateRegistry);
909
1079
  // The loader validates package identity, bytes and static exports BEFORE import.
910
1080
  // Only this installation's metadata is needed; unrelated absent modules do not block it.
911
- const loadingRegistry = structuredClone(registry);
912
- for (const provider of Object.values(loadingRegistry.providers)) delete provider.workerAdapter;
913
- loadingRegistry.workerAdapters = { [adapter.id]: adapter };
1081
+ // Preserve the canonical provider slot used to distinguish upgrades from ID conflicts.
1082
+ const loadingRegistry = structuredClone(candidateRegistry);
1083
+ const canonical = loadingRegistry.providers[adapter.id]?.workerAdapter?.id === adapter.id;
1084
+ for (const [name, provider] of Object.entries(loadingRegistry.providers)) {
1085
+ if (canonical && name === adapter.id) provider.workerAdapter = adapter;
1086
+ else delete provider.workerAdapter;
1087
+ }
1088
+ loadingRegistry.workerAdapters = canonical ? {} : { [adapter.id]: adapter };
1089
+ reason = 'module-registration';
914
1090
  await loadInstalledWorkerAdapters({ registry: loadingRegistry, modules: installation.modules,
915
1091
  workerWritableRoots: installation.workerWritableRoots, platform: process.platform });
1092
+ reason = 'binary-identity';
916
1093
  const instance = createProvider(adapter.id);
917
- const { scope } = await recheckWorkerInstallation(instance, installation);
1094
+ const scope = await inspectWorkerInstallation(instance, installation);
1095
+ reason = 'scope-digest';
1096
+ await assertWorkerInstallationEvidence(installation, scope.scopeDigest);
1097
+ reason = 'binary-identity';
918
1098
  if (parallelism && isOperatorQualifiedWorkerAdapter(adapter)) {
919
1099
  const { resolveWorkerGroupInstallation } = await import('../config/aiwg-config.js');
920
1100
  const configured = resolveWorkerGroupInstallation(parallelism, cli, candidateRegistry);
@@ -931,15 +1111,19 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
931
1111
  }
932
1112
  if (!executable || await realpath(executable) !== scope.binary.realpath) throw fail();
933
1113
  }
1114
+ reason = 'scope-digest';
934
1115
  if (scope.scopeDigest !== proof.scopeDigest) throw fail();
935
1116
  // A launcher can keep the same bytes while its CLI payload is updated.
936
1117
  // Reuse the bounded, credential-free doctor probe before granting authority.
1118
+ reason = 'version';
937
1119
  const { version } = await boundedVersionProbe(scope.binary.realpath,
938
1120
  adapter.invocation.versionArgv, workerVersionProbeEnvironment(), cwd,
939
1121
  workerVersionProbeDefaults.timeoutMs, workerVersionProbeDefaults.maxBytes,
940
1122
  adapter.invocation.versionPattern);
941
- if (version !== binding.cliVersion
942
- || (await inspectWorkerInstallation(instance, installation)).scopeDigest !== scope.scopeDigest) throw fail();
1123
+ if (version !== binding.cliVersion) throw fail();
1124
+ reason = 'scope-digest';
1125
+ if ((await inspectWorkerInstallation(instance, installation)).scopeDigest !== scope.scopeDigest) throw fail();
1126
+ reason = 'driver';
943
1127
  const profile = adapter.invocationProfiles[binding.profile];
944
1128
  let mcpRegistry;
945
1129
  if (profile.transport === 'mcp') {
@@ -979,8 +1163,17 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
979
1163
  installations[cli] = structuredClone(binding);
980
1164
  if (record.modelObservations) observations[cli] = structuredClone(record.modelObservations);
981
1165
  Object.assign(resolvedRegistry, candidateRegistry);
982
- } catch { /* One missing/changed installation cannot grant authority or hide qualified peers. */ }
1166
+ } catch (error) {
1167
+ // One rejected installation cannot grant authority or hide qualified peers.
1168
+ const failureReason = reason === 'module-registration' && error?.code === 'WORKER_ADAPTER_CONFLICT'
1169
+ ? 'adapter-conflict' : reason;
1170
+ rejections.push({ registration, reason: failureReason });
1171
+ if (typeof record?.cli === 'string' && /^[a-z][a-z0-9-]{0,62}$/.test(record.cli)) qualificationFailures[record.cli] = failureReason;
1172
+ }
1173
+ }
1174
+ if (!Object.keys(installations).length) {
1175
+ if (!rejections.length) rejections.push({ registration: null, reason: 'no-records' });
1176
+ throw fail();
983
1177
  }
984
- if (!Object.keys(installations).length) throw fail();
985
- return { registry: resolvedRegistry, installations, drivers, observations };
1178
+ return { registry: resolvedRegistry, installations, drivers, observations, qualificationFailures };
986
1179
  }
@@ -47,7 +47,7 @@ export function buildWrapperRouteEnvelope(request) {
47
47
  ? [
48
48
  `Run this assignment through ${wrapper}.`,
49
49
  `Selected capability: ${request.capability.type} ${request.capability.name} (${request.capability.id}).`,
50
- `Load it with aiwg show ${request.capability.type === 'workflow' ? 'metadata' : request.capability.type} ${request.capability.id} after discover-first validation.`,
50
+ `Load it with aiwg show ${request.capability.type === 'workflow' ? 'metadata' : request.capability.type} ${request.capability.id}${request.capability.type === 'workflow' ? '' : ' --body'} after discover-first validation.`,
51
51
  `Assignment: ${request.assignment.trim()}`,
52
52
  'Return capability evidence, changed artifacts, validation results, and remaining risks.',
53
53
  ].join('\n')
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto';
5
5
  import { dirname, extname, join, resolve } from 'node:path';
6
6
  import { parse, stringify } from 'yaml';
7
7
  import { z } from 'zod';
8
+ import { integer } from '../schema/zod-validation.mjs';
8
9
  import { getWritingChannelPack } from '../writing/channel-packs.js';
9
10
  import { WriterProfileStore } from '../writing/writer-profile-store.js';
10
11
  import { compileWriterProfile } from '../writing/writer-profile.js';
@@ -14,28 +15,28 @@ const PROTECTED = ['code', 'commands', 'citations', 'quoted-text', 'identifiers'
14
15
  const STAGE_ORDER = ['semantic', 'voice', 'controlled-language', 'structure', 'presentation'];
15
16
  const modeIdSchema = z.string().regex(MODE_ID_PATTERN, 'must start with a lowercase letter or number and contain only lowercase letters, numbers, dots, or hyphens');
16
17
  const uniqueModeIds = z.array(modeIdSchema).refine(values => new Set(values).size === values.length, 'must not contain duplicates');
17
- const outputModeProfileSchema = z.object({
18
+ const outputModeProfileSchema = z.strictObject({
18
19
  id: modeIdSchema,
19
20
  version: z.string().min(1),
20
21
  description: z.string().min(1),
21
22
  kind: z.enum(['voice', 'controlled-language', 'structure', 'presentation']),
22
23
  stage: z.enum(STAGE_ORDER),
23
- order: z.number().int().optional(),
24
+ order: integer().optional(),
24
25
  instructions: z.string(),
25
- provenance: z.object({ source: z.string().min(1), license: z.string().min(1) }).strict(),
26
- validation: z.object({
26
+ provenance: z.strictObject({ source: z.string().min(1), license: z.string().min(1) }),
27
+ validation: z.strictObject({
27
28
  level: z.enum(['advisory', 'validated', 'conformance']),
28
29
  hook: z.string().min(1).optional(),
29
30
  standardVersion: z.string().min(1).optional(),
30
- }).strict(),
31
+ }),
31
32
  compatible: uniqueModeIds.optional(),
32
33
  conflicts: uniqueModeIds.optional(),
33
34
  requires: uniqueModeIds.optional(),
34
35
  supersedes: uniqueModeIds.optional(),
35
36
  protectedContent: z.array(z.enum(PROTECTED)).refine(values => new Set(values).size === values.length, 'must not contain duplicates').optional(),
36
- contextCost: z.number().int().nonnegative().optional(),
37
+ contextCost: integer().nonnegative().optional(),
37
38
  mergeStrategy: z.literal('weighted-voice').optional(),
38
- }).strict().superRefine((profile, ctx) => {
39
+ }).superRefine((profile, ctx) => {
39
40
  if (profile.validation.level !== 'advisory' && !profile.validation.hook) {
40
41
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['validation', 'hook'], message: `${profile.validation.level} modes require a validator hook` });
41
42
  }