agentworkshop 0.8.5 → 0.8.6

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 (349) hide show
  1. package/.AgentWorkShop/prompts/host-tools.en.json +3 -3
  2. package/.AgentWorkShop/prompts/host-tools.json +14 -4
  3. package/.output/.AgentWorkShop/prompts/host-tools.en.json +3 -3
  4. package/.output/.AgentWorkShop/prompts/host-tools.json +14 -4
  5. package/.output/nitro.json +1 -1
  6. package/.output/public/_nuxt/{U_n8pryv2.js → 7op-BMk82.js} +1 -1
  7. package/.output/public/_nuxt/{zWZ26UWi.js → B1SIgyrl.js} +1 -1
  8. package/.output/public/_nuxt/{C4tXwpSt.js → BA2lmfW4.js} +1 -1
  9. package/.output/public/_nuxt/{EJkmXP432.js → BDXQqSkl2.js} +1 -1
  10. package/.output/public/_nuxt/{CY_7u1ST2.js → BI9jj13f2.js} +1 -1
  11. package/.output/public/_nuxt/{CGf9neEK2.js → BiXqyYNw2.js} +1 -1
  12. package/.output/public/_nuxt/{D9GoOkyP2.js → BlOm8JIs2.js} +1 -1
  13. package/.output/public/_nuxt/{CQ31GiJU2.js → BoXKUOiN2.js} +1 -1
  14. package/.output/public/_nuxt/{DZzbCFY2.js → BuLoPmcf.js} +1 -1
  15. package/.output/public/_nuxt/{D9ppMCbw.js → BwOK42JQ.js} +1 -1
  16. package/.output/public/_nuxt/{CZJ5cw6r2.js → C1RMfPoB2.js} +1 -1
  17. package/.output/public/_nuxt/{CGviZq4E.js → COfo_Bg3.js} +1 -1
  18. package/.output/public/_nuxt/CTGJHZ8X2.js +1 -0
  19. package/.output/public/_nuxt/{SgsmmrsZ.js → CY2NkFvI.js} +1 -1
  20. package/.output/public/_nuxt/{B4GOTVfN.js → C_4mzQ5X.js} +1 -1
  21. package/.output/public/_nuxt/{DGmFHb7m.js → CdvMP3lv.js} +1 -1
  22. package/.output/public/_nuxt/{iqxZGXsY.js → CooauJ7D.js} +1 -1
  23. package/.output/public/_nuxt/{C3vxePED.js → D1eVi1zr.js} +1 -1
  24. package/.output/public/_nuxt/{DnuJ66Jy.js → D6PUmBb0.js} +1 -1
  25. package/.output/public/_nuxt/{BBhroEeN.js → DC8sMA36.js} +1 -1
  26. package/.output/public/_nuxt/{DOUuGOQR2.js → DDHh85c42.js} +1 -1
  27. package/.output/public/_nuxt/{Cy_HDgSl.js → DDXOfmo2.js} +2 -2
  28. package/.output/public/_nuxt/{Cr2xJ_CY.js → DMFwLxfu.js} +1 -1
  29. package/.output/public/_nuxt/{BZg_MIK42.js → DoprKJ5a2.js} +1 -1
  30. package/.output/public/_nuxt/{BzBBEE-o2.js → Dr1FVeQG2.js} +1 -1
  31. package/.output/public/_nuxt/{W0sNnJQh.js → DtnsTW7e.js} +1 -1
  32. package/.output/public/_nuxt/{gUYi1hLJ2.js → DxDqd6T42.js} +1 -1
  33. package/.output/public/_nuxt/{B0UUODeh.js → DzU3zEKX.js} +1 -1
  34. package/.output/public/_nuxt/{BkO5WdX5.js → EgO70wrE.js} +1 -1
  35. package/.output/public/_nuxt/{7OZQiw002.js → HJKu4eEV2.js} +1 -1
  36. package/.output/public/_nuxt/{ChF6qLpY2.js → K5Xen0wZ2.js} +1 -1
  37. package/.output/public/_nuxt/{CNrRZmSx2.js → Rb4o4o-82.js} +1 -1
  38. package/.output/public/_nuxt/{BqBTlfiz.js → Tf6SJSDj.js} +1 -1
  39. package/.output/public/_nuxt/{DJaGVY4v2.js → ZzOyoXex2.js} +1 -1
  40. package/.output/public/_nuxt/{PbzVdCTA2.js → azOGy_tU2.js} +1 -1
  41. package/.output/public/_nuxt/{D1thFC3z.js → be-4wyTo.js} +1 -1
  42. package/.output/public/_nuxt/builds/latest.json +1 -1
  43. package/.output/public/_nuxt/builds/meta/593b65bd-cf1d-4b7f-a993-57e939c38f36.json +1 -0
  44. package/.output/public/_nuxt/{8b9CKi6F.js → dcjPjLPI.js} +1 -1
  45. package/.output/public/_nuxt/{ae2-5ZPt.js → h-Fn7pRD.js} +1 -1
  46. package/.output/public/_nuxt/{-gy9k_TE2.js → hyLdZsOd2.js} +1 -1
  47. package/.output/public/_nuxt/{BHxG41IQ2.js → o8sYG6xx2.js} +1 -1
  48. package/.output/server/chunks/_/access.mjs +1 -1
  49. package/.output/server/chunks/_/agent-dto.mjs +26 -0
  50. package/.output/server/chunks/_/agent-dto.mjs.map +1 -0
  51. package/.output/server/chunks/_/data-export.mjs +6 -6
  52. package/.output/server/chunks/_/data-query.mjs +3 -3
  53. package/.output/server/chunks/_/eda-run.repo.mjs +2 -2
  54. package/.output/server/chunks/_/features.mjs +5 -5
  55. package/.output/server/chunks/_/golden.mjs +3 -3
  56. package/.output/server/chunks/_/harness-spawn-policy.mjs +77 -0
  57. package/.output/server/chunks/_/harness-spawn-policy.mjs.map +1 -0
  58. package/.output/server/chunks/_/index.mjs +1 -1
  59. package/.output/server/chunks/_/line-scope.mjs +32 -0
  60. package/.output/server/chunks/_/line-scope.mjs.map +1 -0
  61. package/.output/server/chunks/_/mes-console.mjs +1 -1
  62. package/.output/server/chunks/_/nitro.mjs +2429 -1726
  63. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  64. package/.output/server/chunks/_/ocap.mjs +1 -1
  65. package/.output/server/chunks/_/rest-auth.mjs +1 -1
  66. package/.output/server/chunks/_/spc.mjs +1 -1
  67. package/.output/server/chunks/build/TownView-BWMDbosY.mjs +1 -1
  68. package/.output/server/chunks/build/daq-CdxcfY_G.mjs +1 -1
  69. package/.output/server/chunks/build/dcw-BlDQtS4q.mjs +1 -1
  70. package/.output/server/chunks/build/types-D6J7slEa.mjs +1 -1
  71. package/.output/server/chunks/build/useDaqAddNode-D_Rkju0f.mjs +1 -1
  72. package/.output/server/chunks/build/useDaqDetailNode-DeOsDxK5.mjs +1 -1
  73. package/.output/server/chunks/build/useDaqDetailParams-Cm9a9r5G.mjs +1 -1
  74. package/.output/server/chunks/build/useDaqFilters-CctmO03p.mjs +1 -1
  75. package/.output/server/chunks/build/useDaqStream-B9ZBVQrI.mjs +1 -1
  76. package/.output/server/chunks/build/useDaqTemplates-CK37sMfi.mjs +1 -1
  77. package/.output/server/chunks/build/useDcwAddNode-DACVcEHs.mjs +1 -1
  78. package/.output/server/chunks/build/useDcwStream-Dir5ufAU.mjs +1 -1
  79. package/.output/server/chunks/routes/api/mcp/workshop.mjs +1 -1
  80. package/.output/server/chunks/routes/api/system/backup/run.post.mjs +2 -2
  81. package/.output/server/chunks/routes/api/workshop/agent-tools/index.post.mjs +17 -9
  82. package/.output/server/chunks/routes/api/workshop/agent-tools/index.post.mjs.map +1 -1
  83. package/.output/server/chunks/routes/api/workshop/agents/_id_.patch.mjs +3 -0
  84. package/.output/server/chunks/routes/api/workshop/agents/_id_.patch.mjs.map +1 -1
  85. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  86. package/.output/server/chunks/routes/api/workshop/channel-templates/_id_.patch.mjs +6 -0
  87. package/.output/server/chunks/routes/api/workshop/channel-templates/_id_.patch.mjs.map +1 -1
  88. package/.output/server/chunks/routes/api/workshop/channels/_id/agents/_agentId_.patch.mjs +5 -0
  89. package/.output/server/chunks/routes/api/workshop/channels/_id/agents/_agentId_.patch.mjs.map +1 -1
  90. package/.output/server/chunks/routes/api/workshop/channels/_id/index.get.mjs +5 -2
  91. package/.output/server/chunks/routes/api/workshop/channels/_id/index.get.mjs.map +1 -1
  92. package/.output/server/chunks/routes/api/workshop/channels/_id/index.post.mjs +7 -1
  93. package/.output/server/chunks/routes/api/workshop/channels/_id/index.post.mjs.map +1 -1
  94. package/.output/server/chunks/routes/api/workshop/channels/index.get.mjs +3 -1
  95. package/.output/server/chunks/routes/api/workshop/channels/index.get.mjs.map +1 -1
  96. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +8 -6
  97. package/.output/server/chunks/routes/api/workshop/channels.post.mjs.map +1 -1
  98. package/.output/server/chunks/routes/api/workshop/daq/_id/ingest.post.mjs +29 -49
  99. package/.output/server/chunks/routes/api/workshop/daq/_id/ingest.post.mjs.map +1 -1
  100. package/.output/server/chunks/routes/api/workshop/daq/_id/samples.get.mjs +1 -1
  101. package/.output/server/chunks/routes/api/workshop/daq/_id_.patch.mjs +6 -1
  102. package/.output/server/chunks/routes/api/workshop/daq/_id_.patch.mjs.map +1 -1
  103. package/.output/server/chunks/routes/api/workshop/daq/alarms.get.mjs +4 -2
  104. package/.output/server/chunks/routes/api/workshop/daq/alarms.get.mjs.map +1 -1
  105. package/.output/server/chunks/routes/api/workshop/daq/index2.post.mjs +1 -1
  106. package/.output/server/chunks/routes/api/workshop/daq/infra/reconnect.post.mjs +14 -5
  107. package/.output/server/chunks/routes/api/workshop/daq/infra/reconnect.post.mjs.map +1 -1
  108. package/.output/server/chunks/routes/api/workshop/daq/infra.get.mjs +1 -1
  109. package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.delete.mjs +1 -1
  110. package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.patch.mjs +1 -1
  111. package/.output/server/chunks/routes/api/workshop/daq/test-driver.post.mjs +7 -4
  112. package/.output/server/chunks/routes/api/workshop/daq/test-driver.post.mjs.map +1 -1
  113. package/.output/server/chunks/routes/api/workshop/dcw/_id/bind.post.mjs +1 -1
  114. package/.output/server/chunks/routes/api/workshop/dcw/_id/bindings.put.mjs +1 -1
  115. package/.output/server/chunks/routes/api/workshop/dcw/_id/param-ledger.get.mjs +6 -2
  116. package/.output/server/chunks/routes/api/workshop/dcw/_id/param-ledger.get.mjs.map +1 -1
  117. package/.output/server/chunks/routes/api/workshop/dcw/_id/read.post.mjs +6 -2
  118. package/.output/server/chunks/routes/api/workshop/dcw/_id/read.post.mjs.map +1 -1
  119. package/.output/server/chunks/routes/api/workshop/dcw/_id/test.post.mjs +1 -1
  120. package/.output/server/chunks/routes/api/workshop/dcw/_id/write.post.mjs +1 -1
  121. package/.output/server/chunks/routes/api/workshop/dcw/_id_.delete.mjs +1 -1
  122. package/.output/server/chunks/routes/api/workshop/dcw/_id_.patch.mjs +6 -1
  123. package/.output/server/chunks/routes/api/workshop/dcw/_id_.patch.mjs.map +1 -1
  124. package/.output/server/chunks/routes/api/workshop/dcw/controller.post.mjs +1 -1
  125. package/.output/server/chunks/routes/api/workshop/dcw/index.get.mjs +7 -4
  126. package/.output/server/chunks/routes/api/workshop/dcw/index.get.mjs.map +1 -1
  127. package/.output/server/chunks/routes/api/workshop/dcw/index.post.mjs +1 -1
  128. package/.output/server/chunks/routes/api/workshop/dcw/index2.get.mjs +1 -1
  129. package/.output/server/chunks/routes/api/workshop/dcw/index2.post.mjs +1 -1
  130. package/.output/server/chunks/routes/api/workshop/dcw/index3.get.mjs +6 -2
  131. package/.output/server/chunks/routes/api/workshop/dcw/index3.get.mjs.map +1 -1
  132. package/.output/server/chunks/routes/api/workshop/dcw/index3.post.mjs +1 -1
  133. package/.output/server/chunks/routes/api/workshop/dcw/index4.get.mjs +7 -4
  134. package/.output/server/chunks/routes/api/workshop/dcw/index4.get.mjs.map +1 -1
  135. package/.output/server/chunks/routes/api/workshop/dcw/index4.post.mjs +1 -1
  136. package/.output/server/chunks/routes/api/workshop/dcw/index5.get.mjs +1 -1
  137. package/.output/server/chunks/routes/api/workshop/dcw/index5.post.mjs +1 -1
  138. package/.output/server/chunks/routes/api/workshop/dcw/index6.get.mjs +6 -2
  139. package/.output/server/chunks/routes/api/workshop/dcw/index6.get.mjs.map +1 -1
  140. package/.output/server/chunks/routes/api/workshop/dcw/journal/node/_nodeId/rollback.post.mjs +1 -1
  141. package/.output/server/chunks/routes/api/workshop/dcw/line/query.get.mjs +1 -1
  142. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/start.post.mjs +1 -1
  143. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/stop.post.mjs +1 -1
  144. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id_.delete.mjs +1 -1
  145. package/.output/server/chunks/routes/api/workshop/dcw/mes-artifacts/file.get.mjs +6 -2
  146. package/.output/server/chunks/routes/api/workshop/dcw/mes-artifacts/file.get.mjs.map +1 -1
  147. package/.output/server/chunks/routes/api/workshop/dcw/mes-hook-test.post.mjs +1 -1
  148. package/.output/server/chunks/routes/api/workshop/dcw/mes-test-read.post.mjs +1 -1
  149. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/judge.post.mjs +1 -1
  150. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/rollback.post.mjs +1 -1
  151. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/series.get.mjs +6 -2
  152. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/series.get.mjs.map +1 -1
  153. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id_.get.mjs +4 -2
  154. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id_.get.mjs.map +1 -1
  155. package/.output/server/chunks/routes/api/workshop/dcw/params/_id/read.post.mjs +5 -2
  156. package/.output/server/chunks/routes/api/workshop/dcw/params/_id/read.post.mjs.map +1 -1
  157. package/.output/server/chunks/routes/api/workshop/dcw/params/_id/write.post.mjs +1 -1
  158. package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.delete.mjs +1 -1
  159. package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.patch.mjs +1 -1
  160. package/.output/server/chunks/routes/api/workshop/dcw/products/_id_.delete.mjs +1 -1
  161. package/.output/server/chunks/routes/api/workshop/dcw/products/_id_.patch.mjs +1 -1
  162. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/apply.post.mjs +1 -1
  163. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/rollback-good.post.mjs +1 -1
  164. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs +7 -3
  165. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs.map +1 -1
  166. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id_.delete.mjs +8 -1
  167. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id_.delete.mjs.map +1 -1
  168. package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/close.post.mjs +6 -2
  169. package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/close.post.mjs.map +1 -1
  170. package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/data.get.mjs +1 -1
  171. package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/rollback.post.mjs +1 -1
  172. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.delete.mjs +1 -1
  173. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.patch.mjs +1 -1
  174. package/.output/server/chunks/routes/api/workshop/dcw/test-driver.post.mjs +7 -4
  175. package/.output/server/chunks/routes/api/workshop/dcw/test-driver.post.mjs.map +1 -1
  176. package/.output/server/chunks/routes/api/workshop/eda/commonality.get.mjs +1 -1
  177. package/.output/server/chunks/routes/api/workshop/eda/vm/predict.post.mjs +1 -1
  178. package/.output/server/chunks/routes/api/workshop/exp/confirmations/_id/decide.post.mjs +1 -1
  179. package/.output/server/chunks/routes/api/workshop/exp/index.get.mjs +1 -1
  180. package/.output/server/chunks/routes/api/workshop/fs/dirs.get.mjs +1 -1
  181. package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
  182. package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +1 -1
  183. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
  184. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
  185. package/.output/server/chunks/routes/api/workshop/index.mjs +1 -1
  186. package/.output/server/chunks/routes/api/workshop/index.post.mjs +2 -0
  187. package/.output/server/chunks/routes/api/workshop/index.post.mjs.map +1 -1
  188. package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
  189. package/.output/server/chunks/routes/api/workshop/index3.post.mjs +8 -3
  190. package/.output/server/chunks/routes/api/workshop/index3.post.mjs.map +1 -1
  191. package/.output/server/chunks/routes/api/workshop/index5.post.mjs +1 -1
  192. package/.output/server/chunks/routes/api/workshop/index6.get.mjs +1 -1
  193. package/.output/server/chunks/routes/api/workshop/index7.get.mjs +1 -1
  194. package/.output/server/chunks/routes/api/workshop/index7.post.mjs +1 -1
  195. package/.output/server/chunks/routes/api/workshop/mes/datasets.get.mjs +1 -1
  196. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/autofix.mjs +1 -1
  197. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/compile.mjs +1 -1
  198. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/evidence.mjs +1 -1
  199. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/publish.mjs +1 -1
  200. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/approval-decision.mjs +1 -1
  201. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/approval.mjs +1 -1
  202. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/transition.mjs +1 -1
  203. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/subgraph.mjs +1 -1
  204. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/validate.mjs +1 -1
  205. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId_.mjs +1 -1
  206. package/.output/server/chunks/routes/api/workshop/ontology/export/_ontologyId_.mjs +1 -1
  207. package/.output/server/chunks/routes/api/workshop/ontology/index.mjs +1 -1
  208. package/.output/server/chunks/routes/api/workshop/ontology/scene.mjs +1 -1
  209. package/.output/server/chunks/routes/api/workshop/ontology/traces/_ontologyId_.mjs +1 -1
  210. package/.output/server/chunks/routes/api/workshop/ontology/versions/_ontologyId/compare.mjs +1 -1
  211. package/.output/server/chunks/routes/api/workshop/ontology/versions/_ontologyId_.mjs +1 -1
  212. package/.output/server/chunks/routes/api/workshop/optimization/capabilities.get.mjs +1 -1
  213. package/.output/server/chunks/routes/api/workshop/optimization/index.get.mjs +1 -1
  214. package/.output/server/chunks/routes/api/workshop/optimization/index.mjs +1 -1
  215. package/.output/server/chunks/routes/api/workshop/optimization/index2.mjs +1 -1
  216. package/.output/server/chunks/routes/api/workshop/optimization/profiles/_id/publish.post.mjs +1 -1
  217. package/.output/server/chunks/routes/api/workshop/optimization/recommendations/_id/crosscheck.post.mjs +1 -1
  218. package/.output/server/chunks/routes/api/workshop/optimization/recommendations/_id/dispatch.post.mjs +1 -1
  219. package/.output/server/chunks/routes/api/workshop/optimization/solve.post.mjs +1 -1
  220. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  221. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  222. package/.output/server/chunks/routes/api/workshop/process-model/datasets.post.mjs +1 -1
  223. package/.output/server/chunks/routes/api/workshop/process-model/eligibility.mjs +1 -1
  224. package/.output/server/chunks/routes/api/workshop/process-model/index.mjs +1 -1
  225. package/.output/server/chunks/routes/api/workshop/process-model/specs/_id/publish.post.mjs +1 -1
  226. package/.output/server/chunks/routes/api/workshop/process-model/train.post.mjs +1 -1
  227. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  228. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  229. package/.output/server/chunks/routes/api/workshop/tools/sync.post.mjs +1 -1
  230. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  231. package/.output/server/chunks/routes/renderer.mjs +2 -2
  232. package/.output/server/chunks/virtual/entry.mjs +1 -1
  233. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  234. package/.output/server/package.json +1 -1
  235. package/README-zh.md +4 -2
  236. package/README.md +9 -2
  237. package/config.yml +1 -1
  238. package/docker-compose.yml +4 -2
  239. package/docs/cli.en.md +2 -2
  240. package/docs/cli.md +2 -2
  241. package/docs/plugins.en.md +1 -1
  242. package/docs/plugins.md +1 -1
  243. package/docs/sdk.en.md +1 -1
  244. package/docs/sdk.md +1 -1
  245. package/package.json +1 -1
  246. package/scripts/_audit/_live-mqtt-e2e-20261028.mjs +219 -0
  247. package/scripts/_audit/prompt-injection-probe.mts +76 -0
  248. package/scripts/api-live-e2e.mjs +9 -3
  249. package/scripts/e2e-multiexpert-remediation-real.mjs +453 -0
  250. package/scripts/e2e-node-binding-3mode-real.mjs +697 -0
  251. package/scripts/e2e-node-binding-authz.mjs +332 -0
  252. package/scripts/security-acceptance.mjs +218 -0
  253. package/server/api/workshop/agent-tools/bindings/index.post.ts +33 -17
  254. package/server/api/workshop/agents/[id].patch.ts +3 -0
  255. package/server/api/workshop/agents/index.post.ts +3 -0
  256. package/server/api/workshop/channel-templates/[id].patch.ts +5 -0
  257. package/server/api/workshop/channel-templates/index.post.ts +6 -0
  258. package/server/api/workshop/channels/[id]/agents/[agentId].patch.ts +5 -0
  259. package/server/api/workshop/channels/[id]/agents/index.get.ts +8 -2
  260. package/server/api/workshop/channels/[id]/agents/index.post.ts +7 -0
  261. package/server/api/workshop/channels/[id]/index.get.ts +10 -4
  262. package/server/api/workshop/channels.post.ts +3 -0
  263. package/server/api/workshop/daq/[id]/ingest.post.ts +26 -22
  264. package/server/api/workshop/daq/[id].patch.ts +8 -0
  265. package/server/api/workshop/daq/alarms.get.ts +18 -16
  266. package/server/api/workshop/daq/infra/reconnect.post.ts +21 -5
  267. package/server/api/workshop/daq/test-driver.post.ts +10 -3
  268. package/server/api/workshop/dcw/[id]/param-ledger.get.ts +7 -1
  269. package/server/api/workshop/dcw/[id]/read.post.ts +6 -1
  270. package/server/api/workshop/dcw/[id].patch.ts +49 -40
  271. package/server/api/workshop/dcw/journal/index.get.ts +7 -3
  272. package/server/api/workshop/dcw/mes-artifacts/file.get.ts +7 -1
  273. package/server/api/workshop/dcw/mes-artifacts/index.get.ts +7 -1
  274. package/server/api/workshop/dcw/optimizations/[id]/series.get.ts +6 -1
  275. package/server/api/workshop/dcw/optimizations/[id].get.ts +3 -1
  276. package/server/api/workshop/dcw/optimizations/index.get.ts +6 -3
  277. package/server/api/workshop/dcw/params/[id]/read.post.ts +22 -17
  278. package/server/api/workshop/dcw/recipes/[id]/versions.get.ts +20 -14
  279. package/server/api/workshop/dcw/recipes/[id].delete.ts +33 -25
  280. package/server/api/workshop/dcw/recipes/index.get.ts +17 -12
  281. package/server/api/workshop/dcw/runs/[id]/close.post.ts +21 -15
  282. package/server/api/workshop/dcw/test-driver.post.ts +21 -16
  283. package/server/api/workshop/ws/hub.ts +2 -1
  284. package/server/plugins/01-instance-guard.ts +82 -0
  285. package/server/plugins/daq.ts +40 -8
  286. package/server/plugins/workshop.ts +12 -0
  287. package/server/services/workshop/agents/adapters/line-spawn.ts +24 -1
  288. package/server/services/workshop/agents/agent-dto.ts +54 -0
  289. package/server/services/workshop/agents/harness-spawn-policy.ts +114 -0
  290. package/server/services/workshop/agents/host-tool-bridge/dispatch.ts +22 -0
  291. package/server/services/workshop/agents/host-tool-bridge/index.ts +1 -1
  292. package/server/services/workshop/agents/host-tool-bridge/per-agent.ts +17 -3
  293. package/server/services/workshop/agents/host-tool-bridge/tools/tasks.ts +1 -1
  294. package/server/services/workshop/agents/host-tool-bridge/tools/team-delegation.ts +5 -1
  295. package/server/services/workshop/agents/industrial/dcw-tools.ts +6 -2
  296. package/server/services/workshop/agents/industrial/ops-tools.ts +54 -12
  297. package/server/services/workshop/agents/industrial/recipe-gate.ts +68 -15
  298. package/server/services/workshop/agents/industrial/tool-verify-legacy-specs.ts +2 -2
  299. package/server/services/workshop/agents/industrial/tool-verify.ts +31 -2
  300. package/server/services/workshop/agents/node-bindings.repo.ts +4 -3
  301. package/server/services/workshop/agents/node-delegation.ts +49 -4
  302. package/server/services/workshop/agents/tool-approvals.ts +23 -0
  303. package/server/services/workshop/agents/tool-reference-spec.ts +3 -0
  304. package/server/services/workshop/daq/bus/mqtt.adapter.ts +12 -0
  305. package/server/services/workshop/daq/daq-controller/crud.ts +5 -3
  306. package/server/services/workshop/daq/daq-controller/ingest.ts +23 -0
  307. package/server/services/workshop/daq/daq-controller/push.ts +24 -3
  308. package/server/services/workshop/daq/daq-controller/runtime-control.ts +6 -0
  309. package/server/services/workshop/daq/daq-controller/sweep-alarms.ts +3 -1
  310. package/server/services/workshop/daq/daq-controller/tsdb-twin.ts +21 -1
  311. package/server/services/workshop/daq/daq-node.repo.ts +10 -7
  312. package/server/services/workshop/daq/daq-node.ts +2 -0
  313. package/server/services/workshop/daq/daq-runtime.ts +10 -2
  314. package/server/services/workshop/daq/drivers/modbus-tcp.ts +8 -5
  315. package/server/services/workshop/daq/drivers/registry.ts +5 -4
  316. package/server/services/workshop/daq/infra.ts +13 -0
  317. package/server/services/workshop/daq/ingest-policy.ts +24 -1
  318. package/server/services/workshop/daq/storage/timescale.adapter.ts +42 -3
  319. package/server/services/workshop/db/database/migrations.ts +83 -68
  320. package/server/services/workshop/dcw/dcw-controller/crud.ts +27 -2
  321. package/server/services/workshop/dcw/dcw-controller/lines.ts +14 -3
  322. package/server/services/workshop/dcw/dcw-controller/recipes.ts +63 -5
  323. package/server/services/workshop/dcw/dcw-recipe.repo.ts +10 -1
  324. package/server/services/workshop/dcw/dcw-runtime.ts +37 -14
  325. package/server/services/workshop/dcw/drivers/shared.ts +62 -6
  326. package/server/services/workshop/dcw/recipe-op-anchor.ts +28 -0
  327. package/server/services/workshop/dcw/recipe-rollback-manager/rollback.ts +204 -190
  328. package/server/services/workshop/dcw/recipe-rollback-manager/sweep.ts +97 -77
  329. package/server/services/workshop/line-scope.ts +80 -0
  330. package/server/services/workshop/optimization/dispatch.service.ts +18 -2
  331. package/server/services/workshop/permissions.ts +2 -0
  332. package/server/services/workshop/runtime/agent-runtime/message.ts +17 -0
  333. package/server/services/workshop/runtime/agent-runtime/state.ts +9 -3
  334. package/server/services/workshop/runtime/agent-runtime/types.ts +2 -1
  335. package/server/services/workshop/runtime/channel-runtime.ts +3 -1
  336. package/server/services/workshop/runtime/mailbox.ts +6 -3
  337. package/server/services/workshop/runtime/manager/a2a.ts +4 -2
  338. package/server/services/workshop/runtime/manager/admin-channel.ts +1 -1
  339. package/server/services/workshop/runtime/manager/chat.ts +3 -1
  340. package/server/services/workshop/runtime/manager/host-tools.ts +1 -1
  341. package/server/services/workshop/runtime/manager/runtime-wiring.ts +31 -19
  342. package/server/services/workshop/runtime/manager/state.ts +43 -1
  343. package/server/services/workshop/runtime/permission-scope.ts +21 -0
  344. package/server/services/workshop/runtime/scheduler-loop/tick.ts +16 -4
  345. package/server/services/workshop/scene-events.ts +19 -0
  346. package/shared/daq-protocol.ts +24 -5
  347. package/shared/dcw-protocol/catalog.ts +4 -4
  348. package/.output/public/_nuxt/CCI-fwcS2.js +0 -1
  349. package/.output/public/_nuxt/builds/meta/5cd11284-05bf-4fcd-9a69-56a01c9f56dd.json +0 -1
