talos-code 0.0.1 → 0.1.0

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 (401) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +111 -3
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  4. package/dist/archive/zip.js +73 -0
  5. package/dist/args.js +97 -0
  6. package/dist/automations/history-store.js +34 -0
  7. package/dist/automations/policy-store.js +43 -0
  8. package/dist/automations/runner.js +107 -0
  9. package/dist/automations/schedule.js +85 -0
  10. package/dist/automations/windows-task.js +53 -0
  11. package/dist/commands/advanced-cli.js +233 -0
  12. package/dist/commands/checkpoint-cli.js +45 -0
  13. package/dist/commands/config-cli.js +107 -0
  14. package/dist/commands/context.js +17 -0
  15. package/dist/commands/extensions-cli.js +207 -0
  16. package/dist/commands/project-cli.js +74 -0
  17. package/dist/commands/project-commands.js +143 -0
  18. package/dist/commands/provider-cli.js +185 -0
  19. package/dist/commands/services-cli.js +248 -0
  20. package/dist/commands/session-cli.js +171 -0
  21. package/dist/commands/system-cli.js +405 -0
  22. package/dist/config/commands.js +60 -0
  23. package/dist/config/load.js +382 -0
  24. package/dist/config/migrations.js +48 -0
  25. package/dist/config/types.js +11 -0
  26. package/dist/diagnostics/development-log.js +154 -0
  27. package/dist/diagnostics/doctor.js +104 -0
  28. package/dist/diagnostics/redact.js +80 -0
  29. package/dist/diagnostics/zip.js +52 -0
  30. package/dist/errors.js +156 -0
  31. package/dist/events/bridge.js +130 -0
  32. package/dist/extensions/installer.js +178 -0
  33. package/dist/extensions/package-schema.js +24 -0
  34. package/dist/headless/result.js +84 -0
  35. package/dist/headless/run.js +230 -0
  36. package/dist/i18n/en/approval.js +43 -0
  37. package/dist/i18n/en/common.js +9 -0
  38. package/dist/i18n/en/credentials.js +82 -0
  39. package/dist/i18n/en/errors.js +246 -0
  40. package/dist/i18n/en/firstrun.js +89 -0
  41. package/dist/i18n/en/providers.js +37 -0
  42. package/dist/i18n/en/screen.js +736 -0
  43. package/dist/i18n/en/tools.js +127 -0
  44. package/dist/i18n/en.js +14 -0
  45. package/dist/i18n/error-view.js +307 -0
  46. package/dist/i18n/index.js +19 -0
  47. package/dist/i18n/kernel-map.js +139 -0
  48. package/dist/io.js +45 -0
  49. package/dist/main.js +330 -0
  50. package/dist/paths.js +41 -0
  51. package/dist/protocol/v2/codec.js +9 -0
  52. package/dist/protocol/v2/events.js +555 -0
  53. package/dist/protocol/v2/index.js +4 -0
  54. package/dist/protocol/v2/replay.js +28 -0
  55. package/dist/protocol/v2/types.js +1 -0
  56. package/dist/provider/control-plane.js +135 -0
  57. package/dist/provider/environment-keys.js +275 -0
  58. package/dist/provider/health.js +110 -0
  59. package/dist/provider/missing-key.js +48 -0
  60. package/dist/provider/model-catalog.js +168 -0
  61. package/dist/provider/provider-text.js +13 -0
  62. package/dist/provider/store.js +95 -0
  63. package/dist/provider/system-keyring.js +214 -0
  64. package/dist/runtime/active-run.js +63 -0
  65. package/dist/runtime/agent-tree.js +74 -0
  66. package/dist/runtime/attachments.js +84 -0
  67. package/dist/runtime/brokered-executor.js +395 -0
  68. package/dist/runtime/context-status.js +76 -0
  69. package/dist/runtime/create-runtime.js +111 -0
  70. package/dist/runtime/model-profile.js +361 -0
  71. package/dist/runtime/output-store.js +137 -0
  72. package/dist/runtime/provider-attempts.js +185 -0
  73. package/dist/runtime/replay-buffer.js +153 -0
  74. package/dist/runtime/repo.js +95 -0
  75. package/dist/runtime/session-facade.js +135 -0
  76. package/dist/runtime/session-summary.js +125 -0
  77. package/dist/runtime/supervisor.js +262 -0
  78. package/dist/runtime/talos-composition.js +1006 -0
  79. package/dist/runtime/types.js +5 -0
  80. package/dist/runtime/usage-snapshot.js +82 -0
  81. package/dist/security/auto-classifier.js +38 -0
  82. package/dist/security/credential-free-environment.js +54 -0
  83. package/dist/security/evaluate.js +243 -0
  84. package/dist/security/execution-backends.js +236 -0
  85. package/dist/security/execution-broker.js +102 -0
  86. package/dist/security/forge-scan.js +74 -0
  87. package/dist/security/from-approval.js +39 -0
  88. package/dist/security/mxc-execution-backend.js +281 -0
  89. package/dist/security/permission-engine.js +138 -0
  90. package/dist/security/permission-explanation.js +54 -0
  91. package/dist/security/persist.js +179 -0
  92. package/dist/security/plugin-guard.js +230 -0
  93. package/dist/security/process-tree-evidence-store.js +74 -0
  94. package/dist/security/process-tree-probe.js +554 -0
  95. package/dist/security/project-resource-inventory.js +186 -0
  96. package/dist/security/project-trust-gate.js +38 -0
  97. package/dist/security/project-trust.js +367 -0
  98. package/dist/security/rule-parser.js +201 -0
  99. package/dist/security/shell-segmentation.js +68 -0
  100. package/dist/security/trust-authority.js +324 -0
  101. package/dist/security/types.js +1 -0
  102. package/dist/security/workspace-identity.js +102 -0
  103. package/dist/services/automation-facade.js +59 -0
  104. package/dist/services/forge-facade.js +77 -0
  105. package/dist/services/hook-facade.js +240 -0
  106. package/dist/services/index.js +16 -0
  107. package/dist/services/library-facade.js +174 -0
  108. package/dist/services/mcp-facade.js +348 -0
  109. package/dist/services/memory-facade.js +126 -0
  110. package/dist/services/notes-facade.js +157 -0
  111. package/dist/services/plugin-facade.js +308 -0
  112. package/dist/services/research-facade.js +110 -0
  113. package/dist/services/task-board-facade.js +236 -0
  114. package/dist/services/workflow-catalog.js +246 -0
  115. package/dist/sessions/export-format.js +98 -0
  116. package/dist/sessions/metadata-store.js +160 -0
  117. package/dist/sessions/share.js +122 -0
  118. package/dist/sessions/transfer.js +16 -0
  119. package/dist/subcommands.js +62 -0
  120. package/dist/tui/agent-roster.js +36 -0
  121. package/dist/tui/app.js +3761 -0
  122. package/dist/tui/approval.js +36 -0
  123. package/dist/tui/boot/boot-sequence.js +76 -0
  124. package/dist/tui/boot/cinematic.js +243 -0
  125. package/dist/tui/boot/desktop-logo.js +82 -0
  126. package/dist/tui/boot.js +3 -0
  127. package/dist/tui/busy-input.js +58 -0
  128. package/dist/tui/catalog-service.js +559 -0
  129. package/dist/tui/components/assistant-stream.js +1 -0
  130. package/dist/tui/components/command-menu.js +68 -0
  131. package/dist/tui/components/composer.js +219 -0
  132. package/dist/tui/components/diff.js +35 -0
  133. package/dist/tui/components/footer.js +48 -0
  134. package/dist/tui/components/header.js +6 -0
  135. package/dist/tui/components/markdown.js +305 -0
  136. package/dist/tui/components/status-indicator.js +32 -0
  137. package/dist/tui/components/terminal-shell.js +205 -0
  138. package/dist/tui/components/thinking-row.js +19 -0
  139. package/dist/tui/components/tool-row.js +97 -0
  140. package/dist/tui/components/transcript-virtualizer.js +165 -0
  141. package/dist/tui/components/transcript.js +43 -0
  142. package/dist/tui/diff-model.js +77 -0
  143. package/dist/tui/editor-history.js +21 -0
  144. package/dist/tui/editor.js +210 -0
  145. package/dist/tui/event-adapter.js +436 -0
  146. package/dist/tui/exit-output.js +58 -0
  147. package/dist/tui/external-editor.js +53 -0
  148. package/dist/tui/file-completion.js +89 -0
  149. package/dist/tui/focus-manager.js +5 -0
  150. package/dist/tui/highlight.js +30 -0
  151. package/dist/tui/input-router.js +18 -0
  152. package/dist/tui/interrupt.js +99 -0
  153. package/dist/tui/keybindings.js +194 -0
  154. package/dist/tui/keymap-resolver.js +91 -0
  155. package/dist/tui/launch-state.js +139 -0
  156. package/dist/tui/line-diff.js +57 -0
  157. package/dist/tui/live-activity.js +45 -0
  158. package/dist/tui/metrics.js +123 -0
  159. package/dist/tui/onboarding.js +36 -0
  160. package/dist/tui/overlays/agent-tree.js +39 -0
  161. package/dist/tui/overlays/approval-dialog.js +640 -0
  162. package/dist/tui/overlays/automation-center.js +51 -0
  163. package/dist/tui/overlays/checkpoint-picker.js +47 -0
  164. package/dist/tui/overlays/context-inspector.js +36 -0
  165. package/dist/tui/overlays/effort-line.js +110 -0
  166. package/dist/tui/overlays/forge-center.js +46 -0
  167. package/dist/tui/overlays/help-dialog.js +16 -0
  168. package/dist/tui/overlays/history-picker.js +37 -0
  169. package/dist/tui/overlays/hook-center.js +70 -0
  170. package/dist/tui/overlays/library-center.js +55 -0
  171. package/dist/tui/overlays/mcp-center.js +60 -0
  172. package/dist/tui/overlays/memory-center.js +63 -0
  173. package/dist/tui/overlays/model-picker.js +72 -0
  174. package/dist/tui/overlays/notes-center.js +53 -0
  175. package/dist/tui/overlays/overlay-host.js +8 -0
  176. package/dist/tui/overlays/plugin-center.js +76 -0
  177. package/dist/tui/overlays/provider-picker.js +145 -0
  178. package/dist/tui/overlays/queue-editor.js +32 -0
  179. package/dist/tui/overlays/research-center.js +59 -0
  180. package/dist/tui/overlays/scroll-window.js +234 -0
  181. package/dist/tui/overlays/session-picker.js +115 -0
  182. package/dist/tui/overlays/tasks-center.js +83 -0
  183. package/dist/tui/overlays/theme-picker.js +21 -0
  184. package/dist/tui/overlays/transcript-search.js +58 -0
  185. package/dist/tui/overlays/trust-center.js +29 -0
  186. package/dist/tui/overlays/workflow-center.js +46 -0
  187. package/dist/tui/project-file-index.js +214 -0
  188. package/dist/tui/project-references.js +85 -0
  189. package/dist/tui/project-trust-prompt.js +287 -0
  190. package/dist/tui/prompt-history-store.js +116 -0
  191. package/dist/tui/queue-store.js +110 -0
  192. package/dist/tui/regions/budget.js +59 -0
  193. package/dist/tui/regions/views.js +96 -0
  194. package/dist/tui/render-coordinator.js +148 -0
  195. package/dist/tui/render-scheduler.js +4 -0
  196. package/dist/tui/run.js +69 -0
  197. package/dist/tui/selection-list.js +29 -0
  198. package/dist/tui/session-controller.js +778 -0
  199. package/dist/tui/session-export.js +54 -0
  200. package/dist/tui/shell-input.js +35 -0
  201. package/dist/tui/shell-layout.js +48 -0
  202. package/dist/tui/shell-model.js +98 -0
  203. package/dist/tui/slash-commands.js +80 -0
  204. package/dist/tui/state.js +151 -0
  205. package/dist/tui/status-bar.js +125 -0
  206. package/dist/tui/status-view.js +40 -0
  207. package/dist/tui/terminal-capabilities.js +9 -0
  208. package/dist/tui/terminal-session.js +11 -0
  209. package/dist/tui/text-width.js +66 -0
  210. package/dist/tui/theme-catalog.js +25 -0
  211. package/dist/tui/theme-store.js +23 -0
  212. package/dist/tui/theme.js +23 -0
  213. package/dist/tui/tool-display.js +135 -0
  214. package/dist/tui/tool-facts.js +1 -0
  215. package/dist/tui/tools/bash-renderer.js +32 -0
  216. package/dist/tui/tools/change.js +61 -0
  217. package/dist/tui/tools/edit-renderer.js +17 -0
  218. package/dist/tui/tools/generic-renderer.js +13 -0
  219. package/dist/tui/tools/list-renderer.js +14 -0
  220. package/dist/tui/tools/read-renderer.js +13 -0
  221. package/dist/tui/tools/registry.js +20 -0
  222. package/dist/tui/tools/row-format.js +187 -0
  223. package/dist/tui/tools/search-renderer.js +38 -0
  224. package/dist/tui/tools/shared.js +97 -0
  225. package/dist/tui/tools/write-renderer.js +15 -0
  226. package/dist/tui/transcript-model.js +441 -0
  227. package/dist/tui/ui-preferences.js +79 -0
  228. package/dist/tui/usage-view.js +77 -0
  229. package/dist/tui/vim-mode.js +99 -0
  230. package/dist/update/check.js +15 -0
  231. package/dist/update/npm.js +51 -0
  232. package/dist/update/run.js +129 -0
  233. package/dist/version.js +2 -0
  234. package/dist/workspace/checkpoint-store.js +210 -0
  235. package/dist/workspace/checkpoint.js +413 -0
  236. package/dist/workspace/restore.js +232 -0
  237. package/package.json +63 -5
  238. package/vendor/context-engine/package.json +11 -0
  239. package/vendor/context-engine/src/compaction-planner.mjs +57 -0
  240. package/vendor/context-engine/src/contracts.mjs +48 -0
  241. package/vendor/context-engine/src/engine.mjs +445 -0
  242. package/vendor/context-engine/src/node/context-export.mjs +164 -0
  243. package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
  244. package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
  245. package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
  246. package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
  247. package/vendor/context-engine/src/profiles.mjs +10 -0
  248. package/vendor/context-engine/src/retrieval.mjs +104 -0
  249. package/vendor/context-engine/src/summary.mjs +97 -0
  250. package/vendor/context-engine/src/usage.mjs +34 -0
  251. package/vendor/harness-ui/package.json +38 -0
  252. package/vendor/harness-ui/src/acp-agent.mjs +350 -0
  253. package/vendor/harness-ui/src/agent-service.mjs +2188 -0
  254. package/vendor/harness-ui/src/agui-events.mjs +452 -0
  255. package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
  256. package/vendor/harness-ui/src/artifact-store.mjs +39 -0
  257. package/vendor/harness-ui/src/assistenza.mjs +123 -0
  258. package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
  259. package/vendor/harness-ui/src/automation-store.mjs +145 -0
  260. package/vendor/harness-ui/src/browser-annota.mjs +639 -0
  261. package/vendor/harness-ui/src/browser-frame.mjs +211 -0
  262. package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
  263. package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
  264. package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
  265. package/vendor/harness-ui/src/browser-stream.mjs +445 -0
  266. package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
  267. package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
  268. package/vendor/harness-ui/src/config.mjs +697 -0
  269. package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
  270. package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
  271. package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
  272. package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
  273. package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
  274. package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
  275. package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
  276. package/vendor/harness-ui/src/context-runtime.mjs +118 -0
  277. package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
  278. package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
  279. package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
  280. package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
  281. package/vendor/harness-ui/src/custom-task.mjs +171 -0
  282. package/vendor/harness-ui/src/doctor.mjs +142 -0
  283. package/vendor/harness-ui/src/document-filename.mjs +97 -0
  284. package/vendor/harness-ui/src/document-generator.mjs +493 -0
  285. package/vendor/harness-ui/src/document-report.mjs +331 -0
  286. package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
  287. package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
  288. package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
  289. package/vendor/harness-ui/src/forge-contract.mjs +221 -0
  290. package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
  291. package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
  292. package/vendor/harness-ui/src/generation-idle.mjs +332 -0
  293. package/vendor/harness-ui/src/gguf-header.mjs +207 -0
  294. package/vendor/harness-ui/src/git-service.mjs +626 -0
  295. package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
  296. package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
  297. package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
  298. package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
  299. package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
  300. package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
  301. package/vendor/harness-ui/src/hook-registry.mjs +186 -0
  302. package/vendor/harness-ui/src/http-app.mjs +6259 -0
  303. package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
  304. package/vendor/harness-ui/src/id-archivio.mjs +27 -0
  305. package/vendor/harness-ui/src/image-generator.mjs +143 -0
  306. package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
  307. package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
  308. package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
  309. package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
  310. package/vendor/harness-ui/src/library-store.mjs +652 -0
  311. package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
  312. package/vendor/harness-ui/src/local-model-store.mjs +339 -0
  313. package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
  314. package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
  315. package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
  316. package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
  317. package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
  318. package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
  319. package/vendor/harness-ui/src/mcp-client.mjs +98 -0
  320. package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
  321. package/vendor/harness-ui/src/mcp-session.mjs +177 -0
  322. package/vendor/harness-ui/src/memory-store.mjs +227 -0
  323. package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
  324. package/vendor/harness-ui/src/model-catalog.mjs +129 -0
  325. package/vendor/harness-ui/src/model-destination.mjs +189 -0
  326. package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
  327. package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
  328. package/vendor/harness-ui/src/notes-store.mjs +250 -0
  329. package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
  330. package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
  331. package/vendor/harness-ui/src/path-policy.mjs +442 -0
  332. package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
  333. package/vendor/harness-ui/src/plugin-session.mjs +180 -0
  334. package/vendor/harness-ui/src/process-policy.mjs +345 -0
  335. package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
  336. package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
  337. package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
  338. package/vendor/harness-ui/src/provider-probe.mjs +582 -0
  339. package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
  340. package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
  341. package/vendor/harness-ui/src/public-problem.mjs +109 -0
  342. package/vendor/harness-ui/src/research/card.mjs +235 -0
  343. package/vendor/harness-ui/src/research/citations.mjs +142 -0
  344. package/vendor/harness-ui/src/research/collector.mjs +275 -0
  345. package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
  346. package/vendor/harness-ui/src/research/dossier.mjs +114 -0
  347. package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
  348. package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
  349. package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
  350. package/vendor/harness-ui/src/research/independence.mjs +159 -0
  351. package/vendor/harness-ui/src/research/ledger.mjs +166 -0
  352. package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
  353. package/vendor/harness-ui/src/research/narration.mjs +181 -0
  354. package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
  355. package/vendor/harness-ui/src/research/opposing.mjs +305 -0
  356. package/vendor/harness-ui/src/research/outline.mjs +111 -0
  357. package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
  358. package/vendor/harness-ui/src/research/pdf.mjs +291 -0
  359. package/vendor/harness-ui/src/research/plan.mjs +301 -0
  360. package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
  361. package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
  362. package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
  363. package/vendor/harness-ui/src/research/recheck.mjs +194 -0
  364. package/vendor/harness-ui/src/research/report.mjs +203 -0
  365. package/vendor/harness-ui/src/research/run.mjs +527 -0
  366. package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
  367. package/vendor/harness-ui/src/research/verification.mjs +572 -0
  368. package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
  369. package/vendor/harness-ui/src/research-store.mjs +1133 -0
  370. package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
  371. package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
  372. package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
  373. package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
  374. package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
  375. package/vendor/harness-ui/src/search-source-store.mjs +172 -0
  376. package/vendor/harness-ui/src/session-registry.mjs +6095 -0
  377. package/vendor/harness-ui/src/session-store.mjs +220 -0
  378. package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
  379. package/vendor/harness-ui/src/setup-stato.mjs +31 -0
  380. package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
  381. package/vendor/harness-ui/src/skill-registry.mjs +120 -0
  382. package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
  383. package/vendor/harness-ui/src/static-files.mjs +96 -0
  384. package/vendor/harness-ui/src/stream-partition.mjs +123 -0
  385. package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
  386. package/vendor/harness-ui/src/task-catalog.mjs +65 -0
  387. package/vendor/harness-ui/src/tasks-store.mjs +220 -0
  388. package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
  389. package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
  390. package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
  391. package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
  392. package/vendor/harness-ui/src/usage-cache.mjs +315 -0
  393. package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
  394. package/vendor/harness-ui/src/workspace-context.mjs +124 -0
  395. package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
  396. package/vendor/harness-ui/src/workspace-files.mjs +589 -0
  397. package/vendor/harness-ui/src/workspace-info.mjs +189 -0
  398. package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
  399. package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
  400. package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
  401. package/vendor/manifest.json +170 -0
