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,332 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 节点级写授权(双轨授权)实机 E2E —— 隔离实例 + 真实 HTTP + agent token 自证。
4
+ *
5
+ * 覆盖 docs/plans/2026-10-28-node-scoped-binding-plan.md 的 US-01..US-05:
6
+ * M1 REST 恢复 kind='dcw' 绑定(operate 权限 + 首绑 manual + confirm 切 auto)
7
+ * M2 双轨授权:trial 覆盖集 ⊆ 节点绑定面 / apply 全覆盖 / update 版本权仍归配方绑定
8
+ * M3 换配方:line_id 解析活动配方,节点绑定 Agent 无需新绑定继续操作
9
+ * M4 委派建/撤 dcw 绑定 + 在飞审批收回即失活 + 频道 manual 钳位叠加
10
+ * M5 回归:越线绑定拒绝、tool-verify 写面拒绝、直写工具仍不可达
11
+ *
12
+ * 运行:node scripts/e2e-node-binding-authz.mjs
13
+ * 证据:docs/audit/evidence/2026-10-28-node-binding-authz/report.json(可用 AW_E2E_EVIDENCE 覆盖)
14
+ */
15
+ import { spawn } from 'node:child_process'
16
+ import { mkdirSync, openSync, writeFileSync } from 'node:fs'
17
+ import { dirname, join, resolve } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { tmpdir } from 'node:os'
20
+ import { DatabaseSync } from 'node:sqlite'
21
+
22
+ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
23
+ const TAG = `nb${Date.now().toString(36).slice(-4)}`
24
+ const ROOT = join(tmpdir(), `aw-node-binding-e2e-${TAG}`)
25
+ mkdirSync(join(ROOT, 'home'), { recursive: true })
26
+ writeFileSync(join(ROOT, 'home', 'runtime-settings.json'), JSON.stringify({
27
+ version: 1,
28
+ overrides: { 'security.hitl_timeout_ms': 20_000 },
29
+ }))
30
+ const results = []
31
+ const ok = (id, name, cond, detail = '') => {
32
+ results.push({ id, name, pass: Boolean(cond), detail: String(detail).slice(0, 600) })
33
+ console.log(`${cond ? '✔' : '✖'} [${id}] ${name}${cond ? '' : ` — ${String(detail).slice(0, 600)}`}`)
34
+ return Boolean(cond)
35
+ }
36
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
37
+
38
+ let port = 0
39
+ for (const p of [3983, 3982, 3981, 3980]) {
40
+ try {
41
+ const r = await fetch(`http://127.0.0.1:${p}/api/health`, { signal: AbortSignal.timeout(1200) })
42
+ if (r.ok) continue
43
+ }
44
+ catch { /* 空闲 */ }
45
+ port = p
46
+ break
47
+ }
48
+ if (!port) {
49
+ console.error('无空闲端口')
50
+ process.exit(1)
51
+ }
52
+ const BASE = `http://127.0.0.1:${port}`
53
+ process.env.NO_PROXY = '127.0.0.1,localhost'
54
+ const logFd = openSync(join(ROOT, 'dev.log'), 'a')
55
+ const child = spawn(process.execPath, [join(REPO, 'bin', 'aw.mjs'), 'dev', '--port', String(port)], {
56
+ cwd: REPO,
57
+ env: {
58
+ ...process.env,
59
+ AW_MODE: 'home',
60
+ AW_HOME: join(ROOT, 'home'),
61
+ AW_RATE_LIMIT_OFF: '1',
62
+ AW_TRIAL_INTERVAL_MS: '0',
63
+ NUXT_SESSION_PASSWORD: 'NodeBinding#2026-e2e-key',
64
+ NO_PROXY: '127.0.0.1,localhost',
65
+ },
66
+ stdio: ['ignore', logFd, logFd],
67
+ windowsHide: true,
68
+ })
69
+
70
+ async function api(method, path, body, token, extraHeaders = {}, timeoutMs = 120_000) {
71
+ const res = await fetch(BASE + path, {
72
+ method,
73
+ headers: {
74
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
75
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
76
+ ...extraHeaders,
77
+ },
78
+ body: body === undefined ? undefined : JSON.stringify(body),
79
+ signal: AbortSignal.timeout(timeoutMs),
80
+ })
81
+ let json = null
82
+ try {
83
+ json = await res.json()
84
+ }
85
+ catch { /* 非 JSON */ }
86
+ return { status: res.status, ...json }
87
+ }
88
+ const invText = r => String(r?.data?.result?.text ?? r?.data?.text ?? '')
89
+ const invErr = r => Boolean(r?.data?.result?.isError ?? r?.data?.isError)
90
+ const dbPath = join(ROOT, 'home', 'data', 'workshop.sqlite')
91
+ const agentTokenOf = (agentId) => {
92
+ const db = new DatabaseSync(dbPath)
93
+ try {
94
+ return String(db.prepare('SELECT token FROM channel_agents WHERE id = ?').get(agentId)?.token ?? '')
95
+ }
96
+ finally {
97
+ db.close()
98
+ }
99
+ }
100
+
101
+ async function waitPending(agentId, timeoutMs = 15_000) {
102
+ const started = Date.now()
103
+ while (Date.now() - started < timeoutMs) {
104
+ const r = await api('GET', `/api/workshop/agent-tools/approvals?agentId=${encodeURIComponent(agentId)}`, undefined, admin)
105
+ const card = (r?.data?.approvals ?? []).find(a => a.status === 'pending')
106
+ if (card) return card
107
+ await sleep(300)
108
+ }
109
+ return null
110
+ }
111
+
112
+ async function decide(cardId, approved, comment) {
113
+ return api('POST', `/api/workshop/agent-tools/approvals/${cardId}/decide`, { approved, comment }, admin)
114
+ }
115
+
116
+ let admin = ''
117
+ try {
118
+ let healthy = false
119
+ for (let i = 0; i < 90; i++) {
120
+ await sleep(2000)
121
+ try {
122
+ const h = await api('GET', '/api/health', undefined, undefined, {}, 3000)
123
+ if (h?.data?.status === 'ok' && h?.data?.workshop === 'ready') {
124
+ healthy = true
125
+ break
126
+ }
127
+ }
128
+ catch { /* 未就绪 */ }
129
+ }
130
+ ok('T0', '隔离实例启动 + workshop ready', healthy, `port=${port}`)
131
+ if (!healthy) throw new Error('实例未就绪')
132
+
133
+ admin = (await api('POST', '/api/users/register', { name: `nb-admin-${TAG}`, email: `nb-${TAG}@awshop.local`, password: 'NbAdmin#2026' })).data?.token
134
+ ok('T0b', 'admin 注册', Boolean(admin))
135
+
136
+ // 普通用户(只读授权)用于验证 dcw 写向绑定需要 operate
137
+ const plainEmail = `nb-plain-${TAG}@awshop.local`
138
+ const created = await api('POST', '/api/users', { name: `nb-plain-${TAG}`, email: plainEmail, password: 'NbPlain#2026', role: 'user' }, admin)
139
+ const plainId = created?.data?.id ?? created?.data?.user?.id ?? ''
140
+ const plain = (await api('POST', '/api/users/login', { email: plainEmail, password: 'NbPlain#2026' })).data?.token
141
+ ok('T0c', '普通用户创建+登录', Boolean(plainId && plain), `create=${created.status} id=${plainId}`)
142
+
143
+ // ── 场景:产线 + 3 个数控节点 + 产品 + 配方 A,开跑 ──
144
+ const line = (await api('POST', '/api/workshop/dcw/lines', { name: `节点授权线-${TAG}` }, admin)).data?.line?.id
145
+ await api('PUT', '/api/workshop/permissions', { userId: plainId, grants: [{ lineId: line, mode: 'readonly' }] }, admin)
146
+ // 自定义参数模板(0~100℃):内置模板的安全量程是 150~200℃,与本场景候选值不匹配
147
+ const tpl = (await api('POST', '/api/workshop/dcw/templates', { name: `授权参数-${TAG}`, ch: '温度', unit: '℃', min: 0, max: 100, decimals: 1 }, admin)).data?.template?.key
148
+ const nodeBody = i => ({
149
+ templateRef: tpl,
150
+ name: `授权节点${i}-${TAG}`,
151
+ driver: 'mock',
152
+ lineId: line,
153
+ stepLimit: 20,
154
+ writeLockSeconds: 0,
155
+ readIntervalMs: 0,
156
+ driverConfig: { key: `nb-${TAG}-${i}` },
157
+ })
158
+ const n1 = (await api('POST', '/api/workshop/dcw', nodeBody(1), admin)).data?.node?.id
159
+ const n2 = (await api('POST', '/api/workshop/dcw', nodeBody(2), admin)).data?.node?.id
160
+ const n3 = (await api('POST', '/api/workshop/dcw', nodeBody(3), admin)).data?.node?.id
161
+ const product = (await api('POST', '/api/workshop/dcw/products', { lineId: line, name: `授权产品-${TAG}` }, admin)).data?.product?.id
162
+ const recipeA = (await api('POST', '/api/workshop/dcw/recipes', {
163
+ productId: product,
164
+ name: `配方A-${TAG}`,
165
+ opIntervalMs: 0,
166
+ params: [{ nodeId: n1, value: 50 }, { nodeId: n2, value: 50 }],
167
+ }, admin)).data?.recipe?.id
168
+ ok('T1', '产线/参数模板/3 数控节点/产品/配方A 就绪', Boolean(line && tpl && n1 && n2 && n3 && product && recipeA), JSON.stringify({ line, tpl, n1, n2, n3, recipeA }))
169
+ const startA = await api('POST', `/api/workshop/dcw/lines/${line}/start`, { recipeId: recipeA }, admin)
170
+ ok('T1b', '产线开跑(配方A 整批下发,活动批次建立)', startA.status === 200 && Boolean(startA?.data?.run?.id), `status=${startA.status} msg=${String(startA?.message ?? '').slice(0, 160)}`)
171
+
172
+ // 产线总闸切 auto(绑定 manual 仍是 manual;后续显式 PATCH 绑定)
173
+ const lineAuto = await api('PATCH', `/api/workshop/dcw/lines/${line}`, { controlMode: 'auto', confirm: true }, admin)
174
+ ok('T1c', '产线 controlMode 切 auto(confirm)', lineAuto.status === 200 && lineAuto?.data?.line?.controlMode === 'auto', `status=${lineAuto.status}`)
175
+
176
+ // ── 频道(绑线)+ lead + 3 worker ──
177
+ const ch = (await api('POST', '/api/workshop/channels', { name: `节点授权频道-${TAG}`, leadAgent: { name: `nb-lead-${TAG}`, harness: 'mock' } }, admin)).data
178
+ const channelId = ch?.channelId
179
+ const leadId = String(ch?.leadAgent?.id ?? ch?.leadAgentId ?? '')
180
+ await api('PUT', `/api/workshop/channels/${channelId}/line`, { lineId: line }, admin)
181
+ const w1raw = (await api('POST', `/api/workshop/channels/${channelId}/agents`, { name: `nb-w1-${TAG}`, harness: 'mock', role: 'worker' }, admin)).data
182
+ const w2raw = (await api('POST', `/api/workshop/channels/${channelId}/agents`, { name: `nb-w2-${TAG}`, harness: 'mock', role: 'worker' }, admin)).data
183
+ const w3raw = (await api('POST', `/api/workshop/channels/${channelId}/agents`, { name: `nb-w3-${TAG}`, harness: 'mock', role: 'worker' }, admin)).data
184
+ const w1 = String(w1raw?.id ?? w1raw?.agentId ?? '')
185
+ const w2 = String(w2raw?.id ?? w2raw?.agentId ?? '')
186
+ const w3 = String(w3raw?.id ?? w3raw?.agentId ?? '')
187
+ ok('T2', '频道绑线 + lead + 3 worker', Boolean(channelId && leadId && w1 && w2 && w3), JSON.stringify({ channelId, leadId, w1, w2, w3 }))
188
+ const tok = { lead: agentTokenOf(leadId), w1: agentTokenOf(w1), w2: agentTokenOf(w2), w3: agentTokenOf(w3) }
189
+ ok('T2b', 'Agent token 自证可用(4 个独立 token)', Object.values(tok).every(t => t.length > 20), JSON.stringify(Object.fromEntries(Object.entries(tok).map(([k, v]) => [k, v.length]))))
190
+
191
+ const callAs = (agentId, token, tool, args) => api('POST', '/api/workshop/agent-tools/invoke', { agentId, tool, args }, undefined, { 'x-aw-agent-token': token })
192
+ const bind = (agentId, nodeId, kind, mode) => api('POST', '/api/workshop/agent-tools/bindings', { agentId, nodeId, kind, ...(mode ? { mode } : {}) }, admin)
193
+ const setBindingMode = (id, mode, confirm = false) => api('PATCH', `/api/workshop/agent-tools/bindings/${id}`, confirm ? { mode, confirm: true } : { mode }, admin)
194
+
195
+ // ── M1:dcw 绑定恢复 + 首绑 manual + readonly 403 + confirm 切 auto ──
196
+ const b1 = await bind(w1, n1, 'dcw', 'auto')
197
+ ok('T3', 'POST kind=dcw 绑定 → 200 且首绑强制 manual(modeForced 回执)',
198
+ b1.status === 200 && b1?.data?.binding?.mode === 'manual' && b1?.data?.modeForced === 'manual',
199
+ `status=${b1.status} mode=${b1?.data?.binding?.mode} forced=${b1?.data?.modeForced} msg=${String(b1?.message ?? '').slice(0, 160)}`)
200
+ const b1Id = String(b1?.data?.binding?.id ?? '')
201
+
202
+ const plainBind = await api('POST', '/api/workshop/agent-tools/bindings', { agentId: w1, nodeId: n1, kind: 'dcw' }, plain)
203
+ ok('T3b', '只读用户建 dcw 绑定 → 403(写授权面需 operate)', plainBind.status === 403, `status=${plainBind.status} code=${String(plainBind?.code ?? '')} msg=${String(plainBind?.message ?? '').slice(0, 160)}`)
204
+
205
+ const noConfirm = await setBindingMode(b1Id, 'auto')
206
+ ok('T3c', '绑定 manual→auto 无 confirm → 400 MODE_CONFIRM_REQUIRED', noConfirm.status === 400 && noConfirm?.code === 'MODE_CONFIRM_REQUIRED', `status=${noConfirm.status} code=${String(noConfirm?.code ?? '')}`)
207
+ const withConfirm = await setBindingMode(b1Id, 'auto', true)
208
+ ok('T3d', '绑定 manual→auto 携 confirm:true → 200 auto', withConfirm.status === 200 && withConfirm?.data?.binding?.mode === 'auto', `status=${withConfirm.status} mode=${withConfirm?.data?.binding?.mode}`)
209
+ const listW1 = await api('GET', `/api/workshop/agent-tools/list?agentId=${encodeURIComponent(w1)}`, undefined, undefined, { 'x-aw-agent-token': tok.w1 })
210
+ const trialDesc = String((listW1?.data?.tools ?? []).find(t => t.name === 'recipe_trial')?.description ?? '')
211
+ ok('T3e', '工具注入面带点位写授权清单(模型无需探测绑定面)', trialDesc.includes(n1) && /写授权面/.test(trialDesc), trialDesc.slice(-240))
212
+
213
+ // ── M4:频道 manual 钳位对节点绑定 Agent 同样生效(挂 HITL,批准后执行) ──
214
+ const chManual = await api('PATCH', `/api/workshop/channels/${channelId}/execution-mode`, { mode: 'manual' }, admin)
215
+ ok('T4', '频道切 manual(降险方向免 confirm)', chManual.status === 200 && chManual?.data?.executionMode === 'manual', `status=${chManual.status} mode=${chManual?.data?.executionMode}`)
216
+ const trialP = callAs(w1, tok.w1, 'recipe_trial', {
217
+ line_id: line,
218
+ params: [{ node_id: n1, value: 55 }],
219
+ hypothesis: 'E2E:频道 manual 钳位对节点绑定 Agent 生效',
220
+ })
221
+ const cardM = await waitPending(w1, 15_000)
222
+ ok('T4b', '频道 manual + dcw auto 绑定 → trial 挂起人工审批', Boolean(cardM), cardM ? `card=${cardM.id}` : '未出现审批卡')
223
+ if (cardM) await decide(cardM.id, true, 'E2E 批准:频道 manual 钳位验证')
224
+ const trialM = await trialP
225
+ ok('T4c', '人工批准后节点绑定 trial 执行成功(ACK 证实)', !invErr(trialM) && /试验已整批下发/.test(invText(trialM)), invText(trialM).slice(0, 240))
226
+ const chAuto = await api('PATCH', `/api/workshop/channels/${channelId}/execution-mode`, { mode: 'auto', confirm: true }, admin)
227
+ ok('T4d', '频道切回 auto(confirm)', chAuto.status === 200 && chAuto?.data?.executionMode === 'auto', `status=${chAuto.status} mode=${chAuto?.data?.executionMode}`)
228
+
229
+ // ── M2:覆盖集判定 ──
230
+ const trialOk = await callAs(w1, tok.w1, 'recipe_trial', { line_id: line, params: [{ node_id: n1, value: 56 }], hypothesis: 'E2E:节点绑定覆盖集内' })
231
+ ok('T5', 'trial 覆盖集 ⊆ 节点绑定面 → 成功(line_id 解析活动配方)', !invErr(trialOk) && /试验已整批下发/.test(invText(trialOk)), invText(trialOk).slice(0, 220))
232
+ const trialNo = await callAs(w1, tok.w1, 'recipe_trial', { line_id: line, params: [{ node_id: n2, value: 56 }], hypothesis: 'E2E:未授权节点' })
233
+ ok('T5b', 'trial 含未授权节点 → 写面拒绝且点名节点', invErr(trialNo) && /写授权|绑定面/.test(invText(trialNo)) && invText(trialNo).includes(n2), invText(trialNo).slice(0, 260))
234
+ const applyNo = await callAs(w1, tok.w1, 'recipe_apply', { line_id: line, reason: 'E2E:部分覆盖整批下发应被拒' })
235
+ ok('T5c', 'apply 整批(2 参数)仅 1 节点绑定 → 拒绝并给出补绑指引', invErr(applyNo) && /补绑|绑定该配方|不在你的数控节点绑定面/.test(invText(applyNo)), invText(applyNo).slice(0, 260))
236
+ const updateNo = await callAs(w1, tok.w1, 'recipe_update', { line_id: line, params: [{ node_id: n1, value: 57 }], reason: 'E2E:节点绑定不应授予版本权' })
237
+ ok('T5d', 'recipe_update(版本操作)纯 dcw 绑定 → 拒绝', invErr(updateNo) && /版本操作|配方级操作权/.test(invText(updateNo)), invText(updateNo).slice(0, 240))
238
+
239
+ // 绑定第二节点 → apply 全覆盖放行
240
+ const b2 = await bind(w1, n2, 'dcw', 'auto')
241
+ await setBindingMode(String(b2?.data?.binding?.id ?? ''), 'auto', true)
242
+ ok('T6', '补齐第二节点绑定(auto)', b2.status === 200 && b2?.data?.binding?.kind === 'dcw', `status=${b2.status} id=${b2?.data?.binding?.id}`)
243
+ const applyOk = await callAs(w1, tok.w1, 'recipe_apply', { line_id: line, reason: 'E2E:全覆盖整批下发' })
244
+ ok('T6b', 'apply 全覆盖 → 成功(整批 ACK 证实)', !invErr(applyOk) && /已整批下发/.test(invText(applyOk)), invText(applyOk).slice(0, 240))
245
+
246
+ // ── US-05:收权即失活 ──
247
+ const revoke1 = await api('DELETE', `/api/workshop/agent-tools/bindings/${b1Id}`, undefined, admin)
248
+ ok('T7', '删除 n1 绑定 200', revoke1.status === 200, `status=${revoke1.status}`)
249
+ const afterRevoke = await callAs(w1, tok.w1, 'recipe_trial', { line_id: line, params: [{ node_id: n1, value: 58 }], hypothesis: 'E2E:收权后应拒绝' })
250
+ ok('T7b', '撤销 n1 绑定后 trial n1 → 拒绝(收权即失活)', invErr(afterRevoke) && /写授权|绑定面/.test(invText(afterRevoke)), invText(afterRevoke).slice(0, 220))
251
+
252
+ // ── M3:换配方(rc-A → rc-B),节点绑定 Agent 无需新绑定 ──
253
+ const stopA = await api('POST', `/api/workshop/dcw/lines/${line}/stop`, undefined, admin)
254
+ const recipeB = (await api('POST', '/api/workshop/dcw/recipes', {
255
+ productId: product,
256
+ name: `配方B-${TAG}`,
257
+ opIntervalMs: 0,
258
+ params: [{ nodeId: n1, value: 45 }, { nodeId: n3, value: 65 }],
259
+ }, admin)).data?.recipe?.id
260
+ const startB = await api('POST', `/api/workshop/dcw/lines/${line}/start`, { recipeId: recipeB }, admin)
261
+ ok('T8', '换配方:停 A 开 B(活动批次切换)', stopA.status === 200 && startB.status === 200 && Boolean(recipeB), `stop=${stopA.status} start=${startB.status} recipeB=${recipeB}`)
262
+ const b1again = await bind(w1, n1, 'dcw', 'auto')
263
+ await setBindingMode(String(b1again?.data?.binding?.id ?? ''), 'auto', true)
264
+ const b3 = await bind(w1, n3, 'dcw', 'auto')
265
+ await setBindingMode(String(b3?.data?.binding?.id ?? ''), 'auto', true)
266
+ ok('T8b', '为配方B 绑定 n1+n3 点位授权', b1again.status === 200 && b3.status === 200, `status=${b1again.status}/${b3.status}`)
267
+ const trialB = await callAs(w1, tok.w1, 'recipe_trial', { line_id: line, params: [{ node_id: n3, value: 66 }], hypothesis: 'E2E:换配方后节点绑定继续生效' })
268
+ ok('T8c', '换配方后同一 Agent 对 B 的已授权节点 trial 成功(无新配方绑定)', !invErr(trialB) && /试验已整批下发/.test(invText(trialB)), invText(trialB).slice(0, 240))
269
+ const bW2 = await bind(w2, recipeA, 'recipe')
270
+ ok('T8d', 'w2 绑定配方A(配方级操作权)', bW2.status === 200, `status=${bW2.status}`)
271
+ const trialW2 = await callAs(w2, tok.w2, 'recipe_trial', { line_id: line, params: [{ node_id: n1, value: 47 }], hypothesis: 'E2E:配方绑定不随换配方生效' })
272
+ ok('T8e', '配方绑定 Agent 对 B 仍被拒(授权是配方级,不随换配方漂移)', invErr(trialW2) && /绑定面|未绑定/.test(invText(trialW2)), invText(trialW2).slice(0, 240))
273
+
274
+ // ── M4:委派建/撤 dcw 绑定 + 在飞审批收回即失活 ──
275
+ const leadBind = await bind(leadId, n1, 'dcw', 'auto')
276
+ await setBindingMode(String(leadBind?.data?.binding?.id ?? ''), 'auto', true)
277
+ const grant = await callAs(leadId, tok.lead, 'team_grant_nodes', { agent_id: w3, node_ids: [n1], mode: 'auto' })
278
+ ok('T9', 'lead 委派 dcw 节点给 worker(team_grant_nodes)成功', !invErr(grant) && /已把\s*1\s*条节点授权授予/.test(invText(grant)), invText(grant).slice(0, 240))
279
+ const w3b = await api('GET', `/api/workshop/agent-tools/bindings?agentId=${encodeURIComponent(w3)}`, undefined, admin)
280
+ const w3has = (w3b?.data?.bindings ?? []).some(b => b.nodeId === n1 && b.kind === 'dcw')
281
+ ok('T9b', '委派落库:worker 绑定面出现 n1(dcw)', w3has, JSON.stringify(w3b?.data?.bindings ?? []).slice(0, 280))
282
+
283
+ const chManual2 = await api('PATCH', `/api/workshop/channels/${channelId}/execution-mode`, { mode: 'manual' }, admin)
284
+ ok('T9c', '频道 manual(为在飞审批收回测试)', chManual2.status === 200, `status=${chManual2.status}`)
285
+ const trialW3P = callAs(w3, tok.w3, 'recipe_trial', { line_id: line, params: [{ node_id: n1, value: 46 }], hypothesis: 'E2E:委派绑定在飞审批' })
286
+ const cardW3 = await waitPending(w3, 15_000)
287
+ ok('T9d', '委派绑定(auto)在频道 manual 下仍挂人工审批', Boolean(cardW3), cardW3 ? `card=${cardW3.id}` : '未出现审批卡')
288
+ const revoke = await callAs(leadId, tok.lead, 'team_revoke_nodes', { agent_id: w3, node_ids: [n1] })
289
+ ok('T9e', 'lead 收回委派(dcw 写授权)', !invErr(revoke) && /已收回\s*1\s*条授权/.test(invText(revoke)), invText(revoke).slice(0, 220))
290
+ const trialW3 = await trialW3P
291
+ ok('T9f', '收回后在飞审批按拒收敛(trial 未执行)', /未批准|失效|授权/.test(invText(trialW3)), invText(trialW3).slice(0, 220))
292
+ const pendW3 = await api('GET', `/api/workshop/agent-tools/approvals?agentId=${encodeURIComponent(w3)}`, undefined, admin)
293
+ ok('T9g', '审批面板无残留 pending(收回即失活)', (pendW3?.data?.approvals ?? []).filter(a => a.status === 'pending').length === 0, JSON.stringify((pendW3?.data?.approvals ?? []).slice(0, 3)).slice(0, 240))
294
+ const trialW3After = await callAs(w3, tok.w3, 'recipe_trial', { line_id: line, params: [{ node_id: n1, value: 46 }], hypothesis: 'E2E:收回后再次提交' })
295
+ ok('T9h', '收回后再提交 → 写面拒绝', invErr(trialW3After) && /写授权|绑定面/.test(invText(trialW3After)), invText(trialW3After).slice(0, 220))
296
+ await api('PATCH', `/api/workshop/channels/${channelId}/execution-mode`, { mode: 'auto', confirm: true }, admin)
297
+
298
+ // ── M5 回归:越线绑定拒绝 + 直写工具仍不可用 ──
299
+ const lineB = (await api('POST', '/api/workshop/dcw/lines', { name: `节点授权线B-${TAG}` }, admin)).data?.line?.id
300
+ const nB = (await api('POST', '/api/workshop/dcw', { ...nodeBody(9), lineId: lineB, name: `他线节点-${TAG}` }, admin)).data?.node?.id
301
+ const cross = await bind(w2, nB, 'dcw')
302
+ ok('T10', '频道绑 A 线 → 成员绑定 B 线节点被拒(403 产线隔离)', cross.status === 403, `status=${cross.status} code=${String(cross?.code ?? '')}`)
303
+ const direct = await callAs(w1, tok.w1, 'dcw_control', { node_id: n1, value: 50, hypothesis: 'E2E:直写工具必须仍不可达' })
304
+ ok('T10b', '直写工具 dcw_control 仍不可达(未注册/禁写)', invErr(direct) || /未知工具|不可用|停用|拒绝/.test(invText(direct)), invText(direct).slice(0, 200))
305
+ }
306
+ catch (err) {
307
+ ok('FATAL', 'E2E 未捕获异常', false, err instanceof Error ? `${err.message}\n${err.stack ?? ''}` : String(err))
308
+ }
309
+ finally {
310
+ try {
311
+ child.kill()
312
+ }
313
+ catch { /* noop */ }
314
+ const passed = results.filter(r => r.pass).length
315
+ const total = results.length
316
+ const evidenceDir = process.env.AW_E2E_EVIDENCE
317
+ ? dirname(process.env.AW_E2E_EVIDENCE)
318
+ : join(REPO, 'docs', 'audit', 'evidence', '2026-10-28-node-binding-authz')
319
+ mkdirSync(evidenceDir, { recursive: true })
320
+ const evidencePath = process.env.AW_E2E_EVIDENCE
321
+ ? process.env.AW_E2E_EVIDENCE
322
+ : join(evidenceDir, 'report.json')
323
+ writeFileSync(evidencePath, JSON.stringify({
324
+ at: new Date().toISOString(),
325
+ env: { base: BASE, home: join(ROOT, 'home'), devLog: join(ROOT, 'dev.log'), mode: 'isolated' },
326
+ summary: { total, passed, failed: total - passed, verdict: passed === total ? 'PASS' : 'FAIL' },
327
+ results,
328
+ }, null, 2))
329
+ console.log(`\n结论:${passed === total ? 'PASS' : 'FAIL'}(${passed}/${total})`)
330
+ console.log(`证据:${evidencePath}`)
331
+ process.exit(passed === total ? 0 : 1)
332
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * 2026-10-28 安全加固验收脚本(可复跑)。
3
+ * 前置:隔离实例已由文档中的 setup 流程建立场景(scenario.json/admin.json)。
4
+ * 用法: AW_BASE_URL=http://127.0.0.1:3410 AW_E2E_DIR=/path/to/e2e node scripts/security-acceptance.mjs
5
+ * 环境: AW_BASE_URL / AW_E2E_DIR;证据默认写 docs/audit/evidence/<date>-security-hardening。
6
+ */
7
+ /**
8
+ * 2026-10-28 安全加固验收(S1-S9 已实施项)。
9
+ * 前置:隔离实例运行在 127.0.0.1:3410,场景已由 setup.mjs 建立。
10
+ * 输出:docs/audit/evidence/2026-10-28-security-hardening/report.json
11
+ */
12
+ import fs from 'node:fs'
13
+
14
+ const BASE = process.env.AW_BASE_URL ?? 'http://127.0.0.1:3410'
15
+ const ROOT = process.cwd()
16
+ const E2E_DIR = process.env.AW_E2E_DIR ?? 'D:/tmp/aw-e2e-20261028'
17
+ const scenario = JSON.parse(fs.readFileSync(`${E2E_DIR}/scenario.json`, 'utf8'))
18
+ const admin = JSON.parse(fs.readFileSync(`${E2E_DIR}/admin.json`, 'utf8'))
19
+
20
+ const results = []
21
+ function record(id, name, pass, expected, actual, note = '') {
22
+ results.push({ id, name, pass, expected, actual, note })
23
+ console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${name} | expected=${expected} actual=${actual}${note ? ' | ' + note : ''}`)
24
+ }
25
+
26
+ async function call(method, path, { body, token, headers = {} } = {}) {
27
+ const r = await fetch(BASE + path, {
28
+ method,
29
+ headers: {
30
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
31
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
32
+ ...headers,
33
+ },
34
+ body: body === undefined ? undefined : JSON.stringify(body),
35
+ })
36
+ const text = await r.text()
37
+ let json = null
38
+ try {
39
+ json = JSON.parse(text)
40
+ }
41
+ catch { /* non-json */ }
42
+ return { status: r.status, json, text }
43
+ }
44
+
45
+ const ADMIN = admin.resp.data.token
46
+ const runId = (await call('GET', `/api/workshop/dcw/recipes`, { token: ADMIN })).json?.data?.runs?.[0]?.id
47
+
48
+ // ---- 普通用户(幂等:密码存 E2E_DIR/user2.json;已存在则重置密码) ----
49
+ const USER_FILE = `${E2E_DIR}/user2.json`
50
+ let userFixture = null
51
+ try {
52
+ userFixture = JSON.parse(fs.readFileSync(USER_FILE, 'utf8'))
53
+ }
54
+ catch { /* 首次运行 */ }
55
+ const USER_PW = userFixture?.password ?? (`u2-${Math.random().toString(36).slice(2, 14)}`)
56
+ const u2reg = await call('POST', '/api/users', { token: ADMIN, body: { name: 'Sec User2', email: 'sec-user2@aw.local', password: USER_PW, role: 'user' } })
57
+ if (u2reg.status !== 200 && u2reg.status !== 201) {
58
+ const list = await call('GET', '/api/users?keyword=sec-user2&pageSize=50', { token: ADMIN })
59
+ const rows = list.json?.data?.items ?? list.json?.data?.users ?? list.json?.data ?? []
60
+ const row = Array.isArray(rows) ? rows.find(u => u.email === 'sec-user2@aw.local') : null
61
+ if (row?.id) await call('PUT', `/api/users/${row.id}`, { token: ADMIN, body: { password: USER_PW } })
62
+ }
63
+ const u2login = await call('POST', '/api/users/login', { body: { email: 'sec-user2@aw.local', password: USER_PW } })
64
+ const USER = u2login.json?.data?.token
65
+ const USER_ID = u2login.json?.data?.user?.id
66
+ fs.writeFileSync(USER_FILE, JSON.stringify({ email: 'sec-user2@aw.local', password: USER_PW, id: USER_ID }, null, 2))
67
+ record('AUTH-0', '普通用户创建/登录(幂等)', Boolean(USER), 'token', u2login.status)
68
+
69
+ // 每次运行先撤销历史授权,保证"无产线授权"负例可重复
70
+ if (USER_ID) {
71
+ await call('PUT', '/api/workshop/permissions', {
72
+ token: ADMIN,
73
+ body: {
74
+ userId: USER_ID,
75
+ grants: [{ lineId: scenario.A.line.id, mode: null }, { lineId: scenario.B.line.id, mode: null }],
76
+ },
77
+ })
78
+ }
79
+
80
+ // 1) 未授权/无产线授权
81
+ const noAuth = await call('GET', '/api/workshop/dcw/recipes')
82
+ record('S4-1', '未认证读配方 → 401', noAuth.status === 401, 401, noAuth.status)
83
+
84
+ const u2Recipes = await call('GET', '/api/workshop/dcw/recipes', { token: USER })
85
+ const empty = (u2Recipes.json?.data?.recipes ?? []).length === 0 && (u2Recipes.json?.data?.runs ?? []).length === 0
86
+ record('S4-2', '无产线授权读配方列表 → 空集(非全量)', u2Recipes.status === 200 && empty, '200 + []', `${u2Recipes.status} + recipes=${u2Recipes.json?.data?.recipes?.length}`)
87
+
88
+ const u2Journal = await call('GET', '/api/workshop/dcw/journal', { token: USER })
89
+ record('S4-3', '无授权读写账本 → 空集', (u2Journal.json?.data?.anchors ?? []).length === 0, '[]', (u2Journal.json?.data?.anchors ?? []).length)
90
+
91
+ const u2Alarms = await call('GET', '/api/workshop/daq/alarms', { token: USER })
92
+ record('S4-4', '无授权读报警 → 空集', (u2Alarms.json?.data?.alarms ?? []).length === 0, '[]', (u2Alarms.json?.data?.alarms ?? []).length)
93
+
94
+ const u2Read = await call('POST', `/api/workshop/dcw/${scenario.A.dcwTemp.id}/read`, { token: USER, body: {} })
95
+ record('S4-5', '无授权读节点 PLC 值 → 403', u2Read.status === 403, 403, u2Read.status)
96
+
97
+ const u2Del = await call('DELETE', `/api/workshop/dcw/recipes/${scenario.A.recipe.id}`, { token: USER })
98
+ record('S4-6', '无授权删配方 → 403', u2Del.status === 403, 403, u2Del.status)
99
+
100
+ const u2Close = await call('POST', `/api/workshop/dcw/runs/${runId}/close`, { token: USER, body: {} })
101
+ record('S4-7', '无授权关批次 → 403', u2Close.status === 403, 403, u2Close.status)
102
+
103
+ // 2) 控制面权限
104
+ const u2Test = await call('POST', '/api/workshop/daq/test-driver', { token: USER, body: { driver: 'mock', driverConfig: {} } })
105
+ record('S5-1', '普通用户 test-driver → 403', u2Test.status === 403, 403, u2Test.status)
106
+ const u2TestDcw = await call('POST', '/api/workshop/dcw/test-driver', { token: USER, body: { driver: 'mock', driverConfig: {} } })
107
+ record('S5-2', '普通用户 dcw test-driver → 403', u2TestDcw.status === 403, 403, u2TestDcw.status)
108
+ const u2Reconnect = await call('POST', '/api/workshop/daq/infra/reconnect', { token: USER, body: {} })
109
+ record('S5-3', '普通用户 infra/reconnect → 403', u2Reconnect.status === 403, 403, u2Reconnect.status)
110
+ const adminTest = await call('POST', '/api/workshop/daq/test-driver', { token: ADMIN, body: { driver: 'mock', driverConfig: {} } })
111
+ record('S5-4', 'admin test-driver 仍可用', adminTest.status === 200, 200, adminTest.status)
112
+
113
+ // S12:ingest 时间戳方向
114
+ const futureAt = new Date(Date.now() + 5 * 60_000).toISOString()
115
+ const futureIngest = await call('POST', `/api/workshop/daq/${scenario.A.daqTemp.id}/ingest`, {
116
+ token: ADMIN,
117
+ body: { at: futureAt, value: 1 },
118
+ })
119
+ record('S12-1', '未来时间戳 ingest → 400 FUTURE_TIMESTAMP', futureIngest.status === 400 && futureIngest.json?.code === 'FUTURE_TIMESTAMP', '400 FUTURE_TIMESTAMP', `${futureIngest.status} ${futureIngest.json?.code}`)
120
+
121
+ const backfillAt = new Date(Date.now() - 10 * 60_000).toISOString()
122
+ const backfillIngest = await call('POST', `/api/workshop/daq/${scenario.A.daqTemp.id}/ingest`, {
123
+ token: ADMIN,
124
+ body: { at: backfillAt, value: 1 },
125
+ })
126
+ record('S12-2', '10 分钟前回填 ingest → 200', backfillIngest.status === 200, 200, backfillIngest.status)
127
+
128
+ // S15:Modbus 原始量程不可表示 → 配置期拒绝
129
+ const badRaw = await call('POST', '/api/workshop/dcw', {
130
+ token: ADMIN,
131
+ body: {
132
+ templateRef: 'temp-sp',
133
+ name: '验收-坏原始量程',
134
+ driver: 'modbus-tcp',
135
+ driverConfig: { host: '127.0.0.1', register: 1, dataType: 'int16', rawMin: 0, rawMax: 70000 },
136
+ lineId: scenario.A.line.id,
137
+ },
138
+ })
139
+ record('S15-1', 'int16 rawMax=70000 → 400 配置期拒绝', badRaw.status === 400 && /int16/.test(String(badRaw.text ?? '')), '400 + int16', `${badRaw.status} ${String(badRaw.text).slice(0, 120)}`)
140
+
141
+ // 3) harness 启动面
142
+ const u2Ch = await call('POST', '/api/workshop/channels', {
143
+ token: USER,
144
+ body: { name: 'u2-注入测试', leadAgent: { name: 'evil', harness: 'omp', config: { command: 'cmd.exe', args: ['/c', 'whoami'] } } },
145
+ })
146
+ record('S1-1', '非 admin 创建 Channel 带 command → 403', u2Ch.status === 403 && u2Ch.json?.code === 'HARNESS_SPAWN_FORBIDDEN', '403 HARNESS_SPAWN_FORBIDDEN', `${u2Ch.status} ${u2Ch.json?.code}`)
147
+
148
+ const u2Agent = await call('POST', '/api/workshop/agents', {
149
+ token: USER,
150
+ body: { name: 'evil-agent', harness: 'omp', config: { command: '/bin/sh', args: ['-c', 'id'] } },
151
+ })
152
+ record('S1-2', '非 admin 创建 Agent 带 command → 403', u2Agent.status === 403 && u2Agent.json?.code === 'HARNESS_SPAWN_FORBIDDEN', '403 HARNESS_SPAWN_FORBIDDEN', `${u2Agent.status} ${u2Agent.json?.code}`)
153
+
154
+ const adminCmd = await call('POST', '/api/workshop/agents', {
155
+ token: ADMIN,
156
+ body: { name: 'evil-admin-agent', harness: 'omp', config: { command: 'cmd.exe', args: ['/c', 'whoami'] } },
157
+ })
158
+ record('S1-3', 'admin 指定通用解释器 → 403 HARNESS_COMMAND_DENIED', adminCmd.status === 403 && adminCmd.json?.code === 'HARNESS_COMMAND_DENIED', '403 HARNESS_COMMAND_DENIED', `${adminCmd.status} ${adminCmd.json?.code}`)
159
+
160
+ const u2ChPlain = await call('POST', '/api/workshop/channels', { token: USER, body: { name: 'u2-成员注入测试' } })
161
+ const u2ChId = u2ChPlain.json?.data?.channelId
162
+ const u2Member = u2ChId
163
+ ? await call('POST', `/api/workshop/channels/${u2ChId}/agents`, { token: USER, body: { name: 'm', harness: 'omp', config: { args: ['--no-session'] } } })
164
+ : { status: 0, json: null }
165
+ record('S1-4', '非 admin 新增成员带 args → 403', u2Member.status === 403 && u2Member.json?.code === 'HARNESS_SPAWN_FORBIDDEN', '403 HARNESS_SPAWN_FORBIDDEN', `${u2Member.status} ${u2Member.json?.code}`)
166
+
167
+ const u2Tpl = await call('POST', '/api/workshop/channel-templates', {
168
+ token: USER,
169
+ body: { name: 'u2-tpl', lead: { name: 'l', harness: 'omp', config: { args: ['--no-session'] } } },
170
+ })
171
+ record('S1-5', '非 admin 建模板内联 args → 403', u2Tpl.status === 403 && u2Tpl.json?.code === 'HARNESS_SPAWN_FORBIDDEN', '403 HARNESS_SPAWN_FORBIDDEN', `${u2Tpl.status} ${u2Tpl.json?.code}`)
172
+
173
+ // 4) Agent token 不回显
174
+ const rotate = await call('POST', `/api/workshop/channels/${scenario.manual.channelId}/agents/${scenario.manual.worker.id}/rotate-token`, { token: ADMIN, body: {} })
175
+ const workerToken = rotate.json?.data?.token
176
+ const roster = await call('GET', `/api/workshop/channels/${scenario.manual.channelId}/agents`, { headers: { authorization: `Bearer ${workerToken ?? ''}` } })
177
+ const leaked = JSON.stringify(roster.json ?? {})
178
+ record('S3-1', 'worker Agent token 拉名册 → 200', roster.status === 200, 200, roster.status)
179
+ record('S3-2', '名册不含 token 字段', !leaked.includes('"token"'), 'no "token"', leaked.includes('"token"') ? 'LEAK' : 'clean')
180
+ record('S3-3', '名册不含 config 字段(非属主)', !leaked.includes('"config"'), 'no "config"', leaked.includes('"config"') ? 'LEAK' : 'clean')
181
+
182
+ // 5) 授权 operate 后的数据面
183
+ if (USER_ID) {
184
+ await call('PUT', '/api/workshop/permissions', { token: ADMIN, body: { userId: USER_ID, grants: [{ lineId: scenario.A.line.id, mode: 'operate' }] } })
185
+ }
186
+ const u2Recipes2 = await call('GET', '/api/workshop/dcw/recipes', { token: USER })
187
+ const gotOne = (u2Recipes2.json?.data?.recipes ?? []).length === 1
188
+ record('S4-8', '仅授权线A → 只看到线A的1个配方', gotOne, 1, (u2Recipes2.json?.data?.recipes ?? []).length)
189
+
190
+ const u2PatchMin = await call('PATCH', `/api/workshop/dcw/${scenario.A.dcwTemp.id}`, { token: USER, body: { min: 10 } })
191
+ record('S2-1', 'operate 改 min(安全字段) → 403', u2PatchMin.status === 403 && u2PatchMin.json?.code === 'DCW_CONFIG_ADMIN_ONLY', '403 DCW_CONFIG_ADMIN_ONLY', `${u2PatchMin.status} ${u2PatchMin.json?.code}`)
192
+
193
+ const u2PatchName = await call('PATCH', `/api/workshop/dcw/${scenario.A.dcwTemp.id}`, { token: USER, body: { name: '温度设定(手动线)' } })
194
+ record('S2-2', 'operate 改 name(运行字段) → 200', u2PatchName.status === 200, 200, u2PatchName.status)
195
+
196
+ const u2PatchDaq = await call('PATCH', `/api/workshop/daq/${scenario.A.daqTemp.id}`, { token: USER, body: { driver: 'mock' } })
197
+ record('S2-3', 'operate 改 DAQ driver → 403', u2PatchDaq.status === 403 && u2PatchDaq.json?.code === 'DAQ_CONFIG_ADMIN_ONLY', '403 DAQ_CONFIG_ADMIN_ONLY', `${u2PatchDaq.status} ${u2PatchDaq.json?.code}`)
198
+
199
+ const u2Read2 = await call('POST', `/api/workshop/dcw/${scenario.A.dcwTemp.id}/read`, { token: USER, body: {} })
200
+ record('S4-9', '授权线A后可读节点 PLC 值 → 200', u2Read2.status === 200, 200, u2Read2.status)
201
+
202
+ const u2Query = await call('GET', `/api/workshop/daq/query?line_id=${scenario.A.line.id}&last_minutes=5&limit=2`, { token: USER })
203
+ record('S4-10', '授权线A后可查数采 → 200', u2Query.status === 200, 200, u2Query.status)
204
+
205
+ // ---- 汇总 ----
206
+ const pass = results.filter(r => r.pass).length
207
+ const fail = results.length - pass
208
+ const outDir = `${ROOT}/docs/audit/evidence/2026-10-28-security-hardening`
209
+ fs.mkdirSync(outDir, { recursive: true })
210
+ fs.writeFileSync(`${outDir}/report.json`, JSON.stringify({
211
+ suite: 'security-hardening',
212
+ at: new Date().toISOString(),
213
+ base: BASE,
214
+ totals: { total: results.length, pass, fail },
215
+ results,
216
+ }, null, 2))
217
+ console.log(`\n结论:${fail === 0 ? 'PASS' : 'FAIL'}(${pass}/${results.length}) → ${outDir}/report.json`)
218
+ process.exit(fail === 0 ? 0 : 1)
@@ -1,14 +1,20 @@
1
1
  /**
2
- * POST /api/workshop/agent-tools/bindings —— 绑定 Agent ↔ 工业节点。
3
- * body: { agentId, nodeId, kind: 'dcw'|'daq', mode?: 'auto'|'manual' }
2
+ * POST /api/workshop/agent-tools/bindings —— 绑定 Agent ↔ 工业对象。
3
+ * body: { agentId, nodeId, kind: 'dcw'|'daq'|'recipe', mode?: 'auto'|'manual' }
4
4
  * 产线权限:普通用户仅可绑定自己有权产线的节点——daq 绑定需「仅查看」及以上,
5
- * dcw(写控)绑定需「可操控」;admin/editor 不受限。
5
+ * dcw/recipe(写向)绑定需「可操控」;admin/editor 不受限。
6
6
  *
7
- * auto 治理(产线 Co-Pilot P2,计划 §5.4 铁律 2):**dcw 绑定首绑强制 manual**——
8
- * dcw 写控面挂着逐次人工审批闸,创建即 auto 等于免审上线(旧实现 body.mode 缺省
9
- * 还是 'auto'),故 body.mode 对 dcw 一律忽略;显式传 auto 不静默吞掉,响应附
10
- * modeForced + notice 提示(响应形状向后兼容,多余字段无害)。如需 auto,应在
11
- * 绑定创建后走 PATCH 携 confirm:true 显式切换(见 [id].patch.ts)。
7
+ * 双轨授权(2026-10-28 节点级写授权计划 M1):
8
+ * - kind='recipe'(nodeId=配方 id)= 配方操作权(update/apply/trial/rollback);
9
+ * - kind='dcw'(nodeId=数控节点 id)= **点位写授权面** —— Agent 仍不能直写节点
10
+ * (param_control/dcw_control 恒拒桩不动),但持有该节点后,对"当前活动配方"的
11
+ * trial/apply 只要覆盖集 ⊆ 自己的节点绑定面即放行;换配方不停摆。
12
+ *
13
+ * auto 治理(产线 Co-Pilot P2,计划 §5.4 铁律 2):**写向绑定首绑强制 manual**——
14
+ * dcw/recipe 写控面挂着逐次人工审批闸,创建即 auto 等于免审上线,故 body.mode 对
15
+ * 写向绑定一律忽略;显式传 auto 不静默吞掉,响应附 modeForced + notice 提示
16
+ * (响应形状向后兼容,多余字段无害)。如需 auto,应在绑定创建后走 PATCH 携
17
+ * confirm:true 显式切换(见 [id].patch.ts)。
12
18
  * daq 绑定仅授权 daq_query 读(工具面无写族、mode 无审批语义可摘),维持原样。
13
19
  */
14
20
  import { readBody } from 'h3'
@@ -28,13 +34,6 @@ export default defineApiHandler(async (event) => {
28
34
  const nodeId = String(body.nodeId ?? '')
29
35
  const agentId = String(body.agentId ?? '')
30
36
 
31
- // 权限模型 v2:数控节点不再直接绑定 —— Agent 只对 recipe 做参数写入与下发;
32
- // 节点级写控/dcw 绑定一律拒绝(存量绑定仅历史兼容,工具层已全部收敛到 recipe 面)。
33
- if (kind === 'dcw') {
34
- throw new AppError(400, 'BINDING_KIND_DEPRECATED',
35
- `数控节点(dcw)不再支持直接绑定:Agent 通过绑定 recipe(kind='recipe',nodeId=配方id)获得该配方参数写入与下发能力;数采观察用 kind='daq'。`)
36
- }
37
-
38
37
  // 绑定主体必须是**已部署的 Channel 成员实例**(运行时就是它持这条绑定做工具鉴权)。
39
38
  // 传 Agent 模板 id 是极易发生的误用:旧实现照单全收,绑定落在模板 id 上,
40
39
  // 成员侧 daq_query / dcw_control 一律报"尚未绑定节点" —— 一条**静默失效**的授权,
@@ -55,7 +54,7 @@ export default defineApiHandler(async (event) => {
55
54
 
56
55
  // 权限模型 v3:绑线 channel 的产线一致性 —— channel 绑了产线后,其成员 agent 只能
57
56
  // 绑定同产线的节点/配方(产线隔离的工作空间语义;未绑线的纯协作频道不受此限)。
58
- const nodeLineOf = (nid: string, k: 'daq' | 'recipe'): string | null | undefined => {
57
+ const nodeLineOf = (nid: string, k: 'daq' | 'dcw' | 'recipe'): string | null | undefined => {
59
58
  if (k === 'recipe') return getDcwController().listRecipes().find(r => r.id === nid)?.lineId
60
59
  return k === 'daq' ? getDaqController().byId(nid)?.lineId : getDcwController().byId(nid)?.lineId
61
60
  }
@@ -96,8 +95,25 @@ export default defineApiHandler(async (event) => {
96
95
  throw new AppError(404, ErrorCodes.NOT_FOUND, `${kind === 'daq' ? '数采' : '写控'}节点不存在: ${nodeId}`)
97
96
  }
98
97
  const mode = lineMode(user, lineId)
98
+ // daq 只读绑定「仅查看」即可;dcw 是写授权面,需「可操控」(与 binding-authz 同口径)
99
+ const need = kind === 'daq' ? '仅查看' : '可操控'
99
100
  if (mode === 'none') {
100
- throw new AppError(403, 'LINE_FORBIDDEN', `无该产线权限:绑定数采节点需对产线「${lineId}」拥有仅查看及以上权限`)
101
+ throw new AppError(403, 'LINE_FORBIDDEN', `无该产线权限:绑定${kind === 'daq' ? '数采' : '数控'}节点需对产线「${lineId}」拥有${need}及以上权限`)
102
+ }
103
+ if (kind === 'dcw' && mode !== 'operate') {
104
+ throw new AppError(403, 'LINE_READONLY', `该绑定涉及写控节点,需对产线「${lineId}」拥有可操控权限(当前:仅查看)`)
105
+ }
106
+ if (kind === 'dcw') {
107
+ // 写向绑定首绑恒 manual(每次下发/试验需人工批准);auto 须 PATCH confirm:true 显式切换
108
+ const binding = getAgentNodeBindingRepo().bind(agentId, nodeId, 'dcw', 'manual')
109
+ if (body.mode === 'auto') {
110
+ return {
111
+ binding,
112
+ modeForced: 'manual',
113
+ notice: '数控节点绑定默认 manual(点位写授权,每次下发/试验需人工批准),body.mode=auto 已被忽略;如需 auto 请对该绑定 PATCH { mode: "auto", confirm: true } 显式确认切换',
114
+ }
115
+ }
116
+ return { binding }
101
117
  }
102
118
  // daq 维持 body.mode ?? 'auto' 缺省
103
119
  const bindingMode = body.mode ?? 'auto'