synabun 2.0.10 → 2.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 (517) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +17 -8
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +1 -1
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +228 -0
  24. package/lib/virustotal.mjs +192 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  42. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  44. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  45. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  46. package/mcp-server/dist/services/neural-interface.js +67 -11
  47. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  48. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  49. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  50. package/mcp-server/dist/services/server-instructions.js +20 -62
  51. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  52. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  53. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  54. package/mcp-server/dist/services/tool-catalog.js +339 -0
  55. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  56. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  60. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  61. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  62. package/mcp-server/dist/services/tool-list.js +334 -0
  63. package/mcp-server/dist/services/tool-list.js.map +1 -0
  64. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  66. package/mcp-server/dist/services/tool-runtime.js +400 -0
  67. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  68. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  70. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  71. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  72. package/mcp-server/dist/tools/agents.d.ts +10 -10
  73. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  74. package/mcp-server/dist/tools/agents.js +22 -13
  75. package/mcp-server/dist/tools/agents.js.map +1 -1
  76. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  77. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  78. package/mcp-server/dist/tools/image-tools.js +1 -1
  79. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  80. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  81. package/mcp-server/dist/tools/skill-tools.js +639 -0
  82. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  83. package/mcp-server/dist/tools/skill.d.ts +7 -0
  84. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  85. package/mcp-server/dist/tools/skill.js +11 -0
  86. package/mcp-server/dist/tools/skill.js.map +1 -0
  87. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  89. package/mcp-server/dist/tools/tool-groups.js +147 -0
  90. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  91. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  93. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  94. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  97. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  98. package/mcp-server/package-lock.json +2 -2
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/src/http.ts +75 -12
  101. package/mcp-server/src/index.ts +147 -154
  102. package/mcp-server/src/preload.ts +12 -12
  103. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  104. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  105. package/mcp-server/src/services/neural-interface.ts +79 -20
  106. package/mcp-server/src/services/server-instructions.ts +27 -63
  107. package/mcp-server/src/services/tool-catalog.ts +403 -0
  108. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  109. package/mcp-server/src/services/tool-list.ts +410 -0
  110. package/mcp-server/src/services/tool-runtime.ts +457 -0
  111. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  112. package/mcp-server/src/tools/agents.ts +22 -14
  113. package/mcp-server/src/tools/image-tools.ts +1 -1
  114. package/mcp-server/src/tools/skill-tools.ts +589 -0
  115. package/mcp-server/src/tools/skill.ts +13 -0
  116. package/mcp-server/src/tools/tool-groups.ts +164 -0
  117. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  118. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  119. package/neural-interface/i18n/en.json +3463 -200
  120. package/neural-interface/i18n/pt-BR.json +3411 -151
  121. package/neural-interface/lib/assistant-api.js +127 -28
  122. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  123. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  124. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  125. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  126. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  127. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  128. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  129. package/neural-interface/lib/assistant-catalog.js +6 -7
  130. package/neural-interface/lib/assistant-dispatch.js +871 -156
  131. package/neural-interface/lib/assistant-envelope.js +3 -1
  132. package/neural-interface/lib/assistant-persona.js +80 -11
  133. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  134. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  135. package/neural-interface/lib/assistant-route-gate.js +6 -4
  136. package/neural-interface/lib/assistant-router.js +13 -8
  137. package/neural-interface/lib/assistant-runtime.js +56 -18
  138. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  139. package/neural-interface/lib/assistant-tools.js +51 -0
  140. package/neural-interface/lib/backup-service.js +620 -3
  141. package/neural-interface/lib/backup-zip-reader.js +10 -5
  142. package/neural-interface/lib/backup-zip-writer.js +257 -0
  143. package/neural-interface/lib/browser-execution.js +10 -2
  144. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  145. package/neural-interface/lib/browser-network-guard.js +284 -0
  146. package/neural-interface/lib/browser-policy-roots.js +64 -0
  147. package/neural-interface/lib/browser-tool-policy.js +186 -31
  148. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  149. package/neural-interface/lib/claude-hooks-status.js +103 -0
  150. package/neural-interface/lib/claude-temporary.js +5 -11
  151. package/neural-interface/lib/cli-install.js +18 -138
  152. package/neural-interface/lib/cli-resolver.js +4 -1
  153. package/neural-interface/lib/cli-update-planner.js +32 -31
  154. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  155. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  156. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  157. package/neural-interface/lib/confined-fs.js +30 -3
  158. package/neural-interface/lib/crash-notice-api.js +98 -0
  159. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  160. package/neural-interface/lib/exec-loop-command.js +4 -8
  161. package/neural-interface/lib/http-guards.js +8 -2
  162. package/neural-interface/lib/install-details.js +4 -1
  163. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  164. package/neural-interface/lib/mcp-client-config.js +0 -29
  165. package/neural-interface/lib/mcp-external-servers.js +323 -0
  166. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  167. package/neural-interface/lib/morelogin.js +30 -0
  168. package/neural-interface/lib/native-loop-providers.js +547 -47
  169. package/neural-interface/lib/native-loop-runtime.js +268 -41
  170. package/neural-interface/lib/opencode-config.js +557 -0
  171. package/neural-interface/lib/opencode-db-schema.js +17 -34
  172. package/neural-interface/lib/opencode-history.js +48 -218
  173. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  174. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  175. package/neural-interface/lib/opencode-protocol.js +249 -112
  176. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  177. package/neural-interface/lib/opencode-serve-records.js +39 -21
  178. package/neural-interface/lib/opencode-v2-client.js +48 -153
  179. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  180. package/neural-interface/lib/opencode-worker-data.js +27 -0
  181. package/neural-interface/lib/opencode2-client.js +73 -37
  182. package/neural-interface/lib/opencode2-proxy.js +23 -63
  183. package/neural-interface/lib/opencode2-translate.js +51 -2
  184. package/neural-interface/lib/owner-authority.js +249 -0
  185. package/neural-interface/lib/page-session.js +275 -0
  186. package/neural-interface/lib/peer-identity.js +763 -0
  187. package/neural-interface/lib/request-gate.js +17 -2
  188. package/neural-interface/lib/rulesets/api.js +1 -1
  189. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  190. package/neural-interface/lib/rulesets/installer.js +92 -27
  191. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  192. package/neural-interface/lib/session-cache-builder.js +17 -30
  193. package/neural-interface/lib/session-title-generator.js +58 -36
  194. package/neural-interface/lib/skills/api-v2.js +495 -0
  195. package/neural-interface/lib/skills/bundle.js +418 -0
  196. package/neural-interface/lib/skills/catalog.js +294 -0
  197. package/neural-interface/lib/skills/creation-api.js +191 -0
  198. package/neural-interface/lib/skills/creation-brief.js +247 -0
  199. package/neural-interface/lib/skills/creation-sources.js +267 -0
  200. package/neural-interface/lib/skills/creation.js +843 -0
  201. package/neural-interface/lib/skills/deploy.js +564 -0
  202. package/neural-interface/lib/skills/digest.js +119 -0
  203. package/neural-interface/lib/skills/discover.js +111 -0
  204. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  205. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  206. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  207. package/neural-interface/lib/skills/eval-trace.js +119 -0
  208. package/neural-interface/lib/skills/evals-api.js +212 -0
  209. package/neural-interface/lib/skills/evals.js +1573 -0
  210. package/neural-interface/lib/skills/export.js +200 -0
  211. package/neural-interface/lib/skills/frontmatter.js +64 -0
  212. package/neural-interface/lib/skills/health.js +75 -0
  213. package/neural-interface/lib/skills/json-edit.js +165 -0
  214. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  215. package/neural-interface/lib/skills/known-bridges.js +96 -0
  216. package/neural-interface/lib/skills/library.js +823 -0
  217. package/neural-interface/lib/skills/local-store.js +21 -0
  218. package/neural-interface/lib/skills/migration.js +433 -0
  219. package/neural-interface/lib/skills/operations.js +45 -0
  220. package/neural-interface/lib/skills/ownership.js +427 -0
  221. package/neural-interface/lib/skills/packages.js +88 -0
  222. package/neural-interface/lib/skills/reviews.js +212 -0
  223. package/neural-interface/lib/skills/revision.js +80 -0
  224. package/neural-interface/lib/skills/roots.js +262 -0
  225. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  226. package/neural-interface/lib/skills/scan-assets.js +252 -0
  227. package/neural-interface/lib/skills/scan-context.js +26 -0
  228. package/neural-interface/lib/skills/scan-shell.js +324 -0
  229. package/neural-interface/lib/skills/scan.js +364 -0
  230. package/neural-interface/lib/skills/scanners.js +54 -0
  231. package/neural-interface/lib/skills/service.js +522 -0
  232. package/neural-interface/lib/skills/telemetry.js +113 -0
  233. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  234. package/neural-interface/lib/tool-lists.js +124 -0
  235. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  236. package/neural-interface/lib/worker-confinement.js +1192 -0
  237. package/neural-interface/lib/worker-guard-setting.js +56 -0
  238. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  239. package/neural-interface/lib/worker-relay-ports.js +227 -0
  240. package/neural-interface/lib/worker-resume.js +229 -0
  241. package/neural-interface/lib/worker-sandbox.js +299 -0
  242. package/neural-interface/package-lock.json +18 -12
  243. package/neural-interface/package.json +2 -2
  244. package/neural-interface/public/index.html +3 -1
  245. package/neural-interface/public/index2d.html +19 -11
  246. package/neural-interface/public/onboarding.html +2 -2
  247. package/neural-interface/public/shared/account-menu.js +8 -5
  248. package/neural-interface/public/shared/agent-runtime-options.js +35 -19
  249. package/neural-interface/public/shared/api.js +1527 -1336
  250. package/neural-interface/public/shared/app-frame.js +192 -0
  251. package/neural-interface/public/shared/assistant/asst-agents-dock.js +33 -2
  252. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  253. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  254. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  255. package/neural-interface/public/shared/assistant/asst-brain-picker.js +49 -35
  256. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  257. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  258. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  259. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  260. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  261. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  262. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  263. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  264. package/neural-interface/public/shared/assistant/asst-panel.js +106 -11
  265. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  266. package/neural-interface/public/shared/assistant/asst-render.js +401 -77
  267. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  268. package/neural-interface/public/shared/assistant/asst-scenes.js +2 -0
  269. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  270. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  271. package/neural-interface/public/shared/assistant/asst-state.js +11 -15
  272. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  273. package/neural-interface/public/shared/assistant/asst-styles.js +59 -21
  274. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +156 -16
  275. package/neural-interface/public/shared/automation-context.js +5 -2
  276. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  277. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  278. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  279. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  280. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  281. package/neural-interface/public/shared/cdx/cdx-panel.js +45 -149
  282. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  283. package/neural-interface/public/shared/cdx/cdx-render.js +47 -29
  284. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  285. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  286. package/neural-interface/public/shared/cdx/cdx-styles.js +11 -3
  287. package/neural-interface/public/shared/cdx/cdx-tabs.js +52 -39
  288. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  289. package/neural-interface/public/shared/cli-install-view.js +4 -8
  290. package/neural-interface/public/shared/cli-status.js +2 -2
  291. package/neural-interface/public/shared/constants.js +2 -2
  292. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  293. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  294. package/neural-interface/public/shared/context-menu-model.js +366 -0
  295. package/neural-interface/public/shared/context-menu.js +372 -0
  296. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  297. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  298. package/neural-interface/public/shared/desktop-update-view.js +20 -0
  299. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  300. package/neural-interface/public/shared/html-shell.js +9 -11
  301. package/neural-interface/public/shared/i18n.js +2 -0
  302. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  303. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  304. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  305. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  306. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  307. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  308. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  309. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  310. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  311. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  312. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  313. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +47 -252
  314. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  315. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  316. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +6 -21
  317. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  318. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  319. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  320. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  321. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  322. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  323. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  324. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +0 -21
  325. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  326. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +14 -47
  327. package/neural-interface/public/shared/page-session.js +96 -0
  328. package/neural-interface/public/shared/registry.js +5 -3
  329. package/neural-interface/public/shared/retired-2d.js +3 -0
  330. package/neural-interface/public/shared/schedules-view.js +195 -61
  331. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  332. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  333. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  334. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  335. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  336. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  337. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  338. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  339. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  340. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  341. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  342. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  343. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  344. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  345. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  346. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  347. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  348. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  349. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  350. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  351. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  352. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  353. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  354. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  355. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  356. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  357. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  358. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  359. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  360. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  361. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  362. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  363. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  364. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  365. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  366. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  367. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  368. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  369. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  370. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  371. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  372. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  373. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  374. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  375. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  376. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  377. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  378. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  379. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  380. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  381. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  382. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  383. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  384. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  385. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  386. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  387. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  388. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  389. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  390. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  391. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  392. package/neural-interface/public/shared/styles.css +1525 -1779
  393. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  394. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  395. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  396. package/neural-interface/public/shared/tool-list-view.js +112 -0
  397. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  398. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  399. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  400. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  401. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  402. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  403. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  404. package/neural-interface/public/shared/tour/tour.css +144 -0
  405. package/neural-interface/public/shared/ui-automation-studio.js +91 -128
  406. package/neural-interface/public/shared/ui-claude-panel.js +77 -5
  407. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  408. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  409. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  410. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  411. package/neural-interface/public/shared/ui-detail.js +15 -3
  412. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  413. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  414. package/neural-interface/public/shared/ui-menubar.js +8 -18
  415. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  416. package/neural-interface/public/shared/ui-native-loop-router.js +7 -1
  417. package/neural-interface/public/shared/ui-navbar.js +4 -24
  418. package/neural-interface/public/shared/ui-panels.js +96 -65
  419. package/neural-interface/public/shared/ui-schedules-studio.js +507 -423
  420. package/neural-interface/public/shared/ui-sessions.js +5 -3
  421. package/neural-interface/public/shared/ui-settings.js +594 -462
  422. package/neural-interface/public/shared/ui-sidepanel-sessions.js +1 -1
  423. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  424. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  425. package/neural-interface/public/shared/ui-skills.js +33 -2830
  426. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  427. package/neural-interface/public/shared/ui-sync.js +15 -5
  428. package/neural-interface/public/shared/ui-terminal.js +124 -180
  429. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  430. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  431. package/neural-interface/public/shared/ui-trash.js +6 -4
  432. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  433. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  434. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  435. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  436. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  437. package/neural-interface/public/shared/ui-update.js +38 -2
  438. package/neural-interface/public/shared/ui-whiteboard.js +311 -58
  439. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  440. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  441. package/neural-interface/public/shared/view-variant.js +66 -0
  442. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  443. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  444. package/neural-interface/public/shared/worker-end.js +10 -3
  445. package/neural-interface/public/shared/worker-restart.js +12 -0
  446. package/neural-interface/public/sidepanel-session.html +1 -1
  447. package/neural-interface/public/variant/2d/background.js +2 -1
  448. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  449. package/neural-interface/public/variant/2d/graph.js +42 -20
  450. package/neural-interface/public/variant/2d/hulls.js +14 -9
  451. package/neural-interface/public/variant/2d/lasso.js +11 -7
  452. package/neural-interface/public/variant/2d/main.js +32 -1
  453. package/neural-interface/public/variant/2d/minimap.js +11 -13
  454. package/neural-interface/public/variant/3d/main.js +69 -6
  455. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  456. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  457. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  458. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  459. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  460. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  461. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  462. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  463. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  464. package/neural-interface/scripts/build-vendor.mjs +32 -0
  465. package/neural-interface/server.js +3934 -2454
  466. package/neural-interface/templates/rulesets/core.md +1 -1
  467. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  468. package/neural-interface/templates/rulesets/manifest.json +8 -8
  469. package/npm-shrinkwrap.json +18 -12
  470. package/package.json +16 -3
  471. package/postinstall.js +9 -4
  472. package/preuninstall.js +1 -1
  473. package/setup.js +97 -322
  474. package/skills/skill-forge/SKILL.md +177 -0
  475. package/skills/skill-forge/evals/evals.json +54 -0
  476. package/skills/skill-forge/references/package.md +105 -0
  477. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  478. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  479. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  480. package/skills/synabun/SKILL.md +10 -7
  481. package/skills/synabun/modules/audit.md +1 -5
  482. package/skills/synabun/modules/changelog.md +1 -5
  483. package/skills/synabun/modules/idea.md +1 -1
  484. package/skills/synabun/modules/memorize.md +6 -9
  485. package/skills/synabun/modules/schedule.md +4 -4
  486. package/mcp-server/dist/services/profiles.d.ts +0 -120
  487. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  488. package/mcp-server/dist/services/profiles.js +0 -387
  489. package/mcp-server/dist/services/profiles.js.map +0 -1
  490. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  491. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  492. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  493. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  494. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  495. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  496. package/mcp-server/dist/tools/leonardo.js +0 -21
  497. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  498. package/mcp-server/dist/tools/profile.d.ts +0 -17
  499. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  500. package/mcp-server/dist/tools/profile.js +0 -114
  501. package/mcp-server/dist/tools/profile.js.map +0 -1
  502. package/mcp-server/src/services/profiles.ts +0 -428
  503. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  504. package/mcp-server/src/tools/leonardo.ts +0 -30
  505. package/mcp-server/src/tools/profile.ts +0 -122
  506. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  507. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  508. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  509. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  510. package/skills/leonardo/SKILL.md +0 -168
  511. package/skills/leonardo/icon.svg +0 -1
  512. package/skills/leonardo/modules/browser-guide.md +0 -378
  513. package/skills/leonardo/modules/image-prompter.md +0 -283
  514. package/skills/leonardo/modules/model-advisor.md +0 -100
  515. package/skills/leonardo/modules/prompt-library.md +0 -110
  516. package/skills/leonardo/modules/style-guide.md +0 -186
  517. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -0,0 +1,280 @@