@@ -0,0 +1,780 @@
1
+ /**
2
+ * plugin-registry.mjs — FASE G del piano `elegant-spinning-dongarra.md`
3
+ * (29/8): un plugin dichiara hook e/o tool locali in UN manifesto,
4
+ * dentro il workspace — `.harness-ui-plugins/<nome>/plugin.json`,
5
+ * stessa convenzione di hook/MCP/skill.
6
+ *
7
+ * ⛔⛔⛔ Ricerca fatta PRIMA di scrivere (vedi il piano madre, FASE G):
8
+ * un approccio marketplace-centrico (verificato empiricamente altrove)
9
+ * è una scelta deliberatamente NON seguita qui, per coerenza con
10
+ * hook/MCP/skill (tutte per-workspace, mai un registro esterno). Un
11
+ * altro approccio noto fa una scansione statica pre-attivazione, ma la
12
+ * sua stessa documentazione di sicurezza ammette che uno scanner di
13
+ * pattern non costituisce un vero confine — è un avviso, non un
14
+ * blocco. Questo file segue la stessa distinzione: `scansionaPatternSospetti`
15
+ * produce AVVISI, mai un blocco automatico — il confine vero resta il
16
+ * trust hash-vincolato, identico a hook-registry.mjs/mcp-registry.mjs.
17
+ *
18
+ * ⛔⛔ Hash su TUTTO il manifesto, non per componente (a differenza dei
19
+ * server MCP, dove ogni server ha il proprio trust): un plugin è
20
+ * un'unità, cambiare una riga di `plugin.json` invalida l'intero
21
+ * plugin, mai un aggiornamento parziale sotto un trust vecchio.
22
+ *
23
+ * ⛔⛔⛔ CLI-REQ-02 (17/09/2026) — E L'UNITÀ È IL PACCHETTO, NON IL MANIFESTO. Il paragrafo qui
24
+ * sopra restava vero e insufficiente: il manifesto non fa niente, quello che gira è il file che
25
+ * il manifesto NOMINA (`plugin-session.mjs:76-100`, `eseguiComandoPlugin`), e quel file non
26
+ * entrava nell'impronta. Un plugin approvato una volta continuava a girare dopo che il suo codice
27
+ * era stato sostituito — un `git pull`, un cambio di ramo, il commit di un altro — senza che
28
+ * nessuno riapprovasse niente. Misurato dalla corsia della CLI il 17/09/2026 ESEGUENDO il codice
29
+ * su questi stessi due file: dopo lo scambio di `run.js` l'hash ricalcolato era byte per byte
30
+ * identico e `verificaTrustPlugin` rispondeva `true`. Riprodotto qui in
31
+ * `tests/plugin-trust-intero-pacchetto.test.mjs` prima della cura.
32
+ * ⇒ Adesso `hash` è l'impronta di TUTTI i file del pacchetto: la fiducia copre ciò che il plugin
33
+ * esegue, non solo ciò che dichiara.
34
+ */
35
+ import { createHash } from 'node:crypto';
36
+ import { createReadStream, promises as fsp } from 'node:fs';
37
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
38
+
39
+ import { EVENTI_VALIDI } from './hook-registry.mjs';
40
+ import { parseProcessCommand } from './process-policy.mjs';
41
+
42
+ export class PluginRegistryError extends Error {
43
+ constructor(message, code = 'PLUGIN_INVALID') {
44
+ super(message);
45
+ this.name = 'PluginRegistryError';
46
+ this.code = code;
47
+ }
48
+ }
49
+
50
+ const NOME_CARTELLA_PLUGIN = '.harness-ui-plugins';
51
+ const NOME_FILE_MANIFESTO = 'plugin.json';
52
+
53
+ /**
54
+ * ⛔ CLI-REQ-02 — il tetto sul numero di file di UN pacchetto, e fallisce CHIUSO.
55
+ *
56
+ * Senza tetto, un pacchetto che si porta dentro `node_modules` trasformerebbe il controllo della
57
+ * fiducia in uno stallo a ogni avvio di sessione (`agent-service.mjs:700`) e a ogni apertura del
58
+ * pannello. Troncare la camminata invece di rifiutarla riaprirebbe lo stesso buco più in fondo,
59
+ * quindi il tetto RIFIUTA: `PLUGIN_PACKAGE_TOO_LARGE`, mai un'impronta parziale.
60
+ *
61
+ * Il numero è lo stesso della corsia della CLI, perché i due archivi di fiducia devono rifiutare
62
+ * le stesse cose. ⛔ Citazione precisa, perché nel primo giro era approssimativa: la costante si
63
+ * chiama `PLUGIN_PACKAGE_MAX_FILES` e sta in `cli/src/security/project-resource-inventory.ts`,
64
+ * letta al commit `154a7296` con `git show` — NON nel loro documento di consegna, che parla di
65
+ * «10,000 files» e del codice `RESOURCE_PACKAGE_TOO_LARGE` senza nominare la costante.
66
+ * Nel commento di quel file, e solo lì, stanno anche i numeri di prestazione: su Windows 11 e
67
+ * Node 24.18 con cache calda, ~98 ms di camminata più ~370 ms di impronta per 10.000 file a
68
+ * concorrenza 32, e una morte per EMFILE senza il limite di concorrenza.
69
+ * ⛔ Quei numeri sono LORO, non rimisurati qui.
70
+ */
71
+ export const MAX_FILE_PACCHETTO_PLUGIN = 10_000;
72
+
73
+ /** Quanti file si leggono in parallelo. Stesso valore della CLI, per la stessa ragione (EMFILE). */
74
+ const CONCORRENZA_IMPRONTA = 32;
75
+
76
+ /**
77
+ * ⛔ CLI-REQ-02 — la versione dello SCHEMA scritto nell'archivio di fiducia.
78
+ *
79
+ * Serve a distinguere due cose che senza di lei sono indistinguibili, e che vogliono due frasi
80
+ * diverse a schermo: «questo plugin non l'hai mai approvato / il suo contenuto è cambiato» e
81
+ * «questo plugin l'avevi approvato con la regola vecchia, che guardava solo il manifesto».
82
+ * Senza il marcatore, il costo accettato di questa richiesta — ogni approvazione esistente
83
+ * decade — arriverebbe alla persona con l'aspetto esatto di una manomissione.
84
+ */
85
+ export const SCHEMA_TRUST_PLUGIN = 2;
86
+
87
+ /**
88
+ * ⭐ Pura, nessun blocco — un array di stringhe-avviso, vuoto se nulla
89
+ * sembra sospetto. Pattern letterali OVVI, non un tentativo di
90
+ * copertura esaustiva (stesso principio anti-denylist del resto del
91
+ * progetto: questo NON sostituisce il trust, lo accompagna). Un
92
+ * comando che non matcha nessun pattern non è "sicuro" — è solo
93
+ * "niente di ovvio trovato", distinzione dichiarata a chi legge gli
94
+ * avvisi, non solo a chi legge questo commento.
95
+ */
96
+ export function scansionaPatternSospetti(comando) {
97
+ const testo = String(comando ?? '');
98
+ const avvisi = [];
99
+ if (/\brm\s+-rf\s+\/(?:\s|$)/.test(testo) || /\bdel\s+\/[sq]\b/i.test(testo)) {
100
+ avvisi.push('cancellazione ricorsiva di una radice del filesystem');
101
+ }
102
+ if (/curl\s[^|]*\|\s*(sh|bash)\b/.test(testo) || /wget\s[^|]*\|\s*(sh|bash)\b/.test(testo)) {
103
+ avvisi.push('scarica ed esegue uno script remoto in un solo passo (curl/wget | sh)');
104
+ }
105
+ if (/\b[A-Z_]*(API_KEY|SECRET|TOKEN|PASSWORD)\b/.test(testo) && /\b(curl|wget|nc\s|ncat)\b/.test(testo)) {
106
+ avvisi.push('legge una credenziale e la manda in rete nello stesso comando');
107
+ }
108
+ if (/\bnc\s+-[a-z]*e\b|\/dev\/tcp\//.test(testo)) {
109
+ avvisi.push('pattern di reverse shell (nc -e / /dev/tcp)');
110
+ }
111
+ return avvisi;
112
+ }
113
+
114
+ /**
115
+ * ⛔⛔⛔ CLI-REQ-02 — l'elenco dei file di UN pacchetto, ordinato e senza collegamenti.
116
+ *
117
+ * ⛔ Ogni collegamento è RIFIUTATO, mai seguito e mai saltato: sono la stessa falla vista dai due
118
+ * lati. Seguirlo lascerebbe la camminata uscire dal pacchetto e mettere nell'impronta file che il
119
+ * pacchetto non contiene; saltarlo lascerebbe FUORI dall'impronta un file che il plugin può
120
+ * caricare lo stesso. Nessuna delle due si tollera.
121
+ *
122
+ * ⛔ Su Windows conta la GIUNZIONE, non solo il symlink POSIX. MISURATO su questa macchina
123
+ * (Windows 11, Node v24.18.0, 17/09/2026): una giunzione creata con
124
+ * `symlink(bersaglio, percorso, 'junction')` viene vista da `readdir(withFileTypes)` come
125
+ * `isSymbolicLink() === true` e `isDirectory() === false` — libuv riporta entrambi i tipi di punto
126
+ * di re-parse come `UV_DIRENT_LINK` (nodejs/node#12737, nodejs/node#30646, letti il 17/09/2026).
127
+ * ⇒ `isSymbolicLink()` copre le giunzioni, e la sonda lo conferma invece di dedurlo. Nella stessa
128
+ * misura un symlink vero ('dir'/'file') è fallito con EPERM senza privilegi: la giunzione è
129
+ * proprio la forma che un utente qualunque riesce a creare qui.
130
+ *
131
+ * Una voce che non è né file, né cartella, né collegamento (una pipe, un socket) si salta: non è
132
+ * codice che un plugin possa caricare, e non ha un contenuto stabile da imprimere.
133
+ *
134
+ * ⛔ Residuo dichiarato: se `.harness-ui-plugins/` STESSA fosse una giunzione, questa funzione
135
+ * imprimerebbe comunque per intero i pacchetti che ci sono dentro — cambia da dove vengono, non
136
+ * che cosa copre la fiducia. La CLI rifiuta anche quel caso; qui costerebbe una `lstat` in più su
137
+ * una strada che non passa dalle dipendenze iniettabili, e non è ciò che questa richiesta chiede.
138
+ */
139
+ async function elencaFileDelPacchetto(radice, { readdirFn, maxFile }) {
140
+ const percorsi = [];
141
+ async function scendi(cartella) {
142
+ for (const voce of await readdirFn(cartella, { withFileTypes: true })) {
143
+ const percorso = join(cartella, voce.name);
144
+ if (voce.isSymbolicLink()) {
145
+ throw new PluginRegistryError(
146
+ `Il pacchetto del plugin contiene un collegamento (${voce.name}): un collegamento porterebbe la fiducia fuori dal pacchetto`,
147
+ 'PLUGIN_PACKAGE_SYMLINK_UNSUPPORTED',
148
+ );
149
+ }
150
+ if (voce.isDirectory()) { await scendi(percorso); continue; }
151
+ if (!voce.isFile()) continue;
152
+ if (percorsi.length >= maxFile) {
153
+ throw new PluginRegistryError(
154
+ `Il pacchetto del plugin supera ${maxFile} file: la fiducia non può coprirlo`,
155
+ 'PLUGIN_PACKAGE_TOO_LARGE',
156
+ );
157
+ }
158
+ percorsi.push(percorso);
159
+ }
160
+ }
161
+ await scendi(radice);
162
+ /*
163
+ * ⛔ Percorsi RELATIVI alla radice del pacchetto e con `/`, così due copie identiche in due
164
+ * cartelle danno la stessa impronta (è il criterio già provato in
165
+ * `plugin-registry.test.mjs:75-89`) e Windows non ordina diversamente da POSIX — `/` (0x2F) e
166
+ * `\` (0x5C) si ordinano fra loro in modo diverso. ⛔ Confronto per unità di codice, MAI
167
+ * `localeCompare`: quello dipende dalla lingua della macchina, e un'impronta che cambia con la
168
+ * lingua non è un'impronta.
169
+ */
170
+ return percorsi
171
+ .map((percorso) => ({ percorso, relativo: relative(radice, percorso).split(sep).join('/') }))
172
+ .sort((a, b) => (a.relativo < b.relativo ? -1 : a.relativo > b.relativo ? 1 : 0));
173
+ }
174
+
175
+ /**
176
+ * ⛔⛔⛔ A4 del secondo giro — L'IMPRONTA DI UN FILE SI CALCOLA A FLUSSO.
177
+ *
178
+ * La prima versione leggeva ogni file INTERO con `readFile` e teneva tutti i Buffer vivi insieme
179
+ * per poterli ripiegare in ordine. Il revisore l'ha misurato: **RSS da 134 a 786 MB** con 12 file
180
+ * da 64 MB. Non era un caso di laboratorio — un pacchetto che si porta dentro un modello, un
181
+ * archivio o un video ci arriva da solo.
182
+ *
183
+ * ⛔ Ricerca prima di scrivere, 17/09/2026: lo stato dell'arte per l'impronta di file grandi in
184
+ * Node è `createReadStream` + `hash.update(pezzo)` a pezzi, MAI `readFile`, proprio perché la
185
+ * memoria non deve crescere con la dimensione del file (nodejs.org/api/crypto.html; guida
186
+ * «Generating checksum hashes in node.js», wellingguzman.com/notes/node-checksum). ⇒ Qui la
187
+ * memoria è limitata dal PEZZO, non dal file: di ogni file resta solo il suo digest, 32 byte.
188
+ *
189
+ * ⛔ I tetti di byte restano, e servono a un'altra cosa: il TEMPO. Con il flusso la memoria è già
190
+ * al sicuro, ma imprimere gigabyte a ogni avvio di sessione (`agent-service.mjs:701`) e a ogni
191
+ * apertura del pannello resterebbe uno stallo. Falliscono CHIUSI, come il tetto sui file: mai
192
+ * un'impronta parziale.
193
+ */
194
+ export const MAX_BYTE_FILE_PLUGIN = 32 * 1024 * 1024;
195
+ export const MAX_BYTE_PACCHETTO_PLUGIN = 128 * 1024 * 1024;
196
+
197
+ /** L'impronta di UN file, senza mai tenerlo intero in memoria. */
198
+ async function improntaDelFile(percorso, { flussoFn, maxBytePerFile, restanti }) {
199
+ const digest = createHash('sha256');
200
+ let letti = 0;
201
+ const flusso = flussoFn(percorso);
202
+ try {
203
+ for await (const pezzo of flusso) {
204
+ letti += pezzo.length;
205
+ if (letti > maxBytePerFile) {
206
+ throw new PluginRegistryError(
207
+ `Un file del pacchetto supera ${maxBytePerFile} byte: la fiducia non può coprirlo`,
208
+ 'PLUGIN_PACKAGE_FILE_TOO_LARGE',
209
+ );
210
+ }
211
+ if (restanti.scala(pezzo.length) < 0) {
212
+ throw new PluginRegistryError(
213
+ `Il pacchetto del plugin supera ${restanti.tetto} byte in totale: la fiducia non può coprirlo`,
214
+ 'PLUGIN_PACKAGE_BYTES_TOO_LARGE',
215
+ );
216
+ }
217
+ digest.update(pezzo);
218
+ }
219
+ } finally {
220
+ /* ⛔ Un flusso abbandonato a metà tiene aperto un descrittore: si chiude sempre, anche sul rifiuto. */
221
+ flusso.destroy?.();
222
+ }
223
+ return digest.digest('hex');
224
+ }
225
+
226
+ /** Le impronte dei file, al massimo `CONCORRENZA_IMPRONTA` flussi aperti insieme (guardia EMFILE). */
227
+ async function improntePerFile(voci, opzioni) {
228
+ const impronte = new Array(voci.length);
229
+ let cursore = 0;
230
+ await Promise.all(Array.from({ length: Math.min(CONCORRENZA_IMPRONTA, voci.length) }, async () => {
231
+ for (;;) {
232
+ const indice = cursore++;
233
+ if (indice >= voci.length) return;
234
+ impronte[indice] = await improntaDelFile(voci[indice].percorso, opzioni);
235
+ }
236
+ }));
237
+ return impronte;
238
+ }
239
+
240
+ /**
241
+ * ⛔ L'impronta del pacchetto: `sha256` su, per ogni file nell'ordine sopra,
242
+ * `percorsoRelativo + '\0' + sha256(byte) + '\n'`.
243
+ *
244
+ * ⛔⛔ IL NOME ENTRA NELL'IMPRONTA QUANTO IL CONTENUTO, e nel primo giro nessuna prova lo
245
+ * copriva: il revisore ha tolto `voce.relativo` dal digest e **372 prove su 372 sono rimaste
246
+ * verdi**. Senza il nome, rinominare il file che il manifesto esegue — o scambiare fra loro due
247
+ * file del pacchetto — non cambierebbe niente. Adesso c'è una prova che rinomina `run.js` in
248
+ * `run2.js` a contenuto identico e pretende un'impronta diversa.
249
+ *
250
+ * Il prefisso di dominio `talos-plugin-package-v2` fa due cose: dice a chi legge di che schema è
251
+ * quell'esadecimale, e garantisce che un'impronta nuova non possa MAI coincidere con una vecchia
252
+ * (quelle erano `sha256` del solo testo del manifesto). La forma è la stessa che la corsia della
253
+ * CLI usa per ogni risorsa (`fingerprintExecutableResource` in `cli/src/security/project-trust.ts`
254
+ * a `154a7296`, letto lì e non nel loro handoff, prefisso `talos-executable-resource-v1`); lì
255
+ * l'archivio tiene una voce per file, qui una stringa opaca per plugin, perché è la forma che
256
+ * `verificaTrustPlugin`/`fidaPlugin` già portano — la COPERTURA è la stessa, che è ciò che i due
257
+ * archivi dovevano mettere d'accordo.
258
+ *
259
+ * ⛔ Due cose che NON entrano nell'impronta, misurate e dichiarate invece che scoperte dopo:
260
+ * · una cartella VUOTA (la camminata imprime i file, non le cartelle);
261
+ * · un FLUSSO ALTERNATIVO NTFS (`file:flusso`), che `readdir` non enumera.
262
+ * Nessuna delle due è sfruttabile da qui — una cartella vuota non è codice, e l'unico
263
+ * eseguibile consentito è `node`, che non carica un flusso alternativo come modulo — ma il
264
+ * limite si scrive, perché quello che non è scritto non è stato deciso.
265
+ */
266
+ function ripiegaImpronte(voci, impronte) {
267
+ const digest = createHash('sha256').update('talos-plugin-package-v2\0');
268
+ voci.forEach((voce, indice) => {
269
+ digest.update(voce.relativo).update('\0').update(impronte[indice]).update('\n');
270
+ });
271
+ return digest.digest('hex');
272
+ }
273
+
274
+ /**
275
+ * ⭐⭐⭐ L'impronta di UN pacchetto, dal disco. Esportata perché serve DUE volte: al caricamento,
276
+ * e di nuovo al momento dell'USO (A2, vedi `agent-service.mjs`).
277
+ */
278
+ export async function improntaPacchettoPlugin({ cartella, pluginId }, deps = {}) {
279
+ const readdirFn = deps.readdirFn ?? fsp.readdir;
280
+ const maxFile = deps.maxFilePacchetto ?? MAX_FILE_PACCHETTO_PLUGIN;
281
+ const radice = join(cartella, NOME_CARTELLA_PLUGIN, pluginId);
282
+ return improntaDelleVoci(await elencaFileDelPacchetto(radice, { readdirFn, maxFile }), deps);
283
+ }
284
+
285
+ /** La stessa impronta, quando la camminata è già stata fatta (il caricamento la riusa). */
286
+ async function improntaDelleVoci(voci, deps = {}) {
287
+ const flussoFn = deps.flussoFn ?? ((percorso) => createReadStream(percorso));
288
+ const maxBytePerFile = deps.maxBytePerFile ?? MAX_BYTE_FILE_PLUGIN;
289
+ const tetto = deps.maxBytePacchetto ?? MAX_BYTE_PACCHETTO_PLUGIN;
290
+ let rimasti = tetto;
291
+ const restanti = { tetto, scala: (n) => (rimasti -= n) };
292
+ return ripiegaImpronte(voci, await improntePerFile(voci, { flussoFn, maxBytePerFile, restanti }));
293
+ }
294
+
295
+ /*
296
+ * ⛔⛔⛔ A1 del secondo giro — IL FILE CHE IL PLUGIN ESEGUE PUÒ STARE FUORI DAL PACCHETTO.
297
+ *
298
+ * L'impronta sull'intero pacchetto era giusta e INSUFFICIENTE. `eseguiComandoPlugin`
299
+ * (`plugin-session.mjs:89`) esegue con il WORKSPACE come cartella di lavoro, quindi un manifesto
300
+ * che dichiara `node strumenti/aiuto.js` fa girare un file che nel pacchetto non c'è e quindi
301
+ * nell'impronta nemmeno. RIPRODOTTO sul codice del primo giro, eseguendolo:
302
+ * impronta identica dopo lo scambio: true · ancora fidato: true
303
+ * prima: "aiuto-BUONO, approvato" · dopo: "aiuto-CATTIVO, mai approvato"
304
+ * Vale uguale per gli HOOK, che passano da `hook-registry.mjs` con lo stesso `cwd`.
305
+ * ⛔ La corsia della CLI l'aveva scritto nel suo handoff, sotto «Risk of the change», e io non
306
+ * l'ho riportato: il commit del primo giro affermava il contrario. Questa è la correzione.
307
+ *
308
+ * ⛔ Ricerca prima di scrivere, 17/09/2026: il modo giusto di legare un punto d'ingresso alla sua
309
+ * cartella è canonicalizzare e poi verificare il CONTENIMENTO con `path.relative` sul percorso
310
+ * normalizzato, non con un confronto di prefisso (che confonde `/pkg` con `/pkg-cattivo`) né con
311
+ * un controllo di sola estensione (che `../` scavalca) — OpenHands/software-agent-sdk #5101
312
+ * «enforce package path containment in plugin formats», WindWang2/exp-rs #756 «Manifest entrypoint
313
+ * escapes the plugin directory». Le stesse fonti dicono di fare il controllo DUE volte, alla
314
+ * validazione e all'uso: la seconda metà è A2.
315
+ *
316
+ * ⇒ Qui: ogni argomento del comando che ha forma di percorso si risolve — prima contro il
317
+ * PACCHETTO, poi contro il workspace — e deve cadere DENTRO il pacchetto. Il comando si
318
+ * riscrive con quel percorso assoluto, così la cartella di lavoro non decide più QUALE file
319
+ * gira. Un comando che nomina un file fuori è RIFIUTATO al caricamento.
320
+ *
321
+ * ⛔ Cosa resta fuori PER NATURA, e non si addolcisce: un `require('../../x.js')` scritto DENTRO
322
+ * un file del pacchetto carica un file che l'impronta non copre, e nessun controllo sugli
323
+ * argomenti lo vede. Lo stesso vale per un interprete che carica altro per conto suo
324
+ * (`NODE_OPTIONS`, un file di configurazione fuori, un modulo nativo). ⇒ La fiducia copre CIÒ
325
+ * CHE IL MANIFESTO NOMINA e il contenuto del pacchetto, non la chiusura transitiva di ciò che
326
+ * quel codice decide di caricare a runtime. Chiuderla vorrebbe dire eseguire in un contenitore,
327
+ * che è un'altra decisione e non questa riga.
328
+ */
329
+ const ESTENSIONI_CON_FORMA_DI_FILE = new Set([
330
+ '.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.json', '.py', '.rb', '.pl',
331
+ '.sh', '.bash', '.bat', '.cmd', '.ps1', '.jar', '.exe', '.wasm',
332
+ ]);
333
+
334
+
335
+ /** Ha forma di percorso: contiene un separatore, oppure finisce con un'estensione da file. */
336
+ function haFormaDiPercorso(argomento) {
337
+ if (argomento.includes('/') || argomento.includes('\\')) return true;
338
+ const punto = argomento.lastIndexOf('.');
339
+ return punto > 0 && ESTENSIONI_CON_FORMA_DI_FILE.has(argomento.slice(punto).toLowerCase());
340
+ }
341
+
342
+ /**
343
+ * Il percorso assoluto dentro il pacchetto, o `null` se esce.
344
+ *
345
+ * ⛔ Contenimento con `relative`, non con un confronto di prefisso: `startsWith(radice)` direbbe
346
+ * di sì anche per una cartella sorella che comincia allo stesso modo.
347
+ *
348
+ * ⛔⛔ E il contenimento DA SOLO non basta: scrivendo la prova ho misurato che
349
+ * `node strumenti/aiuto.js` passava, perché risolto contro il pacchetto dà
350
+ * `<pacchetto>/strumenti/aiuto.js`, che è «dentro» — solo che lì non c'è niente, e a valle
351
+ * sarebbe stato il workspace a fornire il file vero. ⇒ Il percorso deve essere UNO DEI FILE
352
+ * CHE L'IMPRONTA HA APPENA COPERTO: `fileDelPacchetto` è l'insieme della camminata. Così
353
+ * «contenuto» e «coperto dalla fiducia» diventano la stessa cosa invece di due cose che si
354
+ * somigliano.
355
+ *
356
+ * Due basi, in ordine: prima il PACCHETTO (la forma corta, `run.js`), poi il workspace (la forma
357
+ * lunga che i manifesti usano oggi, `.harness-ui-plugins/demo/run.js`). Vince la prima che cade
358
+ * su un file davvero presente nel pacchetto.
359
+ */
360
+ function dentroIlPacchetto(argomento, { radicePacchetto, radiceWorkspace, fileDelPacchetto }) {
361
+ for (const base of [radicePacchetto, radiceWorkspace]) {
362
+ const assoluto = resolve(base, argomento);
363
+ const rel = relative(radicePacchetto, assoluto);
364
+ if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) continue;
365
+ if (fileDelPacchetto.has(rel.split(sep).join('/'))) return assoluto;
366
+ }
367
+ return null;
368
+ }
369
+
370
+ /**
371
+ * Riscrive il comando dichiarato in modo che ogni percorso sia ASSOLUTO e dentro il pacchetto.
372
+ * Lancia un `PluginRegistryError` con una frase umana se qualcosa esce, o se il comando non si può
373
+ * riscrivere senza cambiarne il significato.
374
+ */
375
+ /*
376
+ * ⛔⛔⛔⛔ A-1, TERZO TENTATIVO (17/09/2026) — LA DENYLIST NON SI CHIUDE MAI, E SI CAMBIA FORMA.
377
+ *
378
+ * Il giro scorso avevo messo un elenco di opzioni VIETATE. Il terzo controllo l'ha bucato in sei
379
+ * modi, tutti misurati sul codice di allora:
380
+ * node -pe "require(process.cwd()+String.fromCharCode(47,…))" PASSAVA (forma UNITA `-pe`)
381
+ * node --run build PASSAVA (esegue una riga di package.json che il cancello non legge)
382
+ * node - PASSAVA (il programma legge lo script da stdin)
383
+ * node --input-type=module -PASSAVA
384
+ * node --inspect-brk run.js PASSAVA
385
+ * deno eval "1" PASSAVA (un SOTTOCOMANDO non comincia per `-`)
386
+ * node PASSAVA (da solo: REPL)
387
+ * ⇒ Ogni buco tappato ne lascia uno che non ho pensato. Una denylist sulle opzioni di un
388
+ * interprete è una gara che si perde per costruzione: gli interpreti aggiungono modi di
389
+ * eseguire, noi no.
390
+ *
391
+ * ⇒ DECISIONE (coordinatore, 17/09): si passa a una GRAMMATICA AMMESSA, stretta e dichiarata.
392
+ * Tutto ciò che non è esattamente una di queste forme è RIFIUTATO:
393
+ *
394
+ * echo <qualunque cosa>
395
+ * node <file coperto dall'impronta> [argomenti dello script…]
396
+ * <file coperto dall'impronta> [argomenti dello script…]
397
+ *
398
+ * ⛔ NESSUNA opzione dell'interprete è ammessa, nemmeno quelle innocue come `--inspect-brk`:
399
+ * qualunque pezzo che cominci con `-` (o con `/`, che su Windows è una opzione) PRIMA del file
400
+ * fa rifiutare. Il giorno in cui una servisse davvero si aggiunge PER NOME a un elenco
401
+ * ammesso, con la sua prova — cioè si paga una decisione, invece di lasciare una porta.
402
+ * ⛔ Gli argomenti DOPO il file sono argomenti dello SCRIPT, non di Node: restano ammessi, con
403
+ * la regola di sempre (se hanno forma di percorso devono cadere su un file coperto).
404
+ * ⛔ Un programma che non è `node`/`echo` e non è un file coperto è RIFIUTATO anche se un domani
405
+ * `allowedExecutables` di `process-policy` si allargasse: questo cancello non deve dipendere
406
+ * da quel parametro, che è iniettabile.
407
+ *
408
+ * ⛔ DOVE NON ARRIVA, detto invece che lasciato credere: questo è il cancello del CARICAMENTO dei
409
+ * plugin. `eseguiComandoPlugin` resta generico (le sue prove usano `node -e` apposta) e gli hook
410
+ * STANDALONE di `.harness-ui-hooks.json` non passano di qui: per loro `node -e` è ancora
411
+ * ammesso. È un'altra riga, non questa.
412
+ */
413
+ /**
414
+ * ⛔⛔⛔ A-3, SECONDA STESURA (17/09/2026) — UNA SOLA GRAMMATICA PER L'ID, USATA DAI DUE LATI.
415
+ *
416
+ * La prima cura rifiutava al caricamento solo gli id con `__`, ma chi RILEGGE l'id dal nome
417
+ * esposto pretendeva di più: la sua espressione escludeva anche gli id che cominciano con `_`.
418
+ * ⇒ Un plugin `_sano` si caricava, veniva offerto come `plugin___sano__ping`, e ogni chiamata
419
+ * rispondeva «non riconosco a quale plugin appartiene». Caricato e inutilizzabile, per sempre,
420
+ * senza che nessuno lo dicesse. MISURATO dal terzo controllo e riprodotto qui prima di curare.
421
+ * ⛔ È lo stesso difetto di sempre in forma nuova: due verità sulla stessa cosa, in due posti,
422
+ * che divergono. Adesso la verità è UNA e sta qui.
423
+ *
424
+ * Un id è utilizzabile se non è vuoto, non comincia per `_`, non contiene `__` e non è un
425
+ * percorso. `__` renderebbe ambiguo il taglio del nome esposto; `_` iniziale creerebbe `plugin___`,
426
+ * cioè tre trattini bassi di fila, dove il taglio non sa più dove finisce il prefisso.
427
+ */
428
+ export function idPluginValido(pluginId) {
429
+ return typeof pluginId === 'string'
430
+ && pluginId.length > 0
431
+ && !pluginId.startsWith('_')
432
+ && !pluginId.includes('__')
433
+ && !/[\\/]|\.\./.test(pluginId);
434
+ }
435
+
436
+ const PROGRAMMI_NUDI_AMMESSI = new Set(['node', 'node.exe', 'echo', 'echo.exe']);
437
+
438
+ /** Un pezzo che il sistema leggerebbe come OPZIONE, non come nome di file. */
439
+ function haFormaDiOpzione(pezzo) {
440
+ return pezzo === '-' || pezzo.startsWith('-') || /^\/[A-Za-z?]$/.test(pezzo);
441
+ }
442
+
443
+ export function comandoDentroIlPacchetto(comando, { radicePacchetto, radiceWorkspace, fileDelPacchetto, dove }) {
444
+ let pezzi;
445
+ try {
446
+ pezzi = parseProcessCommand(comando);
447
+ } catch (errore) {
448
+ throw new PluginRegistryError(
449
+ `${dove}: il comando non è eseguibile così com'è scritto (${errore.code ?? 'non valido'})`,
450
+ 'PLUGIN_COMANDO_NON_LEGGIBILE',
451
+ );
452
+ }
453
+ if (pezzi.length === 0) {
454
+ throw new PluginRegistryError(`${dove}: il comando è vuoto`, 'PLUGIN_COMANDO_NON_LEGGIBILE');
455
+ }
456
+
457
+ const rifiutaCodice = (pezzo) => {
458
+ throw new PluginRegistryError(
459
+ `${dove}: «${pezzo}» non è un file del plugin — il comando può eseguire solo un file della sua cartella`,
460
+ 'PLUGIN_COMANDO_ESEGUE_CODICE',
461
+ );
462
+ };
463
+
464
+ const citaSeServe = (pezzo) => {
465
+ /*
466
+ * ⛔ A-5: si ri-cita anche senza spazi. `parseProcessCommand` RIFIUTA `& | ; < > ( )` fuori
467
+ * dalle virgolette, quindi un pezzo citato nel manifesto ma senza spazi — `"a&b"` — tornava
468
+ * fuori nudo e poi veniva rifiutato all'esecuzione: il plugin si caricava e moriva dopo.
469
+ * ⛔ Un pezzo che va citato ma contiene `"` o `\` non si può riemettere senza cambiarne il
470
+ * significato (`\` è un escape DENTRO le virgolette): si RIFIUTA invece di indovinare.
471
+ */
472
+ const daCitare = /[\s&|;<>()]/u.test(pezzo);
473
+ if (!daCitare) return pezzo;
474
+ if (/["\\]/u.test(pezzo)) {
475
+ throw new PluginRegistryError(
476
+ `${dove}: il comando contiene un pezzo che non si può riscrivere senza cambiarne il significato`,
477
+ 'PLUGIN_COMANDO_NON_LEGGIBILE',
478
+ );
479
+ }
480
+ return `"${pezzo}"`;
481
+ };
482
+
483
+ const dentro = (pezzo, comeProgramma) => {
484
+ const assoluto = dentroIlPacchetto(pezzo, { radicePacchetto, radiceWorkspace, fileDelPacchetto });
485
+ if (assoluto === null) {
486
+ /*
487
+ * ⛔ Due rifiuti diversi per due cose diverse, e la differenza si vede nella frase: un pezzo
488
+ * che HA forma di file ma sta fuori è «fuori dal pacchetto»; un pezzo che non somiglia
489
+ * nemmeno a un file (un'opzione, un programma che non conosciamo) è «esegue codice».
490
+ * Confonderli darebbe alla persona una frase che non spiega cosa cambiare.
491
+ */
492
+ if (comeProgramma && !haFormaDiPercorso(pezzo)) rifiutaCodice(pezzo);
493
+ throw new PluginRegistryError(
494
+ `${dove}: il comando nomina «${pezzo}», che non è un file della cartella del plugin`,
495
+ 'PLUGIN_COMANDO_FUORI_DAL_PACCHETTO',
496
+ );
497
+ }
498
+ /*
499
+ * ⛔ Sempre con `/`, mai con `\`: un backslash dentro le virgolette sarebbe letto come escape
500
+ * da `parseProcessCommand`. Node accetta le barre in avanti anche su Windows.
501
+ */
502
+ const conBarre = assoluto.split(sep).join('/');
503
+ if (conBarre.includes('"')) {
504
+ throw new PluginRegistryError(
505
+ `${dove}: il percorso del file contiene una virgoletta e non si può citare`,
506
+ 'PLUGIN_COMANDO_NON_LEGGIBILE',
507
+ );
508
+ }
509
+ return citaSeServe(conBarre);
510
+ };
511
+
512
+ /** Gli argomenti DELLO SCRIPT: liberi, tranne che un percorso deve cadere su un file coperto. */
513
+ const argomentiDelloScript = (resto) => resto.map((p) => (haFormaDiPercorso(p) ? dentro(p, false) : citaSeServe(p)));
514
+
515
+ const programma = pezzi[0];
516
+
517
+ /* 1 · `echo …` — resta com'era: non esegue un file, e i suoi argomenti seguono la regola di sempre. */
518
+ if (programma === 'echo' || programma === 'echo.exe') {
519
+ return [citaSeServe(programma), ...argomentiDelloScript(pezzi.slice(1))].join(' ');
520
+ }
521
+
522
+ /* 2 · `node <file coperto> …` — nessuna opzione dell'interprete, nemmeno una. */
523
+ if (PROGRAMMI_NUDI_AMMESSI.has(programma)) {
524
+ const script = pezzi[1];
525
+ if (script === undefined) rifiutaCodice(programma);
526
+ if (haFormaDiOpzione(script)) rifiutaCodice(script);
527
+ return [citaSeServe(programma), dentro(script, true), ...argomentiDelloScript(pezzi.slice(2))].join(' ');
528
+ }
529
+
530
+ /* 3 · un FILE COPERTO usato come programma (`./run.js`): i suoi argomenti sono suoi. */
531
+ if (haFormaDiOpzione(programma)) rifiutaCodice(programma);
532
+ return [dentro(programma, true), ...argomentiDelloScript(pezzi.slice(1))].join(' ');
533
+ }
534
+
535
+ /** La frase umana di un guasto del pacchetto: mai un codice, mai un percorso di sistema. */
536
+ const FRASI_GUASTO_PLUGIN = Object.freeze({
537
+ PLUGIN_COMANDO_FUORI_DAL_PACCHETTO: 'Questo plugin vuole eseguire un file che sta fuori dalla sua cartella. Finché è così non può essere approvato, perché il controllo non potrebbe accorgersi se quel file cambiasse.',
538
+ PLUGIN_COMANDO_ESEGUE_CODICE: 'Questo plugin vuole eseguire istruzioni scritte dentro la sua scheda, invece di un file della sua cartella. Finché è così non può essere approvato, perché il controllo non coprirebbe quello che fa davvero.',
539
+ PLUGIN_ID_AMBIGUO: 'Il nome della cartella di questo plugin contiene «__», e con quel nome non si distingue più a quale plugin appartiene un suo strumento. Rinominala e riprova.',
540
+ PLUGIN_COMANDO_NON_LEGGIBILE: 'Non riesco a capire con certezza quale file eseguirebbe questo plugin, quindi non lo offro.',
541
+ PLUGIN_PACKAGE_SYMLINK_UNSUPPORTED: 'Questo plugin contiene un collegamento a un\'altra cartella. Finché c\'è non può essere approvato, perché il controllo non coprirebbe ciò che sta dall\'altra parte.',
542
+ PLUGIN_PACKAGE_TOO_LARGE: 'Questo plugin contiene troppi file perché il controllo possa coprirli tutti.',
543
+ PLUGIN_PACKAGE_FILE_TOO_LARGE: 'Questo plugin contiene un file troppo grande perché il controllo possa coprirlo.',
544
+ PLUGIN_PACKAGE_BYTES_TOO_LARGE: 'Questo plugin è troppo grande perché il controllo possa coprirlo tutto.',
545
+ PLUGIN_READ_FAILED: 'Non riesco a leggere i file di questo plugin.',
546
+ });
547
+
548
+ export function frasePerGuastoPlugin(codice) {
549
+ return FRASI_GUASTO_PLUGIN[codice] ?? 'Questo plugin non si può offrire in questa sessione.';
550
+ }
551
+
552
+ /**
553
+ * Legge `<cartella>/.harness-ui-plugins/*\/plugin.json`. Un progetto
554
+ * senza plugin dichiarati è uno stato valido — mai un errore, torna
555
+ * `{plugin: [], falliti: []}`.
556
+ *
557
+ * ⛔⛔ Due classi di guasto, e non è una sfumatura.
558
+ * · Un MANIFESTO malformato ferma ancora l'intero caricamento, come dal primo giorno: è il
559
+ * contratto documentato («stesso principio di caricaSkill») e le prove lo fissano.
560
+ * · Un guasto del PACCHETTO — un collegamento, troppi file, troppi byte, un comando che esce
561
+ * dalla cartella — è guasto di QUEL plugin soltanto, e finisce in `falliti` con la sua frase.
562
+ *
563
+ * ⛔⛔⛔ A3 del secondo giro: prima non era così, e il revisore l'ha misurato. Con una giunzione
564
+ * dentro `demo/` e un `sano/` perfettamente valido, il risultato era **0 tool offerti e
565
+ * `falliti: []`** — tutti i plugin spenti, in silenzio, per colpa di uno. Il guasto di un
566
+ * pacchetto non è un motivo per disarmare gli altri, e soprattutto non è un motivo per non
567
+ * dirlo: chi ha approvato un plugin e non lo vede più deve poter sapere perché.
568
+ */
569
+ export async function caricaPlugin({ cartella }, deps = {}) {
570
+ const readdirFn = deps.readdirFn ?? fsp.readdir;
571
+ const readFileFn = deps.readFileFn ?? fsp.readFile;
572
+ const maxFile = deps.maxFilePacchetto ?? MAX_FILE_PACCHETTO_PLUGIN;
573
+ const maxBytePerFile = deps.maxBytePerFile ?? MAX_BYTE_FILE_PLUGIN;
574
+ const maxBytePacchetto = deps.maxBytePacchetto ?? MAX_BYTE_PACCHETTO_PLUGIN;
575
+ const flussoFn = deps.flussoFn ?? ((percorso) => createReadStream(percorso));
576
+ const cartellaPlugin = join(cartella, NOME_CARTELLA_PLUGIN);
577
+ let voci;
578
+ try {
579
+ voci = await readdirFn(cartellaPlugin, { withFileTypes: true });
580
+ } catch (errore) {
581
+ if (errore?.code === 'ENOENT') return { plugin: [], falliti: [] };
582
+ throw new PluginRegistryError(`Impossibile leggere ${NOME_CARTELLA_PLUGIN}: ${errore.message}`, 'PLUGIN_READ_FAILED');
583
+ }
584
+ const plugin = [];
585
+ const falliti = [];
586
+ for (const voce of voci) {
587
+ if (!voce.isDirectory()) continue;
588
+ const pluginId = voce.name;
589
+ const percorso = join(cartellaPlugin, pluginId, NOME_FILE_MANIFESTO);
590
+ let testo;
591
+ try {
592
+ testo = await readFileFn(percorso, 'utf8');
593
+ } catch (errore) {
594
+ if (errore?.code === 'ENOENT') continue; // una sottocartella senza plugin.json non è un plugin
595
+ throw new PluginRegistryError(`Impossibile leggere ${pluginId}/${NOME_FILE_MANIFESTO}: ${errore.message}`, 'PLUGIN_READ_FAILED');
596
+ }
597
+ let dati;
598
+ try {
599
+ dati = JSON.parse(testo);
600
+ } catch {
601
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO} non è un JSON valido`, 'PLUGIN_MALFORMED');
602
+ }
603
+ if (typeof dati?.nome !== 'string' || dati.nome.length === 0) {
604
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO} manca di "nome" (stringa non vuota)`, 'PLUGIN_MALFORMED');
605
+ }
606
+ if (typeof dati?.descrizione !== 'string' || dati.descrizione.length === 0) {
607
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO} manca di "descrizione" (stringa non vuota)`, 'PLUGIN_MALFORMED');
608
+ }
609
+ const hooks = dati.hooks ?? [];
610
+ if (!Array.isArray(hooks)) {
611
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO} ha "hooks" non valido — atteso un array (anche vuoto)`, 'PLUGIN_MALFORMED');
612
+ }
613
+ hooks.forEach((h, indice) => {
614
+ if (typeof h?.id !== 'string' || h.id.length === 0) {
615
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO}: hooks[${indice}] manca di "id"`, 'PLUGIN_MALFORMED');
616
+ }
617
+ if (!Array.isArray(h.eventi) || h.eventi.length === 0 || !h.eventi.every((e) => EVENTI_VALIDI.has(e))) {
618
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO}: hooks[${indice}] ("${h.id}") ha "eventi" non valido — atteso un array non vuoto fra ${[...EVENTI_VALIDI].join('/')}`, 'PLUGIN_MALFORMED');
619
+ }
620
+ if (typeof h.comando !== 'string' || h.comando.length === 0) {
621
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO}: hooks[${indice}] ("${h.id}") manca di "comando"`, 'PLUGIN_MALFORMED');
622
+ }
623
+ });
624
+ const tools = dati.tools ?? [];
625
+ if (!Array.isArray(tools)) {
626
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO} ha "tools" non valido — atteso un array (anche vuoto)`, 'PLUGIN_MALFORMED');
627
+ }
628
+ tools.forEach((t, indice) => {
629
+ if (typeof t?.nome !== 'string' || t.nome.length === 0) {
630
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO}: tools[${indice}] manca di "nome"`, 'PLUGIN_MALFORMED');
631
+ }
632
+ if (typeof t.descrizione !== 'string' || t.descrizione.length === 0) {
633
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO}: tools[${indice}] ("${t.nome}") manca di "descrizione"`, 'PLUGIN_MALFORMED');
634
+ }
635
+ if (typeof t.comando !== 'string' || t.comando.length === 0) {
636
+ throw new PluginRegistryError(`${pluginId}/${NOME_FILE_MANIFESTO}: tools[${indice}] ("${t.nome}") manca di "comando"`, 'PLUGIN_MALFORMED');
637
+ }
638
+ });
639
+ /*
640
+ * ⭐ CLI-REQ-02: l'impronta è su TUTTO il pacchetto, manifesto compreso — un carattere
641
+ * cambiato in un file qualunque invalida la fiducia, nessuna eccezione. Il manifesto non ha
642
+ * bisogno di un trattamento a parte: è uno dei file della camminata.
643
+ * ⛔ A1: e prima ancora si controlla che il comando NON esca dal pacchetto. Il controllo va
644
+ * fatto qui e non a valle, perché a valle si esegue e basta.
645
+ */
646
+ const radicePacchetto = join(cartellaPlugin, pluginId);
647
+ try {
648
+ /*
649
+ * ⛔⛔⛔ A-3 del terzo giro (17/09/2026) — UN ID CON `__` DENTRO RENDE AMBIGUO IL NOME ESPOSTO.
650
+ *
651
+ * Il nome che il modello vede è `plugin__<id>__<tool>` (`plugin-session.mjs:52`), e chi deve
652
+ * risalire all'id lo fa tagliando su `__` (`agent-service.mjs`, la riverifica di A2). Con un
653
+ * id che contiene `__` quel taglio dà la cosa SBAGLIATA: il revisore l'ha misurato con un
654
+ * plugin `a__b` accanto a un gemello `a` già fidato — la riverifica controllava `a` ed
655
+ * eseguiva il tool di `a__b`, scambiato.
656
+ * ⇒ La grammatica del nome esposto non può rappresentare un id così senza ambiguità, quindi
657
+ * l'id non può contenerlo. Rifiutato al CARICAMENTO con una frase, mai «smette di
658
+ * funzionare» in silenzio — che era l'altra metà della bocciatura.
659
+ * ⛔ Il nome del TOOL può contenere `__` senza danno: il taglio cerca il SECONDO segmento, e
660
+ * quello resta l'id qualunque cosa venga dopo.
661
+ */
662
+ if (!idPluginValido(pluginId)) {
663
+ throw new PluginRegistryError(
664
+ `${pluginId}: il nome della cartella del plugin non è utilizzabile come identità`,
665
+ 'PLUGIN_ID_AMBIGUO',
666
+ );
667
+ }
668
+ /* ⛔ La camminata si fa UNA volta: è la stessa che copre l'impronta e che autorizza i comandi. */
669
+ const fileDelPacchetto = await elencaFileDelPacchetto(radicePacchetto, { readdirFn, maxFile });
670
+ const nomiDelPacchetto = new Set(fileDelPacchetto.map((v) => v.relativo));
671
+ const contieni = (comandoDichiarato, dove) => comandoDentroIlPacchetto(comandoDichiarato, {
672
+ radicePacchetto, radiceWorkspace: cartella, fileDelPacchetto: nomiDelPacchetto, dove,
673
+ });
674
+ const toolsContenuti = tools.map((t) => ({ ...t, comando: contieni(t.comando, `tool "${t.nome}"`) }));
675
+ const hooksContenuti = hooks.map((h) => ({ ...h, comando: contieni(h.comando, `hook "${h.id}"`) }));
676
+ const hash = await improntaDelleVoci(fileDelPacchetto, { flussoFn, maxBytePerFile, maxBytePacchetto });
677
+ plugin.push({
678
+ id: pluginId, nome: dati.nome, descrizione: dati.descrizione,
679
+ hooks: hooksContenuti, tools: toolsContenuti, hash,
680
+ });
681
+ } catch (errore) {
682
+ /* ⛔ Solo i guasti DEL PACCHETTO si raccolgono: qualunque altra cosa risale, come prima. */
683
+ if (!(errore instanceof PluginRegistryError)) throw errore;
684
+ falliti.push({
685
+ pluginId,
686
+ codice: errore.code,
687
+ messaggio: errore.message,
688
+ frase: frasePerGuastoPlugin(errore.code),
689
+ });
690
+ }
691
+ }
692
+ plugin.sort((a, b) => a.id.localeCompare(b.id));
693
+ falliti.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
694
+ return { plugin, falliti };
695
+ }
696
+
697
+ function percorsoTrust(cartellaTrust, pluginId) {
698
+ if (typeof pluginId !== 'string' || pluginId.length === 0 || /[\\/]|\.\./.test(pluginId)) {
699
+ throw new PluginRegistryError('pluginId non valido — un nome, non un percorso', 'PLUGIN_ID_INVALID');
700
+ }
701
+ return join(cartellaTrust, `${pluginId}.json`);
702
+ }
703
+
704
+ /*
705
+ * ⛔⛔⛔ CLI-REQ-02 — LE FRASI. Owner, 17/09/2026: il costo di questa richiesta (ogni plugin già
706
+ * approvato va riapprovato) deve arrivare alla persona come una RICHIESTA con una frase umana,
707
+ * mai come un errore e mai con un nome tecnico. Le frasi stanno qui, in un posto solo, accanto
708
+ * allo stato che le determina: sono la traduzione del `motivo`, e chi disegna non deve
709
+ * reinventarle. ⛔ Il `motivo` NON si mostra a schermo: è il nome tecnico, la frase è la sua
710
+ * faccia umana.
711
+ */
712
+ const FRASI_TRUST_PLUGIN = Object.freeze({
713
+ 'mai-approvato': null, // niente da spiegare: è il caso normale, la UI offre «Fida» e basta.
714
+ 'regola-precedente': 'Questo plugin era stato approvato quando il controllo guardava solo la sua scheda. Adesso copre tutti i suoi file: approvalo di nuovo.',
715
+ 'contenuto-cambiato': 'Il contenuto di questo plugin è cambiato da quando l\'hai approvato.',
716
+ fidato: null,
717
+ });
718
+
719
+ /**
720
+ * ⭐⭐⭐ CLI-REQ-02 — lo stato della fiducia di UN plugin, con il perché.
721
+ *
722
+ * Tre «no» che sembravano uno solo: mai approvato, approvato con la regola precedente, contenuto
723
+ * cambiato dopo l'approvazione. Il secondo e il terzo hanno lo stesso aspetto — un plugin che
724
+ * smette di funzionare — ma vogliono due frasi diverse: uno è un costo nostro, l'altro è una
725
+ * manomissione possibile. Distinguerli costa il marcatore `schema` scritto da `fidaPlugin`.
726
+ *
727
+ * ⛔ Un archivio senza `schema` non è MAI fidato, qualunque cosa dica il suo `hash`: fallisce
728
+ * chiuso. (Non potrebbe combaciare comunque — le impronte v2 hanno un prefisso di dominio — ma
729
+ * la regola è scritta, non lasciata a un corollario.)
730
+ * ⛔ Non lancia mai: un archivio assente o illeggibile è «non fidato», esattamente come prima.
731
+ *
732
+ * @returns {Promise<{fidato:boolean, motivo:'fidato'|'mai-approvato'|'regola-precedente'|'contenuto-cambiato', frase:string|null}>}
733
+ */
734
+ export async function statoTrustPlugin({ cartellaTrust, pluginId, hash }, deps = {}) {
735
+ const readFileFn = deps.readFileFn ?? fsp.readFile;
736
+ const esito = (motivo) => ({ fidato: motivo === 'fidato', motivo, frase: FRASI_TRUST_PLUGIN[motivo] });
737
+ let testo;
738
+ try {
739
+ testo = await readFileFn(percorsoTrust(cartellaTrust, pluginId), 'utf8');
740
+ } catch {
741
+ return esito('mai-approvato');
742
+ }
743
+ let dati;
744
+ try {
745
+ dati = JSON.parse(testo);
746
+ } catch {
747
+ return esito('mai-approvato'); // un archivio illeggibile non autorizza e non accusa nessuno
748
+ }
749
+ if (dati?.schema !== SCHEMA_TRUST_PLUGIN) return esito('regola-precedente');
750
+ return esito(dati.hash === hash ? 'fidato' : 'contenuto-cambiato');
751
+ }
752
+
753
+ /**
754
+ * Stesso comportamento esatto di verificaTrustMcp/verificaTrust — assente = non fidato, mai un
755
+ * errore. ⛔ CLI-REQ-02: resta un BOOLEANO e resta la porta che i cancelli usano
756
+ * (`plugin-session.mjs:135`, `session-registry.mjs:4965`). Cambiarne il tipo in un oggetto
757
+ * avrebbe reso fidato TUTTO, perché un oggetto è sempre vero e quei due chiamanti scrivono
758
+ * `if (!fidato)`. Una sola verità: la calcola `statoTrustPlugin`, questa la restringe.
759
+ */
760
+ export async function verificaTrustPlugin({ cartellaTrust, pluginId, hash }, deps = {}) {
761
+ return (await statoTrustPlugin({ cartellaTrust, pluginId, hash }, deps)).fidato;
762
+ }
763
+
764
+ /**
765
+ * Stesso comportamento esatto di fidaServerMcp/fidaHook — chiamata SOLO da un'azione owner esplicita.
766
+ * ⛔ CLI-REQ-02: scrive anche `schema`, così la prossima lettura sa con quale regola è stata data
767
+ * questa fiducia. Senza, ogni approvazione fatta prima di oggi arriverebbe alla persona con
768
+ * l'aspetto esatto di una manomissione.
769
+ */
770
+ export async function fidaPlugin({ cartellaTrust, pluginId, hash }, deps = {}) {
771
+ const mkdirFn = deps.mkdirFn ?? fsp.mkdir;
772
+ const writeFileFn = deps.writeFileFn ?? fsp.writeFile;
773
+ await mkdirFn(cartellaTrust, { recursive: true });
774
+ await writeFileFn(
775
+ percorsoTrust(cartellaTrust, pluginId),
776
+ JSON.stringify({ schema: SCHEMA_TRUST_PLUGIN, hash, fidatoIl: new Date().toISOString() }),
777
+ 'utf8',
778
+ );
779
+ return { fidato: true };
780
+ }