@lesliechan721/aw 0.1.0-beta.6 → 0.1.0-beta.60

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