@lesliechan721/aw 0.1.0-beta.3 → 0.1.0-beta.31

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 (529) hide show
  1. package/dist/authoring/assets/aw-config.mjs +100 -501
  2. package/dist/authoring/assets/aw-config.mjs.map +1 -1
  3. package/dist/authoring/scripts/add-skill.mjs +4 -4
  4. package/dist/authoring/scripts/add-skill.mjs.map +1 -1
  5. package/dist/authoring/scripts/init-plugin.mjs +67 -97
  6. package/dist/authoring/scripts/init-plugin.mjs.map +1 -1
  7. package/dist/authoring/scripts/lib/plugin-version-check.d.mts +15 -0
  8. package/dist/authoring/scripts/lib/plugin-version-check.mjs +239 -232
  9. package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -1
  10. package/dist/authoring/scripts/lib/skill-scaffold.d.mts +77 -90
  11. package/dist/authoring/scripts/lib/skill-scaffold.mjs +99 -120
  12. package/dist/authoring/scripts/lib/skill-scaffold.mjs.map +1 -1
  13. package/dist/authoring/scripts/plugin-status.mjs +8 -11
  14. package/dist/authoring/scripts/plugin-status.mjs.map +1 -1
  15. package/dist/authoring/scripts/remove-plugin.mjs +367 -0
  16. package/dist/authoring/scripts/remove-plugin.mjs.map +1 -0
  17. package/dist/authoring/scripts/validate-plugin.mjs +307 -1173
  18. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  19. package/dist/scripts/aw-skill-eval-materializer.d.ts +26 -0
  20. package/dist/scripts/aw-skill-eval-materializer.js +306 -0
  21. package/dist/scripts/aw-skill-eval-materializer.js.map +1 -0
  22. package/dist/scripts/bundle-plugins.d.ts +1 -2
  23. package/dist/scripts/bundle-plugins.js +24 -7
  24. package/dist/scripts/bundle-plugins.js.map +1 -1
  25. package/dist/scripts/plugin-staging.d.ts +7 -24
  26. package/dist/scripts/plugin-staging.js +22 -142
  27. package/dist/scripts/plugin-staging.js.map +1 -1
  28. package/dist/scripts/test-plugin-scripts.d.ts +2 -0
  29. package/dist/scripts/test-plugin-scripts.js +27 -75
  30. package/dist/scripts/test-plugin-scripts.js.map +1 -1
  31. package/dist/src/adapters/helpers.js +2 -2
  32. package/dist/src/adapters/helpers.js.map +1 -1
  33. package/dist/src/authoring/workspace-commands.d.ts +26 -2
  34. package/dist/src/authoring/workspace-commands.js +44 -19
  35. package/dist/src/authoring/workspace-commands.js.map +1 -1
  36. package/dist/src/commands/aw-capabilities.d.ts +78 -0
  37. package/dist/src/commands/aw-capabilities.js +256 -0
  38. package/dist/src/commands/aw-capabilities.js.map +1 -0
  39. package/dist/src/commands/catalog-dev-source.d.ts +154 -0
  40. package/dist/src/commands/catalog-dev-source.js +655 -0
  41. package/dist/src/commands/catalog-dev-source.js.map +1 -0
  42. package/dist/src/commands/catalog-generation.d.ts +21 -0
  43. package/dist/src/commands/catalog-generation.js +9 -0
  44. package/dist/src/commands/catalog-generation.js.map +1 -0
  45. package/dist/src/commands/catalog-publication-inspection.d.ts +138 -0
  46. package/dist/src/commands/catalog-publication-inspection.js +757 -0
  47. package/dist/src/commands/catalog-publication-inspection.js.map +1 -0
  48. package/dist/src/commands/catalog-publication-journal.d.ts +68 -0
  49. package/dist/src/commands/catalog-publication-journal.js +292 -0
  50. package/dist/src/commands/catalog-publication-journal.js.map +1 -0
  51. package/dist/src/commands/catalog-publication-target.d.ts +18 -0
  52. package/dist/src/commands/catalog-publication-target.js +95 -0
  53. package/dist/src/commands/catalog-publication-target.js.map +1 -0
  54. package/dist/src/commands/catalog-publish.d.ts +77 -7
  55. package/dist/src/commands/catalog-publish.js +746 -316
  56. package/dist/src/commands/catalog-publish.js.map +1 -1
  57. package/dist/src/commands/catalog-source.d.ts +60 -22
  58. package/dist/src/commands/catalog-source.js +311 -91
  59. package/dist/src/commands/catalog-source.js.map +1 -1
  60. package/dist/src/commands/catalog-status.d.ts +56 -0
  61. package/dist/src/commands/catalog-status.js +217 -0
  62. package/dist/src/commands/catalog-status.js.map +1 -0
  63. package/dist/src/commands/catalog-store.d.ts +104 -0
  64. package/dist/src/commands/catalog-store.js +685 -0
  65. package/dist/src/commands/catalog-store.js.map +1 -0
  66. package/dist/src/commands/catalog-workspace-resolution.d.ts +13 -0
  67. package/dist/src/commands/catalog-workspace-resolution.js +42 -0
  68. package/dist/src/commands/catalog-workspace-resolution.js.map +1 -0
  69. package/dist/src/commands/console.js +28 -17
  70. package/dist/src/commands/console.js.map +1 -1
  71. package/dist/src/commands/dependency-operation-plan.d.ts +52 -15
  72. package/dist/src/commands/dependency-operation-plan.js +289 -7
  73. package/dist/src/commands/dependency-operation-plan.js.map +1 -1
  74. package/dist/src/commands/init.d.ts +5 -11
  75. package/dist/src/commands/init.js +30 -51
  76. package/dist/src/commands/init.js.map +1 -1
  77. package/dist/src/commands/offline-cleanup-evidence.d.ts +67 -0
  78. package/dist/src/commands/offline-cleanup-evidence.js +181 -0
  79. package/dist/src/commands/offline-cleanup-evidence.js.map +1 -0
  80. package/dist/src/commands/operation-plan.d.ts +127 -0
  81. package/dist/src/commands/operation-plan.js +71 -0
  82. package/dist/src/commands/operation-plan.js.map +1 -0
  83. package/dist/src/commands/operation-planner.d.ts +46 -0
  84. package/dist/src/commands/operation-planner.js +141 -0
  85. package/dist/src/commands/operation-planner.js.map +1 -0
  86. package/dist/src/commands/options.d.ts +9 -7
  87. package/dist/src/commands/options.js +83 -21
  88. package/dist/src/commands/options.js.map +1 -1
  89. package/dist/src/commands/plugin-capabilities.d.ts +22 -0
  90. package/dist/src/commands/plugin-capabilities.js +159 -0
  91. package/dist/src/commands/plugin-capabilities.js.map +1 -0
  92. package/dist/src/commands/rebuild-command.d.ts +77 -30
  93. package/dist/src/commands/rebuild-command.js +3993 -711
  94. package/dist/src/commands/rebuild-command.js.map +1 -1
  95. package/dist/src/commands/rebuild-integration.d.ts +120 -21
  96. package/dist/src/commands/rebuild-integration.js +2765 -1326
  97. package/dist/src/commands/rebuild-integration.js.map +1 -1
  98. package/dist/src/commands/rebuild-operations.d.ts +58 -30
  99. package/dist/src/commands/rebuild-operations.js +260 -319
  100. package/dist/src/commands/rebuild-operations.js.map +1 -1
  101. package/dist/src/commands/reconcile-operation-plan.d.ts +28 -0
  102. package/dist/src/commands/reconcile-operation-plan.js +47 -0
  103. package/dist/src/commands/reconcile-operation-plan.js.map +1 -0
  104. package/dist/src/commands/review-inspection.d.ts +228 -0
  105. package/dist/src/commands/review-inspection.js +1341 -0
  106. package/dist/src/commands/review-inspection.js.map +1 -0
  107. package/dist/src/commands/review-preparation.d.ts +49 -0
  108. package/dist/src/commands/review-preparation.js +427 -0
  109. package/dist/src/commands/review-preparation.js.map +1 -0
  110. package/dist/src/commands/runtime.d.ts +1 -5
  111. package/dist/src/commands/runtime.js +24 -50
  112. package/dist/src/commands/runtime.js.map +1 -1
  113. package/dist/src/commands/skill-eval.d.ts +38 -0
  114. package/dist/src/commands/skill-eval.js +388 -0
  115. package/dist/src/commands/skill-eval.js.map +1 -0
  116. package/dist/src/commands/status-v6.d.ts +279 -0
  117. package/dist/src/commands/status-v6.js +2697 -0
  118. package/dist/src/commands/status-v6.js.map +1 -0
  119. package/dist/src/commands/sync-command.d.ts +40 -0
  120. package/dist/src/commands/sync-command.js +250 -0
  121. package/dist/src/commands/sync-command.js.map +1 -0
  122. package/dist/src/commands/wizard.d.ts +3 -4
  123. package/dist/src/commands/wizard.js +2 -2
  124. package/dist/src/commands/wizard.js.map +1 -1
  125. package/dist/src/commands/write-commands.d.ts +9 -3
  126. package/dist/src/commands/write-commands.js +124 -31
  127. package/dist/src/commands/write-commands.js.map +1 -1
  128. package/dist/src/constants.d.ts +7 -1
  129. package/dist/src/constants.js +7 -1
  130. package/dist/src/constants.js.map +1 -1
  131. package/dist/src/eval/manifest-v1.d.ts +71 -0
  132. package/dist/src/eval/manifest-v1.js +370 -0
  133. package/dist/src/eval/manifest-v1.js.map +1 -0
  134. package/dist/src/generators/local-plugin.d.ts +5 -1
  135. package/dist/src/generators/local-plugin.js +225 -104
  136. package/dist/src/generators/local-plugin.js.map +1 -1
  137. package/dist/src/index.d.ts +10 -4
  138. package/dist/src/index.js +1787 -476
  139. package/dist/src/index.js.map +1 -1
  140. package/dist/src/output/command-receipt.d.ts +43 -46
  141. package/dist/src/output/command-receipt.js +256 -80
  142. package/dist/src/output/command-receipt.js.map +1 -1
  143. package/dist/src/output/model.d.ts +3 -44
  144. package/dist/src/output/model.js +1 -144
  145. package/dist/src/output/model.js.map +1 -1
  146. package/dist/src/output/rebuild-progress.js +1 -13
  147. package/dist/src/output/rebuild-progress.js.map +1 -1
  148. package/dist/src/portable/commands.d.ts +46 -7
  149. package/dist/src/portable/commands.js +349 -37
  150. package/dist/src/portable/commands.js.map +1 -1
  151. package/dist/src/portable/materializer.d.ts +17 -4
  152. package/dist/src/portable/materializer.js +160 -38
  153. package/dist/src/portable/materializer.js.map +1 -1
  154. package/dist/src/portable/skills-cli.d.ts +15 -7
  155. package/dist/src/portable/skills-cli.js +66 -19
  156. package/dist/src/portable/skills-cli.js.map +1 -1
  157. package/dist/src/review/eval-source-input.d.ts +43 -0
  158. package/dist/src/review/eval-source-input.js +207 -0
  159. package/dist/src/review/eval-source-input.js.map +1 -0
  160. package/dist/src/review/review-profile.d.ts +17 -0
  161. package/dist/src/review/review-profile.js +177 -0
  162. package/dist/src/review/review-profile.js.map +1 -0
  163. package/dist/src/review/review-selection.d.ts +30 -0
  164. package/dist/src/review/review-selection.js +527 -0
  165. package/dist/src/review/review-selection.js.map +1 -0
  166. package/dist/src/review/review-types.d.ts +68 -0
  167. package/dist/src/review/review-types.js +16 -0
  168. package/dist/src/review/review-types.js.map +1 -0
  169. package/dist/src/review/skill-materialization.d.ts +13 -0
  170. package/dist/src/review/skill-materialization.js +75 -0
  171. package/dist/src/review/skill-materialization.js.map +1 -0
  172. package/dist/src/review/source-binding.d.ts +182 -0
  173. package/dist/src/review/source-binding.js +682 -0
  174. package/dist/src/review/source-binding.js.map +1 -0
  175. package/dist/src/tui/catalog-freshness.d.ts +1 -0
  176. package/dist/src/tui/catalog-freshness.js +2 -0
  177. package/dist/src/tui/catalog-freshness.js.map +1 -0
  178. package/dist/src/tui/catalog-generation.d.ts +21 -0
  179. package/dist/src/tui/catalog-generation.js +156 -0
  180. package/dist/src/tui/catalog-generation.js.map +1 -0
  181. package/dist/src/tui/catalog-sources/projection.d.ts +4 -0
  182. package/dist/src/tui/catalog-sources/projection.js +17 -0
  183. package/dist/src/tui/catalog-sources/projection.js.map +1 -0
  184. package/dist/src/tui/catalog-sources/review.d.ts +3 -0
  185. package/dist/src/tui/catalog-sources/review.js +13 -0
  186. package/dist/src/tui/catalog-sources/review.js.map +1 -0
  187. package/dist/src/tui/catalog-sources/screens.d.ts +12 -0
  188. package/dist/src/tui/catalog-sources/screens.js +40 -0
  189. package/dist/src/tui/catalog-sources/screens.js.map +1 -0
  190. package/dist/src/tui/catalog-sources/session.d.ts +28 -0
  191. package/dist/src/tui/catalog-sources/session.js +295 -0
  192. package/dist/src/tui/catalog-sources/session.js.map +1 -0
  193. package/dist/src/tui/catalog-sources/types.d.ts +53 -0
  194. package/dist/src/tui/catalog-sources/types.js +2 -0
  195. package/dist/src/tui/catalog-sources/types.js.map +1 -0
  196. package/dist/src/tui/catalog-store-task.d.ts +16 -0
  197. package/dist/src/tui/catalog-store-task.js +19 -0
  198. package/dist/src/tui/catalog-store-task.js.map +1 -0
  199. package/dist/src/tui/components.d.ts +128 -0
  200. package/dist/src/tui/components.js +251 -7
  201. package/dist/src/tui/components.js.map +1 -1
  202. package/dist/src/tui/console-app-v4.d.ts +14 -4
  203. package/dist/src/tui/console-app-v4.js +9248 -1461
  204. package/dist/src/tui/console-app-v4.js.map +1 -1
  205. package/dist/src/tui/controller-runtime.d.ts +13 -1
  206. package/dist/src/tui/controller-runtime.js +3338 -410
  207. package/dist/src/tui/controller-runtime.js.map +1 -1
  208. package/dist/src/tui/controller.d.ts +614 -40
  209. package/dist/src/tui/controller.js +3478 -708
  210. package/dist/src/tui/controller.js.map +1 -1
  211. package/dist/src/tui/dependency-graph.d.ts +81 -0
  212. package/dist/src/tui/dependency-graph.js +455 -0
  213. package/dist/src/tui/dependency-graph.js.map +1 -0
  214. package/dist/src/tui/diagnostic-projection.d.ts +23 -0
  215. package/dist/src/tui/diagnostic-projection.js +567 -0
  216. package/dist/src/tui/diagnostic-projection.js.map +1 -0
  217. package/dist/src/tui/diagnostics.d.ts +30 -0
  218. package/dist/src/tui/diagnostics.js +76 -0
  219. package/dist/src/tui/diagnostics.js.map +1 -0
  220. package/dist/src/tui/glossary.d.ts +23 -0
  221. package/dist/src/tui/glossary.js +42 -0
  222. package/dist/src/tui/glossary.js.map +1 -0
  223. package/dist/src/tui/index.d.ts +11 -4
  224. package/dist/src/tui/index.js +8 -2
  225. package/dist/src/tui/index.js.map +1 -1
  226. package/dist/src/tui/input-display.d.ts +8 -0
  227. package/dist/src/tui/input-display.js +36 -0
  228. package/dist/src/tui/input-display.js.map +1 -0
  229. package/dist/src/tui/keys.d.ts +44 -0
  230. package/dist/src/tui/keys.js +43 -15
  231. package/dist/src/tui/keys.js.map +1 -1
  232. package/dist/src/tui/lifecycle-action-view.d.ts +6 -0
  233. package/dist/src/tui/lifecycle-action-view.js +36 -0
  234. package/dist/src/tui/lifecycle-action-view.js.map +1 -0
  235. package/dist/src/tui/model.d.ts +235 -27
  236. package/dist/src/tui/model.js +1652 -304
  237. package/dist/src/tui/model.js.map +1 -1
  238. package/dist/src/tui/operation-diagnostic-screens.d.ts +21 -0
  239. package/dist/src/tui/operation-diagnostic-screens.js +102 -0
  240. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -0
  241. package/dist/src/tui/operation-history.d.ts +81 -0
  242. package/dist/src/tui/operation-history.js +135 -0
  243. package/dist/src/tui/operation-history.js.map +1 -0
  244. package/dist/src/tui/operation-review-planner.d.ts +21 -0
  245. package/dist/src/tui/operation-review-planner.js +1254 -0
  246. package/dist/src/tui/operation-review-planner.js.map +1 -0
  247. package/dist/src/tui/plugin-content.d.ts +23 -0
  248. package/dist/src/tui/plugin-content.js +336 -0
  249. package/dist/src/tui/plugin-content.js.map +1 -0
  250. package/dist/src/tui/plugin-write-task.d.ts +69 -0
  251. package/dist/src/tui/plugin-write-task.js +286 -0
  252. package/dist/src/tui/plugin-write-task.js.map +1 -0
  253. package/dist/src/tui/prompt.js +4 -15
  254. package/dist/src/tui/prompt.js.map +1 -1
  255. package/dist/src/tui/router.d.ts +183 -11
  256. package/dist/src/tui/router.js +352 -29
  257. package/dist/src/tui/router.js.map +1 -1
  258. package/dist/src/tui/screen-registry.d.ts +702 -0
  259. package/dist/src/tui/screen-registry.js +231 -0
  260. package/dist/src/tui/screen-registry.js.map +1 -0
  261. package/dist/src/tui/session-console-app.d.ts +6 -0
  262. package/dist/src/tui/session-console-app.js +13 -0
  263. package/dist/src/tui/session-console-app.js.map +1 -0
  264. package/dist/src/tui/session-host.d.ts +45 -0
  265. package/dist/src/tui/session-host.js +209 -0
  266. package/dist/src/tui/session-host.js.map +1 -0
  267. package/dist/src/tui/source-ref-presets.d.ts +3 -0
  268. package/dist/src/tui/source-ref-presets.js +11 -0
  269. package/dist/src/tui/source-ref-presets.js.map +1 -0
  270. package/dist/src/tui/theme.d.ts +18 -10
  271. package/dist/src/tui/theme.js +18 -10
  272. package/dist/src/tui/theme.js.map +1 -1
  273. package/dist/src/tui/viewport.d.ts +20 -0
  274. package/dist/src/tui/viewport.js +29 -0
  275. package/dist/src/tui/viewport.js.map +1 -0
  276. package/dist/src/tui/views.js +31 -3
  277. package/dist/src/tui/views.js.map +1 -1
  278. package/dist/src/tui/wizard.js +11 -9
  279. package/dist/src/tui/wizard.js.map +1 -1
  280. package/dist/src/types.d.ts +242 -20
  281. package/dist/src/types.js +1 -0
  282. package/dist/src/types.js.map +1 -1
  283. package/dist/src/utils/agent-version.d.ts +7 -1
  284. package/dist/src/utils/agent-version.js +41 -8
  285. package/dist/src/utils/agent-version.js.map +1 -1
  286. package/dist/src/utils/artifact-plan.d.ts +5 -4
  287. package/dist/src/utils/artifact-plan.js +18 -7
  288. package/dist/src/utils/artifact-plan.js.map +1 -1
  289. package/dist/src/utils/artifact-renderer.d.ts +13 -7
  290. package/dist/src/utils/artifact-renderer.js +196 -219
  291. package/dist/src/utils/artifact-renderer.js.map +1 -1
  292. package/dist/src/utils/artifact-request.d.ts +21 -0
  293. package/dist/src/utils/artifact-request.js +82 -0
  294. package/dist/src/utils/artifact-request.js.map +1 -0
  295. package/dist/src/utils/aw-home-mutation-lock.d.ts +1 -0
  296. package/dist/src/utils/aw-home-mutation-lock.js +32 -4
  297. package/dist/src/utils/aw-home-mutation-lock.js.map +1 -1
  298. package/dist/src/utils/bundle-integrity.d.ts +0 -1
  299. package/dist/src/utils/bundle-integrity.js +0 -25
  300. package/dist/src/utils/bundle-integrity.js.map +1 -1
  301. package/dist/src/utils/bundle-layout.js +14 -29
  302. package/dist/src/utils/bundle-layout.js.map +1 -1
  303. package/dist/src/utils/catalog-capability-projection.d.ts +8 -0
  304. package/dist/src/utils/catalog-capability-projection.js +14 -0
  305. package/dist/src/utils/catalog-capability-projection.js.map +1 -0
  306. package/dist/src/utils/catalog-digests.d.ts +10 -0
  307. package/dist/src/utils/catalog-digests.js +50 -2
  308. package/dist/src/utils/catalog-digests.js.map +1 -1
  309. package/dist/src/utils/catalog-generation-control-snapshot.d.ts +11 -0
  310. package/dist/src/utils/catalog-generation-control-snapshot.js +45 -0
  311. package/dist/src/utils/catalog-generation-control-snapshot.js.map +1 -0
  312. package/dist/src/utils/catalog-generation-error.d.ts +16 -0
  313. package/dist/src/utils/catalog-generation-error.js +30 -0
  314. package/dist/src/utils/catalog-generation-error.js.map +1 -0
  315. package/dist/src/utils/catalog-generation-resolver.d.ts +44 -0
  316. package/dist/src/utils/catalog-generation-resolver.js +273 -0
  317. package/dist/src/utils/catalog-generation-resolver.js.map +1 -0
  318. package/dist/src/utils/catalog-generation-service.d.ts +172 -0
  319. package/dist/src/utils/catalog-generation-service.js +1271 -0
  320. package/dist/src/utils/catalog-generation-service.js.map +1 -0
  321. package/dist/src/utils/catalog-generation-state.d.ts +22 -0
  322. package/dist/src/utils/catalog-generation-state.js +192 -0
  323. package/dist/src/utils/catalog-generation-state.js.map +1 -0
  324. package/dist/src/utils/catalog-generation-store.d.ts +58 -0
  325. package/dist/src/utils/catalog-generation-store.js +452 -0
  326. package/dist/src/utils/catalog-generation-store.js.map +1 -0
  327. package/dist/src/utils/catalog-manifest-v7.d.ts +5 -0
  328. package/dist/src/utils/{catalog-manifest-v4.js → catalog-manifest-v7.js} +35 -43
  329. package/dist/src/utils/catalog-manifest-v7.js.map +1 -0
  330. package/dist/src/utils/catalog-operation-context.d.ts +20 -0
  331. package/dist/src/utils/catalog-operation-context.js +142 -0
  332. package/dist/src/utils/catalog-operation-context.js.map +1 -0
  333. package/dist/src/utils/catalog-public-error.d.ts +7 -3
  334. package/dist/src/utils/catalog-public-error.js +28 -8
  335. package/dist/src/utils/catalog-public-error.js.map +1 -1
  336. package/dist/src/utils/catalog-runtime-projection.d.ts +4 -7
  337. package/dist/src/utils/catalog-runtime-projection.js +7 -102
  338. package/dist/src/utils/catalog-runtime-projection.js.map +1 -1
  339. package/dist/src/utils/catalog-snapshot.d.ts +5 -3
  340. package/dist/src/utils/catalog-snapshot.js +11 -3
  341. package/dist/src/utils/catalog-snapshot.js.map +1 -1
  342. package/dist/src/utils/catalog-source-identity.d.ts +1 -0
  343. package/dist/src/utils/catalog-source-identity.js +21 -2
  344. package/dist/src/utils/catalog-source-identity.js.map +1 -1
  345. package/dist/src/utils/catalog-source-impact.d.ts +21 -0
  346. package/dist/src/utils/catalog-source-impact.js +344 -0
  347. package/dist/src/utils/catalog-source-impact.js.map +1 -0
  348. package/dist/src/utils/catalog-source-references.d.ts +3 -2
  349. package/dist/src/utils/catalog-source-references.js +195 -347
  350. package/dist/src/utils/catalog-source-references.js.map +1 -1
  351. package/dist/src/utils/catalog-sources.d.ts +15 -1
  352. package/dist/src/utils/catalog-sources.js +206 -27
  353. package/dist/src/utils/catalog-sources.js.map +1 -1
  354. package/dist/src/utils/codex-config.d.ts +2 -0
  355. package/dist/src/utils/codex-config.js +26 -5
  356. package/dist/src/utils/codex-config.js.map +1 -1
  357. package/dist/src/utils/codex-manager.d.ts +79 -0
  358. package/dist/src/utils/codex-manager.js +745 -0
  359. package/dist/src/utils/codex-manager.js.map +1 -0
  360. package/dist/src/utils/codex-marketplace-path.d.ts +5 -0
  361. package/dist/src/utils/codex-marketplace-path.js +45 -0
  362. package/dist/src/utils/codex-marketplace-path.js.map +1 -0
  363. package/dist/src/utils/codex-native-observation.d.ts +49 -0
  364. package/dist/src/utils/codex-native-observation.js +154 -0
  365. package/dist/src/utils/codex-native-observation.js.map +1 -0
  366. package/dist/src/utils/codex-native-render-receipt.d.ts +40 -0
  367. package/dist/src/utils/codex-native-render-receipt.js +205 -0
  368. package/dist/src/utils/codex-native-render-receipt.js.map +1 -0
  369. package/dist/src/utils/codex-reconciler.d.ts +126 -0
  370. package/dist/src/utils/codex-reconciler.js +298 -0
  371. package/dist/src/utils/codex-reconciler.js.map +1 -0
  372. package/dist/src/utils/codex-shared-native.d.ts +43 -0
  373. package/dist/src/utils/codex-shared-native.js +81 -0
  374. package/dist/src/utils/codex-shared-native.js.map +1 -0
  375. package/dist/src/utils/codex-skill-staging.d.ts +2 -0
  376. package/dist/src/utils/codex-skill-staging.js.map +1 -1
  377. package/dist/src/utils/codex-surface-analysis.d.ts +46 -0
  378. package/dist/src/utils/codex-surface-analysis.js +247 -0
  379. package/dist/src/utils/codex-surface-analysis.js.map +1 -0
  380. package/dist/src/utils/dynamic-skill-tree.d.ts +4 -0
  381. package/dist/src/utils/dynamic-skill-tree.js +36 -6
  382. package/dist/src/utils/dynamic-skill-tree.js.map +1 -1
  383. package/dist/src/utils/git-catalog-provider.d.ts +18 -0
  384. package/dist/src/utils/git-catalog-provider.js +170 -144
  385. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  386. package/dist/src/utils/local-marketplace.d.ts +15 -1
  387. package/dist/src/utils/local-marketplace.js +307 -84
  388. package/dist/src/utils/local-marketplace.js.map +1 -1
  389. package/dist/src/utils/manifest.d.ts +1 -4
  390. package/dist/src/utils/manifest.js +1 -4
  391. package/dist/src/utils/manifest.js.map +1 -1
  392. package/dist/src/utils/multi-source-catalog.d.ts +3 -9
  393. package/dist/src/utils/multi-source-catalog.js +54 -110
  394. package/dist/src/utils/multi-source-catalog.js.map +1 -1
  395. package/dist/src/utils/native-cli.d.ts +55 -7
  396. package/dist/src/utils/native-cli.js +386 -57
  397. package/dist/src/utils/native-cli.js.map +1 -1
  398. package/dist/src/utils/orphan-marketplace-cleanup.js +0 -4
  399. package/dist/src/utils/orphan-marketplace-cleanup.js.map +1 -1
  400. package/dist/src/utils/ownership.d.ts +84 -20
  401. package/dist/src/utils/ownership.js +179 -222
  402. package/dist/src/utils/ownership.js.map +1 -1
  403. package/dist/src/utils/path-catalog-provider.d.ts +1 -1
  404. package/dist/src/utils/path-catalog-provider.js +54 -65
  405. package/dist/src/utils/path-catalog-provider.js.map +1 -1
  406. package/dist/src/utils/paths.d.ts +0 -1
  407. package/dist/src/utils/paths.js +0 -1
  408. package/dist/src/utils/paths.js.map +1 -1
  409. package/dist/src/utils/plugin-config-impact.d.ts +33 -0
  410. package/dist/src/utils/plugin-config-impact.js +298 -0
  411. package/dist/src/utils/plugin-config-impact.js.map +1 -0
  412. package/dist/src/utils/plugin-config-write.d.ts +52 -27
  413. package/dist/src/utils/plugin-config-write.js +990 -298
  414. package/dist/src/utils/plugin-config-write.js.map +1 -1
  415. package/dist/src/utils/plugin-config.d.ts +140 -93
  416. package/dist/src/utils/plugin-config.js +992 -980
  417. package/dist/src/utils/plugin-config.js.map +1 -1
  418. package/dist/src/utils/plugin-intent-state.d.ts +49 -0
  419. package/dist/src/utils/plugin-intent-state.js +320 -0
  420. package/dist/src/utils/plugin-intent-state.js.map +1 -0
  421. package/dist/src/utils/plugin-scripts.d.ts +62 -125
  422. package/dist/src/utils/plugin-scripts.js +468 -525
  423. package/dist/src/utils/plugin-scripts.js.map +1 -1
  424. package/dist/src/utils/process-tree.d.ts +2 -2
  425. package/dist/src/utils/process-tree.js +1 -1
  426. package/dist/src/utils/process-tree.js.map +1 -1
  427. package/dist/src/utils/project-boundary.d.ts +11 -13
  428. package/dist/src/utils/project-boundary.js +28 -34
  429. package/dist/src/utils/project-boundary.js.map +1 -1
  430. package/dist/src/utils/safe-fs.d.ts +21 -0
  431. package/dist/src/utils/safe-fs.js +111 -0
  432. package/dist/src/utils/safe-fs.js.map +1 -0
  433. package/dist/src/utils/safe-operation-diagnostic.d.ts +15 -0
  434. package/dist/src/utils/safe-operation-diagnostic.js +33 -0
  435. package/dist/src/utils/safe-operation-diagnostic.js.map +1 -0
  436. package/dist/src/utils/sensitive-content.d.ts +4 -0
  437. package/dist/src/utils/sensitive-content.js +36 -0
  438. package/dist/src/utils/sensitive-content.js.map +1 -0
  439. package/dist/src/utils/skill-validation.d.ts +1 -1
  440. package/dist/src/utils/skill-validation.js +1 -1
  441. package/dist/src/utils/skill-validation.js.map +1 -1
  442. package/dist/src/utils/state-paths.d.ts +1 -0
  443. package/dist/src/utils/state-paths.js +12 -2
  444. package/dist/src/utils/state-paths.js.map +1 -1
  445. package/dist/src/utils/target-eligibility.d.ts +8 -2
  446. package/dist/src/utils/target-eligibility.js +17 -8
  447. package/dist/src/utils/target-eligibility.js.map +1 -1
  448. package/dist/src/utils/windows-claude-shim.d.ts +9 -0
  449. package/dist/src/utils/windows-claude-shim.js +141 -0
  450. package/dist/src/utils/windows-claude-shim.js.map +1 -0
  451. package/dist/vitest.config.js +91 -10
  452. package/dist/vitest.config.js.map +1 -1
  453. package/package.json +22 -19
  454. package/dist/authoring/scripts/check-plugin-versions.mjs +0 -5
  455. package/dist/authoring/scripts/check-plugin-versions.mjs.map +0 -1
  456. package/dist/scripts/aw-skill-eval-engine.d.ts +0 -13
  457. package/dist/scripts/aw-skill-eval-engine.js +0 -31
  458. package/dist/scripts/aw-skill-eval-engine.js.map +0 -1
  459. package/dist/scripts/codex-eval-app-server.d.ts +0 -89
  460. package/dist/scripts/codex-eval-app-server.js +0 -1228
  461. package/dist/scripts/codex-eval-app-server.js.map +0 -1
  462. package/dist/scripts/codex-eval-isolation.d.ts +0 -61
  463. package/dist/scripts/codex-eval-isolation.js +0 -318
  464. package/dist/scripts/codex-eval-isolation.js.map +0 -1
  465. package/dist/scripts/codex-eval-process-management.d.ts +0 -8
  466. package/dist/scripts/codex-eval-process-management.js +0 -69
  467. package/dist/scripts/codex-eval-process-management.js.map +0 -1
  468. package/dist/scripts/codex-eval-process-tree.d.ts +0 -38
  469. package/dist/scripts/codex-eval-process-tree.js +0 -803
  470. package/dist/scripts/codex-eval-process-tree.js.map +0 -1
  471. package/dist/scripts/codex-eval-runtime-config.d.ts +0 -62
  472. package/dist/scripts/codex-eval-runtime-config.js +0 -843
  473. package/dist/scripts/codex-eval-runtime-config.js.map +0 -1
  474. package/dist/scripts/codex-trigger-eval.d.ts +0 -84
  475. package/dist/scripts/codex-trigger-eval.js +0 -968
  476. package/dist/scripts/codex-trigger-eval.js.map +0 -1
  477. package/dist/scripts/eval-plugin-skills-audited.d.ts +0 -6
  478. package/dist/scripts/eval-plugin-skills-audited.js +0 -261
  479. package/dist/scripts/eval-plugin-skills-audited.js.map +0 -1
  480. package/dist/scripts/eval-plugin-skills.d.ts +0 -45
  481. package/dist/scripts/eval-plugin-skills.js +0 -1586
  482. package/dist/scripts/eval-plugin-skills.js.map +0 -1
  483. package/dist/scripts/eval-run-layout.d.ts +0 -17
  484. package/dist/scripts/eval-run-layout.js +0 -26
  485. package/dist/scripts/eval-run-layout.js.map +0 -1
  486. package/dist/scripts/plugin-eval-contract.d.ts +0 -185
  487. package/dist/scripts/plugin-eval-contract.js +0 -695
  488. package/dist/scripts/plugin-eval-contract.js.map +0 -1
  489. package/dist/scripts/plugin-eval-materializer.d.ts +0 -13
  490. package/dist/scripts/plugin-eval-materializer.js +0 -421
  491. package/dist/scripts/plugin-eval-materializer.js.map +0 -1
  492. package/dist/scripts/skill-reporting.d.ts +0 -212
  493. package/dist/scripts/skill-reporting.js +0 -922
  494. package/dist/scripts/skill-reporting.js.map +0 -1
  495. package/dist/src/commands/status-v4.d.ts +0 -102
  496. package/dist/src/commands/status-v4.js +0 -844
  497. package/dist/src/commands/status-v4.js.map +0 -1
  498. package/dist/src/commands/status.d.ts +0 -93
  499. package/dist/src/commands/status.js +0 -442
  500. package/dist/src/commands/status.js.map +0 -1
  501. package/dist/src/utils/active-marketplace-runtime-lock.d.ts +0 -12
  502. package/dist/src/utils/active-marketplace-runtime-lock.js +0 -133
  503. package/dist/src/utils/active-marketplace-runtime-lock.js.map +0 -1
  504. package/dist/src/utils/catalog-manifest-v3.d.ts +0 -2
  505. package/dist/src/utils/catalog-manifest-v3.js +0 -3
  506. package/dist/src/utils/catalog-manifest-v3.js.map +0 -1
  507. package/dist/src/utils/catalog-manifest-v4.d.ts +0 -5
  508. package/dist/src/utils/catalog-manifest-v4.js.map +0 -1
  509. package/dist/src/utils/catalog-source-cache.d.ts +0 -61
  510. package/dist/src/utils/catalog-source-cache.js +0 -276
  511. package/dist/src/utils/catalog-source-cache.js.map +0 -1
  512. package/dist/src/utils/codex-project-surface.d.ts +0 -3
  513. package/dist/src/utils/codex-project-surface.js +0 -49
  514. package/dist/src/utils/codex-project-surface.js.map +0 -1
  515. package/dist/src/utils/lifecycle-active-store.d.ts +0 -175
  516. package/dist/src/utils/lifecycle-active-store.js +0 -1276
  517. package/dist/src/utils/lifecycle-active-store.js.map +0 -1
  518. package/dist/src/utils/plugin-authoring-capabilities.d.ts +0 -23
  519. package/dist/src/utils/plugin-authoring-capabilities.js +0 -37
  520. package/dist/src/utils/plugin-authoring-capabilities.js.map +0 -1
  521. package/dist/src/utils/plugin-authoring-capabilities.json +0 -92
  522. package/dist/src/utils/plugin-release.d.ts +0 -2
  523. package/dist/src/utils/plugin-release.js +0 -2
  524. package/dist/src/utils/plugin-release.js.map +0 -1
  525. package/dist/src/utils/skill-frontmatter-capabilities.d.ts +0 -201
  526. package/dist/src/utils/skill-frontmatter-capabilities.js +0 -5
  527. package/dist/src/utils/skill-frontmatter-capabilities.js.map +0 -1
  528. package/dist/src/utils/skill-frontmatter-capabilities.json +0 -200
  529. /package/dist/authoring/scripts/{check-plugin-versions.d.mts → remove-plugin.d.mts} +0 -0