1
+ // ── The OS sandbox of an evaluation's shell (P4, macOS) ──
2
+ // A task case that asks for a shell (`shell: true`) runs untrusted skill text with a real shell. Every command of that
3
+ // shell, and nothing else, runs under a Seatbelt profile of its own (evalShellProfile), through a small wrapper the
4
+ // runtime is told to use for its commands: Claude Code's CLAUDE_CODE_SHELL_PREFIX (its Bash tool and any hook command
5
+ // go through it), OpenCode's SHELL (its bash tool runs `$SHELL -c <command>`). The runtime itself stays outside, with
6
+ // its own sign-in; its shell, inside, has:
7
+ //
8
+ // network none at all: SynaBun's ports, local servers and the internet alike (the deny is `network*`)
9
+ // writes its job folder, its own temp, home and git folders, the terminal devices; inside the job folder, the
10
+ // folders and files a runtime reads its settings, skills or code from (.claude, .opencode, .git,
11
+ // opencode.json, … ss-evals.js EVAL_CONFIG_DIRS / EVAL_CONFIG_FILES, any letter case, a folder at any
12
+ // depth) stay as SynaBun wrote them. No symlink can be created in any writable place (the last file-write
13
+ // rule denies vnode-type SYMLINK): a shell step is the only thing that could plant or repoint a job-folder
14
+ // symlink, and that link is the one way the (unsandboxed) eval read tool could be steered outside the folder
15
+ // between its permission ask and its open — removing it is what makes the ask-time realpath check race-free
16
+ // git a repository of its own beside the job folder, never in it: GIT_DIR is the trial's git folder and
17
+ // GIT_WORK_TREE the job folder, so `git init`, `add`, `commit`, `log`, `diff`, `branch` and `checkout` work on
18
+ // the folder's files while no `.git` (a folder, or a `gitdir:` file) can be made anywhere in it. The runtime
19
+ // outside runs git in the job folder too (Claude Code's status, OpenCode's project and snapshot reads):
20
+ // there is no repository there to read, and that side's git takes no bare repository it was not pointed at
21
+ // and reads no global or system configuration (lib/skills/eval-confinement.js evalGitSafetyEnv), so nothing
22
+ // a command writes is read by it. A commit's identity is the trial home's .gitconfig (a test may set its own)
23
+ // reads nothing under /Users (every home folder), /Volumes, SynaBun's data home, /private/tmp or
24
+ // /private/var/folders (other programs' temp files), except the job folder, its own temp, home and git
25
+ // folders, the folders on PATH and a few language toolchains (runtimes, no settings), and Claude Code's
26
+ // shell snapshot (its shell setup, which every one of its commands sources first)
27
+ // others a signal only to a process of its own sandbox (`kill -0` of any other process is refused); no launchd job
28
+ // (`job-creation` is denied: `launchctl submit` is refused, while `launchctl print` still reads); no Apple
29
+ // event (`appleevent-send` is denied: a real event, to System Events, fails with -4960, while
30
+ // `osascript -e 1` still runs, because it sends none); no lookup of the Mach services that hold the user's
31
+ // data or start programs outside it (EVAL_SHELL_MACH_DENY: a keychain read and pbcopy / pbpaste were seen
32
+ // to fail; LaunchServices, the window server, screen capture and TCC are denied by name)
33
+ // env a short allowlist (locale, terminal, PATH and what the runtime's own command line needs): no token,
34
+ // key or password of the runtime's environment reaches a command; HOME and TMPDIR are the trial's own, and
35
+ // git's variables are the wrapper's (none of the runtime's)
36
+ //
37
+ // The `others` line is what the P4 verification saw. tests/skills-p4-eval-sandbox.test.mjs repeats the signal, keychain
38
+ // and launchd checks (a job its `launchctl submit` did make would be removed at once, and the test would fail); it
39
+ // sends no Apple event and asks no screen or app service again, since one that got through would act on the owner's
40
+ // session (a TCC prompt, an app started).
41
+ // Seatbelt is last-match-wins and matches resolved paths (lib/worker-sandbox.js: canonicalPath), so every path here is
42
+ // canonical and every allow comes after the deny it opens. This module only builds and writes; whether sandbox-exec
43
+ // works is lib/worker-sandbox.js sandboxExecAvailable(). A shell that cannot be set up this way is not given: the run
44
+ // fails closed (lib/skills/evals.js, server.js).
45
+
46
+ import { chmodSync, existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
47
+ import { basename, dirname, isAbsolute, join, sep } from 'node:path';
48
+ import { SANDBOX_EXEC, canonicalPath, sandboxExecAvailable, sandboxSupported } from '../worker-sandbox.js';
49
+ import { EVAL_CONFIG_DIRS, EVAL_CONFIG_FILES } from '../../public/shared/skills/ss-evals.js';
50
+
51
+ /** Part of an evaluation's confinement version: a change to what an evaluation's shell may do is a new one. */
52
+ export const EVAL_SHELL_VERSION = 'skill-eval-shell/3';
53
+
54
+ /** Mach services an evaluation's shell never reaches (the keychain, the clipboard, apps, the screen). */
55
+ export const EVAL_SHELL_MACH_DENY = Object.freeze([
56
+ ['global-name', 'com.apple.SecurityServer'], ['global-name', 'com.apple.securityd'], ['global-name', 'com.apple.securityd.xpc'],
57
+ ['global-name', 'com.apple.security.agent'], ['global-name', 'com.apple.security.authhost'], ['global-name', 'com.apple.pasteboard.1'],
58
+ ['global-name-prefix', 'com.apple.coreservices.launchservicesd'], ['global-name-prefix', 'com.apple.lsd.'],
59
+ ['global-name', 'com.apple.windowserver.active'], ['global-name-prefix', 'com.apple.screencapture'], ['global-name', 'com.apple.tccd'],
60
+ ]);
61
+
62
+ /** Folders under a home that hold language runtimes only: readable from an evaluation's shell when present. */
63
+ export const EVAL_SHELL_TOOLCHAINS = Object.freeze([
64
+ '.nvm/versions', '.volta/tools/image', '.fnm/node-versions', '.local/share/fnm/node-versions', '.pyenv/versions', '.rbenv/versions',
65
+ '.rustup/toolchains', '.asdf/installs', '.local/share/mise/installs', '.bun/bin', '.deno/bin',
66
+ ]);
67
+
68
+ /** What of the runtime's environment a command keeps (everything else, its tokens and keys among it, is left out). */
69
+ export const EVAL_SHELL_ENV_KEEP = Object.freeze([
70
+ 'PATH', 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LC_COLLATE', 'LC_NUMERIC', 'LC_TIME', 'LC_MONETARY', 'TERM', 'COLORTERM',
71
+ 'NO_COLOR', 'FORCE_COLOR', 'COLUMNS', 'LINES', 'USER', 'LOGNAME', 'TZ', 'SHLVL',
72
+ // Claude Code's command line reads these (it sets BUN_OPTIONS from the first, and refuses to run without it).
73
+ '__CLAUDE_CC_BUN_OPTIONS', 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT',
74
+ ]);
75
+
76
+ /** The identity of a commit a shell step makes, in the trial home's .gitconfig (its lowest place: a test's own wins). */
77
+ export const EVAL_SHELL_GIT_IDENTITY = Object.freeze({ name: 'SynaBun evaluation', email: 'evaluation@synabun.invalid' });
78
+
79
+ const SHELLS = Object.freeze({ zsh: { path: '/bin/zsh', args: ['-f'] }, bash: { path: '/bin/bash', args: ['--noprofile', '--norc'] }, sh: { path: '/bin/sh', args: [] } });
80
+
81
+ /** Whether an evaluation's shell can be held here: macOS with a working sandbox-exec. */
82
+ export function evalShellSupported({ platform = process.platform, probe = sandboxExecAvailable } = {}) {
83
+ return sandboxSupported(platform) && probe({ platform }) === true;
84
+ }
85
+
86
+ /** A Seatbelt string literal. */
87
+ function lit(text) { return `"${String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; }
88
+ /**
89
+ * Text as a Seatbelt regular expression that matches it and nothing else. A #"…" literal is raw (a backslash is not an
90
+ * escape there), so ASCII punctuation goes in a bracket ([.] is a dot); letters, digits and other characters stay as
91
+ * they are. A path holding a quote, a backslash or a control character cannot be named this way: refused.
92
+ */
93
+ function regexText(text) {
94
+ let out = '';
95
+ for (const c of String(text)) {
96
+ if (c === '"' || c === '\\' || /[\u0000-\u001f\u007f]/.test(c)) throw new Error('The evaluation folder\'s path has a character the sandbox cannot name.');
97
+ if (/[A-Za-z0-9_/ -]/.test(c) || c.charCodeAt(0) > 0x7f) out += c;
98
+ else if (c === ']') out += '[]]';
99
+ else if (c === '^') out += '[*^]';
100
+ else out += `[${c}]`;
101
+ }
102
+ return out;
103
+ }
104
+ /** A Seatbelt regular expression literal (#"…", raw). */
105
+ const rx = (source) => `#"${source}"`;
106
+ /** A name for a regex in any letter case ([cC][lL]…), every other character as itself. */
107
+ function anyCase(name) { return [...String(name)].map((c) => (/[a-z]/i.test(c) ? `[${c.toLowerCase()}${c.toUpperCase()}]` : regexText(c))).join(''); }
108
+ /** The deepest existing ancestor resolved, and absolute: what Seatbelt sees. */
109
+ const canon = (p) => canonicalPath(isAbsolute(String(p)) ? String(p) : join(process.cwd(), String(p)));
110
+ const inside = (p, root) => p === root || p.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
111
+
112
+ /**
113
+ * The directory nodes on the way to each allowed path that fall in a denied tree: `stat` of those nodes only (a
114
+ * literal, never their contents), so a command can resolve its way into the job folder without listing a home.
115
+ */
116
+ function metadataAncestors(allows, denies) {
117
+ const out = new Set();
118
+ for (const allow of allows) {
119
+ for (let dir = dirname(allow), i = 0; i < 256; i++) {
120
+ if (denies.some((d) => inside(dir, d))) out.add(dir);
121
+ const up = dirname(dir);
122
+ if (up === dir) break;
123
+ dir = up;
124
+ }
125
+ }
126
+ return [...out].sort();
127
+ }
128
+
129
+ /**
130
+ * The profile of one evaluation's shell.
131
+ * @param {object} o
132
+ * @param {string} o.root the job folder (the run's cwd)
133
+ * @param {string} o.temp the trial's own temp folder
134
+ * @param {string} o.home the trial's own home folder (HOME of every command)
135
+ * @param {string} o.git the trial's own git folder (GIT_DIR of every command: the repository of the job folder's files)
136
+ * @param {string[]} [o.denyReadTrees] trees nothing is read from (the home folders, SynaBun's data home, …), on top of
137
+ * /Users, /Volumes, /private/tmp and /private/var/folders
138
+ * @param {string[]} [o.readOnly] paths read back after the denies (PATH folders, toolchains, Claude Code's snapshots)
139
+ * @param {string} [o.note] a comment line
140
+ * @returns {string}
141
+ */
142
+ export function evalShellProfile({ root, temp, home, git, denyReadTrees = [], readOnly = [], note = '' } = {}) {
143
+ if (!root || !temp || !home || !git) throw new Error('An evaluation shell profile needs its job, temp, home and git folders.');
144
+ const job = canon(root);
145
+ const tmp = canon(temp);
146
+ const own = canon(home);
147
+ const repo = canon(git);
148
+ if (inside(repo, job) || inside(job, repo)) throw new Error('An evaluation shell\'s git folder is beside its job folder, never in it or around it.');
149
+ const denies = [...new Set(['/Users', '/Volumes', '/private/tmp', '/private/var/folders', ...denyReadTrees.filter(Boolean).map(canon)])];
150
+ const reads = [...new Set(readOnly.filter(Boolean).map(canon))].filter((p) => p !== '/' && !inside(job, p));
151
+ const lines = ['(version 1)', `; synabun ${EVAL_SHELL_VERSION}${note ? `: ${String(note).replace(/[\r\n]+/g, ' ').slice(0, 200)}` : ''}`, '(allow default)'];
152
+ lines.push('; no network at all (SynaBun, local servers, the internet)');
153
+ lines.push('(deny network*)');
154
+ lines.push('; a signal only to a process of this sandbox; no launchd job (launchctl submit refused); no Apple event sent');
155
+ lines.push('(deny signal)', '(allow signal (target same-sandbox))', '(deny job-creation)', '(deny appleevent-send)');
156
+ lines.push('; the keychain, the clipboard, LaunchServices, the window server, screen capture and TCC, by service name');
157
+ lines.push(`(deny mach-lookup ${EVAL_SHELL_MACH_DENY.map(([kind, name]) => `(${kind} ${lit(name)})`).join(' ')})`);
158
+ lines.push('; writes: the job folder, the trial\'s temp, home and git folders, the terminal devices');
159
+ lines.push('(deny file-write*)');
160
+ lines.push(`(allow file-write* (subpath ${lit(job)}) (subpath ${lit(tmp)}) (subpath ${lit(own)}) (subpath ${lit(repo)}) (literal "/dev/null") (literal "/dev/zero") (literal "/dev/random") (literal "/dev/urandom") (literal "/dev/dtracehelper") (literal "/dev/tty") (literal "/dev/stdout") (literal "/dev/stderr") (regex #"^/dev/ttys[0-9]+$") (regex #"^/dev/fd/[0-9]+$"))`);
161
+ lines.push('; the folders (at any depth: .git among them, so no repository is ever made here) and top files a runtime reads its settings or code from stay as SynaBun wrote them (any letter case)');
162
+ const base = regexText(job);
163
+ const dirRules = Object.keys(EVAL_CONFIG_DIRS).map((name) => `(regex ${rx(`^${base}/(.+/)?${anyCase(name)}(/|$)`)})`);
164
+ const fileRules = Object.keys(EVAL_CONFIG_FILES).map((name) => `(regex ${rx(`^${base}/${anyCase(name)}$`)})`);
165
+ lines.push(`(deny file-write* ${[...dirRules, ...fileRules].join(' ')})`);
166
+ lines.push('; reads: no home folder, other volume, other temp files or SynaBun data');
167
+ lines.push(`(deny file-read* ${denies.map((d) => `(subpath ${lit(d)})`).join(' ')})`);
168
+ lines.push('; read back: the job folder, the trial\'s temp, home and git folders, the tools on PATH and the runtime\'s shell setup');
169
+ lines.push(`(allow file-read* (subpath ${lit(job)}) (subpath ${lit(tmp)}) (subpath ${lit(own)}) (subpath ${lit(repo)})${reads.map((p) => ` (subpath ${lit(p)})`).join('')})`);
170
+ // Apple's command-line tool shims (git, python3 from the Command Line Tools) keep an SDK lookup cache in the user's
171
+ // temp folder whatever TMPDIR says: that one file name only.
172
+ lines.push('(allow file-read* file-write* (regex #"^/private/var/folders/[^/]+/[^/]+/T/xcrun_db"))');
173
+ // No symlink may be created in any writable place (last of the file-write rules, so it wins over every allow above,
174
+ // xcrun_db included). A symlink is the one way a shell step of a trial could repoint a path inside the job folder at
175
+ // a file OUTSIDE it between the read tool's permission ask (evalOpenCodeDecision, which resolves the real path and
176
+ // refuses one leaving the folder) and the tool's open — a TOCTOU the ask alone cannot close, because the eval read
177
+ // tool runs in the runtime's own process, not under this sandbox, so SynaBun cannot bind the path it finally opens.
178
+ // The job folder is created fresh with no symlink, SynaBun's skill copy follows none, and Write/Edit make plain files
179
+ // only, so the shell is the sole writer that could introduce one; deny it here and no symlink can ever exist in the
180
+ // folder, which is what makes the ask-time realpath check race-free. Regular writes, mkdir, rename (mv) and hardlinks
181
+ // to in-folder files are untouched; a hardlink to an outside file already fails (its lookup needs a read the denies
182
+ // above refuse). Verified on macOS 26 (scratch probes, 2026-10-25): ln -s / ln -sf are refused, mv and mkdir work.
183
+ lines.push('(deny file-write* (vnode-type SYMLINK))');
184
+ const ancestors = metadataAncestors([job, tmp, own, repo, ...reads], denies);
185
+ if (ancestors.length) lines.push(`(allow file-read-metadata ${ancestors.map((d) => `(literal ${lit(d)})`).join(' ')})`);
186
+ return `${lines.join('\n')}\n`;
187
+ }
188
+
189
+ /**
190
+ * The folders of a trial an evaluation's shell uses: the job folder, and beside it its temp, home, git (the repository
191
+ * of the job folder's files, GIT_DIR) and sandbox folders.
192
+ */
193
+ export function evalShellPaths(cwd) {
194
+ const root = realpathSync(String(cwd || ''));
195
+ const trialDir = dirname(root);
196
+ return { root, trialDir, temp: join(trialDir, 'tmp'), home: join(trialDir, 'home'), git: join(trialDir, 'git'), dir: join(trialDir, 'sandbox') };
197
+ }
198
+
199
+ /** The real shell a command runs in (the runtime's shell's family: Claude Code's snapshot is written for it). */
200
+ export function evalInnerShell(shell = process.env.SHELL) {
201
+ const name = basename(String(shell || '')).toLowerCase();
202
+ return name === 'bash' ? 'bash' : name === 'sh' ? 'sh' : 'zsh';
203
+ }
204
+
205
+ /**
206
+ * The wrapper one command goes through. Claude Code calls it as `wrapper '<command>'` (its shell prefix), OpenCode as
207
+ * `wrapper -c '<command>'` (its shell); `-l`/`-i` before `-c` are let go. Anything else is refused (exit 126). Its git
208
+ * works on the job folder's files from the trial's git folder (GIT_DIR, GIT_WORK_TREE), never looks above the trial's
209
+ * folder for another repository and reads no system configuration; the global one is the trial home's.
210
+ */
211
+ export function evalShellWrapper({ profilePath, shell = 'zsh', home, temp, root, git }) {
212
+ if (!root || !git) throw new Error('An evaluation shell\'s wrapper needs its job and git folders.');
213
+ const sh = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
214
+ const inner = SHELLS[shell] || SHELLS.zsh;
215
+ const keep = EVAL_SHELL_ENV_KEEP.map((name) => `\${${name}+"${name}=$${name}"}`).join(' ');
216
+ const gitEnv = `GIT_DIR=${sh(git)} GIT_WORK_TREE=${sh(root)} GIT_CEILING_DIRECTORIES=${sh(dirname(root))} GIT_CONFIG_NOSYSTEM=1`;
217
+ return [
218
+ '#!/bin/sh',
219
+ `# SynaBun (${EVAL_SHELL_VERSION}): one command of a skill evaluation, under the evaluation's sandbox. Written per trial.`,
220
+ 'cmd=',
221
+ 'if [ $# -eq 1 ]; then cmd=$1',
222
+ 'else',
223
+ ' while [ $# -gt 0 ]; do',
224
+ ' case "$1" in',
225
+ ' -c|-lc|-cl|-ic|-ci|-ilc|-lic) shift; cmd=${1-}; break ;;',
226
+ ' -l|-i|--login|--interactive|--noprofile|--norc) shift ;;',
227
+ ' *) break ;;',
228
+ ' esac',
229
+ ' done',
230
+ 'fi',
231
+ 'if [ -z "$cmd" ]; then echo "synabun: an evaluation shell runs one command (-c)." >&2; exit 126; fi',
232
+ `exec /usr/bin/env -i ${keep} HOME=${sh(home)} TMPDIR=${sh(`${temp}/`)} CLAUDE_CODE_TMPDIR=${sh(temp)} SHELL=${sh(inner.path)} ${gitEnv} ${sh(SANDBOX_EXEC)} -f ${sh(profilePath)} ${sh(inner.path)}${inner.args.map((a) => ` ${a}`).join('')} -c "$cmd"`,
233
+ '',
234
+ ].join('\n');
235
+ }
236
+
237
+ /** The folders on PATH that sit in a denied tree, and the toolchains under each home: read back for the shell. */
238
+ export function evalShellReadPaths({ path = process.env.PATH, homes = [], claudeConfigDir = null } = {}) {
239
+ const out = new Set();
240
+ for (const dir of String(path || '').split(':')) if (dir && isAbsolute(dir) && existsSync(dir)) out.add(canon(dir));
241
+ for (const h of homes.filter(Boolean)) for (const rel of EVAL_SHELL_TOOLCHAINS) { const p = join(h, rel); if (existsSync(p)) out.add(canon(p)); }
242
+ if (claudeConfigDir) out.add(canon(join(claudeConfigDir, 'shell-snapshots')));
243
+ return [...out];
244
+ }
245
+
246
+ /** The trial home's .gitconfig: a commit's identity (EVAL_SHELL_GIT_IDENTITY) and git's default-branch hint off. */
247
+ export function evalShellGitConfig() {
248
+ return [
249
+ `# SynaBun (${EVAL_SHELL_VERSION}): git's settings for a skill evaluation's shell. Written per trial.`,
250
+ '[user]', `\tname = ${EVAL_SHELL_GIT_IDENTITY.name}`, `\temail = ${EVAL_SHELL_GIT_IDENTITY.email}`,
251
+ '[advice]', '\tdefaultBranchName = false', '',
252
+ ].join('\n');
253
+ }
254
+
255
+ /**
256
+ * Set up one trial's shell: its temp, home and git folders (the home with its .gitconfig), its profile and its wrapper
257
+ * (in the trial's own sandbox folder, outside the job folder, so nothing the shell does can change them).
258
+ * → { wrapperPath, profilePath, shell, env, paths }
259
+ * `env` is what the runtime gets (TMPDIR and CLAUDE_CODE_TMPDIR: its own temp files go into the trial's).
260
+ * @param {object} o
261
+ * @param {string} o.cwd the job folder
262
+ * @param {string[]} [o.denyReadTrees] SynaBun's data home, the home folders
263
+ * @param {string[]} [o.readOnly] evalShellReadPaths()
264
+ * @param {string} [o.shell] the runtime's shell (its family is the inner shell)
265
+ * @param {string} [o.wrapperName] the wrapper's file name (OpenCode accepts a shell by its name: zsh, bash or sh)
266
+ */
267
+ export function prepareEvalShell({ cwd, denyReadTrees = [], readOnly = [], shell = process.env.SHELL, wrapperName = null, note = '' } = {}) {
268
+ const paths = evalShellPaths(cwd);
269
+ for (const dir of [paths.temp, paths.home, paths.git, paths.dir]) mkdirSync(dir, { recursive: true, mode: 0o700 });
270
+ writeFileSync(join(paths.home, '.gitconfig'), evalShellGitConfig(), { mode: 0o600 });
271
+ const inner = evalInnerShell(shell);
272
+ const profile = evalShellProfile({ root: paths.root, temp: paths.temp, home: paths.home, git: paths.git, denyReadTrees, readOnly, note });
273
+ const profilePath = join(paths.dir, 'shell.sb');
274
+ writeFileSync(profilePath, profile, { mode: 0o600 });
275
+ const name = wrapperName && /^(?:zsh|bash|sh)$/.test(wrapperName) ? wrapperName : inner;
276
+ const wrapperPath = join(paths.dir, name);
277
+ writeFileSync(wrapperPath, evalShellWrapper({ profilePath, shell: inner, home: canon(paths.home), temp: canon(paths.temp), root: paths.root, git: canon(paths.git) }), { mode: 0o700 });
278
+ chmodSync(wrapperPath, 0o700);
279
+ return { wrapperPath, profilePath, shell: inner, paths, env: { TMPDIR: `${canon(paths.temp)}/`, CLAUDE_CODE_TMPDIR: canon(paths.temp) } };
280
+ }
@@ -0,0 +1,119 @@
1
+ // ── What an evaluation run did: its tool calls, and whether the runtime picked the skill (P4, SK-085) ──
2
+ // The dispatcher keeps the tool calls of an evaluation run (assistant-dispatch.js, transcript format `tools`), read
3
+ // here from each provider's own events; a trigger trial is read from them:
4
+ //
5
+ // Claude Code a `Skill` call naming the skill: method `native_skill_tool` (seen). A Read of its SKILL.md without
6
+ // one: `successful_skill_file_read` (an inference).
7
+ // OpenCode a `skill` tool call naming it (`{ id }`): `native_skill_tool`;
8
+ // a read of its SKILL.md: the inference.
9
+ // Codex Codex has no skill tool: it lists its skills and the model reads the file with its shell. A command
10
+ // that ran (exit 0) and names the skill's SKILL.md: `successful_skill_file_read` (an inference).
11
+ //
12
+ // A skill named that is not the one under test is evidence too (the runtime picked another), never a "yes". The
13
+ // runtime's own listing at start (Claude's init `skills`) is kept as the catalog the run saw: a baseline that lists
14
+ // the skill under test was not a baseline. Startup discovery is never counted as a selection. A call's input is kept with
15
+ // its secrets redacted, the way an answer and a written file are (lib/log-redact.js). Pure.
16
+
17
+ import { redactSecrets } from '../../../lib/log-redact.js';
18
+
19
+ const clipJson = (value, max = 600) => {
20
+ let text;
21
+ try { text = typeof value === 'string' ? value : JSON.stringify(value ?? null); } catch { text = String(value); }
22
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
23
+ };
24
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
25
+
26
+ /**
27
+ * The tool calls one provider event carries: [{ id, tool, input, phase?, ok? }]. Claude: the tool_use blocks of a
28
+ * whole assistant message; Codex: a command (started, then done with its exit code), an MCP call, a file change;
29
+ * OpenCode: a tool part (each update of a call has its id: the caller keeps one entry per id).
30
+ */
31
+ export function toolCallsOf(payload = {}) {
32
+ const event = payload?.event || {};
33
+ const eventType = String(payload?.eventType || event.type || '');
34
+ const out = [];
35
+ if (payload.provider === 'claude-code') {
36
+ if (event.type === 'assistant' && Array.isArray(event.message?.content)) {
37
+ for (const block of event.message.content) {
38
+ if (block?.type === 'tool_use') out.push({ id: String(block.id || ''), tool: String(block.name || ''), input: isObject(block.input) ? block.input : {}, sub: !!event.parent_tool_use_id });
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+ if (payload.provider === 'codex') {
44
+ if (event.type === 'item.started' || event.type === 'item.completed') {
45
+ const item = event.item || {};
46
+ const done = event.type === 'item.completed';
47
+ if (item.type === 'command_execution') {
48
+ const command = typeof item.command === 'string' ? item.command : Array.isArray(item.command) ? item.command.join(' ') : '';
49
+ out.push({ id: String(item.id || ''), tool: 'shell', input: { command }, phase: done ? 'done' : 'started', ok: done ? Number(item.exit_code) === 0 : null });
50
+ } else if (item.type === 'mcp_tool_call') out.push({ id: String(item.id || ''), tool: `${item.server || ''}.${item.tool || ''}`, input: {}, phase: done ? 'done' : 'started' });
51
+ else if (item.type === 'file_change') out.push({ id: String(item.id || ''), tool: 'file_change', input: { paths: (item.changes || []).map((c) => c?.path).filter(Boolean) }, phase: done ? 'done' : 'started' });
52
+ else if (item.type === 'web_search') out.push({ id: String(item.id || ''), tool: 'web_search', input: { query: item.query || '' }, phase: done ? 'done' : 'started' });
53
+ }
54
+ return out;
55
+ }
56
+ if (payload.provider === 'opencode') {
57
+ if (/^message[.:]part[.:]updated$/i.test(eventType)) {
58
+ const part = event.part || {};
59
+ if (part.type === 'tool') out.push({ id: String(part.callID || part.id || ''), tool: String(part.tool || part.name || ''), input: isObject(part.state?.input) ? part.state.input : {}, phase: String(part.state?.status || ''), ok: part.state?.status === 'completed' ? true : part.state?.status === 'error' ? false : null });
60
+ }
61
+ return out;
62
+ }
63
+ return out;
64
+ }
65
+
66
+ /** Claude's init event, as the catalog the run saw: { model, skills, tools, cwd } (null for any other event). */
67
+ export function initOf(payload = {}) {
68
+ const event = payload?.event || {};
69
+ if (payload.provider !== 'claude-code' || event.type !== 'system' || event.subtype !== 'init') return null;
70
+ const names = (list) => (Array.isArray(list) ? list.map((x) => (typeof x === 'string' ? x : x?.name)).filter((x) => typeof x === 'string').slice(0, 200) : null);
71
+ return { model: typeof event.model === 'string' ? event.model : null, skills: names(event.skills), tools: names(event.tools), cwd: typeof event.cwd === 'string' ? event.cwd : null };
72
+ }
73
+
74
+ /** A value with a secret in any of its texts redacted (each string as it is, before it is written as JSON). Bounded. */
75
+ function redactValue(value, depth = 0) {
76
+ if (typeof value === 'string') return redactSecrets(value.length > 20_000 ? value.slice(0, 20_000) : value);
77
+ if (depth > 8 || value === null || typeof value !== 'object') return value;
78
+ if (Array.isArray(value)) return value.slice(0, 200).map((v) => redactValue(v, depth + 1));
79
+ return Object.fromEntries(Object.entries(value).slice(0, 200).map(([k, v]) => [k, redactValue(v, depth + 1)]));
80
+ }
81
+
82
+ /** A tool call kept for the record: its tool, its input as bounded JSON text, every secret in it redacted first. */
83
+ export const callRecord = (c) => ({ tool: c.tool, input: clipJson(redactValue(c.input), 400), ...(c.phase ? { phase: c.phase } : {}), ...(c.ok === true || c.ok === false ? { ok: c.ok } : {}) });
84
+
85
+ const skillFilePattern = (name) => new RegExp(`(?:^|[\\\\/\\s'"])skills[\\\\/]${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\\\/]SKILL\\.md\\b`, 'i');
86
+ const SKILL_MD = /(?:^|[\\/\s'"])skills[\\/]([a-z0-9][a-z0-9._-]{0,63})[\\/]SKILL\.md\b/gi;
87
+
88
+ /**
89
+ * Whether the run picked the skill: { observed: true|false, method, evidence: [{ tool, input }], others: [names] }.
90
+ * `observed` is false when nothing named it (the caller decides whether the run got far enough to say so).
91
+ */
92
+ export function detectSelection({ provider, calls = [], skillName }) {
93
+ const name = String(skillName || '');
94
+ const evidence = [];
95
+ const others = new Set();
96
+ let native = false;
97
+ let read = false;
98
+ const file = name ? skillFilePattern(name) : null;
99
+ for (const c of calls) {
100
+ const tool = String(c.tool || '');
101
+ const input = isObject(c.input) ? c.input : {};
102
+ if ((provider === 'claude-code' && tool === 'Skill') || (provider === 'opencode' && tool === 'skill')) {
103
+ // Claude Code's Skill sends { skill }, OpenCode's skill tool { id } (seen in a live run).
104
+ const asked = String(input.skill ?? input.id ?? input.command ?? '').replace(/^\//, '');
105
+ const bare = asked.includes(':') ? asked.split(':').pop() : asked;
106
+ if (name && (asked === name || bare === name)) { native = true; evidence.push(callRecord(c)); } else if (asked) others.add(asked.slice(0, 64));
107
+ continue;
108
+ }
109
+ const text = provider === 'codex' ? String(input.command || '') : String(input.file_path ?? input.filePath ?? input.path ?? '');
110
+ if (!text) continue;
111
+ if (provider === 'codex' && c.phase === 'started') continue; // a command that ran (its done item, exit 0) is a read
112
+ if (provider === 'codex' && c.ok === false) continue;
113
+ const readTool = provider === 'codex' ? tool === 'shell' : /^(?:read|Read)$/.test(tool);
114
+ if (!readTool) continue;
115
+ if (file && file.test(text)) { read = true; evidence.push(callRecord(c)); continue; }
116
+ for (const m of text.matchAll(SKILL_MD)) if (m[1] !== name) others.add(m[1]);
117
+ }
118
+ return { observed: native || read, method: native ? 'native_skill_tool' : read ? 'successful_skill_file_read' : null, evidence: evidence.slice(0, 5), others: [...others].slice(0, 10) };
119
+ }
@@ -0,0 +1,212 @@
1
+ // ── /api/skills-studio/v2/evals: skill evaluations over HTTP (P4) ──
2
+ // The routes of lib/skills/evals.js, beside the Skills Service's other v2 routes. Who may do what (as AI creation,
3
+ // lib/skills/creation-api.js: an evaluation runs workers and spends money):
4
+ //
5
+ // the owner from SynaBun's own page: every route, a read included, needs this boot's page secret cookie
6
+ // (lib/page-session.js: never handed to, nor accepted from, SynaBun's automation browser or a peer
7
+ // process the owner-authority lookup refuses) and the owner route guard (its Host, no guest); a
8
+ // write also its X-SynaBun-UI: 1 header (and the guard its Origin). Estimates, starts, reads, waits
9
+ // on and cancels every evaluation, writes review notes on its trials, exports and compares runs,
10
+ // converts another tool's suite, confirms Codex for a skill version. Nothing starts without the
11
+ // owner's Run (or the Assistant's request below). Where the peer lookup cannot run (not macOS, no
12
+ // `ps`), the page secret and the guard are what is asked, as before it (docs/skills-studio.md).
13
+ // the Assistant a request carrying the credential its brain's launch was given (X-Synabun-Brain-Token, resolved
14
+ // to that brain's session): estimates, starts (under its session's budget), reads, waits on,
15
+ // cancels and reports the evaluations of its own session, and reads whether Codex is confirmed for
16
+ // a version (it never confirms it). Its results are the owner's too (the Studio lists every
17
+ // evaluation). X-Synabun-Role and X-Synabun-Terminal prove nothing; a dispatched worker is
18
+ // refused whatever it sends.
19
+ // anyone else 403 (a guest, an agent without a brain's credential).
20
+ //
21
+ // A trial's output file is answered as JSON text (never as a page of this origin), bounded, never through a link.
22
+
23
+ import AdmZip from 'adm-zip';
24
+ import { sendGuardError } from '../http-guards.js';
25
+ import { PathConfineError } from '../path-confine.js';
26
+ import { V2_BASE, principalOf } from './api-v2.js';
27
+ import { isTrialId } from './evals.js';
28
+ import { isEvalRunId } from '../../public/shared/skills/ss-eval-plan.js';
29
+
30
+ const IDEMPOTENCY_TTL_MS = 10 * 60 * 1000;
31
+ const FORMATS = new Set(['native', 'skill-creator', 'agent-skills']);
32
+
33
+ function fail(res, err) {
34
+ const status = err instanceof PathConfineError ? err.status : (Number.isInteger(err?.status) && err.status >= 400 && err.status < 600 ? err.status : 500);
35
+ const body = { ok: false, error: status >= 500 && !(err instanceof PathConfineError) ? `The evaluation failed: ${err?.message || err}` : (err?.message || String(err)) };
36
+ if (typeof err?.code === 'string' && !/^E[A-Z]+$/.test(err.code)) body.code = err.code;
37
+ for (const key of ['problems', 'estimate', 'skipped', 'codex']) if (err?.[key] !== undefined) body[key] = err[key];
38
+ return res.status(status).json(body);
39
+ }
40
+
41
+ /**
42
+ * @param {object} app express app
43
+ * @param {object} o
44
+ * @param {object} o.evals lib/skills/evals.js
45
+ * @param {(req) => boolean} [o.isGuestRequest]
46
+ * @param {Function} [o.ownerGuard] lib/http-guards.js ownerRouteGuard(…) of this server
47
+ * @param {(req) => ({ kind: 'assistant', assistantSessionId } | { kind: 'worker' } | null)} [o.assistantCaller]
48
+ * @param {{ verify(req), require(req, res, next), attach(res) }} [o.pageSession] lib/page-session.js
49
+ */
50
+ export function registerSkillEvalRoutes(app, { evals, isGuestRequest = null, ownerGuard = null, assistantCaller = null, pageSession = null } = {}) {
51
+ const idem = new Map();
52
+ const route = (method, path, ...handlers) => app[method](`${V2_BASE}${path}`, ...handlers);
53
+ const safe = (fn) => async (req, res) => { try { await fn(req, res); } catch (err) { fail(res, err); } };
54
+
55
+ const json = (req, res, next) => {
56
+ if (String(req.headers['sec-fetch-site'] || '').toLowerCase() === 'cross-site') return sendGuardError(res, 403, 'CROSS_SITE', 'Cross-site requests are refused.');
57
+ const hasBody = Number(req.headers['content-length'] || 0) > 0 || req.headers['transfer-encoding'] !== undefined;
58
+ if (hasBody && typeof req.is === 'function' && !req.is('application/json')) return sendGuardError(res, 415, 'UNSUPPORTED_MEDIA_TYPE', 'Send JSON (Content-Type: application/json).');
59
+ if (req.body !== undefined && req.body !== null && (typeof req.body !== 'object' || Array.isArray(req.body))) return res.status(400).json({ ok: false, error: 'The body is a JSON object.', code: 'invalid_body' });
60
+ next();
61
+ };
62
+
63
+ /**
64
+ * Who may evaluate: the owner from the page (a read too: this boot's page secret and the owner route guard; a write
65
+ * also the page header), or an Assistant brain by its credential.
66
+ */
67
+ const caller = (write, { ownerOnly = false } = {}) => (req, res, next) => {
68
+ const principal = principalOf(req, isGuestRequest);
69
+ if (principal.kind === 'guest') return sendGuardError(res, 403, 'GUEST_FORBIDDEN', 'Available to the owner only.');
70
+ if (principal.kind === 'owner') {
71
+ req.evaluator = { kind: 'owner', assistantSessionId: null };
72
+ if (write && req.headers['x-synabun-ui'] !== '1') return sendGuardError(res, 403, 'UI_HEADER_REQUIRED', 'Send the X-SynaBun-UI: 1 header.');
73
+ // The page secret (lib/page-session.js verify: absent, stale, carried by the automation browser or by a peer the
74
+ // owner-authority lookup refuses, it is refused): what a local process that merely left the agent headers out lacks.
75
+ if (pageSession && !pageSession.verify(req)) {
76
+ if (write) return pageSession.require(req, res, next);
77
+ return sendGuardError(res, 403, 'PAGE_SECRET_REQUIRED', 'Evaluations are read from SynaBun\'s own page. Reload the page and try again.');
78
+ }
79
+ return ownerGuard ? ownerGuard(req, res, next) : next();
80
+ }
81
+ let who = null;
82
+ try { who = typeof assistantCaller === 'function' ? assistantCaller(req) : null; } catch { who = null; }
83
+ if (who?.kind === 'worker') return sendGuardError(res, 403, 'WORKERS_NOT_PERMITTED', 'Dispatched workers cannot run skill evaluations: the Assistant or the owner does.');
84
+ if (who?.kind !== 'assistant' || typeof who.assistantSessionId !== 'string' || !who.assistantSessionId) {
85
+ return sendGuardError(res, 403, 'ASSISTANT_CREDENTIAL_REQUIRED', 'Skill evaluations are the owner\'s (Skills Studio) and the SynaBun Assistant\'s own brain\'s (its launch credential): a role header is not enough.');
86
+ }
87
+ if (ownerOnly) return sendGuardError(res, 403, 'UI_ONLY', 'Review notes and the Codex confirmation are the owner\'s, from Skills Studio.');
88
+ req.evaluator = { kind: 'assistant', assistantSessionId: who.assistantSessionId };
89
+ next();
90
+ };
91
+ /** The owner sees every evaluation; an Assistant brain those its own session started. */
92
+ const ownOnly = (run, req) => req.evaluator.kind === 'owner' || (run.by?.kind === 'assistant' && typeof run.assistantSessionId === 'string' && run.assistantSessionId !== '' && run.assistantSessionId === req.evaluator.assistantSessionId);
93
+ const idCheck = (req, res, next) => {
94
+ if (!isEvalRunId(req.params.evalRunId)) return res.status(400).json({ ok: false, error: 'Not an evaluation id.', code: 'invalid_id' });
95
+ if (req.params.trialId !== undefined && !isTrialId(req.params.trialId)) return res.status(400).json({ ok: false, error: 'Not a trial id.', code: 'invalid_trial' });
96
+ next();
97
+ };
98
+ const mine = (req, res) => {
99
+ const run = evals.get(req.params.evalRunId);
100
+ if (!ownOnly(run, req)) { sendGuardError(res, 403, 'NOT_YOURS', 'This evaluation was started by someone else.'); return null; }
101
+ return run;
102
+ };
103
+ /** Same caller, same route, same key within ten minutes: the first answer again (one evaluation, not two). */
104
+ const once = (handler) => async (req, res) => {
105
+ const key = typeof req.headers['idempotency-key'] === 'string' ? req.headers['idempotency-key'] : (typeof req.body?.idempotencyKey === 'string' ? req.body.idempotencyKey : null);
106
+ if (!key) return handler(req, res);
107
+ if (key.length > 200) return res.status(400).json({ ok: false, error: 'An idempotency key is at most 200 characters.', code: 'invalid_idempotency_key' });
108
+ const slot = `${req.evaluator?.kind}\0${req.evaluator?.assistantSessionId || ''}\0${req.method}\0${req.originalUrl || req.url}\0${key}`;
109
+ const hit = idem.get(slot);
110
+ if (hit && Date.now() - hit.at < IDEMPOTENCY_TTL_MS) return res.status(hit.status).json({ ...hit.body, replayed: true });
111
+ const original = res.json.bind(res);
112
+ res.json = (body) => {
113
+ if (res.statusCode < 300) { idem.set(slot, { at: Date.now(), status: res.statusCode, body }); if (idem.size > 300) idem.delete(idem.keys().next().value); }
114
+ return original(body);
115
+ };
116
+ return handler(req, res);
117
+ };
118
+ const bodyOf = (req) => { const b = { ...(req.body || {}) }; delete b.idempotencyKey; delete b.assistantSessionId; delete b.assistant_session_id; return b; };
119
+
120
+ // The fixed paths first: they are not evaluation ids.
121
+ route('get', '/evals/options', caller(false), safe(async (req, res) => res.json({ ok: true, ...(await evals.options()) })));
122
+ route('get', '/evals/queue', caller(false), safe(async (req, res) => {
123
+ const q = evals.queue();
124
+ if (req.evaluator.kind === 'owner') return res.json({ ok: true, ...q });
125
+ // The Assistant sees the queue's length and its own place in it, not what the owner runs.
126
+ const own = (r) => r && evals.get(r.evalRunId)?.assistantSessionId === req.evaluator.assistantSessionId;
127
+ res.json({ ok: true, active: q.active.filter(own), waiting: q.waiting.filter(own), activeCount: q.active.length, waitingCount: q.waiting.length, maxActive: q.maxActive, queueMax: q.queueMax });
128
+ }));
129
+ route('get', '/evals/compare', caller(false), safe(async (req, res) => {
130
+ const a = String(req.query?.a || ''); const b = String(req.query?.b || '');
131
+ if (!isEvalRunId(a) || !isEvalRunId(b)) return res.status(400).json({ ok: false, error: 'Name two evaluations (a, b).', code: 'invalid_id' });
132
+ for (const id of [a, b]) { const run = evals.get(id); if (!ownOnly(run, req)) return sendGuardError(res, 403, 'NOT_YOURS', 'This evaluation was started by someone else.'); }
133
+ res.json({ ok: true, comparison: evals.compare(a, b) });
134
+ }));
135
+ // Codex is the owner's opt-in per skill version (lib/skills/evals.js): whether it is confirmed, and the confirmation.
136
+ route('get', '/evals/codex-trust', caller(false), safe(async (req, res) => {
137
+ const ids = String(req.query?.revisionIds || '').split(',').map((x) => x.trim()).filter(Boolean).slice(0, 10);
138
+ if (!ids.length || ids.some((x) => !/^rev_[0-9a-f]{20}$/.test(x))) return res.status(400).json({ ok: false, error: 'Name the version(s) (revisionIds).', code: 'invalid_request' });
139
+ res.json({ ok: true, ...evals.codexTrust(ids) });
140
+ }));
141
+ route('post', '/evals/codex-trust', json, caller(true, { ownerOnly: true }), safe(async (req, res) => {
142
+ const b = req.body || {};
143
+ res.json({ ok: true, ...evals.setCodexTrust({ revisionIds: Array.isArray(b.revisionIds) ? b.revisionIds : [], trusted: b.trusted !== false }) });
144
+ }));
145
+ route('post', '/evals/estimate', json, caller(true), safe(async (req, res) => res.json({ ok: true, ...(await evals.estimate(bodyOf(req))) })));
146
+ route('post', '/evals/import', json, caller(true), safe(async (req, res) => {
147
+ const b = req.body || {};
148
+ const tooBig = (v) => typeof v === 'string' ? v.length > 1024 * 1024 : JSON.stringify(v ?? null).length > 1024 * 1024;
149
+ if (tooBig(b.tasks) || tooBig(b.triggers)) return res.status(413).json({ ok: false, error: 'A suite to import is at most 1 MB.', code: 'too_large' });
150
+ const out = evals.importSuite({ tasks: b.tasks ?? null, triggers: b.triggers ?? null, skillName: typeof b.skillName === 'string' ? b.skillName.slice(0, 64) : '', seed: Number.isInteger(b.seed) ? b.seed : 42 });
151
+ res.status(out.ok ? 200 : 422).json({ ok: out.ok, ...out, ...(out.ok ? {} : { error: out.problems[0]?.message || 'The suite cannot be imported.', code: out.problems[0]?.code || 'invalid_suite' }) });
152
+ }));
153
+ route('get', '/evals', caller(false), safe(async (req, res) => {
154
+ const q = req.query || {};
155
+ const skillId = typeof q.skillId === 'string' && /^skl_[0-9a-f]{20}$/.test(q.skillId) ? q.skillId : null;
156
+ const revisionId = typeof q.revisionId === 'string' && /^rev_[0-9a-f]{20}$/.test(q.revisionId) ? q.revisionId : null;
157
+ const list = evals.list({ skillId, revisionId, limit: Number(q.limit) || 30, ...(req.evaluator.kind === 'assistant' ? { assistantSessionId: req.evaluator.assistantSessionId } : {}) });
158
+ res.json({ ok: true, evals: list, queue: req.evaluator.kind === 'owner' ? evals.queue() : null });
159
+ }));
160
+ route('post', '/evals', json, caller(true), once(safe(async (req, res) => {
161
+ const run = await evals.start(bodyOf(req), { by: { kind: req.evaluator.kind }, assistantSessionId: req.evaluator.assistantSessionId });
162
+ res.status(202).json({ ok: true, eval: run });
163
+ })));
164
+ route('get', '/evals/:evalRunId', caller(false), idCheck, safe(async (req, res) => {
165
+ const run = mine(req, res); if (!run) return;
166
+ res.json({ ok: true, eval: run });
167
+ }));
168
+ route('get', '/evals/:evalRunId/wait', caller(false), idCheck, safe(async (req, res) => {
169
+ if (!mine(req, res)) return;
170
+ const since = Number.isInteger(Number(req.query?.since)) ? Number(req.query.since) : -1;
171
+ const run = await evals.wait(req.params.evalRunId, { sinceVersion: since, timeoutMs: Math.min(50, Math.max(1, Number(req.query?.seconds) || 25)) * 1000, compact: req.query?.full !== '1' });
172
+ res.json({ ok: true, eval: run });
173
+ }));
174
+ route('post', '/evals/:evalRunId/cancel', json, caller(true), idCheck, safe(async (req, res) => {
175
+ if (!mine(req, res)) return;
176
+ res.json({ ok: true, eval: await evals.cancel(req.params.evalRunId) });
177
+ }));
178
+ route('post', '/evals/:evalRunId/feedback', json, caller(true, { ownerOnly: true }), idCheck, safe(async (req, res) => {
179
+ const b = req.body || {};
180
+ res.json({ ok: true, feedback: evals.feedback(req.params.evalRunId, { trialId: typeof b.trialId === 'string' ? b.trialId : '', text: typeof b.text === 'string' ? b.text : '', reviewed: typeof b.reviewed === 'boolean' ? b.reviewed : null }) });
181
+ }));
182
+ route('get', '/evals/:evalRunId/trials/:trialId', caller(false), idCheck, safe(async (req, res) => {
183
+ if (!mine(req, res)) return;
184
+ res.json({ ok: true, trial: evals.trial(req.params.evalRunId, req.params.trialId) });
185
+ }));
186
+ route('get', '/evals/:evalRunId/trials/:trialId/file', caller(false), idCheck, safe(async (req, res) => {
187
+ if (!mine(req, res)) return;
188
+ const path = typeof req.query?.path === 'string' ? req.query.path : '';
189
+ if (!path || path.length > 300 || path.split('/').includes('..') || path.startsWith('/')) return res.status(400).json({ ok: false, error: 'Name a file the trial wrote (path).', code: 'invalid_path' });
190
+ res.set('Cache-Control', 'no-store');
191
+ res.json({ ok: true, file: evals.trialFile(req.params.evalRunId, req.params.trialId, path) });
192
+ }));
193
+ route('get', '/evals/:evalRunId/export', caller(false), idCheck, safe(async (req, res) => {
194
+ if (!mine(req, res)) return;
195
+ const format = typeof req.query?.format === 'string' && FORMATS.has(req.query.format) ? req.query.format : 'native';
196
+ const out = evals.exportRun(req.params.evalRunId, format);
197
+ const safeName = out.name.replace(/[^A-Za-z0-9._-]/g, '_');
198
+ res.set('Cache-Control', 'no-store');
199
+ res.set('X-Content-Type-Options', 'nosniff');
200
+ if (out.name.endsWith('.zip')) {
201
+ const zip = new AdmZip();
202
+ const folder = safeName.replace(/\.skill-creator\.zip$/, '');
203
+ for (const e of out.entries) zip.addFile(`${folder}/${e.path}`, Buffer.from(e.text, 'utf-8'));
204
+ res.set('Content-Type', 'application/zip');
205
+ res.set('Content-Disposition', `attachment; filename="${safeName}"`);
206
+ return res.send(zip.toBuffer());
207
+ }
208
+ res.set('Content-Type', 'application/json; charset=utf-8');
209
+ res.set('Content-Disposition', `attachment; filename="${safeName}"`);
210
+ res.send(out.entries[0].text);
211
+ }));
212
+ }