@@ -0,0 +1,114 @@
1
+ /**
2
+ * harness 启动面策略 —— 谁能指定"拉起什么可执行文件/在哪个目录/带什么环境"。
3
+ *
4
+ * 背景(2026-10-28 审计):Channel/Agent 创建接受任意 `config`,manager 原样落库,
5
+ * 各 harness impl 直接拿 `config.command/args/cwd` 去 spawn;`assertHarnessUsable`
6
+ * 只判断"文件存在"。任何登录账号因此可以把 lead 的 command 设成 cmd.exe/sh,
7
+ * 在首个调度回合获得服务账号的命令执行能力。
8
+ *
9
+ * 本模块把判定收敛为纯函数(便于单测):
10
+ * - 非 admin 不得覆盖 command/args/cwd/env(一律 403,文案给出下一步);
11
+ * - admin 覆盖 command 时,basename 必须与引擎自身命令一致(omp/omp.exe…),
12
+ * 拒绝 cmd/sh/node 等通用解释器;
13
+ * - cwd 必须落在 process.cwd() 或 configRoot 之下(admin 可用
14
+ * AW_ALLOW_ARBITRARY_HARNESS_CWD=1 显式豁免,仅限受控部署)。
15
+ *
16
+ * 真正的执行层防御仍在 adapters/line-spawn(不走 shell、参数逐个校验、
17
+ * 敏感 env 剥离),本层负责"授权谁可以改这些参数"。
18
+ */
19
+ import { basename, resolve } from 'node:path'
20
+ import { AppError } from '../../../utils/errors'
21
+
22
+ /** 引擎自身允许的命令 basename(不含扩展名比较;.cmd/.bat shim 也算同族) */
23
+ const ALLOWED_COMMANDS: Record<string, string[]> = {
24
+ omp: ['omp'],
25
+ claude: ['claude'],
26
+ codex: ['codex'],
27
+ opencode: ['opencode'],
28
+ dsh: ['dsh'],
29
+ gemini: ['gemini'],
30
+ copilot: ['copilot'],
31
+ cursor: ['cursor-agent', 'cursor'],
32
+ crush: ['crush'],
33
+ goose: ['goose'],
34
+ qwen: ['qwen'],
35
+ pi: ['pi'],
36
+ hermes: ['hermes'],
37
+ mock: [],
38
+ }
39
+
40
+ /** 允许被 config 覆盖的启动键(出现即需要 admin) */
41
+ export const SPAWN_CONFIG_KEYS = ['command', 'args', 'cwd', 'env'] as const
42
+
43
+ export interface SpawnActor {
44
+ id?: string
45
+ role: string
46
+ }
47
+
48
+ /** 覆盖了哪些启动键(空数组 = 使用平台默认,任何角色都允许) */
49
+ export function spawnOverridesOf(config: Record<string, unknown> | undefined | null): string[] {
50
+ if (!config || typeof config !== 'object') return []
51
+ return SPAWN_CONFIG_KEYS.filter((k) => {
52
+ const v = config[k]
53
+ if (v === undefined || v === null) return false
54
+ if (typeof v === 'string') return v.trim() !== ''
55
+ if (Array.isArray(v)) return v.length > 0
56
+ return true
57
+ })
58
+ }
59
+
60
+ function baseNameOf(command: string): string {
61
+ const base = basename(command.trim()).toLowerCase()
62
+ return base.replace(/\.(exe|cmd|bat|com|sh)$/i, '')
63
+ }
64
+
65
+ function cwdAllowed(cwd: string): boolean {
66
+ if (process.env.AW_ALLOW_ARBITRARY_HARNESS_CWD === '1') return true
67
+ const target = resolve(cwd)
68
+ const roots = [resolve(process.cwd())]
69
+ const configRoot = process.env.AW_HOME
70
+ if (configRoot) roots.push(resolve(configRoot))
71
+ return roots.some(root => target === root || target.startsWith(root + '\\') || target.startsWith(root + '/'))
72
+ }
73
+
74
+ /**
75
+ * 校验一次 harness 启动配置。通过返回 void;拒绝抛 403(message 可直接给用户)。
76
+ * @param harness 引擎 id(用于取命令白名单)
77
+ * @param config 即将落库的 agent/channel config
78
+ * @param actor 调用者(角色决定是否允许覆盖)
79
+ */
80
+ export function assertSpawnConfigAllowed(
81
+ harness: string,
82
+ config: Record<string, unknown> | undefined | null,
83
+ actor: SpawnActor,
84
+ ): void {
85
+ const overrides = spawnOverridesOf(config)
86
+ if (overrides.length === 0) return
87
+ if (actor.role !== 'admin') {
88
+ throw new AppError(
89
+ 403,
90
+ 'HARNESS_SPAWN_FORBIDDEN',
91
+ `自定义 harness 启动参数(${overrides.join('/')})仅管理员可配置。请使用平台提供的引擎默认安装路径,或联系管理员在受控部署中设置。`,
92
+ )
93
+ }
94
+ const command = typeof config?.command === 'string' ? config.command.trim() : ''
95
+ if (command) {
96
+ const base = baseNameOf(command)
97
+ const allowed = ALLOWED_COMMANDS[harness] ?? []
98
+ if (!allowed.includes(base)) {
99
+ throw new AppError(
100
+ 403,
101
+ 'HARNESS_COMMAND_DENIED',
102
+ `harness「${harness}」不允许以「${base}」启动:命令必须与该引擎自身一致(允许 ${allowed.join('/') || '(无)'})。通用解释器(cmd/sh/node/powershell)一律拒绝。`,
103
+ )
104
+ }
105
+ }
106
+ const cwd = typeof config?.cwd === 'string' ? config.cwd.trim() : ''
107
+ if (cwd && !cwdAllowed(cwd)) {
108
+ throw new AppError(
109
+ 403,
110
+ 'HARNESS_CWD_DENIED',
111
+ `harness 工作目录「${cwd}」不在允许范围内(需位于平台工作根或配置根之下)。受控部署可显式设置 AW_ALLOW_ARBITRARY_HARNESS_CWD=1。`,
112
+ )
113
+ }
114
+ }
@@ -38,8 +38,30 @@ import { handleCreateTeamAgent, handleGetQueueOverview, handleListTeamAgents, ha
38
38
  * 分发一次 host tool 调用(全引擎唯一入口):
39
39
  * 插件工具优先 → workspace 面 → 工业工具族 → 插件兜底 → 未知工具。
40
40
  */
41
+ /**
42
+ * 工具活性注册表(2026-10-28 审查 C-P1-3)。
43
+ *
44
+ * 调度停滞看门狗以「最近一次工具调用」为健康信号,但旧实现只在 manager.invokeHostTool
45
+ * (REST/MCP 回程)打点 —— 默认引擎 omp 的原生 host_tool_call 与 pi 扩展完全不经那里,
46
+ * 信号恒空:biz 忙碌 5min 误 notify、回合间隙被误判 stall 而 COMPLETED/CANCELED。
47
+ * 打点必须放在**全引擎唯一分发入口**;这里只存 epoch ms,零业务耦合。
48
+ */
49
+ const toolActivity = new Map<string, number>()
50
+ export function noteToolActivity(agentId: string): void {
51
+ if (agentId) toolActivity.set(agentId, Date.now())
52
+ }
53
+ export function toolActivityAt(agentId: string): number | null {
54
+ return toolActivity.get(agentId) ?? null
55
+ }
56
+ /** 仅供测试:清空活性注册表 */
57
+ export function resetToolActivity(): void {
58
+ toolActivity.clear()
59
+ }
60
+
41
61
  /** 统一出口:所有工具结果经截断收口(单点修改,全族生效) */
42
62
  export async function dispatchHostTool(ctx: HostToolBridgeContext, req: HostToolCall): Promise<HostToolResult> {
63
+ // 活性打点先于一切校验:被拒的工具调用同样证明 agent 在推进(不是死锁)
64
+ noteToolActivity(ctx.identity.agentId)
43
65
  return truncateToolResult(await dispatchHostToolInner(ctx, req), req.toolName)
44
66
  }
45
67
 
@@ -13,4 +13,4 @@ export { HOST_TOOLS, LEAD_ONLY_TOOL_NAMES, hostToolsForRole } from './catalog'
13
13
  export { hostToolsForAgent, agentToolResources } from './per-agent'
14
14
  export type { HostToolBridgeContext, HostToolCall, HostToolResult, HostToolSessionState } from './types'
15
15
  export { createSessionState, partsToText } from './session'
16
- export { dispatchHostTool } from './dispatch'
16
+ export { dispatchHostTool, noteToolActivity, toolActivityAt, resetToolActivity } from './dispatch'
@@ -27,6 +27,8 @@ export interface AgentToolResourceView {
27
27
  daqNodes: Array<{ id: string, name: string, unit: string }>
28
28
  /** 频道绑线带来的只读数采节点(v28;执行面 daqTargetsOf 同样并入) */
29
29
  daqLineNodes: Array<{ id: string, name: string, unit: string }>
30
+ /** Agent 显式绑定的数控节点(kind=dcw)= 点位写授权面(2026-10-28 双轨授权) */
31
+ writeNodes: Array<{ id: string, name: string, unit: string }>
30
32
  recipes: Array<{ id: string, name: string, version: number, params: Array<{ nodeId: string, name: string, unit: string }> }>
31
33
  mesNodes: Array<{ id: string, name: string, unit: string }>
32
34
  }
@@ -46,7 +48,7 @@ function channelLineOf(agentId: string): string {
46
48
 
47
49
  /** 读取 Agent 的绑定面(只读;异常降级为空,绝不影响工具注入) */
48
50
  export function agentToolResources(agentId: string): AgentToolResourceView {
49
- const out: AgentToolResourceView = { daqNodes: [], daqLineNodes: [], recipes: [], mesNodes: [] }
51
+ const out: AgentToolResourceView = { daqNodes: [], daqLineNodes: [], writeNodes: [], recipes: [], mesNodes: [] }
50
52
  if (!agentId) return out
51
53
  try {
52
54
  const bindings = getAgentNodeBindingRepo().byAgent(agentId)
@@ -63,6 +65,11 @@ export function agentToolResources(agentId: string): AgentToolResourceView {
63
65
  const n = daq.byId(b.nodeId)
64
66
  if (n) out.daqNodes.push({ id: n.id, name: n.name, unit: n.unit ?? '' })
65
67
  }
68
+ else if (b.kind === 'dcw') {
69
+ // 点位写授权面:让模型不必先探测就知道自己能对哪些节点 trial/apply
70
+ const n = dcw.byId(b.nodeId)
71
+ if (n) out.writeNodes.push({ id: n.id, name: n.name, unit: n.unit ?? '' })
72
+ }
66
73
  else if (b.kind === 'recipe') {
67
74
  const r = dcw.listRecipes().find(x => x.id === b.nodeId)
68
75
  if (!r) continue
@@ -109,15 +116,22 @@ export function hostToolsForAgent(agentId: string, role: 'lead' | 'worker', chan
109
116
  const res = agentToolResources(agentId)
110
117
  // WP6:排序保证同一绑定面确定性输出(折叠/不折叠都稳定)
111
118
  const sortedDaq = sortedById(res.daqNodes)
119
+ const sortedWrite = sortedById(res.writeNodes)
112
120
  const sortedRecipes = sortedById(res.recipes)
113
121
  const sortedMes = sortedById(res.mesNodes)
114
122
  const lineOnlyDaq = sortedById(res.daqLineNodes.filter(n => !sortedDaq.some(x => x.id === n.id)))
115
- if (sortedDaq.length === 0 && lineOnlyDaq.length === 0 && sortedRecipes.length === 0 && !bindings.length) return filtered
123
+ if (sortedDaq.length === 0 && lineOnlyDaq.length === 0 && sortedWrite.length === 0 && sortedRecipes.length === 0 && !bindings.length) return filtered
116
124
  const daqParts = [
117
125
  sortedDaq.length > 0 ? foldList(tr(locale, `本 Agent 直接绑定的数采节点(仅这些可查):`, `DAQ nodes bound directly to this agent (queryable):`), sortedDaq.map(x => `${x.name}(${x.id}${x.unit ? `,${x.unit}` : ''})`)) : '',
118
126
  lineOnlyDaq.length > 0 ? foldList(tr(locale, `频道绑线只读数采节点(可查):`, `Read-only DAQ nodes from the channel line binding:`), lineOnlyDaq.map(x => `${x.name}(${x.id}${x.unit ? `,${x.unit}` : ''})`)) : '',
119
127
  ].filter(Boolean)
120
128
  const daqLine = daqParts.length > 0 ? `\n\n${daqParts.join('\n')}` : ''
129
+ const writeLine = sortedWrite.length > 0
130
+ ? foldList(
131
+ tr(locale, `\n\n本 Agent 的数控节点写授权面(点位写授权;可在这些节点上 trial/apply,换配方仍有效):`, `\n\nDCW node write-authorization face of this agent (point-level; trial/apply allowed on these nodes, survives recipe switches):`),
132
+ sortedWrite.map(x => `${x.name}(${x.id}${x.unit ? `,${x.unit}` : ''})`),
133
+ )
134
+ : ''
121
135
  const recipeLine = sortedRecipes.length > 0
122
136
  ? foldList(
123
137
  tr(locale, `\n\n本 Agent 已绑定的配方(仅这些可写/下发):`, `\n\nRecipes bound to this agent (writable/dispatchable):`),
@@ -130,7 +144,7 @@ export function hostToolsForAgent(agentId: string, role: 'lead' | 'worker', chan
130
144
  return filtered.map((t) => {
131
145
  let suffix = ''
132
146
  if (DAQ_TOOLS.has(t.name)) suffix = daqLine
133
- else if (RECIPE_TOOLS.has(t.name)) suffix = recipeLine
147
+ else if (RECIPE_TOOLS.has(t.name)) suffix = `${recipeLine}${writeLine}`
134
148
  else if (MES_TOOLS.has(t.name)) suffix = mesLine
135
149
  else if (OPTIMIZER_TOOL_NAMES.has(t.name)) suffix = optimizerLine
136
150
  if (!suffix) return t
@@ -42,7 +42,7 @@ export async function handleDispatchTask(args: Record<string, unknown>, ws: Agen
42
42
  const grantNodeIds = (Array.isArray(rawGrant) ? rawGrant : String(rawGrant).split(',')).map(x => String(x ?? '').trim()).filter(Boolean)
43
43
  if (!grantNodeIds.length) return { text: 'grant_node_ids 为空:要么不传,要么给出至少一个节点 id。', isError: true }
44
44
  const grantMode = args.grant_mode === 'manual' || args.grant_mode === 'auto' ? (args.grant_mode as 'auto' | 'manual') : undefined
45
- const grant = await grantNodesForDispatch(identity, assigneeId, grantNodeIds, grantMode, ws)
45
+ const grant = await grantNodesForDispatch(identity, assigneeId, grantNodeIds, grantMode, ws, args.grant_confirm)
46
46
  if (!grant.ok) return { text: `未派发:${grant.text}\n请先用 team_grant_nodes 确认你持有这些节点,或修正 node id 后重试。`, isError: true }
47
47
  grantNote = `\n${grant.text}`
48
48
  }
@@ -42,6 +42,8 @@ export async function handleGrantNodes(identity: HostToolBridgeContext['identity
42
42
  nodeIds,
43
43
  members: await membersOf(ws),
44
44
  mode,
45
+ // B-P1-3:mode=auto 摘除逐次人工审批,须显式风险确认(与 REST 同语义)
46
+ confirm: args.confirm,
45
47
  })
46
48
  const bindings = applyDelegation(plan, identity.agentId)
47
49
  return {
@@ -84,7 +86,7 @@ export interface DispatchGrantResult {
84
86
  * 派发时随任务授予节点(dispatch_task 的 grant_node_ids;lead 专属)。
85
87
  * fail-closed:任一节点不在 lead 绑定面 → 整单失败,任务不创建。
86
88
  */
87
- export async function grantNodesForDispatch(identity: HostToolBridgeContext['identity'], assigneeId: string, nodeIds: string[], mode: 'auto' | 'manual' | undefined, ws: MembersView): Promise<DispatchGrantResult> {
89
+ export async function grantNodesForDispatch(identity: HostToolBridgeContext['identity'], assigneeId: string, nodeIds: string[], mode: 'auto' | 'manual' | undefined, ws: MembersView, confirm?: unknown): Promise<DispatchGrantResult> {
88
90
  try {
89
91
  const plan = planDelegation({
90
92
  leaderAgentId: identity.agentId,
@@ -93,6 +95,8 @@ export async function grantNodesForDispatch(identity: HostToolBridgeContext['ide
93
95
  nodeIds,
94
96
  members: await membersOf(ws),
95
97
  mode,
98
+ // B-P1-3:随任务授予同样不得绕过 auto 的显式确认(dispatch_task.grant_confirm)
99
+ confirm,
96
100
  })
97
101
  const bindings = applyDelegation(plan, identity.agentId)
98
102
  return { ok: true, text: `随任务授予 ${bindings.length} 条节点授权 → ${assigneeId}${summaryText(bindings)}` }
@@ -263,8 +263,12 @@ async function dcwControlGovernedLegacy(agentId: string, args: { node_id?: strin
263
263
  text: `下发受理:${node.name}(${tpl?.ch ?? node.templateKey})设定 ${value}${node.unit} → PLC 原始值 ${outcome.raw ?? '-'};${ackTxt}。${winTxt}。${outcome.message}\n[调控闭环] ${loopTxt}${manualFeedback ? `\n[人工反馈] ${manualFeedback}` : ''}`,
264
264
  }
265
265
  }
266
- // 失败文案带自我纠正线索:当前保持原值 + 建议动作(缩小步进/稍后重试)
267
- const keptHint = `当前设定值保持 ${node.value ?? '原值'}${node.unit} 未被改动`
266
+ // 失败文案带自我纠正线索:区分「确定未写」与「可能已写但未证实」
267
+ // S15:回读不一致/无确认通道时,PLC 可能已经被改,不能再宣称"值未被改动"。
268
+ const uncertain = outcome.raw != null || outcome.ack === 'unverified'
269
+ const keptHint = uncertain
270
+ ? `写入未获证实,PLC 值可能已变更(原始值 ${outcome.raw ?? '-'}),请先读取核对后再决定是否重试`
271
+ : `当前设定值保持 ${node.value ?? '原值'}${node.unit} 未被改动`
268
272
  const retryHint = /忙|busy/i.test(outcome.message) ? '链路忙属瞬时状态,可稍后重试' : '可缩小步进幅度后重试'
269
273
  return { text: `下发失败:${outcome.message}(节点 ${node.name},物理量 ${tpl?.ch ?? node.templateKey},安全量程 ${node.min}~${node.max}${node.unit};${keptHint};${retryHint})`, isError: true }
270
274
  }
@@ -750,8 +750,8 @@ export async function toolRecipeUpdate(agentId: string, args: {
750
750
  if (!target.ok) return { text: target.text, isError: true }
751
751
  const recipe = target.recipe
752
752
  const recipeId = recipe.id
753
- // 权限模型 v2:绑定 → 二级认证 → 运行门(未在执行的配方不可操作)
754
- const gate = assertRecipeOperation(agentId, recipe, 'update')
753
+ // 权限模型 v2 + 2026-10-28 双轨:版本操作只认 recipe 绑定(节点绑定不授予版本权)
754
+ const gate = assertRecipeOperation(agentId, recipe, 'update', list.map(p => String(p.node_id).trim()))
755
755
  if (!gate.ok) return { text: gate.text, isError: true }
756
756
  for (const p of list) {
757
757
  const nodeId = String(p.node_id).trim()
@@ -761,6 +761,13 @@ export async function toolRecipeUpdate(agentId: string, args: {
761
761
  if (!node.enabled) return { text: `节点「${node.name}」已停用,参数保存被拒。请先让用户恢复节点控制,或保存到其他节点。`, isError: true }
762
762
  }
763
763
  // HITL:manual 绑定(或全局下发审批开关)→ 保存前挂人工审批,理由随审批卡展示
764
+ // B-P1-4(2026-10-28 审查):审批卡展示的是"此刻的"配方定义;审批窗口内若有人并发
765
+ // 编辑同一配方,旧实现会把批准结果静默并进**已变的**定义(本次只改列出的参数,其余
766
+ // 取最新值),人工批准的语义被偷换。进入审批前记录定义指纹,批准后逐字比对。
767
+ const approvedDefinitionSnapshot = JSON.stringify({
768
+ version: recipe.version ?? 1,
769
+ params: recipe.params.map(p => ({ nodeId: p.nodeId, value: p.value, min: p.min ?? null, max: p.max ?? null })),
770
+ })
764
771
  let hitlNote = ''
765
772
  if (gate.hitlRequired) {
766
773
  const gateDecision = await hitlDispatchGate(
@@ -788,8 +795,24 @@ export async function toolRecipeUpdate(agentId: string, args: {
788
795
  }
789
796
  }
790
797
  if (gateDecision.comment) hitlNote = `\n人工附言:${gateDecision.comment}`
791
- const stale = await revalidateApprovedAction(agentId, recipe, 'update', 'recipe_update', { recipe_id: recipeId }, gate.activeContext)
798
+ const stale = await revalidateApprovedAction(agentId, recipe, 'update', 'recipe_update', { recipe_id: recipeId, params: list.map(p => ({ node_id: p.node_id })) }, gate.activeContext)
792
799
  if (stale) return stale
800
+ // B-P1-4:批准后复核配方定义 —— 审批窗口内的并发编辑不得被静默并入已批版本。
801
+ const latestRecipe = getDcwRecipeRepo().byId(recipeId)
802
+ const latestSnapshot = latestRecipe
803
+ ? JSON.stringify({
804
+ version: latestRecipe.version ?? 1,
805
+ params: latestRecipe.params.map(p => ({ nodeId: p.nodeId, value: p.value, min: p.min ?? null, max: p.max ?? null })),
806
+ })
807
+ : ''
808
+ if (!latestRecipe || latestSnapshot !== approvedDefinitionSnapshot) {
809
+ return {
810
+ text: `人工已批准,但审批期间配方「${recipe.name}」的定义已被其他操作修改(v${recipe.version ?? 1} → v${latestRecipe?.version ?? '已删除'});`
811
+ + `为避免把本次批准静默并入已变更的定义,本次未执行、配方未改动。`
812
+ + `请调用 recipe_catalog/recipe_versions 重新读取最新参数与理由后,基于最新定义重新提交 recipe_update。`,
813
+ isError: true,
814
+ }
815
+ }
793
816
  }
794
817
  // 旧值快照(repo.update 原地改写 recipe.params,事后再取会把 before 读成新值)
795
818
  const beforeOf = new Map(recipe.params.map(p => [p.nodeId, p.value]))
@@ -992,11 +1015,12 @@ async function hitlDispatchGate(agentId: string, recipeId: string, detail: strin
992
1015
  */
993
1016
  function approvedContextError(
994
1017
  agentId: string,
995
- recipe: { id: string, name: string, lineId?: string, access?: { requireAuth?: boolean, authorizedAgentIds?: string[] } },
1018
+ recipe: { id: string, name: string, lineId?: string, params?: Array<{ nodeId: string }>, access?: { requireAuth?: boolean, authorizedAgentIds?: string[] } },
996
1019
  op: 'update' | 'dispatch' | 'trial' | 'rollback',
997
1020
  expectedContext: ActiveRecipeContext,
1021
+ targetNodeIds?: string[],
998
1022
  ): { text: string, isError: true } | null {
999
- const currentGate = assertRecipeOperation(agentId, recipe, op)
1023
+ const currentGate = assertRecipeOperation(agentId, recipe, op, targetNodeIds)
1000
1024
  if (!currentGate.ok) {
1001
1025
  return { text: `人工已批准,但审批期间授权已失效,本次未执行:${currentGate.text}`, isError: true }
1002
1026
  }
@@ -1013,15 +1037,26 @@ function approvedContextError(
1013
1037
  return null
1014
1038
  }
1015
1039
 
1040
+ /** 从审批时的工具参数还原「本次覆盖的节点集」(trial/update 只写声明节点;其余=整批)。 */
1041
+ function targetNodesFromArgs(op: 'update' | 'dispatch' | 'trial' | 'rollback', args: Record<string, unknown>): string[] | undefined {
1042
+ if (op !== 'trial' && op !== 'update') return undefined
1043
+ const params = Array.isArray(args.params) ? args.params : []
1044
+ const ids = params
1045
+ .map(p => String((p as { node_id?: unknown } | null)?.node_id ?? '').trim())
1046
+ .filter(Boolean)
1047
+ return ids.length > 0 ? ids : undefined
1048
+ }
1049
+
1016
1050
  export async function revalidateApprovedAction(
1017
1051
  agentId: string,
1018
- recipe: { id: string, name: string, lineId?: string, access?: { requireAuth?: boolean, authorizedAgentIds?: string[] } },
1052
+ recipe: { id: string, name: string, lineId?: string, params?: Array<{ nodeId: string }>, access?: { requireAuth?: boolean, authorizedAgentIds?: string[] } },
1019
1053
  op: 'update' | 'dispatch' | 'trial' | 'rollback',
1020
1054
  toolName: string,
1021
1055
  args: Record<string, unknown>,
1022
1056
  expectedContext: ActiveRecipeContext,
1023
1057
  ): Promise<{ text: string, isError: true } | null> {
1024
- const initialContextError = approvedContextError(agentId, recipe, op, expectedContext)
1058
+ const targetNodeIds = targetNodesFromArgs(op, args)
1059
+ const initialContextError = approvedContextError(agentId, recipe, op, expectedContext, targetNodeIds)
1025
1060
  if (initialContextError) return initialContextError
1026
1061
  try {
1027
1062
  // manager 未装配(单测/嵌入)→ 只做绑定与运行门复核;装配后叠加产线 grant 复核
@@ -1041,7 +1076,7 @@ export async function revalidateApprovedAction(
1041
1076
  // Grant lookup is asynchronous. Re-read the active tuple after that await so
1042
1077
  // queued line/run changes cannot pass between the initial comparison and the
1043
1078
  // caller's dispatch continuation.
1044
- const changedAfterGrantCheck = approvedContextError(agentId, recipe, op, expectedContext)
1079
+ const changedAfterGrantCheck = approvedContextError(agentId, recipe, op, expectedContext, targetNodeIds)
1045
1080
  if (changedAfterGrantCheck) return changedAfterGrantCheck
1046
1081
  return null
1047
1082
  }
@@ -1082,8 +1117,8 @@ export async function toolRecipeTrial(agentId: string, args: {
1082
1117
  if (!target.ok) return { text: target.text, isError: true }
1083
1118
  const recipe = target.recipe
1084
1119
  const recipeId = recipe.id
1085
- // 权限模型 v2:绑定 → 二级认证 → 运行门(未在执行的配方不可试验)
1086
- const gate = assertRecipeOperation(agentId, recipe, 'trial')
1120
+ // 权限模型 v2 + 2026-10-28 双轨:recipe 绑定 ∨ 本次候选覆盖集 ⊆ dcw 节点绑定面
1121
+ const gate = assertRecipeOperation(agentId, recipe, 'trial', list.map(p => p.nodeId))
1087
1122
  if (!gate.ok) return { text: gate.text, isError: true }
1088
1123
  for (const p of list) {
1089
1124
  const node = getDcwController().byId(p.nodeId)
@@ -1129,11 +1164,15 @@ export async function toolRecipeTrial(agentId: string, args: {
1129
1164
  }
1130
1165
  // 人工附言随批准回执回流(Agent 与审计都能看到放行时人的原话)
1131
1166
  if (gateDecision.comment) hitlNote = `\n人工附言:${gateDecision.comment}`
1132
- const stale = await revalidateApprovedAction(agentId, recipe, 'trial', 'recipe_trial', { recipe_id: recipeId }, gate.activeContext)
1167
+ const stale = await revalidateApprovedAction(agentId, recipe, 'trial', 'recipe_trial', { recipe_id: recipeId, params: list.map(p => ({ node_id: p.nodeId })) }, gate.activeContext)
1133
1168
  if (stale) return stale
1134
- const staleAtDispatch = approvedContextError(agentId, recipe, 'trial', gate.activeContext)
1169
+ const staleAtDispatch = approvedContextError(agentId, recipe, 'trial', gate.activeContext, list.map(p => p.nodeId))
1135
1170
  if (staleAtDispatch) return staleAtDispatch
1136
1171
  }
1172
+ // 2026-10-28 执行点复核(双轨授权,D13 同款):审批/节拍等待期间绑定可能被撤销,
1173
+ // 下发前最后一次同步重算覆盖集 —— 收回即失活,不把授权变更带进写入。
1174
+ const execGate = assertRecipeOperation(agentId, recipe, 'trial', list.map(p => p.nodeId))
1175
+ if (!execGate.ok) return { text: `下发前授权复核未通过,本次未执行:${execGate.text}`, isError: true }
1137
1176
  const run = await getDcwController().applyRecipe(recipeId, { overrides: list, trial: true, agentDispatch: true, agentApprovalId: gateApprovalId, emergency })
1138
1177
  // ACK 鉴定三段口径(2026-10-08):ok 只是命令受理,未证实/失败必须如实回给 Agent
1139
1178
  const acks = run.ackSummary ?? { verified: run.results.filter(r => r.ok).length, unverified: 0, failed: run.results.filter(r => !r.ok).length, total: run.results.length }
@@ -1244,6 +1283,9 @@ export async function toolRecipeApply(agentId: string, args: {
1244
1283
  const staleAtDispatch = approvedContextError(agentId, recipe, 'dispatch', gate.activeContext)
1245
1284
  if (staleAtDispatch) return staleAtDispatch
1246
1285
  }
1286
+ // 2026-10-28 执行点复核(双轨授权):recipe 绑定或节点覆盖集在下发前再算一次
1287
+ const execGate = assertRecipeOperation(agentId, recipe, 'dispatch')
1288
+ if (!execGate.ok) return { text: `下发前授权复核未通过,本次未执行:${execGate.text}`, isError: true }
1247
1289
  // 完整覆盖集让控制器复制每个已批准值,逐参数异步写期间也不再共享可变配方参数。
1248
1290
  const run = await getDcwController().applyRecipe(recipeId, { overrides: dispatch.params, agentDispatch: true, agentApprovalId: gateApprovalId, emergency })
1249
1291
  // ACK 鉴定三段口径(2026-10-08):ok 只是命令受理,未证实/失败必须如实回给 Agent
@@ -1,15 +1,24 @@
1
1
  /**
2
- * Recipe 操作统一鉴权门(权限模型 v2)。
2
+ * Recipe 操作统一鉴权门(权限模型 v2 + 2026-10-28 节点级写授权双轨)。
3
3
  *
4
- * Agent 不再直碰数控节点 —— 对产线的全部写动作收敛到「绑定的 recipe」上:
5
- * ① 绑定门:必须持有该 recipe 的绑定(kind='recipe';可绑定多个配方);
4
+ * 对产线的全部写动作仍收敛在配方管道上;授权面有两条轨(取并集):
5
+ * ① 绑定门:
6
+ * - recipe 绑定(kind='recipe',nodeId 字段=配方 id)= **配方操作权**
7
+ * (update/apply/trial/rollback 全部放行,维持权限模型 v2 语义);
8
+ * - dcw 节点绑定(kind='dcw')= **点位写授权**(2026-10-28 恢复,非直写入口):
9
+ * 对「当前活动配方」的 trial/apply,只要本次覆盖的节点 ⊆ 自己的 dcw 绑定面
10
+ * 即放行 —— 授权跟设备节点走(稳定),管控跟活动配方走(动态),换配方不停摆。
11
+ * 版本操作(update/rollback)不授予:配方级操作权仍须 recipe 绑定。
12
+ * - 目标节点集缺省 = 配方全部参数(apply / 无 overrides 的 trial);
13
+ * trial 传本次 overrides 的节点集(只写声明节点,部分覆盖即拒)。
6
14
  * ② 认证门:recipe.access.requireAuth=true 时,只有 authorizedAgentIds 清单内的
7
15
  * Agent 可操作(fail-closed:开认证但清单为空 = 无人可操作);
8
16
  * ③ 运行门:配方必须**正在执行**(该产线存在本配方的活动批次)—— 未运行的配方
9
17
  * 一律不可操作(参数写入/下发/试验/回退同样受限);
10
18
  * ④ HITL 判定:绑定 mode='manual' → 每次动作挂起等人工批准(必带 Agent 理由);
11
19
  * mode='auto'(显式确认切换)→ 免批直执行。全局开关 security.recipeDispatchApproval
12
- * 保持兼容:开启时即便 auto 绑定也挂审批。
20
+ * 保持兼容:开启时即便 auto 绑定也挂审批。节点级轨的 manual 语义跟随覆盖该批
21
+ * 节点的 dcw 绑定(任一 manual 即挂审批)。
13
22
  * 线级总闸(2026-10-08):line.controlMode='manual'(缺省,fail-safe)时 auto 绑定
14
23
  * 也强制人工审批 —— 手动模式下人类必须过目每一次写动作。
15
24
  */
@@ -50,22 +59,59 @@ export function activeRunOfRecipe(recipeId: string): ActiveRecipeContext | null
50
59
  }
51
60
  }
52
61
 
53
- /** 统一鉴权门:绑定 → 二级认证 → 运行门;返回绑定与 HITL 判定(不做挂起,挂起由调用方执行) */
62
+ /** 节点展示名(拒绝文案用;节点已删除时退回 id) */
63
+ function nodeLabel(nodeId: string): string {
64
+ const n = getDcwController().byId(nodeId)
65
+ return n ? `${n.name}(${nodeId})` : nodeId
66
+ }
67
+
68
+ function listNodes(ids: string[]): string {
69
+ return ids.length > 0 ? ids.map(nodeLabel).join('、') : '(无)'
70
+ }
71
+
72
+ /** 统一鉴权门:绑定(配方级 ∪ 节点级)→ 二级认证 → 运行门;返回绑定与 HITL 判定(不做挂起,挂起由调用方执行)
73
+ * targetNodeIds:本次动作实际覆盖的节点集;缺省 = 配方全部参数(整批 apply)。 */
54
74
  export function assertRecipeOperation(
55
75
  agentId: string,
56
- recipe: { id: string, name: string, lineId?: string, access?: { requireAuth?: boolean, authorizedAgentIds?: string[] } },
76
+ recipe: { id: string, name: string, lineId?: string, params?: Array<{ nodeId: string }>, access?: { requireAuth?: boolean, authorizedAgentIds?: string[] } },
57
77
  op: RecipeOp,
78
+ targetNodeIds?: string[],
58
79
  ): RecipeGateResult {
59
80
  const opLabel = op === 'update' ? '参数写入' : op === 'dispatch' ? '下发' : op === 'trial' ? '试验下发' : '回退'
60
- // ① 绑定门(kind='recipe';nodeId 字段 = recipeId)
61
- const binding = getAgentNodeBindingRepo().find(agentId, recipe.id, 'recipe')
62
- if (!binding) {
63
- return {
64
- ok: false,
65
- text: `无权${opLabel}配方「${recipe.name}」(${recipe.id}):你未绑定该配方。权限模型 v2 下 Agent 不直接操作数控节点 —— 通过绑定 recipe 获得参数写入与下发能力(可绑定多个配方);请让用户或 lead 为你绑定(recipe 绑定首绑默认 manual,每次动作需人工批准)。`,
81
+ // ① 绑定门(双轨):recipe 绑定 = 配方操作权;dcw 绑定 = 点位写授权(仅 trial/apply)
82
+ const bindings = getAgentNodeBindingRepo().byAgent(agentId)
83
+ const recipeBinding = bindings.find(b => b.kind === 'recipe' && b.nodeId === recipe.id)
84
+ const dcwBindings = bindings.filter(b => b.kind === 'dcw')
85
+ const dcwBound = new Set(dcwBindings.map(b => b.nodeId))
86
+ const recipeNodes = (recipe.params ?? []).map(p => p.nodeId).filter(Boolean)
87
+ const targets = (targetNodeIds && targetNodeIds.length > 0 ? targetNodeIds : recipeNodes)
88
+ .map(id => String(id ?? '').trim())
89
+ .filter(Boolean)
90
+ const versionOp = op === 'update' || op === 'rollback'
91
+ let coveringBindings: AgentNodeBinding[] = []
92
+ if (!recipeBinding) {
93
+ if (versionOp) {
94
+ return {
95
+ ok: false,
96
+ text: `无权${opLabel}配方「${recipe.name}」(${recipe.id}):节点绑定(dcw)只授予参数下发/试验权(点位授权),配方版本操作(参数保存/回退)需要绑定该配方(kind='recipe',配方级操作权)。请让用户或 lead 为你绑定该配方(recipe 绑定首绑默认 manual,每次动作需人工批准)。`,
97
+ }
98
+ }
99
+ if (targets.length === 0) {
100
+ return {
101
+ ok: false,
102
+ text: `无权${opLabel}配方「${recipe.name}」:配方没有参数节点,无法确认授权覆盖集。请先为配方配置参数节点,或让用户/lead 绑定该配方(kind='recipe')。`,
103
+ }
66
104
  }
105
+ const missing = targets.filter(id => !dcwBound.has(id))
106
+ if (missing.length > 0) {
107
+ return {
108
+ ok: false,
109
+ text: `无权${opLabel}配方「${recipe.name}」:本次覆盖的节点中有 ${missing.length} 个不在你的数控节点绑定面内 —— ${listNodes(missing)}。你已绑定的可写节点:${listNodes([...dcwBound])}。请让用户或 lead 为你补绑这些节点(节点绑定 = 点位写授权),或改用已授权节点重试;若要对整批配方操作,也可改为绑定配方(kind='recipe')。`,
110
+ }
111
+ }
112
+ coveringBindings = dcwBindings.filter(b => targets.includes(b.nodeId))
67
113
  }
68
- // ② 二级认证(用户在配方编辑里开启并勾选授权 Agent)
114
+ // ② 二级认证(用户在配方编辑里开启并勾选授权 Agent;双轨共用)
69
115
  if (recipe.access?.requireAuth && !(recipe.access.authorizedAgentIds ?? []).includes(agentId)) {
70
116
  return {
71
117
  ok: false,
@@ -82,7 +128,7 @@ export function assertRecipeOperation(
82
128
  }
83
129
  // ④ HITL 判定:manual 绑定 → 逐动作人工批准;auto → 免批。
84
130
  // 线级总闸(2026-10-08 生产化):line.controlMode='manual'(缺省)时,即便绑定为
85
- // auto 也强制人工审批 —— 手动模式下人类必须过目每一次写动作(fail-safe)。
131
+ // auto 也强制人工审批 —— 手动模式下人类必须过目每一次写动作。
86
132
  // 频道层钳位(2026-10-28 三模式):Agent 所属频道 executionMode='manual' 时同理强制;
87
133
  // 'auto'(中性缺省)/'observe'(分发层已拒写,不可达)不改变本判定。
88
134
  const lineManual = recipe.lineId ? getDcwLineRepo().controlModeOf(recipe.lineId) === 'manual' : false
@@ -90,7 +136,14 @@ export function assertRecipeOperation(
90
136
  // ⑤ 全局开关兼容位:security.recipeDispatchApproval 开启时即便 auto 也挂审批(保留语义)
91
137
  const globalGate = false // 由调用方按 settingOf('security.recipeDispatchApproval') 叠加(本模块不依赖设置服务)
92
138
  void globalGate
93
- return { ok: true, binding, hitlRequired: binding.mode === 'manual' || lineManual || channelManual, activeContext: run }
139
+ // 节点级轨的 manual 语义:覆盖本次批次的 dcw 绑定任一 manual 即挂审批(fail-safe)
140
+ const nodeManual = coveringBindings.some(b => b.mode === 'manual')
141
+ const binding = recipeBinding ?? coveringBindings[0]
142
+ if (!binding) {
143
+ // 理论上不可达(targets 非空且全部已覆盖);fail-closed 防实现漂移
144
+ return { ok: false, text: `无权${opLabel}配方「${recipe.name}」:未找到有效授权绑定。` }
145
+ }
146
+ return { ok: true, binding, hitlRequired: binding.mode === 'manual' || nodeManual || lineManual || channelManual, activeContext: run }
94
147
  }
95
148
 
96
149
  /** 新建配方的授权门(尚无 recipe 可绑定):要求 Agent 在该线至少有一个写向绑定
@@ -34,9 +34,9 @@ export const LEGACY_VERIFY_SPECS: Record<string, ToolVerifySpec> = {
34
34
  line_status: { refs: [{ param: 'line_id', kind: 'line', mode: 'read' }] },
35
35
  line_start: { refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'write' }] },
36
36
  recipe_versions: { refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'read' }] },
37
- recipe_update: { atLeastOne: [{ params: ['recipe_id', 'line_id'], hint: 'recipe_id 或 line_id 至少提供一个:只给 line_id 时按该线当前活动批次解析配方。' }], refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'write' }, { param: 'line_id', kind: 'line', mode: 'read' }], deepNodeRefs: ['params[].node_id'] },
37
+ recipe_update: { atLeastOne: [{ params: ['recipe_id', 'line_id'], hint: 'recipe_id 或 line_id 至少提供一个:只给 line_id 时按该线当前活动批次解析配方。' }], refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'write' }, { param: 'line_id', kind: 'line', mode: 'read' }], deepNodeRefs: ['params[].node_id'], writeNodeRefs: ['params[].node_id'] },
38
38
  recipe_rollback: { refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'write' }] },
39
- recipe_trial: { atLeastOne: [{ params: ['recipe_id', 'line_id'], hint: 'recipe_id 或 line_id 至少提供一个:只给 line_id 时按该线当前活动批次解析配方。' }], refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'write' }, { param: 'line_id', kind: 'line', mode: 'read' }], deepNodeRefs: ['params[].node_id'] },
39
+ recipe_trial: { atLeastOne: [{ params: ['recipe_id', 'line_id'], hint: 'recipe_id 或 line_id 至少提供一个:只给 line_id 时按该线当前活动批次解析配方。' }], refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'write' }, { param: 'line_id', kind: 'line', mode: 'read' }], deepNodeRefs: ['params[].node_id'], writeNodeRefs: ['params[].node_id'] },
40
40
  recipe_apply: { atLeastOne: [{ params: ['recipe_id', 'line_id'], hint: 'recipe_id 或 line_id 至少提供一个:只给 line_id 时按该线当前活动批次解析配方。' }], refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'write' }, { param: 'line_id', kind: 'line', mode: 'read' }] },
41
41
  recipe_propose: { refs: [{ param: 'recipe_id', kind: 'recipe', mode: 'read' }], deepNodeRefs: ['packages[].params[].node_id'] },
42
42
  // 分析与经验面
@@ -42,6 +42,9 @@ export interface ToolRefFaces {
42
42
  agentId: string
43
43
  /** 绑定面节点:dcw+daq 绑定节点 ∪ 绑定配方参数节点 */
44
44
  boundNodes: Set<string>
45
+ /** 写授权面 W(a):dcw 绑定节点 ∪ 绑定配方参数节点(节点级写授权,与 recipe-gate 同口径;
46
+ * daq 只读绑定与频道绑线只读扩权不进写面) */
47
+ writeFace: Set<string>
45
48
  /** 仅数采绑定(dcw 工具只认 dcw 绑定,读取面只认 daq 绑定——分面保留) */
46
49
  daqBoundNodes: Set<string>
47
50
  /** 绑定的配方(kind='recipe' 的绑定,nodeId 字段存 recipeId) */
@@ -60,6 +63,7 @@ export interface ToolRefFaces {
60
63
  export function resolveRefFaces(agentId: string): ToolRefFaces {
61
64
  const bindings = getAgentNodeBindingRepo().byAgent(agentId)
62
65
  const boundNodes = new Set<string>()
66
+ const writeFace = new Set<string>()
63
67
  const daqBoundNodes = new Set<string>()
64
68
  const boundRecipes = new Set<string>()
65
69
  const recipeLines = new Set<string>()
@@ -72,11 +76,15 @@ export function resolveRefFaces(agentId: string): ToolRefFaces {
72
76
  recipeLines.add(r.lineId)
73
77
  readableLines.add(r.lineId)
74
78
  }
75
- for (const p of (r?.params ?? [])) boundNodes.add(p.nodeId)
79
+ for (const p of (r?.params ?? [])) {
80
+ boundNodes.add(p.nodeId)
81
+ writeFace.add(p.nodeId)
82
+ }
76
83
  continue
77
84
  }
78
85
  boundNodes.add(b.nodeId)
79
86
  if (b.kind === 'daq') daqBoundNodes.add(b.nodeId)
87
+ else writeFace.add(b.nodeId)
80
88
  const n = b.kind === 'dcw' ? getDcwController().byId(b.nodeId) : getDaqNodeRepo().byId(b.nodeId)
81
89
  if (n?.lineId) readableLines.add(n.lineId)
82
90
  }
@@ -86,7 +94,7 @@ export function resolveRefFaces(agentId: string): ToolRefFaces {
86
94
  if (channelLineId) {
87
95
  for (const n of getDaqNodeRepo().all()) if (n.lineId === channelLineId) channelLineNodes.add(n.id)
88
96
  }
89
- return { agentId, boundNodes, daqBoundNodes, boundRecipes, recipeLines, readableLines, channelLineId, channelLineNodes }
97
+ return { agentId, boundNodes, writeFace, daqBoundNodes, boundRecipes, recipeLines, readableLines, channelLineId, channelLineNodes }
90
98
  }
91
99
 
92
100
  // ===== 归属判定 =====
@@ -143,6 +151,17 @@ function denyNode(faces: ToolRefFaces, toolName: string, args: Record<string, un
143
151
  ], faces, hint, { param, value: id, lineId: row?.lineId })
144
152
  }
145
153
 
154
+ /** 写面拒绝(2026-10-28):参数要写到的节点必须落在 W(a)=dcw 绑定 ∪ 绑定配方参数节点。
155
+ * 比 denyNode 更明确:读得到 ≠ 写得到(频道绑线只读扩权/他配方节点都不构成写授权)。 */
156
+ function denyWriteNode(faces: ToolRefFaces, toolName: string, args: Record<string, unknown>, param: string, id: string, exists: boolean): HostToolResult {
157
+ const row = exists ? nodeRow(id) : null
158
+ const where = row ? `(节点「${row.name}」${row.lineId ? `,归属产线 ${row.lineId}` : ''})` : '(节点不存在或已删除)'
159
+ const hint = `该节点不在你的写授权面(节点绑定=点位写授权;配方绑定=配方操作权)。你的可写节点:${listOf(faces.writeFace)};你已绑定的配方参数节点也在此列。如需写入,请让用户或 lead 为你绑定该数控节点(dcw),或改用上述已授权节点;若要对整批配方操作,可请求绑定配方(kind='recipe')。`
160
+ return deny(toolName, args, [
161
+ `工具调用被拒(写授权校验,${toolName}):参数节点「${id}」不在你的写授权面内${where}。`,
162
+ ], faces, hint, { param, value: id, lineId: row?.lineId })
163
+ }
164
+
146
165
  function denyLine(faces: ToolRefFaces, toolName: string, args: Record<string, unknown>, param: string, id: string): HostToolResult {
147
166
  const exists = getDcwController().listLines().some(l => l.id === id)
148
167
  const hint = `你的可读产线:${listOf(faces.readableLines)}${faces.channelLineId ? `(其中「${faces.channelLineId}」来自频道绑线,只读)` : ''}。如需访问该产线,请让用户授予对应产线权限(权限管理),或把当前频道绑定到该产线;也可以改用上述可读产线重试。`
@@ -370,6 +389,16 @@ export function verifyToolReferences(agentId: string, toolName: string, args: Re
370
389
  if (!exists || !nodeAllowed(faces, id)) return denyNode(faces, toolName, args, key, id, exists)
371
390
  }
372
391
  }
392
+ // 写入型参数节点:必须落在写授权面 W(a)(只读面/频道绑线/他配方节点不构成写授权)。
393
+ // 先于 deepNodeRefs 检查:nodeAllowed 允许"绑定配方所在产线的任意节点",对写入过宽,
394
+ // 写面判定才是本工具真正的授权边界(拒绝文案也更能行动)。
395
+ for (const path of spec.writeNodeRefs ?? []) {
396
+ const leaf = path.split('.').pop() ?? path
397
+ for (const id of collectDeep(args, path)) {
398
+ const exists = nodeRow(id) !== null
399
+ if (!exists || !faces.writeFace.has(id)) return denyWriteNode(faces, toolName, args, leaf, id, exists)
400
+ }
401
+ }
373
402
  for (const path of spec.deepNodeRefs ?? []) {
374
403
  // 深路径的 param 名用于文案:取最后一段
375
404
  const leaf = path.split('.').pop() ?? path