@@ -1,20 +1,73 @@
1
1
  import crypto from 'node:crypto';
2
- import { AsyncLocalStorage } from 'node:async_hooks';
3
2
  import * as nodeFs from 'node:fs/promises';
4
3
  import path from 'node:path';
5
4
  import fs from 'fs-extra';
5
+ import { canonicalizePluginConfigValues, formatPluginConfigIssue, matchPluginConfigPreset, redactPluginConfigValue, stablePluginConfigStringify, validatePluginConfigDocument, validatePluginConfigValue as validateCorePluginConfigValue, } from '@lesliechan721/aw-plugin-core/manifest';
6
6
  import { assertTestReadablePath, assertTestWritablePath, } from './test-path-fence.js';
7
- import { markActiveMarketplaceDurableCommit, withActiveMarketplaceRuntimeLease, } from './active-marketplace-runtime-lock.js';
8
7
  import { canonicalizePathThroughExistingAncestor, resolveAwHome, resolveRepoRootSync, } from './paths.js';
9
- import { acquireProcessLock } from './process-lock.js';
10
- const configCommitStorage = new AsyncLocalStorage();
11
- function markConfigDurableCommit() {
12
- let context = configCommitStorage.getStore();
13
- while (context) {
14
- context.durableCommit = true;
15
- context = context.parent;
8
+ import { assertRealFileParentWithinRoot, assertSafeFileMutationPath, atomicReplaceSafeFile, } from './safe-fs.js';
9
+ import { assertProjectSharedConfigPathVisible, PROJECT_CONFIG_SHAREABILITY_UNVERIFIED_WARNING, } from './project-boundary.js';
10
+ export function codexNativeRefreshKeys(manifest) {
11
+ return new Set((manifest.artifacts?.targets ?? []).flatMap((target) => target.platform === 'codex'
12
+ && target.target === 'codex.skill'
13
+ && target.delivery?.mode === 'codex-native'
14
+ ? target.refreshKeys
15
+ : []));
16
+ }
17
+ function readPluginConfigBinding(value, scope) {
18
+ if (!isRecord(value))
19
+ return null;
20
+ if (scope === 'user') {
21
+ return value.kind === 'source'
22
+ && typeof value.sourceIdentity === 'string'
23
+ && /^sha256:[0-9a-f]{64}$/u.test(value.sourceIdentity)
24
+ && Object.keys(value).every((key) => ['kind', 'sourceIdentity'].includes(key))
25
+ ? { kind: 'source', sourceIdentity: value.sourceIdentity }
26
+ : null;
27
+ }
28
+ return value.kind === 'catalog'
29
+ && typeof value.logicalIdentity === 'string'
30
+ && value.logicalIdentity.trim().length > 0
31
+ && Object.keys(value).every((key) => ['kind', 'logicalIdentity'].includes(key))
32
+ ? { kind: 'catalog', logicalIdentity: value.logicalIdentity }
33
+ : null;
34
+ }
35
+ const PLUGIN_CONFIG_ENVELOPE_V4_KEYS = new Set([
36
+ 'schemaVersion',
37
+ 'sourceId',
38
+ 'pluginName',
39
+ 'binding',
40
+ 'values',
41
+ ]);
42
+ function readPluginConfigEnvelopeV4(value, scope) {
43
+ if (!isRecord(value)
44
+ || value.schemaVersion !== 4
45
+ || typeof value.sourceId !== 'string'
46
+ || typeof value.pluginName !== 'string'
47
+ || !isRecord(value.values)
48
+ || Object.keys(value).some((key) => !PLUGIN_CONFIG_ENVELOPE_V4_KEYS.has(key))) {
49
+ return null;
50
+ }
51
+ const binding = readPluginConfigBinding(value.binding, scope);
52
+ if (!binding)
53
+ return null;
54
+ return {
55
+ schemaVersion: 4,
56
+ sourceId: value.sourceId,
57
+ pluginName: value.pluginName,
58
+ binding,
59
+ values: value.values,
60
+ };
61
+ }
62
+ export class PluginConfigResetError extends Error {
63
+ code;
64
+ osCode;
65
+ constructor(code, message, osCode) {
66
+ super(`${code}: ${message}`);
67
+ this.name = 'PluginConfigResetError';
68
+ this.code = code;
69
+ this.osCode = osCode;
16
70
  }
17
- markActiveMarketplaceDurableCommit();
18
71
  }
19
72
  export class PluginConfigSchemaResolutionError extends Error {
20
73
  code;
@@ -40,16 +93,12 @@ const CONFIG_DIR = 'plugin-config';
40
93
  const CONFIG_FILE = 'config.json';
41
94
  let atomicConfigBeforeRenameHookForTests;
42
95
  let atomicConfigAfterRenameHookForTests;
43
- let configFileLockBeforeReleaseHookForTests;
44
96
  export function setAtomicConfigBeforeRenameHookForTests(hook) {
45
97
  atomicConfigBeforeRenameHookForTests = hook;
46
98
  }
47
99
  export function setAtomicConfigAfterRenameHookForTests(hook) {
48
100
  atomicConfigAfterRenameHookForTests = hook;
49
101
  }
50
- export function setConfigFileLockBeforeReleaseHookForTests(hook) {
51
- configFileLockBeforeReleaseHookForTests = hook;
52
- }
53
102
  function assertConfigPathBoundary(allowedRoot, candidatePath) {
54
103
  const root = path.resolve(allowedRoot);
55
104
  const candidate = path.resolve(candidatePath);
@@ -113,179 +162,40 @@ async function inspectConfigInventoryStoreRoot(boundaryRoot, storeRoot) {
113
162
  }
114
163
  return true;
115
164
  }
116
- async function readConfigDirectoryIdentity(directoryPath) {
117
- const stats = await fs.lstat(directoryPath);
118
- if (stats.isSymbolicLink() || !stats.isDirectory()) {
119
- throw new Error(`Config parent must be a real directory: ${directoryPath}`);
120
- }
121
- return {
122
- dev: stats.dev,
123
- ino: stats.ino,
124
- realPath: await fs.realpath(directoryPath),
125
- };
126
- }
127
- function assertConfigDirectoryStable(label, before, after) {
128
- if (before.dev !== after.dev || before.ino !== after.ino || before.realPath !== after.realPath) {
129
- throw new Error(`${label} identity changed before atomic config replacement.`);
130
- }
131
- }
132
- function assertRealConfigBoundary(root, parent) {
133
- const relative = path.relative(root.realPath, parent.realPath);
134
- if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
135
- throw new Error('Config parent realpath escapes its allowed root.');
136
- }
137
- }
138
165
  async function prepareConfigFileParent(filePath, allowedRoot) {
139
166
  assertConfigPathBoundary(allowedRoot, filePath);
140
167
  await assertNoConfigSymlink(allowedRoot, filePath);
141
168
  await fs.ensureDir(path.dirname(filePath));
142
169
  await assertNoConfigSymlink(allowedRoot, filePath);
143
170
  }
144
- export function resolveConfigFileLockPath(filePath) {
145
- return path.join(path.dirname(filePath), `.${path.basename(filePath)}.aw.lock`);
146
- }
147
- async function releaseConfigFileLock(lease) {
148
- let lastError;
149
- for (let attempt = 1; attempt <= 3; attempt += 1) {
150
- try {
151
- await configFileLockBeforeReleaseHookForTests?.(lease.lockPath, attempt);
152
- await lease.release();
153
- return null;
154
- }
155
- catch (error) {
156
- lastError = error;
157
- if (attempt < 3) {
158
- await new Promise((resolve) => setTimeout(resolve, 10));
159
- }
160
- }
161
- }
162
- try {
163
- await lease.release();
164
- }
165
- catch {
166
- // Preserve the first release diagnostic. A final direct retry avoids a hook-created self-lock.
167
- }
168
- return lastError;
169
- }
170
- function emitConfigReleaseWarning(message) {
171
- try {
172
- process.emitWarning(message, { code: 'AW_CONFIG_LOCK_RELEASE_FAILED' });
173
- }
174
- catch {
175
- // Diagnostics cannot replace the primary action result or error.
176
- }
177
- }
178
- export async function withConfigFileLock(options) {
171
+ export async function withConfigFileMutation(options) {
179
172
  await prepareConfigFileParent(options.filePath, options.allowedRoot);
180
- const lockPath = resolveConfigFileLockPath(options.filePath);
181
- await assertNoConfigSymlink(options.allowedRoot, lockPath);
182
- const lease = await acquireProcessLock({ lockPath });
183
- let result;
184
- let actionError;
185
- let actionSucceeded = false;
186
- const commitContext = {
187
- parent: configCommitStorage.getStore(),
188
- durableCommit: false,
189
- };
190
- try {
191
- await assertNoConfigSymlink(options.allowedRoot, options.filePath);
192
- result = await configCommitStorage.run(commitContext, options.action);
193
- actionSucceeded = true;
194
- }
195
- catch (error) {
196
- actionError = error;
197
- }
198
- const releaseError = await releaseConfigFileLock(lease);
199
- if (!actionSucceeded) {
200
- if (releaseError) {
201
- emitConfigReleaseWarning('Config action failed, and its process lock release also failed.');
202
- }
203
- throw actionError;
204
- }
205
- if (releaseError) {
206
- if (commitContext.durableCommit) {
207
- emitConfigReleaseWarning('Config action completed successfully, but its process lock could not be released after 3 attempts. The committed result remains successful; the lock can require process exit or manual recovery before another update.');
208
- }
209
- else {
210
- throw releaseError;
211
- }
212
- }
213
- return result;
173
+ await assertNoConfigSymlink(options.allowedRoot, options.filePath);
174
+ return options.action();
214
175
  }
215
- export async function withPluginConfigTransactionLease(options) {
216
- const storePath = resolvePluginConfigStorePath({
176
+ export async function atomicReplaceConfigFile(options) {
177
+ await atomicReplaceSafeFile({
217
178
  ...options,
218
- ...(options.scope === 'project' ? { access: 'write' } : {}),
219
- });
220
- const allowedRoot = resolvePluginConfigAllowedRoot(options);
221
- return withConfigFileLock({
222
- filePath: `${storePath}.transaction`,
223
- allowedRoot,
224
- action: options.action,
179
+ label: 'Config path',
180
+ temporaryTag: 'tmp',
181
+ beforeRename: atomicConfigBeforeRenameHookForTests,
182
+ afterRename: async (targetPath) => {
183
+ await atomicConfigAfterRenameHookForTests?.(targetPath).catch(() => undefined);
184
+ },
225
185
  });
226
186
  }
227
- export async function atomicReplaceConfigFile(options) {
228
- await prepareConfigFileParent(options.filePath, options.allowedRoot);
229
- const rootPath = path.resolve(options.allowedRoot);
230
- const parentPath = path.dirname(path.resolve(options.filePath));
231
- const rootIdentity = await readConfigDirectoryIdentity(rootPath);
232
- const parentIdentity = await readConfigDirectoryIdentity(parentPath);
233
- assertRealConfigBoundary(rootIdentity, parentIdentity);
234
- const currentTarget = await lstatIfExists(options.filePath);
235
- if (currentTarget && (!currentTarget.isFile() || currentTarget.isSymbolicLink())) {
236
- throw new Error(`Config target must be a regular file: ${options.filePath}`);
237
- }
238
- const temporaryPath = path.join(parentPath, `.${path.basename(options.filePath)}.tmp-${process.pid}-${crypto.randomUUID()}`);
239
- assertConfigPathBoundary(options.allowedRoot, temporaryPath);
240
- let handle;
241
- try {
242
- handle = await nodeFs.open(temporaryPath, 'wx', options.mode ?? (currentTarget ? currentTarget.mode & 0o777 : 0o600));
243
- await handle.writeFile(options.content, 'utf8');
244
- await handle.sync();
245
- await handle.close();
246
- handle = undefined;
247
- await atomicConfigBeforeRenameHookForTests?.(options.filePath, temporaryPath);
248
- await assertNoConfigSymlink(options.allowedRoot, options.filePath);
249
- const currentRootIdentity = await readConfigDirectoryIdentity(rootPath);
250
- const currentParentIdentity = await readConfigDirectoryIdentity(parentPath);
251
- assertRealConfigBoundary(currentRootIdentity, currentParentIdentity);
252
- assertConfigDirectoryStable('Config allowed root', rootIdentity, currentRootIdentity);
253
- assertConfigDirectoryStable('Config parent', parentIdentity, currentParentIdentity);
254
- const targetBeforeRename = await lstatIfExists(options.filePath);
255
- if (targetBeforeRename && (!targetBeforeRename.isFile() || targetBeforeRename.isSymbolicLink())) {
256
- throw new Error(`Config target must be a regular file: ${options.filePath}`);
257
- }
258
- await nodeFs.rename(temporaryPath, options.filePath);
259
- markConfigDurableCommit();
260
- // rename is the commit point. Diagnostics and durability work after it are best effort,
261
- // because reporting failure here would leave the new generation on disk without giving
262
- // the caller a safe way to restore the old generation.
263
- await atomicConfigAfterRenameHookForTests?.(options.filePath).catch(() => undefined);
264
- await (async () => {
265
- const finalTarget = await fs.lstat(options.filePath);
266
- if (!finalTarget.isFile() || finalTarget.isSymbolicLink()) {
267
- throw new Error('Atomic config replacement produced an invalid target.');
268
- }
269
- if (await fs.readFile(options.filePath, 'utf8') !== options.content) {
270
- throw new Error('Atomic config replacement content differs from the committed content.');
271
- }
272
- })().catch(() => undefined);
273
- const directoryHandle = await nodeFs.open(parentPath, 'r').catch(() => null);
274
- await directoryHandle?.sync().catch(() => undefined);
275
- await directoryHandle?.close().catch(() => undefined);
276
- }
277
- finally {
278
- await handle?.close().catch(() => undefined);
279
- await nodeFs.rm(temporaryPath, { force: true }).catch(() => undefined);
280
- }
281
- }
282
187
  export async function atomicRemoveConfigFile(options) {
283
188
  await prepareConfigFileParent(options.filePath, options.allowedRoot);
284
- const rootPath = path.resolve(options.allowedRoot);
285
- const parentPath = path.dirname(path.resolve(options.filePath));
286
- const rootIdentity = await readConfigDirectoryIdentity(rootPath);
287
- const parentIdentity = await readConfigDirectoryIdentity(parentPath);
288
- assertRealConfigBoundary(rootIdentity, parentIdentity);
189
+ await assertSafeFileMutationPath({
190
+ filePath: options.filePath,
191
+ allowedRoot: options.allowedRoot,
192
+ label: 'Config path',
193
+ });
194
+ await assertRealFileParentWithinRoot({
195
+ filePath: options.filePath,
196
+ allowedRoot: options.allowedRoot,
197
+ label: 'Config path',
198
+ });
289
199
  const currentTarget = await lstatIfExists(options.filePath);
290
200
  if (!currentTarget)
291
201
  return false;
@@ -293,12 +203,16 @@ export async function atomicRemoveConfigFile(options) {
293
203
  throw new Error(`Config target must be a regular file: ${options.filePath}`);
294
204
  }
295
205
  await atomicConfigBeforeRenameHookForTests?.(options.filePath, options.filePath);
296
- await assertNoConfigSymlink(options.allowedRoot, options.filePath);
297
- const currentRootIdentity = await readConfigDirectoryIdentity(rootPath);
298
- const currentParentIdentity = await readConfigDirectoryIdentity(parentPath);
299
- assertRealConfigBoundary(currentRootIdentity, currentParentIdentity);
300
- assertConfigDirectoryStable('Config allowed root', rootIdentity, currentRootIdentity);
301
- assertConfigDirectoryStable('Config parent', parentIdentity, currentParentIdentity);
206
+ await assertSafeFileMutationPath({
207
+ filePath: options.filePath,
208
+ allowedRoot: options.allowedRoot,
209
+ label: 'Config path',
210
+ });
211
+ await assertRealFileParentWithinRoot({
212
+ filePath: options.filePath,
213
+ allowedRoot: options.allowedRoot,
214
+ label: 'Config path',
215
+ });
302
216
  const targetBeforeRemove = await lstatIfExists(options.filePath);
303
217
  if (!targetBeforeRemove)
304
218
  return false;
@@ -306,18 +220,22 @@ export async function atomicRemoveConfigFile(options) {
306
220
  throw new Error(`Config target must be a regular file: ${options.filePath}`);
307
221
  }
308
222
  await nodeFs.unlink(options.filePath);
309
- markConfigDurableCommit();
310
223
  await atomicConfigAfterRenameHookForTests?.(options.filePath).catch(() => undefined);
311
- const directoryHandle = await nodeFs.open(parentPath, 'r').catch(() => null);
312
- await directoryHandle?.sync().catch(() => undefined);
313
- await directoryHandle?.close().catch(() => undefined);
314
224
  return true;
315
225
  }
316
226
  function isRecord(value) {
317
227
  return typeof value === 'object' && value !== null && !Array.isArray(value);
318
228
  }
319
229
  function formatValue(value) {
320
- return typeof value === 'string' ? value : JSON.stringify(value);
230
+ if (typeof value === 'string')
231
+ return value;
232
+ if (typeof value === 'number' || typeof value === 'boolean') {
233
+ return stablePluginConfigStringify(value);
234
+ }
235
+ if (isRecord(value)) {
236
+ return stablePluginConfigStringify(value);
237
+ }
238
+ return JSON.stringify(value);
321
239
  }
322
240
  async function calculateConfigManifestDigest(filePath) {
323
241
  const hash = crypto.createHash('sha256');
@@ -341,7 +259,7 @@ function createConfigSchemaEvidenceFromCurrentCatalog(plugin, manifestDigest) {
341
259
  };
342
260
  }
343
261
  function getStoreEntries(values) {
344
- return { ...values };
262
+ return Object.fromEntries(Object.entries(values));
345
263
  }
346
264
  export function assertSafePluginConfigSegment(pluginName) {
347
265
  if (typeof pluginName !== 'string' || pluginName.trim() === '') {
@@ -375,22 +293,15 @@ export function resolvePluginConfigStorePath(options) {
375
293
  const root = options.scope === 'user'
376
294
  ? resolveAwHome(options.env)
377
295
  : path.join(resolveAwProjectRoot(options.cwd), '.agent-workflows');
378
- const configDir = options.scope === 'project' && options.projectLayer === 'local'
379
- ? 'plugin-config.local'
380
- : CONFIG_DIR;
381
- const filePath = path.join(root, configDir, ...(sourceId ? [sourceId] : []), pluginName, CONFIG_FILE);
296
+ const filePath = path.join(root, CONFIG_DIR, ...(sourceId ? [sourceId] : []), pluginName, CONFIG_FILE);
382
297
  if (options.scope !== 'project') {
383
298
  return filePath;
384
299
  }
385
300
  if (options.access === 'read') {
386
- return assertTestReadablePath(filePath, options.projectLayer === 'local'
387
- ? 'project local plugin config store (.agent-workflows/plugin-config.local)'
388
- : 'project shared plugin config store (.agent-workflows/plugin-config)', '用 registerReadonlyFixtureRoot() 或 runWithRepositoryCwdAsReadOnly() 登记只读 project fixture 根;需要写入时改用 withSafeTestCwd() 或 registerWritableStateRoot()。');
301
+ return assertTestReadablePath(filePath, 'project plugin config store (.agent-workflows/plugin-config)', '用 registerReadonlyFixtureRoot() 或 runWithRepositoryCwdAsReadOnly() 登记只读 project fixture 根;需要写入时改用 withSafeTestCwd() 或 registerWritableStateRoot()。');
389
302
  }
390
303
  if (options.access === 'write') {
391
- return assertTestWritablePath(filePath, options.projectLayer === 'local'
392
- ? 'project local plugin config store (.agent-workflows/plugin-config.local)'
393
- : 'project shared plugin config store (.agent-workflows/plugin-config)', '用 withSafeTestCwd() 切到测试可写 cwd,或通过 registerWritableStateRoot() 登记 project fixture 根。');
304
+ return assertTestWritablePath(filePath, 'project plugin config store (.agent-workflows/plugin-config)', '用 withSafeTestCwd() 切到测试可写 cwd,或通过 registerWritableStateRoot() 登记 project fixture 根。');
394
305
  }
395
306
  return filePath;
396
307
  }
@@ -401,56 +312,143 @@ export function resolvePluginConfigStoreTargets(options) {
401
312
  path: resolvePluginConfigStorePath(options),
402
313
  }];
403
314
  }
404
- return [
405
- {
406
- storeKind: 'projectShared',
407
- path: resolvePluginConfigStorePath({
408
- ...options,
409
- projectLayer: 'shared',
410
- }),
411
- projectLayer: 'shared',
412
- },
413
- {
414
- storeKind: 'projectLocal',
415
- path: resolvePluginConfigStorePath({
416
- ...options,
417
- projectLayer: 'local',
418
- }),
419
- projectLayer: 'local',
420
- },
421
- ];
315
+ return [{
316
+ storeKind: 'project',
317
+ path: resolvePluginConfigStorePath(options),
318
+ }];
319
+ }
320
+ function isPluginConfigValue(value) {
321
+ if (typeof value === 'string' || typeof value === 'boolean')
322
+ return true;
323
+ if (typeof value === 'number')
324
+ return Number.isFinite(value);
325
+ if (!isRecord(value))
326
+ return false;
327
+ return Object.values(value).every((child) => typeof child === 'string'
328
+ || typeof child === 'boolean'
329
+ || (typeof child === 'number' && Number.isFinite(child)));
422
330
  }
423
- async function readStoreFile(filePath) {
424
- const value = await fs.readJson(filePath);
425
- if (!isRecord(value)) {
426
- throw new Error(`Invalid plugin config store "${filePath}": expected a JSON object.`);
427
- }
428
- return value;
331
+ function invalidStore(filePath, code, message, rawDigest) {
332
+ return {
333
+ path: filePath,
334
+ exists: true,
335
+ state: 'invalid',
336
+ values: {},
337
+ ...(rawDigest ? { rawDigest } : {}),
338
+ invalidCode: code,
339
+ issues: [message],
340
+ };
429
341
  }
430
- async function readNamespacedStoreFile(options) {
431
- const raw = await fs.readJson(options.filePath);
432
- if (!isRecord(raw) || raw.schemaVersion !== 2 || !isRecord(raw.values)) {
433
- throw new Error(`legacyStateUnsupported: invalid or legacy plugin config store "${options.filePath}".`);
342
+ function readyStore(filePath, values, rawDigest) {
343
+ return {
344
+ path: filePath,
345
+ exists: true,
346
+ state: 'ready',
347
+ values: canonicalizePluginConfigValues(values),
348
+ ...(rawDigest ? { rawDigest } : {}),
349
+ issues: [],
350
+ };
351
+ }
352
+ export function assertReadyPluginConfigStore(store) {
353
+ if (store.state !== 'invalid')
354
+ return;
355
+ throw new Error(`${store.invalidCode}: Invalid plugin config store "${store.path}". ${store.issues.join(' ')}`);
356
+ }
357
+ async function parseStoreJson(filePath) {
358
+ const content = await fs.readFile(filePath);
359
+ const rawDigest = `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`;
360
+ try {
361
+ return {
362
+ state: 'parsed',
363
+ value: JSON.parse(content.toString('utf8')),
364
+ rawDigest,
365
+ };
434
366
  }
435
- if (raw.sourceId !== options.sourceId || raw.pluginName !== options.pluginName) {
436
- throw new Error('Plugin config target identity does not match its namespaced path.');
367
+ catch (error) {
368
+ if (error instanceof SyntaxError) {
369
+ return {
370
+ state: 'invalidJson',
371
+ store: invalidStore(filePath, 'invalidJson', 'The store is not valid JSON.', rawDigest),
372
+ };
373
+ }
374
+ throw error;
437
375
  }
438
- if (raw.sourceIdentity !== options.sourceIdentity) {
439
- throw new Error('sourceChanged: plugin config source identity does not match the configured source.');
376
+ }
377
+ function validateStoreValues(options) {
378
+ if (!isRecord(options.values)) {
379
+ return invalidStore(options.filePath, 'invalidValues', 'The values field must be an object.', options.rawDigest);
440
380
  }
441
- return raw.values;
381
+ const values = Object.create(null);
382
+ const issues = [];
383
+ for (const [key, value] of Object.entries(options.values)) {
384
+ if (!isPluginConfigValue(value)) {
385
+ issues.push(`Config "${key}" is not a supported config value.`);
386
+ continue;
387
+ }
388
+ values[key] = value;
389
+ }
390
+ return issues.length > 0
391
+ ? invalidStore(options.filePath, 'invalidValues', issues.join(' '), options.rawDigest)
392
+ : readyStore(options.filePath, values, options.rawDigest);
393
+ }
394
+ async function readStoreFile(filePath) {
395
+ const parsed = await parseStoreJson(filePath);
396
+ if (parsed.state === 'invalidJson')
397
+ return parsed.store;
398
+ return validateStoreValues({ filePath, values: parsed.value, rawDigest: parsed.rawDigest });
399
+ }
400
+ async function readNamespacedStoreFile(options) {
401
+ const parsed = await parseStoreJson(options.filePath);
402
+ if (parsed.state === 'invalidJson')
403
+ return parsed.store;
404
+ const raw = parsed.value;
405
+ if (!isRecord(raw) || raw.schemaVersion !== 4) {
406
+ return invalidStore(options.filePath, 'schemaVersionUnsupported', 'Only plugin config store schemaVersion 4 is supported.', parsed.rawDigest);
407
+ }
408
+ const envelope = readPluginConfigEnvelopeV4(raw, options.scope);
409
+ if (!envelope) {
410
+ return invalidStore(options.filePath, 'schemaVersionUnsupported', 'The plugin config store schemaVersion 4 envelope is invalid.', parsed.rawDigest);
411
+ }
412
+ if (envelope.sourceId !== options.sourceId
413
+ || envelope.pluginName !== options.pluginName) {
414
+ return invalidStore(options.filePath, 'identityMismatch', 'The store identity does not match its source and namespaced path.', parsed.rawDigest);
415
+ }
416
+ const binding = envelope.binding;
417
+ const matchesBinding = options.scope === 'user'
418
+ ? binding?.kind === 'source'
419
+ && typeof options.sourceIdentity === 'string'
420
+ && binding.sourceIdentity === options.sourceIdentity
421
+ : binding?.kind === 'catalog'
422
+ && typeof options.logicalIdentity === 'string'
423
+ && binding.logicalIdentity === options.logicalIdentity;
424
+ if (!matchesBinding) {
425
+ return invalidStore(options.filePath, 'identityMismatch', options.scope === 'user'
426
+ ? 'The user store is not bound to the requested source identity.'
427
+ : 'The project store is not bound to the requested Catalog logical identity.', parsed.rawDigest);
428
+ }
429
+ return validateStoreValues({
430
+ filePath: options.filePath,
431
+ values: envelope.values,
432
+ rawDigest: parsed.rawDigest,
433
+ });
442
434
  }
443
435
  function namespacedConfigContent(options) {
436
+ const values = canonicalizePluginConfigValues(options.values);
444
437
  if (!options.sourceId)
445
- return `${JSON.stringify(options.values, null, 2)}\n`;
446
- if (!options.sourceIdentity)
447
- throw new Error('sourceIdentity is required for namespaced plugin config.');
438
+ return `${JSON.stringify(values, null, 2)}\n`;
439
+ const binding = options.scope === 'user'
440
+ ? options.sourceIdentity
441
+ ? { kind: 'source', sourceIdentity: options.sourceIdentity }
442
+ : (() => { throw new Error('sourceChanged: user plugin config requires a source identity.'); })()
443
+ : options.logicalIdentity
444
+ ? { kind: 'catalog', logicalIdentity: options.logicalIdentity }
445
+ : (() => { throw new Error('catalogUnavailable: project plugin config requires verified Catalog logical identity evidence.'); })();
448
446
  const envelope = {
449
- schemaVersion: 2,
447
+ schemaVersion: 4,
450
448
  sourceId: options.sourceId,
451
- sourceIdentity: options.sourceIdentity,
452
449
  pluginName: options.pluginName,
453
- values: options.values,
450
+ binding,
451
+ values,
454
452
  };
455
453
  return `${JSON.stringify(envelope, null, 2)}\n`;
456
454
  }
@@ -462,18 +460,12 @@ async function assertNoLegacyConfigCoexistence(options) {
462
460
  scope: options.scope,
463
461
  cwd: options.cwd,
464
462
  env: options.env,
465
- projectLayer: options.projectLayer,
466
463
  ...(options.scope === 'project' ? { access: 'read' } : {}),
467
464
  });
468
465
  if (await lstatIfExists(legacyPath)) {
469
466
  throw new Error('legacyStateUnsupported: legacy and namespaced plugin config stores cannot coexist.');
470
467
  }
471
468
  }
472
- async function withUserPluginConfigRuntimeLease(options, action) {
473
- return options.scope === 'user' || options.sourceId !== undefined
474
- ? withActiveMarketplaceRuntimeLease(options.env, action)
475
- : action();
476
- }
477
469
  export async function readPluginConfigStore(options) {
478
470
  const filePath = resolvePluginConfigStorePath({
479
471
  ...options,
@@ -481,14 +473,11 @@ export async function readPluginConfigStore(options) {
481
473
  });
482
474
  const allowedRoot = resolvePluginConfigAllowedRoot(options);
483
475
  if (options.sourceId) {
484
- if (!options.sourceIdentity)
485
- throw new Error('sourceIdentity is required for namespaced plugin config.');
486
476
  const legacyPath = resolvePluginConfigStorePath({
487
477
  pluginName: options.pluginName,
488
478
  scope: options.scope,
489
479
  cwd: options.cwd,
490
480
  env: options.env,
491
- projectLayer: options.projectLayer,
492
481
  ...(options.scope === 'project' ? { access: 'read' } : {}),
493
482
  });
494
483
  if (await inspectConfigReadTarget(allowedRoot, legacyPath)) {
@@ -499,21 +488,21 @@ export async function readPluginConfigStore(options) {
499
488
  return {
500
489
  path: filePath,
501
490
  exists: false,
491
+ state: 'missing',
502
492
  values: {},
493
+ issues: [],
503
494
  };
504
495
  }
505
- return {
506
- path: filePath,
507
- exists: true,
508
- values: options.sourceId
509
- ? await readNamespacedStoreFile({
510
- filePath,
511
- sourceId: options.sourceId,
512
- sourceIdentity: options.sourceIdentity,
513
- pluginName: options.pluginName,
514
- })
515
- : await readStoreFile(filePath),
516
- };
496
+ return options.sourceId
497
+ ? readNamespacedStoreFile({
498
+ filePath,
499
+ sourceId: options.sourceId,
500
+ scope: options.scope,
501
+ sourceIdentity: options.sourceIdentity,
502
+ logicalIdentity: options.logicalIdentity,
503
+ pluginName: options.pluginName,
504
+ })
505
+ : readStoreFile(filePath);
517
506
  }
518
507
  export async function readNamespacedPluginConfigStoreIdentity(options) {
519
508
  const filePath = resolvePluginConfigStorePath({
@@ -522,15 +511,21 @@ export async function readNamespacedPluginConfigStoreIdentity(options) {
522
511
  });
523
512
  if (!await inspectConfigReadTarget(resolvePluginConfigAllowedRoot(options), filePath))
524
513
  return null;
525
- const raw = await fs.readJson(filePath);
526
- if (!isRecord(raw) || raw.schemaVersion !== 2 || !isRecord(raw.values)
527
- || raw.sourceId !== options.sourceId || raw.pluginName !== options.pluginName
528
- || typeof raw.sourceIdentity !== 'string') {
529
- throw new Error('legacyStateUnsupported: invalid namespaced plugin config store.');
530
- }
514
+ const parsed = await parseStoreJson(filePath);
515
+ if (parsed.state === 'invalidJson') {
516
+ throw new Error(`${parsed.store.invalidCode}: invalid namespaced plugin config store.`);
517
+ }
518
+ const envelope = readPluginConfigEnvelopeV4(parsed.value, options.scope);
519
+ if (!envelope
520
+ || envelope.sourceId !== options.sourceId
521
+ || envelope.pluginName !== options.pluginName) {
522
+ throw new Error('schemaVersionUnsupported: invalid namespaced plugin config store.');
523
+ }
524
+ const store = validateStoreValues({ filePath, values: envelope.values });
525
+ assertReadyPluginConfigStore(store);
531
526
  return {
532
- sourceIdentity: raw.sourceIdentity,
533
- store: { path: filePath, exists: true, values: raw.values },
527
+ binding: envelope.binding,
528
+ store,
534
529
  };
535
530
  }
536
531
  async function listConfigStoreDirectoryPluginNames(storeRoot) {
@@ -561,15 +556,72 @@ export async function listPluginConfigStorePluginNames(options) {
561
556
  const storeRoots = options.scope === 'user'
562
557
  ? [path.join(resolveAwHome(options.env), CONFIG_DIR)]
563
558
  : options.scope === 'project'
564
- ? [path.join(projectRoot, '.agent-workflows', options.projectLayer === 'local' ? 'plugin-config.local' : CONFIG_DIR)]
559
+ ? [path.join(projectRoot, '.agent-workflows', CONFIG_DIR)]
565
560
  : [
566
561
  path.join(resolveAwHome(options.env), CONFIG_DIR),
567
562
  path.join(projectRoot, '.agent-workflows', CONFIG_DIR),
568
- path.join(projectRoot, '.agent-workflows', 'plugin-config.local'),
569
563
  ];
570
564
  const pluginNames = (await Promise.all(storeRoots.map((storeRoot) => listConfigStoreDirectoryPluginNames(storeRoot)))).flat();
571
565
  return [...new Set(pluginNames)].sort((left, right) => left.localeCompare(right));
572
566
  }
567
+ export async function listNamespacedPluginConfigStoreIdentities(options) {
568
+ const boundaryRoot = options.scope === 'user'
569
+ ? resolveAwHome(options.env)
570
+ : resolveAwProjectRoot(options.cwd);
571
+ if (options.scope === 'user') {
572
+ await canonicalizePathThroughExistingAncestor(boundaryRoot);
573
+ }
574
+ else {
575
+ await fs.realpath(boundaryRoot);
576
+ }
577
+ const storeRoot = path.join(boundaryRoot, ...(options.scope === 'project' ? ['.agent-workflows'] : []), CONFIG_DIR);
578
+ if (!await inspectConfigInventoryStoreRoot(boundaryRoot, storeRoot))
579
+ return [];
580
+ const output = [];
581
+ for (const sourceEntry of await fs.readdir(storeRoot, { withFileTypes: true })) {
582
+ if (!sourceEntry.isDirectory() || sourceEntry.isSymbolicLink())
583
+ continue;
584
+ let sourceId;
585
+ try {
586
+ sourceId = assertSafePluginConfigSegment(sourceEntry.name);
587
+ }
588
+ catch {
589
+ continue;
590
+ }
591
+ const sourceRoot = path.join(storeRoot, sourceId);
592
+ let pluginEntries;
593
+ try {
594
+ await assertNoConfigSymlink(boundaryRoot, sourceRoot);
595
+ pluginEntries = await fs.readdir(sourceRoot, { withFileTypes: true });
596
+ }
597
+ catch {
598
+ continue;
599
+ }
600
+ for (const pluginEntry of pluginEntries) {
601
+ if (!pluginEntry.isDirectory() || pluginEntry.isSymbolicLink())
602
+ continue;
603
+ let pluginName;
604
+ try {
605
+ pluginName = assertSafePluginConfigSegment(pluginEntry.name);
606
+ await assertNoConfigSymlink(boundaryRoot, path.join(sourceRoot, pluginName));
607
+ }
608
+ catch {
609
+ continue;
610
+ }
611
+ let target;
612
+ try {
613
+ target = await lstatIfExists(path.join(sourceRoot, pluginName, CONFIG_FILE));
614
+ }
615
+ catch {
616
+ continue;
617
+ }
618
+ if (!target)
619
+ continue;
620
+ output.push({ sourceId, pluginName, pluginId: `${sourceId}/${pluginName}` });
621
+ }
622
+ }
623
+ return output.sort((left, right) => left.pluginId.localeCompare(right.pluginId));
624
+ }
573
625
  export async function listNamespacedPluginConfigStores(options) {
574
626
  const boundaryRoot = options.scope === 'user'
575
627
  ? resolveAwHome(options.env)
@@ -580,9 +632,7 @@ export async function listNamespacedPluginConfigStores(options) {
580
632
  else {
581
633
  await fs.realpath(boundaryRoot);
582
634
  }
583
- const storeRoot = path.join(boundaryRoot, ...(options.scope === 'project' ? ['.agent-workflows'] : []), options.scope === 'project' && options.projectLayer === 'local'
584
- ? 'plugin-config.local'
585
- : CONFIG_DIR);
635
+ const storeRoot = path.join(boundaryRoot, ...(options.scope === 'project' ? ['.agent-workflows'] : []), CONFIG_DIR);
586
636
  const output = [];
587
637
  if (!await inspectConfigInventoryStoreRoot(boundaryRoot, storeRoot))
588
638
  return output;
@@ -604,18 +654,24 @@ export async function listNamespacedPluginConfigStores(options) {
604
654
  const filePath = path.join(storeRoot, sourceId, pluginName, CONFIG_FILE);
605
655
  if (!await inspectConfigReadTarget(storeRoot, filePath))
606
656
  continue;
607
- const raw = await fs.readJson(filePath);
608
- if (!isRecord(raw) || raw.schemaVersion !== 2 || !isRecord(raw.values)
609
- || raw.sourceId !== sourceId || raw.pluginName !== pluginName
610
- || typeof raw.sourceIdentity !== 'string') {
611
- throw new Error(`legacyStateUnsupported: invalid namespaced plugin config store "${filePath}".`);
657
+ const parsed = await parseStoreJson(filePath);
658
+ if (parsed.state === 'invalidJson') {
659
+ throw new Error(`${parsed.store.invalidCode}: invalid namespaced plugin config store "${filePath}".`);
612
660
  }
661
+ const envelope = readPluginConfigEnvelopeV4(parsed.value, options.scope);
662
+ if (!envelope
663
+ || envelope.sourceId !== sourceId
664
+ || envelope.pluginName !== pluginName) {
665
+ throw new Error(`schemaVersionUnsupported: invalid namespaced plugin config store "${filePath}".`);
666
+ }
667
+ const store = validateStoreValues({ filePath, values: envelope.values });
668
+ assertReadyPluginConfigStore(store);
613
669
  output.push({
614
670
  sourceId,
615
- sourceIdentity: raw.sourceIdentity,
671
+ binding: envelope.binding,
616
672
  pluginName,
617
673
  pluginId: `${sourceId}/${pluginName}`,
618
- values: raw.values,
674
+ values: store.values,
619
675
  });
620
676
  }
621
677
  }
@@ -628,22 +684,39 @@ export async function writePluginConfigStore(options) {
628
684
  ...(options.scope === 'project' ? { access: 'write' } : {}),
629
685
  });
630
686
  const allowedRoot = resolvePluginConfigAllowedRoot(options);
631
- await withUserPluginConfigRuntimeLease(options, async () => {
632
- await withConfigFileLock({
633
- filePath,
634
- allowedRoot,
635
- action: async () => atomicReplaceConfigFile({
687
+ const validated = validateStoreValues({ filePath, values: options.values });
688
+ assertReadyPluginConfigStore(validated);
689
+ const warnings = [];
690
+ await withConfigFileMutation({
691
+ filePath,
692
+ allowedRoot,
693
+ action: async () => {
694
+ if (Object.keys(validated.values).length === 0) {
695
+ return atomicRemoveConfigFile({ filePath, allowedRoot });
696
+ }
697
+ if (options.scope === 'project') {
698
+ const visibility = await assertProjectSharedConfigPathVisible({
699
+ cwd: options.cwd,
700
+ targetPath: filePath,
701
+ env: options.env,
702
+ });
703
+ if (visibility?.state === 'unverified') {
704
+ warnings.push(PROJECT_CONFIG_SHAREABILITY_UNVERIFIED_WARNING);
705
+ }
706
+ }
707
+ return atomicReplaceConfigFile({
636
708
  filePath,
637
709
  allowedRoot,
638
- content: namespacedConfigContent(options),
710
+ content: namespacedConfigContent({ ...options, values: validated.values }),
639
711
  mode: 0o600,
640
- }),
641
- });
712
+ });
713
+ },
642
714
  });
643
715
  return {
644
716
  pluginName: options.pluginName,
645
717
  scope: options.scope,
646
718
  path: filePath,
719
+ warnings,
647
720
  };
648
721
  }
649
722
  export async function patchPluginConfigStore(options) {
@@ -654,33 +727,50 @@ export async function patchPluginConfigStore(options) {
654
727
  });
655
728
  const allowedRoot = resolvePluginConfigAllowedRoot(options);
656
729
  const removedKeys = options.removeKeys ?? [];
657
- await withUserPluginConfigRuntimeLease(options, async () => {
658
- await withConfigFileLock({
659
- filePath,
660
- allowedRoot,
661
- action: async () => {
662
- await options.reviewPreflight?.();
663
- const current = (await lstatIfExists(filePath))
664
- ? options.sourceId
665
- ? await readNamespacedStoreFile({
666
- filePath,
667
- sourceId: options.sourceId,
668
- sourceIdentity: options.sourceIdentity,
669
- pluginName: options.pluginName,
670
- })
671
- : await readStoreFile(filePath)
672
- : {};
673
- if (options.requireExistingRemoveKeys) {
674
- const missingKey = removedKeys.find((key) => !Object.hasOwn(current, key));
675
- if (missingKey)
676
- throw new Error(`Unknown config key "${missingKey}".`);
677
- }
678
- const next = { ...current };
679
- for (const key of removedKeys) {
680
- delete next[key];
681
- }
682
- for (const [key, value] of Object.entries(options.updates)) {
683
- next[key] = value;
730
+ const warnings = [];
731
+ await withConfigFileMutation({
732
+ filePath,
733
+ allowedRoot,
734
+ action: async () => {
735
+ await options.reviewPreflight?.();
736
+ const current = (await lstatIfExists(filePath))
737
+ ? options.sourceId
738
+ ? await readNamespacedStoreFile({
739
+ filePath,
740
+ sourceId: options.sourceId,
741
+ scope: options.scope,
742
+ sourceIdentity: options.sourceIdentity,
743
+ logicalIdentity: options.logicalIdentity,
744
+ pluginName: options.pluginName,
745
+ })
746
+ : await readStoreFile(filePath)
747
+ : readyStore(filePath, {});
748
+ assertReadyPluginConfigStore(current);
749
+ if (options.requireExistingRemoveKeys) {
750
+ const missingKey = removedKeys.find((key) => !Object.hasOwn(current.values, key));
751
+ if (missingKey)
752
+ throw new Error(`Unknown config key "${missingKey}".`);
753
+ }
754
+ const next = { ...current.values };
755
+ for (const key of removedKeys) {
756
+ delete next[key];
757
+ }
758
+ for (const [key, value] of Object.entries(options.updates)) {
759
+ next[key] = value;
760
+ }
761
+ if (Object.keys(next).length === 0) {
762
+ await atomicRemoveConfigFile({ filePath, allowedRoot });
763
+ }
764
+ else {
765
+ if (options.scope === 'project') {
766
+ const visibility = await assertProjectSharedConfigPathVisible({
767
+ cwd: options.cwd,
768
+ targetPath: filePath,
769
+ env: options.env,
770
+ });
771
+ if (visibility?.state === 'unverified') {
772
+ warnings.push(PROJECT_CONFIG_SHAREABILITY_UNVERIFIED_WARNING);
773
+ }
684
774
  }
685
775
  await atomicReplaceConfigFile({
686
776
  filePath,
@@ -688,65 +778,170 @@ export async function patchPluginConfigStore(options) {
688
778
  content: namespacedConfigContent({ ...options, values: next }),
689
779
  mode: 0o600,
690
780
  });
691
- },
692
- });
781
+ }
782
+ },
693
783
  });
694
784
  return {
695
785
  pluginName: options.pluginName,
696
786
  scope: options.scope,
697
787
  path: filePath,
788
+ warnings,
698
789
  updatedKeys: Object.keys(options.updates),
699
790
  removedKeys,
700
791
  };
701
792
  }
702
793
  export async function resetPluginConfigStore(options) {
703
- await assertNoLegacyConfigCoexistence(options);
704
794
  const filePath = resolvePluginConfigStorePath({
705
795
  ...options,
706
796
  ...(options.scope === 'project' ? { access: 'write' } : {}),
707
797
  });
708
798
  const allowedRoot = resolvePluginConfigAllowedRoot(options);
709
- const existed = await withUserPluginConfigRuntimeLease(options, async () => {
710
- return withConfigFileLock({
711
- filePath,
712
- allowedRoot,
713
- action: async () => {
714
- await options.reviewPreflight?.();
715
- const target = await lstatIfExists(filePath);
716
- if (!target)
717
- return false;
718
- if (!target.isFile() || target.isSymbolicLink()) {
719
- throw new Error(`Config target must be a regular file: ${filePath}`);
720
- }
721
- if (options.sourceId) {
722
- if (!options.sourceIdentity) {
723
- throw new Error('sourceIdentity is required for namespaced plugin config.');
724
- }
725
- await readNamespacedStoreFile({
726
- filePath,
727
- sourceId: options.sourceId,
728
- sourceIdentity: options.sourceIdentity,
729
- pluginName: options.pluginName,
730
- });
731
- }
732
- else {
733
- await readStoreFile(filePath);
734
- }
735
- return atomicRemoveConfigFile({ filePath, allowedRoot });
736
- },
737
- });
799
+ const publicTarget = `${options.scope}/plugin-config/${[
800
+ options.sourceId,
801
+ options.pluginName,
802
+ CONFIG_FILE,
803
+ ].filter(Boolean).join('/')}`;
804
+ const { root, candidate } = assertConfigPathBoundary(allowedRoot, filePath);
805
+ let current = root;
806
+ try {
807
+ for (const segment of path.relative(root, path.dirname(candidate)).split(path.sep).filter(Boolean)) {
808
+ const stats = await lstatIfExists(current);
809
+ if (!stats) {
810
+ return {
811
+ pluginName: options.pluginName,
812
+ scope: options.scope,
813
+ storeKind: options.scope,
814
+ path: filePath,
815
+ existed: false,
816
+ deleted: false,
817
+ resetDisposition: 'absent',
818
+ };
819
+ }
820
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
821
+ throw new PluginConfigResetError('configResetTargetUnsupported', `${publicTarget} has an unsafe ancestor. Repair it manually, then retry config reset.`);
822
+ }
823
+ current = path.join(current, segment);
824
+ }
825
+ const parentStats = await lstatIfExists(current);
826
+ if (!parentStats) {
827
+ return {
828
+ pluginName: options.pluginName,
829
+ scope: options.scope,
830
+ storeKind: options.scope,
831
+ path: filePath,
832
+ existed: false,
833
+ deleted: false,
834
+ resetDisposition: 'absent',
835
+ };
836
+ }
837
+ if (parentStats.isSymbolicLink() || !parentStats.isDirectory()) {
838
+ throw new PluginConfigResetError('configResetTargetUnsupported', `${publicTarget} has an unsafe ancestor. Repair it manually, then retry config reset.`);
839
+ }
840
+ await options.reviewPreflight?.();
841
+ const target = await lstatIfExists(filePath);
842
+ if (!target) {
843
+ return {
844
+ pluginName: options.pluginName,
845
+ scope: options.scope,
846
+ storeKind: options.scope,
847
+ path: filePath,
848
+ existed: false,
849
+ deleted: false,
850
+ resetDisposition: 'absent',
851
+ };
852
+ }
853
+ if (target.isFile() || target.isSymbolicLink()) {
854
+ await assertNoConfigSymlink(allowedRoot, path.dirname(filePath));
855
+ await nodeFs.unlink(filePath);
856
+ return {
857
+ pluginName: options.pluginName,
858
+ scope: options.scope,
859
+ storeKind: options.scope,
860
+ path: filePath,
861
+ existed: true,
862
+ deleted: true,
863
+ resetDisposition: target.isSymbolicLink() ? 'unlinkedSymlink' : 'deletedFile',
864
+ };
865
+ }
866
+ if (target.isDirectory()) {
867
+ const timestamp = new Date().toISOString().replace(/[-:]/gu, '').replace(/\..*$/u, '');
868
+ const quarantineBasename = `config.broken-${timestamp}-${crypto.randomUUID()}.json`;
869
+ await assertNoConfigSymlink(allowedRoot, path.dirname(filePath));
870
+ await nodeFs.rename(filePath, path.join(path.dirname(filePath), quarantineBasename));
871
+ return {
872
+ pluginName: options.pluginName,
873
+ scope: options.scope,
874
+ storeKind: options.scope,
875
+ path: filePath,
876
+ existed: true,
877
+ deleted: true,
878
+ resetDisposition: 'quarantinedDirectory',
879
+ quarantineBasename,
880
+ };
881
+ }
882
+ throw new PluginConfigResetError('configResetTargetUnsupported', `${publicTarget} is a special filesystem node. Move it manually, then retry config reset.`);
883
+ }
884
+ catch (error) {
885
+ if (error instanceof PluginConfigResetError)
886
+ throw error;
887
+ const osCode = error.code;
888
+ throw new PluginConfigResetError('configResetFailed', `Cannot reset ${publicTarget}${osCode ? ` (${osCode})` : ''}. Fix filesystem access, then retry config reset.`, osCode);
889
+ }
890
+ }
891
+ export async function inspectPluginConfigResetTarget(options) {
892
+ const filePath = resolvePluginConfigStorePath({
893
+ ...options,
894
+ ...(options.scope === 'project' ? { access: 'write' } : {}),
738
895
  });
896
+ const allowedRoot = resolvePluginConfigAllowedRoot(options);
897
+ const publicTarget = `${options.scope}/plugin-config/${[
898
+ options.sourceId,
899
+ options.pluginName,
900
+ CONFIG_FILE,
901
+ ].filter(Boolean).join('/')}`;
902
+ const { root, candidate } = assertConfigPathBoundary(allowedRoot, filePath);
903
+ let current = root;
904
+ for (const segment of path.relative(root, path.dirname(candidate)).split(path.sep).filter(Boolean)) {
905
+ const stats = await lstatIfExists(current);
906
+ if (!stats)
907
+ return resetTargetInspection('absent');
908
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
909
+ throw new PluginConfigResetError('configResetTargetUnsupported', `${publicTarget} has an unsafe ancestor. Repair it manually, then retry config reset.`);
910
+ }
911
+ current = path.join(current, segment);
912
+ }
913
+ const parentStats = await lstatIfExists(current);
914
+ if (!parentStats)
915
+ return resetTargetInspection('absent');
916
+ if (parentStats.isSymbolicLink() || !parentStats.isDirectory()) {
917
+ throw new PluginConfigResetError('configResetTargetUnsupported', `${publicTarget} has an unsafe ancestor. Repair it manually, then retry config reset.`);
918
+ }
919
+ const target = await lstatIfExists(filePath);
920
+ if (!target)
921
+ return resetTargetInspection('absent');
922
+ const kind = target.isSymbolicLink() ? 'symbolicLink'
923
+ : target.isFile() ? 'regularFile'
924
+ : target.isDirectory() ? 'directory'
925
+ : 'special';
926
+ return resetTargetInspection(kind, target);
927
+ }
928
+ function resetTargetInspection(kind, stats) {
929
+ const fingerprint = `sha256:${crypto.createHash('sha256').update(JSON.stringify({
930
+ kind,
931
+ ...(stats ? {
932
+ dev: stats.dev,
933
+ ino: stats.ino,
934
+ mode: stats.mode,
935
+ size: stats.size,
936
+ mtimeMs: stats.mtimeMs,
937
+ ctimeMs: stats.ctimeMs,
938
+ } : {}),
939
+ })).digest('hex')}`;
739
940
  return {
740
- pluginName: options.pluginName,
741
- scope: options.scope,
742
- storeKind: options.scope === 'user'
743
- ? 'user'
744
- : options.projectLayer === 'local'
745
- ? 'projectLocal'
746
- : 'projectShared',
747
- path: filePath,
748
- existed,
749
- deleted: existed,
941
+ exists: kind !== 'absent',
942
+ kind,
943
+ resettable: kind !== 'special',
944
+ fingerprint,
750
945
  };
751
946
  }
752
947
  export async function resetPluginConfigStores(options) {
@@ -757,228 +952,167 @@ export async function resetPluginConfigStores(options) {
757
952
  });
758
953
  const allowedRoot = resolvePluginConfigAllowedRoot(options);
759
954
  const receipts = [];
760
- await withUserPluginConfigRuntimeLease(options, async () => {
761
- const withTargetLocks = async (index, action) => {
762
- const target = targets[index];
763
- if (!target)
764
- return action();
765
- return withConfigFileLock({
766
- filePath: target.path,
767
- allowedRoot,
768
- action: () => withTargetLocks(index + 1, action),
769
- });
770
- };
771
- await withTargetLocks(0, async () => {
772
- const existing = [];
773
- for (const target of targets) {
774
- const stats = await lstatIfExists(target.path);
775
- existing.push(stats !== null);
776
- if (!stats)
777
- continue;
778
- if (!stats.isFile() || stats.isSymbolicLink()) {
779
- throw new Error(`Config target must be a regular file: ${target.path}`);
780
- }
781
- if (options.sourceId) {
782
- if (!options.sourceIdentity) {
783
- throw new Error('sourceIdentity is required for namespaced plugin config.');
784
- }
785
- await readNamespacedStoreFile({
786
- filePath: target.path,
787
- sourceId: options.sourceId,
788
- sourceIdentity: options.sourceIdentity,
789
- pluginName: options.pluginName,
790
- });
791
- }
792
- else {
793
- await readStoreFile(target.path);
794
- }
795
- }
796
- for (const [index, target] of targets.entries()) {
797
- const deleted = await atomicRemoveConfigFile({
798
- filePath: target.path,
799
- allowedRoot,
800
- });
801
- receipts.push({
802
- pluginName: options.pluginName,
803
- scope: options.scope,
804
- storeKind: target.storeKind,
805
- path: target.path,
806
- existed: existing[index],
807
- deleted,
808
- });
809
- }
810
- });
811
- });
812
- return receipts;
813
- }
814
- function coercePrimitiveValue(key, field, value) {
815
- if (field.type === 'string') {
816
- if (typeof value !== 'string') {
817
- throw new Error(`Config "${key}" must be a string.`);
818
- }
819
- return value;
820
- }
821
- if (field.type === 'number') {
822
- const numberValue = typeof value === 'number'
823
- ? value
824
- : typeof value === 'string' && value.trim() !== ''
825
- ? Number(value)
826
- : Number.NaN;
827
- if (!Number.isFinite(numberValue)) {
828
- throw new Error(`Config "${key}" must be a number.`);
829
- }
830
- if (field.min !== undefined && numberValue < field.min) {
831
- throw new Error(`Config "${key}" must be greater than or equal to ${field.min}.`);
832
- }
833
- if (field.max !== undefined && numberValue > field.max) {
834
- throw new Error(`Config "${key}" must be less than or equal to ${field.max}.`);
835
- }
836
- return numberValue;
837
- }
838
- if (field.type === 'boolean') {
839
- if (typeof value === 'boolean') {
840
- return value;
841
- }
842
- if (typeof value === 'string') {
843
- const normalized = value.trim().toLowerCase();
844
- if (['true', '1', 'yes', 'y'].includes(normalized)) {
845
- return true;
846
- }
847
- if (['false', '0', 'no', 'n'].includes(normalized)) {
848
- return false;
849
- }
850
- }
851
- throw new Error(`Config "${key}" must be a boolean.`);
852
- }
853
- if (field.type === 'enum') {
854
- if (typeof value !== 'string') {
855
- throw new Error(`Config "${key}" must be an enum string.`);
856
- }
857
- if (!field.values?.includes(value)) {
858
- throw new Error(`Config "${key}" must be one of: ${(field.values ?? []).join(', ')}.`);
955
+ const existing = [];
956
+ for (const target of targets) {
957
+ await prepareConfigFileParent(target.path, allowedRoot);
958
+ await assertNoConfigSymlink(allowedRoot, target.path);
959
+ const stats = await lstatIfExists(target.path);
960
+ existing.push(stats !== null);
961
+ if (!stats)
962
+ continue;
963
+ if (!stats.isFile() || stats.isSymbolicLink()) {
964
+ throw new Error(`Config target must be a regular file: ${target.path}`);
859
965
  }
860
- return value;
861
966
  }
862
- if (field.type === 'file' || field.type === 'directory') {
863
- if (typeof value !== 'string' || value.trim() === '') {
864
- throw new Error(`Config "${key}" must be a path string.`);
865
- }
866
- return value;
967
+ for (const [index, target] of targets.entries()) {
968
+ const deleted = await atomicRemoveConfigFile({
969
+ filePath: target.path,
970
+ allowedRoot,
971
+ });
972
+ receipts.push({
973
+ pluginName: options.pluginName,
974
+ scope: options.scope,
975
+ storeKind: target.storeKind,
976
+ path: target.path,
977
+ existed: existing[index],
978
+ deleted,
979
+ resetDisposition: deleted ? 'deletedFile' : 'absent',
980
+ });
867
981
  }
868
- throw new Error(`Config "${key}" has unsupported type "${field.type}".`);
982
+ return receipts;
869
983
  }
870
- async function resolvePathConfigValue(options) {
871
- if (options.field.type !== 'file' && options.field.type !== 'directory') {
872
- return String(options.value);
984
+ function coerceConfigValue(key, field, value) {
985
+ let coerced = value;
986
+ if (field.type === 'number' && typeof value === 'string' && value.trim() !== '') {
987
+ coerced = Number(value);
873
988
  }
874
- const rawPath = String(options.value);
875
- const resolvedPath = path.isAbsolute(rawPath)
876
- ? rawPath
877
- : path.resolve(options.projectRoot, rawPath);
878
- const realPath = await fs.realpath(resolvedPath);
879
- const stats = await fs.stat(realPath);
880
- if (options.field.type === 'file' && !stats.isFile()) {
881
- throw new Error(`Config "${options.key}" must point to an existing file.`);
989
+ else if (field.type === 'boolean' && typeof value === 'string') {
990
+ const normalized = value.trim().toLowerCase();
991
+ if (['true', '1', 'yes', 'y'].includes(normalized))
992
+ coerced = true;
993
+ if (['false', '0', 'no', 'n'].includes(normalized))
994
+ coerced = false;
882
995
  }
883
- if (options.field.type === 'directory' && !stats.isDirectory()) {
884
- throw new Error(`Config "${options.key}" must point to an existing directory.`);
996
+ const issues = validateCorePluginConfigValue(field, coerced, key);
997
+ if (issues.length > 0) {
998
+ throw new Error(formatPluginConfigIssue(issues[0]));
885
999
  }
886
- return realPath;
1000
+ return coerced;
887
1001
  }
888
1002
  export async function coercePluginConfigValue(options) {
889
- const primitive = coercePrimitiveValue(options.key, options.field, options.value);
890
- if (options.field.type === 'file' || options.field.type === 'directory') {
891
- return resolvePathConfigValue({
892
- key: options.key,
893
- field: options.field,
894
- value: primitive,
895
- projectRoot: options.projectRoot,
896
- });
1003
+ return coerceConfigValue(options.key, options.field, options.value);
1004
+ }
1005
+ function validateConfigReadValue(key, field, value) {
1006
+ const issues = validateCorePluginConfigValue(field, value, key);
1007
+ if (issues.length > 0) {
1008
+ throw new Error(formatPluginConfigIssue(issues[0]));
897
1009
  }
898
- return primitive;
1010
+ return value;
899
1011
  }
900
1012
  function redactValue(field, value) {
901
- return field.sensitive ? '[redacted]' : value;
1013
+ return redactPluginConfigValue(field, value);
902
1014
  }
903
- function sourceForStoreLayer(scope, projectLayer = 'shared') {
904
- if (scope === 'user') {
905
- return 'user';
906
- }
907
- return projectLayer === 'local' ? 'projectLocal' : 'project';
908
- }
909
- function isPathTypeField(field) {
910
- return field.type === 'file' || field.type === 'directory';
1015
+ function sourceForStoreLayer(scope) {
1016
+ return scope === 'user' ? 'user' : 'project';
911
1017
  }
912
1018
  async function applyConfigLayer(options) {
913
1019
  for (const [key, field] of Object.entries(options.schema)) {
914
1020
  if (!Object.hasOwn(options.entries, key)) {
915
1021
  continue;
916
1022
  }
917
- try {
918
- const coerced = await coercePluginConfigValue({
919
- key,
920
- field,
921
- value: options.entries[key],
922
- projectRoot: options.projectRoot,
923
- });
924
- options.values[key] = coerced;
925
- options.redactedValues[key] = redactValue(field, coerced);
1023
+ const value = options.entries[key];
1024
+ const validationIssues = validateCorePluginConfigValue(field, value, key);
1025
+ if (validationIssues.length === 0) {
1026
+ const redactedValue = redactValue(field, value);
1027
+ options.values[key] = value;
1028
+ options.redactedValues[key] = redactedValue;
926
1029
  options.sources[key] = options.source;
927
- options.invalidIssues.delete(key);
1030
+ options.fieldStates[key] = {
1031
+ effective: { value, redactedValue, source: options.source },
1032
+ };
928
1033
  }
929
- catch (error) {
1034
+ else {
1035
+ const fallback = options.fieldStates[key]?.effective;
930
1036
  delete options.values[key];
931
1037
  delete options.redactedValues[key];
932
1038
  options.sources[key] = options.source;
933
- options.invalidIssues.set(key, {
1039
+ const issues = validationIssues.map((issue) => ({
934
1040
  type: 'invalid-config',
935
1041
  key,
936
1042
  source: options.source,
937
- message: error instanceof Error ? error.message : String(error),
938
- });
1043
+ message: formatPluginConfigIssue(issue),
1044
+ }));
1045
+ options.fieldStates[key] = {
1046
+ invalidOverride: { source: options.source, issues },
1047
+ ...(fallback ? { fallback } : {}),
1048
+ };
939
1049
  }
940
1050
  }
941
1051
  }
942
1052
  export async function resolveEffectivePluginConfig(options) {
943
1053
  const pluginName = assertSafePluginConfigSegment(options.pluginName ?? options.manifest.name);
944
- const schema = options.manifest.config?.schema;
1054
+ const schema = options.manifest.config?.fields;
945
1055
  if (!schema) {
946
1056
  throw new Error(`Plugin "${pluginName}" does not declare an AW config schema.`);
947
1057
  }
948
1058
  const projectRoot = resolveAwProjectRoot(options.cwd);
949
- const [userStore, projectStore, projectLocalStore] = await Promise.all([
950
- readPluginConfigStore({
951
- sourceId: options.sourceId,
952
- sourceIdentity: options.sourceIdentity,
953
- pluginName,
954
- scope: 'user',
955
- cwd: options.cwd,
956
- env: options.env,
957
- }),
958
- readPluginConfigStore({
959
- sourceId: options.sourceId,
960
- sourceIdentity: options.sourceIdentity,
961
- pluginName,
962
- scope: 'project',
963
- cwd: options.cwd,
964
- env: options.env,
965
- }),
966
- readPluginConfigStore({
1059
+ const userStore = await readPluginConfigStore({
1060
+ sourceId: options.sourceId,
1061
+ sourceIdentity: options.sourceIdentity,
1062
+ logicalIdentity: options.logicalIdentity,
1063
+ pluginName,
1064
+ scope: 'user',
1065
+ cwd: options.cwd,
1066
+ env: options.env,
1067
+ });
1068
+ const projectStore = options.targetScope === 'user'
1069
+ ? undefined
1070
+ : await readPluginConfigStore({
967
1071
  sourceId: options.sourceId,
968
1072
  sourceIdentity: options.sourceIdentity,
1073
+ logicalIdentity: options.logicalIdentity,
969
1074
  pluginName,
970
1075
  scope: 'project',
971
1076
  cwd: options.cwd,
972
1077
  env: options.env,
973
- projectLayer: 'local',
974
- }),
975
- ]);
1078
+ });
1079
+ assertReadyPluginConfigStore(userStore);
1080
+ if (projectStore)
1081
+ assertReadyPluginConfigStore(projectStore);
1082
+ return resolveEffectivePluginConfigFromStores({
1083
+ pluginName,
1084
+ manifest: options.manifest,
1085
+ targetScope: options.targetScope,
1086
+ projectRoot,
1087
+ stores: {
1088
+ user: userStore,
1089
+ ...(projectStore ? { project: projectStore } : {}),
1090
+ },
1091
+ });
1092
+ }
1093
+ export async function resolveEffectivePluginConfigFromStores(options) {
1094
+ const pluginName = assertSafePluginConfigSegment(options.pluginName ?? options.manifest.name);
1095
+ const schema = options.manifest.config?.fields;
1096
+ if (!schema) {
1097
+ throw new Error(`Plugin "${pluginName}" does not declare an AW config schema.`);
1098
+ }
1099
+ const userStore = options.stores.user;
1100
+ const projectStore = options.targetScope === 'user' ? undefined : options.stores.project;
1101
+ assertReadyPluginConfigStore(userStore);
1102
+ if (!projectStore && options.targetScope !== 'user') {
1103
+ throw new Error(`Project plugin config store is required for ${pluginName}.`);
1104
+ }
1105
+ if (projectStore)
1106
+ assertReadyPluginConfigStore(projectStore);
976
1107
  const values = {};
977
1108
  const redactedValues = {};
978
1109
  const sources = {};
979
- const invalidIssues = new Map();
1110
+ const fieldStates = {};
980
1111
  const ignoredUnknownKeys = [];
1112
+ const unsupportedLayerOverrides = [];
1113
+ const dangerousKeyIssues = [];
981
1114
  const schemaKeys = new Set(Object.keys(schema));
1115
+ const userOnlyKeys = codexNativeRefreshKeys(options.manifest);
982
1116
  for (const key of schemaKeys) {
983
1117
  sources[key] = 'missing';
984
1118
  }
@@ -990,43 +1124,69 @@ export async function resolveEffectivePluginConfig(options) {
990
1124
  values,
991
1125
  redactedValues,
992
1126
  sources,
993
- invalidIssues,
1127
+ fieldStates,
994
1128
  schema: { [key]: field },
995
1129
  entries: { [key]: field.default },
996
1130
  source: 'default',
997
- projectRoot,
998
1131
  });
999
1132
  }
1000
- for (const [scope, store, projectLayer] of [
1001
- ['user', userStore, 'shared'],
1002
- ['project', projectStore, 'shared'],
1003
- ['project', projectLocalStore, 'local'],
1004
- ]) {
1133
+ const layers = options.targetScope === 'user'
1134
+ ? [['user', userStore]]
1135
+ : [
1136
+ ['user', userStore],
1137
+ ['project', projectStore],
1138
+ ];
1139
+ for (const [scope, store] of layers) {
1005
1140
  const plainEntries = getStoreEntries(store.values);
1141
+ const source = sourceForStoreLayer(scope);
1142
+ const dangerousIssues = new Map(validatePluginConfigDocument({ fields: schema }, plainEntries).filter((issue) => issue.code === 'dangerousKey')
1143
+ .map((issue) => [issue.path.field, issue]));
1006
1144
  for (const key of Object.keys(plainEntries)) {
1007
1145
  if (!schemaKeys.has(key)) {
1008
- ignoredUnknownKeys.push({
1009
- scope,
1010
- projectLayer,
1011
- source: sourceForStoreLayer(scope, projectLayer),
1012
- key,
1013
- });
1146
+ const dangerousIssue = dangerousIssues.get(key);
1147
+ if (dangerousIssue) {
1148
+ dangerousKeyIssues.push({
1149
+ type: 'invalid-config',
1150
+ code: dangerousIssue.code,
1151
+ key,
1152
+ source,
1153
+ message: formatPluginConfigIssue(dangerousIssue),
1154
+ });
1155
+ }
1156
+ else {
1157
+ ignoredUnknownKeys.push({ scope, source, key });
1158
+ }
1014
1159
  }
1015
1160
  }
1161
+ const supportedEntries = scope === 'project'
1162
+ ? Object.fromEntries(Object.entries(plainEntries).filter(([key]) => {
1163
+ if (!schemaKeys.has(key) || !userOnlyKeys.has(key))
1164
+ return true;
1165
+ unsupportedLayerOverrides.push({
1166
+ type: 'configLayerUnsupported',
1167
+ key,
1168
+ scope: 'project',
1169
+ source: 'project',
1170
+ });
1171
+ return false;
1172
+ }))
1173
+ : plainEntries;
1016
1174
  await applyConfigLayer({
1017
1175
  values,
1018
1176
  redactedValues,
1019
1177
  sources,
1020
- invalidIssues,
1178
+ fieldStates,
1021
1179
  schema,
1022
- entries: plainEntries,
1023
- source: sourceForStoreLayer(scope, projectLayer),
1024
- projectRoot,
1180
+ entries: supportedEntries,
1181
+ source,
1025
1182
  });
1026
1183
  }
1027
- const issues = [...invalidIssues.values()];
1184
+ const issues = [
1185
+ ...Object.values(fieldStates).flatMap((state) => state.invalidOverride?.issues ?? []),
1186
+ ...dangerousKeyIssues,
1187
+ ];
1028
1188
  for (const [key, field] of Object.entries(schema)) {
1029
- if (field.required && values[key] === undefined) {
1189
+ if (field.required && values[key] === undefined && !fieldStates[key]?.invalidOverride) {
1030
1190
  issues.push({
1031
1191
  type: 'missing-required',
1032
1192
  key,
@@ -1037,314 +1197,98 @@ export async function resolveEffectivePluginConfig(options) {
1037
1197
  }
1038
1198
  return {
1039
1199
  pluginName,
1040
- projectRoot,
1200
+ projectRoot: options.projectRoot,
1041
1201
  values,
1042
1202
  redactedValues,
1043
1203
  sources,
1204
+ fieldStates,
1044
1205
  issues,
1206
+ unsupportedLayerOverrides,
1045
1207
  ignoredUnknownKeys,
1046
1208
  schema,
1047
1209
  stores: {
1048
1210
  user: userStore,
1049
- project: projectStore,
1050
- projectLocal: projectLocalStore,
1211
+ ...(projectStore ? { project: projectStore } : {}),
1051
1212
  },
1052
1213
  };
1053
1214
  }
1054
1215
  export async function coercePluginConfigUpdates(options) {
1055
- const projectRoot = resolveAwProjectRoot(options.cwd);
1056
1216
  const output = {};
1057
1217
  for (const [key, value] of Object.entries(options.updates)) {
1058
1218
  const field = options.schema[key];
1059
1219
  if (!field) {
1060
1220
  throw new Error(`Unknown config key "${key}".`);
1061
1221
  }
1062
- output[key] = await coercePluginConfigValue({
1222
+ const coerced = await coercePluginConfigValue({
1063
1223
  key,
1064
1224
  field,
1065
1225
  value,
1066
- projectRoot,
1067
1226
  });
1227
+ output[key] = coerced;
1068
1228
  }
1069
1229
  return output;
1070
1230
  }
1071
- function createProjectArtifactBoundaryIssue(options) {
1072
- if (options.reason === 'pathTypeDefault') {
1073
- return {
1074
- key: options.key,
1075
- source: options.source,
1076
- reason: options.reason,
1077
- message: `Config "${options.key}" uses file/directory default value, ` +
1078
- 'which cannot participate in repo-visible project artifact truth.',
1079
- };
1080
- }
1081
- if (options.reason === 'pathTypeRequired') {
1082
- return {
1083
- key: options.key,
1084
- source: options.source,
1085
- reason: options.reason,
1086
- message: `Config "${options.key}" is a required file/directory key, ` +
1087
- 'which is not portable enough for repo-visible project artifacts.',
1088
- };
1089
- }
1090
- if (options.reason === 'sharedPathValue') {
1091
- return {
1092
- key: options.key,
1093
- source: options.source,
1094
- reason: options.reason,
1095
- message: `Config "${options.key}" is stored in project shared config as a file/directory path. ` +
1096
- 'Use user or project local config for machine-local paths.',
1097
- };
1098
- }
1099
- return {
1100
- key: options.key,
1101
- source: options.source,
1102
- reason: options.reason,
1103
- message: `Config "${options.key}" is declared as a refresh key for repo-visible project artifacts, ` +
1104
- 'but file/directory config is machine-local and cannot drive shared artifact refresh.',
1105
- };
1106
- }
1107
- export function collectProjectArtifactConfigBoundaryIssues(options) {
1108
- const issues = [];
1109
- const refreshKeySet = new Set(options.refreshKeys ?? []);
1110
- for (const [key, field] of Object.entries(options.schema)) {
1111
- if (!isPathTypeField(field)) {
1112
- continue;
1113
- }
1114
- if (field.default !== undefined) {
1115
- issues.push(createProjectArtifactBoundaryIssue({
1116
- key,
1117
- source: 'default',
1118
- reason: 'pathTypeDefault',
1119
- }));
1120
- }
1121
- if (field.required) {
1122
- issues.push(createProjectArtifactBoundaryIssue({
1123
- key,
1124
- source: 'missing',
1125
- reason: 'pathTypeRequired',
1126
- }));
1127
- }
1128
- if (options.sharedEntries && Object.hasOwn(options.sharedEntries, key)) {
1129
- issues.push(createProjectArtifactBoundaryIssue({
1130
- key,
1131
- source: 'project',
1132
- reason: 'sharedPathValue',
1133
- }));
1134
- }
1135
- if (refreshKeySet.has(key)) {
1136
- issues.push(createProjectArtifactBoundaryIssue({
1137
- key,
1138
- source: 'project',
1139
- reason: 'pathTypeRefreshKey',
1140
- }));
1141
- }
1142
- }
1143
- return issues;
1144
- }
1145
1231
  export async function resolveProjectArtifactRenderConfig(options) {
1146
- const pluginName = assertSafePluginConfigSegment(options.pluginName ?? options.manifest.name);
1147
- const schema = options.manifest.config?.schema;
1148
- const projectRoot = resolveAwProjectRoot(options.cwd);
1149
- const [userStore, projectStore, projectLocalStore] = await Promise.all([
1150
- readPluginConfigStore({
1151
- sourceId: options.sourceId,
1152
- sourceIdentity: options.sourceIdentity,
1153
- pluginName,
1154
- scope: 'user',
1155
- cwd: options.cwd,
1156
- env: options.env,
1157
- }),
1158
- readPluginConfigStore({
1159
- sourceId: options.sourceId,
1160
- sourceIdentity: options.sourceIdentity,
1161
- pluginName,
1162
- scope: 'project',
1163
- cwd: options.cwd,
1164
- env: options.env,
1165
- }),
1166
- readPluginConfigStore({
1167
- sourceId: options.sourceId,
1168
- sourceIdentity: options.sourceIdentity,
1169
- pluginName,
1170
- scope: 'project',
1171
- cwd: options.cwd,
1172
- env: options.env,
1173
- projectLayer: 'local',
1174
- }),
1175
- ]);
1176
- if (!schema) {
1232
+ if (!options.manifest.config?.fields) {
1233
+ const pluginName = assertSafePluginConfigSegment(options.pluginName ?? options.manifest.name);
1234
+ const [userStore, projectStore] = await Promise.all([
1235
+ readPluginConfigStore({
1236
+ sourceId: options.sourceId,
1237
+ sourceIdentity: options.sourceIdentity,
1238
+ logicalIdentity: options.logicalIdentity,
1239
+ pluginName,
1240
+ scope: 'user',
1241
+ cwd: options.cwd,
1242
+ env: options.env,
1243
+ }),
1244
+ readPluginConfigStore({
1245
+ sourceId: options.sourceId,
1246
+ sourceIdentity: options.sourceIdentity,
1247
+ logicalIdentity: options.logicalIdentity,
1248
+ pluginName,
1249
+ scope: 'project',
1250
+ cwd: options.cwd,
1251
+ env: options.env,
1252
+ }),
1253
+ ]);
1254
+ assertReadyPluginConfigStore(userStore);
1255
+ assertReadyPluginConfigStore(projectStore);
1177
1256
  return {
1178
1257
  pluginName,
1179
- projectRoot,
1258
+ projectRoot: resolveAwProjectRoot(options.cwd),
1180
1259
  values: {},
1181
1260
  sources: {},
1182
1261
  issues: [],
1262
+ unsupportedLayerOverrides: [],
1183
1263
  ignoredUnknownKeys: [],
1184
- boundaryIssues: [],
1185
1264
  schema: {},
1186
- stores: {
1187
- user: userStore,
1188
- project: projectStore,
1189
- projectLocal: projectLocalStore,
1190
- },
1265
+ stores: { user: userStore, project: projectStore },
1191
1266
  };
1192
1267
  }
1193
- const values = {};
1194
- const sources = {};
1195
- const invalidIssues = new Map();
1196
- const ignoredUnknownKeys = [];
1197
- const schemaKeys = new Set(Object.keys(schema));
1198
- const boundaryIssues = collectProjectArtifactConfigBoundaryIssues({
1199
- schema,
1200
- sharedEntries: projectStore.values,
1201
- refreshKeys: options.refreshKeys,
1202
- });
1203
- for (const key of Object.keys(schema)) {
1204
- sources[key] = 'missing';
1205
- }
1206
- const projectPlainEntries = getStoreEntries(projectStore.values);
1207
- const userPlainEntries = getStoreEntries(userStore.values);
1208
- const projectLocalPlainEntries = getStoreEntries(projectLocalStore.values);
1209
- for (const key of Object.keys(projectPlainEntries)) {
1210
- if (!schemaKeys.has(key)) {
1211
- ignoredUnknownKeys.push({ scope: 'project', projectLayer: 'shared', source: 'project', key });
1212
- }
1213
- }
1214
- for (const key of Object.keys(userPlainEntries)) {
1215
- if (!schemaKeys.has(key)) {
1216
- ignoredUnknownKeys.push({ scope: 'user', projectLayer: 'shared', source: 'user', key });
1217
- }
1218
- }
1219
- for (const key of Object.keys(projectLocalPlainEntries)) {
1220
- if (!schemaKeys.has(key)) {
1221
- ignoredUnknownKeys.push({ scope: 'project', projectLayer: 'local', source: 'projectLocal', key });
1222
- }
1223
- }
1224
- for (const [key, field] of Object.entries(schema)) {
1225
- if (!isPathTypeField(field) && field.default !== undefined) {
1226
- await applyConfigLayer({
1227
- values,
1228
- redactedValues: {},
1229
- sources,
1230
- invalidIssues,
1231
- schema: { [key]: field },
1232
- entries: { [key]: field.default },
1233
- source: 'default',
1234
- projectRoot,
1235
- });
1236
- }
1237
- }
1238
- const sharedSchema = Object.fromEntries(Object.entries(schema).filter(([, field]) => !isPathTypeField(field)));
1239
- await applyConfigLayer({
1240
- values,
1241
- redactedValues: {},
1242
- sources,
1243
- invalidIssues,
1244
- schema: sharedSchema,
1245
- entries: projectPlainEntries,
1246
- source: 'project',
1247
- projectRoot,
1268
+ const effective = await resolveEffectivePluginConfig({
1269
+ ...options,
1270
+ targetScope: 'project',
1248
1271
  });
1249
- const issues = [
1250
- ...invalidIssues.values(),
1251
- ...boundaryIssues.map((issue) => ({
1252
- type: 'invalid-config',
1253
- key: issue.key,
1254
- source: issue.source,
1255
- message: issue.message,
1256
- })),
1257
- ];
1258
- for (const [key, field] of Object.entries(schema)) {
1259
- if (isPathTypeField(field)) {
1260
- continue;
1261
- }
1262
- if (field.required && values[key] === undefined) {
1263
- issues.push({
1264
- type: 'missing-required',
1265
- key,
1266
- source: sources[key],
1267
- message: `Config "${key}" is required.`,
1268
- });
1269
- }
1270
- }
1271
- return {
1272
- pluginName,
1273
- projectRoot,
1274
- values,
1275
- sources,
1276
- issues,
1277
- ignoredUnknownKeys,
1278
- boundaryIssues,
1279
- schema,
1280
- stores: {
1281
- user: userStore,
1282
- project: projectStore,
1283
- projectLocal: projectLocalStore,
1284
- },
1285
- };
1286
- }
1287
- export async function resolveProjectLocalPluginConfigView(options) {
1288
- const effective = await resolveEffectivePluginConfig(options);
1289
- const artifactConfig = await resolveProjectArtifactRenderConfig(options);
1290
- const values = {};
1291
- const redactedValues = {};
1292
- const sources = {};
1293
- const issues = [];
1294
- for (const [key, field] of Object.entries(effective.schema)) {
1295
- sources[key] = 'missing';
1296
- const localPlainEntries = getStoreEntries(effective.stores.projectLocal.values);
1297
- if (!Object.hasOwn(localPlainEntries, key)) {
1298
- continue;
1299
- }
1300
- sources[key] = 'projectLocal';
1301
- try {
1302
- const coerced = await coercePluginConfigValue({
1303
- key,
1304
- field,
1305
- value: localPlainEntries[key],
1306
- projectRoot: effective.projectRoot,
1307
- });
1308
- values[key] = coerced;
1309
- redactedValues[key] = redactValue(field, coerced);
1310
- }
1311
- catch (error) {
1312
- issues.push({
1313
- type: 'invalid-config',
1314
- key,
1315
- source: 'projectLocal',
1316
- message: error instanceof Error ? error.message : String(error),
1317
- });
1318
- }
1319
- }
1320
1272
  return {
1321
1273
  pluginName: effective.pluginName,
1322
1274
  projectRoot: effective.projectRoot,
1275
+ values: effective.values,
1276
+ sources: effective.sources,
1277
+ issues: effective.issues,
1278
+ unsupportedLayerOverrides: effective.unsupportedLayerOverrides,
1279
+ ignoredUnknownKeys: effective.ignoredUnknownKeys,
1323
1280
  schema: effective.schema,
1324
- localStore: effective.stores.projectLocal,
1325
- values,
1326
- redactedValues,
1327
- sources,
1328
- issues,
1329
- ignoredUnknownKeys: effective.ignoredUnknownKeys.filter((entry) => entry.source === 'projectLocal'),
1330
- repoVisibleArtifactConfig: {
1331
- values: artifactConfig.values,
1332
- sources: artifactConfig.sources,
1333
- issues: artifactConfig.issues,
1334
- boundaryIssues: artifactConfig.boundaryIssues,
1281
+ stores: {
1282
+ user: effective.stores.user,
1283
+ project: effective.stores.project,
1335
1284
  },
1336
1285
  };
1337
1286
  }
1338
1287
  async function resolveRedactedLayerValue(options) {
1339
1288
  try {
1340
- const coerced = await coercePluginConfigValue({
1341
- key: options.key,
1342
- field: options.field,
1343
- value: options.value,
1344
- projectRoot: options.projectRoot,
1345
- });
1289
+ const value = validateConfigReadValue(options.key, options.field, options.value);
1346
1290
  return {
1347
- value: redactValue(options.field, coerced),
1291
+ value: redactValue(options.field, value),
1348
1292
  };
1349
1293
  }
1350
1294
  catch (error) {
@@ -1360,24 +1304,17 @@ async function resolveRedactedLayerValue(options) {
1360
1304
  }
1361
1305
  export async function resolveProjectScopedPluginConfigView(options) {
1362
1306
  const effective = await resolveEffectivePluginConfig(options);
1363
- const artifactConfig = await resolveProjectArtifactRenderConfig(options);
1364
1307
  const entries = {};
1365
1308
  for (const [key, field] of Object.entries(effective.schema)) {
1366
1309
  const entry = {
1367
1310
  key,
1368
1311
  effectiveValue: effective.redactedValues[key],
1369
1312
  effectiveSource: effective.sources[key] ?? 'missing',
1370
- repoVisibleArtifactValue: artifactConfig.values[key] === undefined
1371
- ? undefined
1372
- : redactValue(field, artifactConfig.values[key]),
1373
- repoVisibleArtifactSource: artifactConfig.sources[key] ?? 'missing',
1374
- repoVisibleArtifactUsesSharedTruth: true,
1375
1313
  };
1376
1314
  if (field.default !== undefined) {
1377
1315
  const resolvedDefault = await resolveRedactedLayerValue({
1378
1316
  key,
1379
1317
  field,
1380
- projectRoot: effective.projectRoot,
1381
1318
  source: 'default',
1382
1319
  value: field.default,
1383
1320
  });
@@ -1390,7 +1327,6 @@ export async function resolveProjectScopedPluginConfigView(options) {
1390
1327
  const resolvedUser = await resolveRedactedLayerValue({
1391
1328
  key,
1392
1329
  field,
1393
- projectRoot: effective.projectRoot,
1394
1330
  source: 'user',
1395
1331
  value: userPlainEntries[key],
1396
1332
  });
@@ -1403,25 +1339,11 @@ export async function resolveProjectScopedPluginConfigView(options) {
1403
1339
  const resolvedProject = await resolveRedactedLayerValue({
1404
1340
  key,
1405
1341
  field,
1406
- projectRoot: effective.projectRoot,
1407
1342
  source: 'project',
1408
1343
  value: projectPlainEntries[key],
1409
1344
  });
1410
1345
  if (resolvedProject.value !== undefined) {
1411
- entry.projectSharedValue = resolvedProject.value;
1412
- }
1413
- }
1414
- const localPlainEntries = getStoreEntries(effective.stores.projectLocal.values);
1415
- if (Object.hasOwn(localPlainEntries, key)) {
1416
- const resolvedLocal = await resolveRedactedLayerValue({
1417
- key,
1418
- field,
1419
- projectRoot: effective.projectRoot,
1420
- source: 'projectLocal',
1421
- value: localPlainEntries[key],
1422
- });
1423
- if (resolvedLocal.value !== undefined) {
1424
- entry.projectLocalValue = resolvedLocal.value;
1346
+ entry.projectValue = resolvedProject.value;
1425
1347
  }
1426
1348
  }
1427
1349
  entries[key] = entry;
@@ -1430,18 +1352,16 @@ export async function resolveProjectScopedPluginConfigView(options) {
1430
1352
  pluginName: effective.pluginName,
1431
1353
  projectRoot: effective.projectRoot,
1432
1354
  schema: effective.schema,
1433
- stores: effective.stores,
1355
+ stores: {
1356
+ user: effective.stores.user,
1357
+ project: effective.stores.project,
1358
+ },
1434
1359
  redactedValues: effective.redactedValues,
1435
1360
  sources: effective.sources,
1436
1361
  entries,
1437
1362
  issues: effective.issues,
1363
+ unsupportedLayerOverrides: effective.unsupportedLayerOverrides,
1438
1364
  ignoredUnknownKeys: effective.ignoredUnknownKeys,
1439
- repoVisibleArtifactConfig: {
1440
- values: artifactConfig.values,
1441
- sources: artifactConfig.sources,
1442
- issues: artifactConfig.issues,
1443
- boundaryIssues: artifactConfig.boundaryIssues,
1444
- },
1445
1365
  };
1446
1366
  }
1447
1367
  function toConfigSourceLayer(source) {
@@ -1454,77 +1374,179 @@ function toConfigSourceLayer(source) {
1454
1374
  if (source === 'project') {
1455
1375
  return 'project';
1456
1376
  }
1457
- if (source === 'projectLocal') {
1458
- return 'local';
1459
- }
1460
1377
  return null;
1461
1378
  }
1379
+ function configFieldProjection(options) {
1380
+ const source = options.source ?? 'missing';
1381
+ const issues = options.issues ?? [];
1382
+ const presetSensitive = options.field.sensitive === true
1383
+ || (options.field.type === 'object'
1384
+ && (Object.values(options.field.properties ?? {}).some((child) => child.sensitive === true)
1385
+ || (options.field.additionalProperties !== false
1386
+ && options.field.additionalProperties?.sensitive === true)));
1387
+ return {
1388
+ value: options.value === undefined ? null : redactValue(options.field, options.value),
1389
+ source,
1390
+ matchedPreset: options.value === undefined || presetSensitive
1391
+ ? null
1392
+ : matchPluginConfigPreset(options.field, options.value)?.id ?? null,
1393
+ valid: issues.length === 0,
1394
+ issues,
1395
+ };
1396
+ }
1462
1397
  async function buildManifestFieldEntries(options) {
1463
- const schema = options.plugin.manifest.config?.schema;
1398
+ const schema = options.plugin.manifest.config?.fields;
1464
1399
  if (!schema) {
1465
1400
  return [];
1466
1401
  }
1467
- const projectRoot = resolveAwProjectRoot(options.cwd);
1468
- const [userStore, projectStore, projectLocalStore] = await Promise.all([
1469
- readPluginConfigStore({
1470
- sourceId: options.plugin.sourceId,
1471
- sourceIdentity: options.plugin.sourceIdentity,
1472
- pluginName: options.plugin.manifest.name,
1473
- scope: 'user',
1474
- cwd: options.cwd,
1475
- env: options.env,
1476
- }),
1477
- readPluginConfigStore({
1478
- sourceId: options.plugin.sourceId,
1479
- sourceIdentity: options.plugin.sourceIdentity,
1480
- pluginName: options.plugin.manifest.name,
1481
- scope: 'project',
1482
- projectLayer: 'shared',
1483
- cwd: options.cwd,
1484
- env: options.env,
1485
- }),
1486
- readPluginConfigStore({
1402
+ const userStore = await readPluginConfigStore({
1403
+ sourceId: options.plugin.sourceId,
1404
+ sourceIdentity: options.plugin.sourceIdentity,
1405
+ logicalIdentity: options.plugin.catalogLogicalIdentity,
1406
+ pluginName: options.plugin.manifest.name,
1407
+ scope: 'user',
1408
+ cwd: options.cwd,
1409
+ env: options.env,
1410
+ schema,
1411
+ });
1412
+ const projectStore = options.scope === 'user'
1413
+ ? undefined
1414
+ : await readPluginConfigStore({
1487
1415
  sourceId: options.plugin.sourceId,
1488
1416
  sourceIdentity: options.plugin.sourceIdentity,
1417
+ logicalIdentity: options.plugin.catalogLogicalIdentity,
1489
1418
  pluginName: options.plugin.manifest.name,
1490
1419
  scope: 'project',
1491
- projectLayer: 'local',
1492
1420
  cwd: options.cwd,
1493
1421
  env: options.env,
1494
- }),
1495
- ]);
1422
+ schema,
1423
+ });
1424
+ assertReadyPluginConfigStore(userStore);
1425
+ if (projectStore)
1426
+ assertReadyPluginConfigStore(projectStore);
1496
1427
  const userPlainEntries = getStoreEntries(userStore.values);
1497
- const projectPlainEntries = getStoreEntries(projectStore.values);
1498
- const localPlainEntries = getStoreEntries(projectLocalStore.values);
1428
+ const projectPlainEntries = projectStore ? getStoreEntries(projectStore.values) : {};
1499
1429
  const effective = await resolveEffectivePluginConfig({
1500
1430
  sourceId: options.plugin.sourceId,
1501
1431
  sourceIdentity: options.plugin.sourceIdentity,
1432
+ logicalIdentity: options.plugin.catalogLogicalIdentity,
1502
1433
  pluginName: options.plugin.manifest.name,
1503
1434
  manifest: options.plugin.manifest,
1435
+ targetScope: options.scope === 'user' ? 'user' : 'project',
1504
1436
  cwd: options.cwd,
1505
1437
  env: options.env,
1506
1438
  });
1439
+ const userOnlyKeys = codexNativeRefreshKeys(options.plugin.manifest);
1440
+ const projectRemovalFallback = (key) => {
1441
+ const field = schema[key];
1442
+ if (Object.hasOwn(userPlainEntries, key)) {
1443
+ const value = userPlainEntries[key];
1444
+ const validationIssues = validateCorePluginConfigValue(field, value, key);
1445
+ if (validationIssues.length > 0) {
1446
+ return {
1447
+ state: 'invalid',
1448
+ sourceLayer: 'user',
1449
+ issues: validationIssues.map((issue) => ({
1450
+ type: 'invalid-config',
1451
+ key,
1452
+ source: 'user',
1453
+ message: formatPluginConfigIssue(issue),
1454
+ })),
1455
+ };
1456
+ }
1457
+ return {
1458
+ state: 'valid',
1459
+ effectiveValue: redactValue(field, value),
1460
+ sourceLayer: 'user',
1461
+ };
1462
+ }
1463
+ if (field.default === undefined)
1464
+ return { state: 'missing' };
1465
+ const validationIssues = validateCorePluginConfigValue(field, field.default, key);
1466
+ if (validationIssues.length > 0) {
1467
+ return {
1468
+ state: 'invalid',
1469
+ sourceLayer: 'default',
1470
+ issues: validationIssues.map((issue) => ({
1471
+ type: 'invalid-config',
1472
+ key,
1473
+ source: 'default',
1474
+ message: formatPluginConfigIssue(issue),
1475
+ })),
1476
+ };
1477
+ }
1478
+ return {
1479
+ state: 'valid',
1480
+ effectiveValue: redactValue(field, field.default),
1481
+ sourceLayer: 'default',
1482
+ };
1483
+ };
1484
+ const effectiveFieldState = (key) => {
1485
+ const source = effective.sources[key];
1486
+ const sourceLayer = toConfigSourceLayer(source);
1487
+ const field = schema[key];
1488
+ const fieldIssues = effective.issues.filter((entry) => entry.key === key);
1489
+ const fieldState = effective.fieldStates[key];
1490
+ const fallback = options.scope === 'project'
1491
+ ? projectRemovalFallback(key)
1492
+ : fieldState?.fallback
1493
+ ? {
1494
+ state: 'valid',
1495
+ effectiveValue: fieldState.fallback.redactedValue,
1496
+ sourceLayer: fieldState.fallback.source,
1497
+ }
1498
+ : undefined;
1499
+ if (source === undefined || source === 'missing') {
1500
+ return {
1501
+ valueState: fieldIssues.length > 0 ? 'invalid' : 'default',
1502
+ effectiveValue: null,
1503
+ sourceLayer: null,
1504
+ ...configFieldProjection({ field, source: 'missing', issues: fieldIssues }),
1505
+ };
1506
+ }
1507
+ const issue = fieldIssues.find((entry) => entry.source === source);
1508
+ return {
1509
+ valueState: issue ? 'invalid' : source === 'default' ? 'default' : 'configured',
1510
+ effectiveValue: issue ? null : effective.redactedValues[key] ?? null,
1511
+ sourceLayer,
1512
+ ...(fieldState?.invalidOverride ? {
1513
+ invalidOverride: {
1514
+ sourceLayer: fieldState.invalidOverride.source,
1515
+ issues: fieldState.invalidOverride.issues,
1516
+ },
1517
+ } : {}),
1518
+ ...(fallback ? { fallback } : {}),
1519
+ ...configFieldProjection({
1520
+ field,
1521
+ value: issue ? undefined : effective.values[key],
1522
+ source,
1523
+ issues: fieldIssues,
1524
+ }),
1525
+ };
1526
+ };
1507
1527
  const fields = [];
1508
1528
  for (const [key, field] of Object.entries(schema)) {
1529
+ const canSet = options.scope !== 'project'
1530
+ || (!userOnlyKeys.has(key) && field.sensitive !== true);
1509
1531
  const base = {
1510
1532
  key,
1511
1533
  title: field.title ?? key,
1512
1534
  kind: 'manifest',
1513
1535
  reserved: false,
1514
- writable: true,
1536
+ writable: canSet,
1537
+ canSet,
1538
+ canUnset: options.scope === 'user'
1539
+ ? Object.hasOwn(userPlainEntries, key)
1540
+ : options.scope === 'project' && Object.hasOwn(projectPlainEntries, key),
1515
1541
  valueState: 'default',
1516
1542
  effectiveValue: null,
1517
1543
  sourceLayer: null,
1544
+ ...configFieldProjection({ field }),
1518
1545
  };
1519
1546
  if (options.scope === 'user') {
1520
1547
  if (Object.hasOwn(userPlainEntries, key)) {
1521
1548
  try {
1522
- const value = await coercePluginConfigValue({
1523
- key,
1524
- field,
1525
- value: userPlainEntries[key],
1526
- projectRoot,
1527
- });
1549
+ const value = validateConfigReadValue(key, field, userPlainEntries[key]);
1528
1550
  fields.push({
1529
1551
  ...base,
1530
1552
  valueState: 'configured',
@@ -1532,6 +1554,7 @@ async function buildManifestFieldEntries(options) {
1532
1554
  sourceLayer: 'user',
1533
1555
  storedValue: redactValue(field, value),
1534
1556
  storedSourceLayer: 'user',
1557
+ ...configFieldProjection({ field, value, source: 'user' }),
1535
1558
  });
1536
1559
  }
1537
1560
  catch {
@@ -1539,22 +1562,22 @@ async function buildManifestFieldEntries(options) {
1539
1562
  ...base,
1540
1563
  valueState: 'invalid',
1541
1564
  sourceLayer: 'user',
1565
+ source: 'user',
1566
+ storedSourceLayer: 'user',
1567
+ valid: false,
1568
+ issues: [{ type: 'invalid-config', key, source: 'user' }],
1542
1569
  });
1543
1570
  }
1544
1571
  continue;
1545
1572
  }
1546
1573
  if (field.default !== undefined) {
1547
1574
  try {
1548
- const value = await coercePluginConfigValue({
1549
- key,
1550
- field,
1551
- value: field.default,
1552
- projectRoot,
1553
- });
1575
+ const value = validateConfigReadValue(key, field, field.default);
1554
1576
  fields.push({
1555
1577
  ...base,
1556
1578
  effectiveValue: redactValue(field, value),
1557
1579
  sourceLayer: 'default',
1580
+ ...configFieldProjection({ field, value, source: 'default' }),
1558
1581
  });
1559
1582
  }
1560
1583
  catch {
@@ -1562,6 +1585,9 @@ async function buildManifestFieldEntries(options) {
1562
1585
  ...base,
1563
1586
  valueState: 'invalid',
1564
1587
  sourceLayer: 'default',
1588
+ source: 'default',
1589
+ valid: false,
1590
+ issues: [{ type: 'invalid-config', key, source: 'default' }],
1565
1591
  });
1566
1592
  }
1567
1593
  continue;
@@ -1569,103 +1595,41 @@ async function buildManifestFieldEntries(options) {
1569
1595
  fields.push(base);
1570
1596
  continue;
1571
1597
  }
1572
- if (options.projectLayer === 'local') {
1573
- if (Object.hasOwn(localPlainEntries, key)) {
1574
- try {
1575
- const value = await coercePluginConfigValue({
1576
- key,
1577
- field,
1578
- value: localPlainEntries[key],
1579
- projectRoot,
1580
- });
1581
- fields.push({
1582
- ...base,
1583
- valueState: 'configured',
1584
- effectiveValue: redactValue(field, value),
1585
- sourceLayer: 'local',
1586
- storedValue: redactValue(field, value),
1587
- storedSourceLayer: 'local',
1588
- });
1589
- }
1590
- catch {
1591
- fields.push({
1592
- ...base,
1593
- valueState: 'invalid',
1594
- sourceLayer: 'local',
1595
- });
1596
- }
1597
- }
1598
- else {
1599
- fields.push(base);
1600
- }
1601
- continue;
1602
- }
1603
1598
  let storedProjectValue;
1604
1599
  if (options.scope === 'project' && Object.hasOwn(projectPlainEntries, key)) {
1605
1600
  try {
1606
- const value = await coercePluginConfigValue({
1607
- key,
1608
- field,
1609
- value: projectPlainEntries[key],
1610
- projectRoot,
1611
- });
1601
+ const value = validateConfigReadValue(key, field, projectPlainEntries[key]);
1612
1602
  storedProjectValue = {
1613
1603
  storedValue: redactValue(field, value),
1614
1604
  storedSourceLayer: 'project',
1615
1605
  };
1616
1606
  }
1617
1607
  catch {
1618
- storedProjectValue = undefined;
1608
+ storedProjectValue = { storedSourceLayer: 'project' };
1619
1609
  }
1620
1610
  }
1621
- const source = effective.sources[key];
1622
- const layer = toConfigSourceLayer(source);
1623
- if (source === 'default') {
1624
- fields.push({
1625
- ...base,
1626
- effectiveValue: effective.redactedValues[key] ?? null,
1627
- sourceLayer: 'default',
1628
- ...storedProjectValue,
1629
- });
1630
- continue;
1631
- }
1632
- if (source === 'user' || source === 'project' || source === 'projectLocal') {
1633
- const issue = effective.issues.find((entry) => entry.key === key && entry.source === source);
1634
- fields.push({
1635
- ...base,
1636
- valueState: issue ? 'invalid' : 'configured',
1637
- effectiveValue: issue ? null : effective.redactedValues[key] ?? null,
1638
- sourceLayer: layer,
1639
- ...storedProjectValue,
1640
- });
1641
- continue;
1642
- }
1643
- fields.push({ ...base, ...storedProjectValue });
1611
+ fields.push({ ...base, ...effectiveFieldState(key), ...storedProjectValue });
1644
1612
  }
1645
1613
  return fields;
1646
1614
  }
1647
1615
  async function buildLegacyStoreFieldEntries(options) {
1648
1616
  const locations = options.scope === 'effective'
1649
1617
  ? [
1650
- { scope: 'user', projectLayer: 'shared', sourceLayer: 'user' },
1651
- { scope: 'project', projectLayer: 'shared', sourceLayer: 'project' },
1652
- { scope: 'project', projectLayer: 'local', sourceLayer: 'local' },
1618
+ { scope: 'user', sourceLayer: 'user' },
1619
+ { scope: 'project', sourceLayer: 'project' },
1653
1620
  ]
1654
1621
  : [{
1655
1622
  scope: options.scope,
1656
- projectLayer: options.projectLayer,
1657
- sourceLayer: options.scope === 'user'
1658
- ? 'user'
1659
- : options.projectLayer === 'local' ? 'local' : 'project',
1623
+ sourceLayer: options.scope === 'user' ? 'user' : 'project',
1660
1624
  }];
1661
1625
  const fields = new Map();
1662
1626
  for (const location of locations) {
1663
1627
  const store = await readPluginConfigStore({
1664
1628
  sourceId: options.sourceId,
1665
1629
  sourceIdentity: options.sourceIdentity,
1630
+ logicalIdentity: options.logicalIdentity,
1666
1631
  pluginName: options.pluginName,
1667
1632
  scope: location.scope,
1668
- ...(location.scope === 'project' ? { projectLayer: location.projectLayer } : {}),
1669
1633
  cwd: options.cwd,
1670
1634
  env: options.env,
1671
1635
  });
@@ -1676,9 +1640,16 @@ async function buildLegacyStoreFieldEntries(options) {
1676
1640
  kind: 'manifest',
1677
1641
  reserved: false,
1678
1642
  writable: false,
1643
+ canSet: false,
1644
+ canUnset: false,
1679
1645
  valueState: 'configured',
1680
1646
  effectiveValue: '[redacted]',
1681
1647
  sourceLayer: location.sourceLayer,
1648
+ value: '[redacted]',
1649
+ source: location.sourceLayer,
1650
+ matchedPreset: null,
1651
+ valid: true,
1652
+ issues: [],
1682
1653
  });
1683
1654
  }
1684
1655
  }
@@ -1708,7 +1679,6 @@ export async function resolveConfigRead(options) {
1708
1679
  currentPlugin: options.plugin,
1709
1680
  currentPluginError: options.pluginError,
1710
1681
  scope: options.scope,
1711
- projectLayer: options.projectLayer,
1712
1682
  cwd: options.cwd,
1713
1683
  env: options.env,
1714
1684
  });
@@ -1716,24 +1686,25 @@ export async function resolveConfigRead(options) {
1716
1686
  ? await buildManifestFieldEntries({
1717
1687
  plugin: schemaAccess.plugin,
1718
1688
  scope: options.scope,
1719
- projectLayer: options.projectLayer,
1720
1689
  cwd: options.cwd,
1721
1690
  env: options.env,
1722
1691
  })
1723
1692
  : await buildLegacyStoreFieldEntries({
1724
1693
  sourceId: options.sourceId,
1725
1694
  sourceIdentity: options.sourceIdentity,
1695
+ logicalIdentity: options.logicalIdentity,
1726
1696
  pluginName,
1727
1697
  scope: options.scope,
1728
- projectLayer: options.projectLayer,
1729
1698
  cwd: options.cwd,
1730
1699
  env: options.env,
1731
1700
  });
1732
1701
  return {
1733
1702
  pluginName,
1734
1703
  scope: options.scope,
1735
- projectLayer: options.projectLayer,
1736
1704
  ...(schemaAccess.schema ? { schema: schemaAccess.schema } : {}),
1705
+ ...(schemaAccess.plugin?.manifest.config?.fields
1706
+ ? { fieldDefinitions: schemaAccess.plugin.manifest.config.fields }
1707
+ : {}),
1737
1708
  fields: manifestFields,
1738
1709
  };
1739
1710
  }
@@ -1747,6 +1718,15 @@ export function summarizeEffectivePluginConfig(config) {
1747
1718
  return {
1748
1719
  values: config.redactedValues,
1749
1720
  sources: config.sources,
1721
+ fieldStates: Object.fromEntries(Object.entries(config.fieldStates).map(([key, state]) => [key, {
1722
+ ...(state.effective ? {
1723
+ effective: { value: state.effective.redactedValue, source: state.effective.source },
1724
+ } : {}),
1725
+ ...(state.invalidOverride ? { invalidOverride: state.invalidOverride } : {}),
1726
+ ...(state.fallback ? {
1727
+ fallback: { value: state.fallback.redactedValue, source: state.fallback.source },
1728
+ } : {}),
1729
+ }])),
1750
1730
  issues: config.issues,
1751
1731
  ignoredUnknownKeys: config.ignoredUnknownKeys,
1752
1732
  };
@@ -1762,52 +1742,75 @@ export function createPluginConfigSchemaIdentity(manifest) {
1762
1742
  .digest('hex');
1763
1743
  return `${manifest.name}:${hash}`;
1764
1744
  }
1745
+ function cliConfigPromptEnumValues(field) {
1746
+ if (!field.choices)
1747
+ return undefined;
1748
+ const values = field.choices.presets
1749
+ .map((preset) => preset.value);
1750
+ if (field.type !== 'boolean') {
1751
+ return field.choices.allowCustom ? undefined : values;
1752
+ }
1753
+ if (field.choices.allowCustom) {
1754
+ for (const value of [true, false]) {
1755
+ if (!values.includes(value))
1756
+ values.push(value);
1757
+ }
1758
+ }
1759
+ return values;
1760
+ }
1765
1761
  export function toCliConfigPromptSchema(schema) {
1766
1762
  const promptSchema = {};
1767
1763
  for (const [key, field] of Object.entries(schema)) {
1764
+ if (field.type === 'object')
1765
+ continue;
1768
1766
  promptSchema[key] = {
1769
- type: field.type === 'enum' ? 'string' : field.type,
1767
+ type: field.type,
1770
1768
  label: field.title ?? key,
1771
1769
  description: field.description,
1772
1770
  required: field.required,
1773
1771
  sensitive: field.sensitive,
1774
1772
  default: field.default,
1775
- enum: field.type === 'enum' ? field.values : undefined,
1776
- min: field.min,
1777
- max: field.max,
1773
+ enum: cliConfigPromptEnumValues(field),
1774
+ min: field.type === 'number' ? field.validation?.minimum : undefined,
1775
+ max: field.type === 'number' ? field.validation?.maximum : undefined,
1778
1776
  };
1779
1777
  }
1780
1778
  return promptSchema;
1781
1779
  }
1782
1780
  export function createPluginConfigFormModel(options) {
1783
- const schema = options.manifest.config?.schema;
1781
+ const schema = options.manifest.config?.fields;
1784
1782
  if (!schema) {
1785
1783
  throw new Error(`Plugin "${options.pluginName ?? options.manifest.name}" does not declare an AW config schema.`);
1786
1784
  }
1787
1785
  const requiredKeys = [...new Set(options.effective.issues
1788
1786
  .filter((issue) => issue.type === 'missing-required' || issue.type === 'invalid-config')
1789
1787
  .map((issue) => issue.key))];
1788
+ const fields = Object.entries(schema).flatMap(([key, field]) => {
1789
+ if (field.type === 'object')
1790
+ return [];
1791
+ return [{
1792
+ key,
1793
+ type: field.type,
1794
+ label: field.title ?? key,
1795
+ description: field.description,
1796
+ required: field.required === true,
1797
+ sensitive: field.sensitive === true,
1798
+ defaultValue: field.default,
1799
+ enumValues: cliConfigPromptEnumValues(field),
1800
+ min: field.type === 'number' ? field.validation?.minimum : undefined,
1801
+ max: field.type === 'number' ? field.validation?.maximum : undefined,
1802
+ configured: options.effective.values[key] !== undefined,
1803
+ currentValue: field.sensitive
1804
+ ? undefined
1805
+ : options.effective.values[key],
1806
+ }];
1807
+ });
1790
1808
  return {
1791
1809
  pluginName: options.pluginName ?? options.manifest.name,
1792
1810
  allowedScopes: options.allowedScopes ?? ['user', 'project'],
1793
1811
  initialScope: options.initialScope,
1794
1812
  requiredKeys,
1795
- fields: Object.entries(schema).map(([key, field]) => ({
1796
- key,
1797
- type: field.type,
1798
- label: field.title ?? key,
1799
- description: field.description,
1800
- required: field.required === true,
1801
- sensitive: field.sensitive === true,
1802
- defaultValue: field.default,
1803
- enumValues: field.type === 'enum' ? field.values : undefined,
1804
- min: field.min,
1805
- max: field.max,
1806
- configured: options.effective.values[key] !== undefined,
1807
- currentValue: field.sensitive || isPathTypeField(field)
1808
- ? undefined
1809
- : options.effective.values[key],
1810
- })),
1813
+ fields,
1811
1814
  };
1812
1815
  }
1813
1816
  export function toCliConfigPromptOptions(model) {
@@ -1826,7 +1829,7 @@ export function toCliConfigPromptOptions(model) {
1826
1829
  schema: Object.fromEntries(model.fields.map((field) => [
1827
1830
  field.key,
1828
1831
  {
1829
- type: field.type === 'enum' ? 'string' : field.type,
1832
+ type: field.type,
1830
1833
  label: field.label,
1831
1834
  description: field.description,
1832
1835
  required: field.required,
@@ -1844,13 +1847,21 @@ export function toCliConfigPromptOptions(model) {
1844
1847
  requiredKeys: [...model.requiredKeys],
1845
1848
  };
1846
1849
  }
1847
- function formatRequiredConfigSetCommands(pluginName, issues, defaultScope) {
1850
+ export function formatPluginConfigSetRecoveryCommand(pluginName, key, field, scope) {
1851
+ return field.sensitive === true
1852
+ ? `aw config set ${pluginName} ${key} --scope user --value-stdin`
1853
+ : `aw config set ${pluginName} ${key} --scope ${scope} --value-json '<json>'`;
1854
+ }
1855
+ function formatRequiredConfigSetCommands(pluginName, issues, defaultScope, schema) {
1848
1856
  const commands = new Set();
1849
1857
  for (const issue of issues) {
1850
1858
  const scope = issue.source === 'user' || issue.source === 'project'
1851
1859
  ? issue.source
1852
1860
  : defaultScope;
1853
- commands.add(`aw config set ${pluginName} ${issue.key} <value> -s ${scope}`);
1861
+ const field = schema[issue.key];
1862
+ if (!field)
1863
+ continue;
1864
+ commands.add(formatPluginConfigSetRecoveryCommand(pluginName, issue.key, field, scope));
1854
1865
  }
1855
1866
  return [...commands].map((command) => `\`${command}\``).join(' 或 ');
1856
1867
  }
@@ -1870,12 +1881,13 @@ export async function preflightRequiredPluginConfigs(options) {
1870
1881
  }
1871
1882
  visited.add(dedupeKey);
1872
1883
  result.checked += 1;
1873
- if (!target.manifest.config?.schema) {
1884
+ if (!target.manifest.config?.fields) {
1874
1885
  continue;
1875
1886
  }
1876
1887
  let effective = await resolveEffectivePluginConfig({
1877
1888
  sourceId: target.sourceId,
1878
1889
  sourceIdentity: target.sourceIdentity,
1890
+ logicalIdentity: target.logicalIdentity,
1879
1891
  pluginName,
1880
1892
  manifest: target.manifest,
1881
1893
  cwd: options.cwd,
@@ -1886,7 +1898,7 @@ export async function preflightRequiredPluginConfigs(options) {
1886
1898
  continue;
1887
1899
  }
1888
1900
  const details = blockingIssues.map((issue) => issue.key).join(', ');
1889
- const setCommands = formatRequiredConfigSetCommands(pluginName, blockingIssues, options.defaultScope);
1901
+ const setCommands = formatRequiredConfigSetCommands(pluginName, blockingIssues, options.defaultScope, target.manifest.config.fields);
1890
1902
  throw new Error(`${pluginName} requires valid AW config before install/upgrade: ${details}. ` +
1891
1903
  `Run ${setCommands}, or open \`aw\` to complete setup.`);
1892
1904
  }