@bevel-software/platform-core-backend 0.22.0 → 0.23.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 (325) hide show
  1. package/dist/core/core-ports.d.ts +11 -1
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts +3 -3
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +30 -11
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +9 -2
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +112 -23
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +34 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +89 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +0 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +11 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +42 -12
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +67 -10
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +214 -35
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/access/access.routes.d.ts.map +1 -1
  31. package/dist/modules/access/access.routes.js +13 -15
  32. package/dist/modules/access/access.routes.js.map +1 -1
  33. package/dist/modules/auth/account-admission.d.ts +58 -7
  34. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  35. package/dist/modules/auth/account-admission.js +44 -1
  36. package/dist/modules/auth/account-admission.js.map +1 -1
  37. package/dist/modules/auth/account.routes.d.ts +1 -1
  38. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  39. package/dist/modules/auth/account.routes.js +61 -5
  40. package/dist/modules/auth/account.routes.js.map +1 -1
  41. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  42. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  43. package/dist/modules/auth/auth.middleware.js +18 -7
  44. package/dist/modules/auth/auth.middleware.js.map +1 -1
  45. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  46. package/dist/modules/auth/auth.routes.js +8 -0
  47. package/dist/modules/auth/auth.routes.js.map +1 -1
  48. package/dist/modules/auth/auth.service.d.ts +70 -2
  49. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  50. package/dist/modules/auth/auth.service.js +176 -7
  51. package/dist/modules/auth/auth.service.js.map +1 -1
  52. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  53. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  54. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  55. package/dist/modules/database/core-schema.d.ts +41 -24
  56. package/dist/modules/database/core-schema.d.ts.map +1 -1
  57. package/dist/modules/database/core-schema.js +35 -27
  58. package/dist/modules/database/core-schema.js.map +1 -1
  59. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  60. package/dist/modules/kb-fs/branch-name.js +12 -2
  61. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  62. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  63. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  64. package/dist/modules/kb-fs/repo-path.js +79 -0
  65. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  66. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  67. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  68. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  69. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  70. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  71. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  72. package/dist/modules/kb-sync/sync-auth.js +1 -1
  73. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  74. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  75. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  76. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  77. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  78. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  79. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  80. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  81. package/dist/modules/settings/deployment-settings.service.js +8 -3
  82. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  83. package/dist/modules/settings/setup.routes.d.ts +52 -1
  84. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  85. package/dist/modules/settings/setup.routes.js +207 -19
  86. package/dist/modules/settings/setup.routes.js.map +1 -1
  87. package/dist/modules/skills/skills.contract.d.ts +90 -18
  88. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  89. package/dist/modules/skills/skills.contract.js +4 -1
  90. package/dist/modules/skills/skills.contract.js.map +1 -1
  91. package/dist/modules/skills/skills.service.d.ts +98 -9
  92. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  93. package/dist/modules/skills/skills.service.js +239 -37
  94. package/dist/modules/skills/skills.service.js.map +1 -1
  95. package/dist/modules/skills/skills.tools.d.ts +7 -0
  96. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  97. package/dist/modules/skills/skills.tools.js +71 -7
  98. package/dist/modules/skills/skills.tools.js.map +1 -1
  99. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  100. package/dist/modules/tool-auth/external-api-key.service.js +4 -2
  101. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  102. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  103. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  104. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  105. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  106. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  107. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  108. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  109. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  110. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  111. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  112. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  113. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  114. package/dist/modules/tool-helpers/validate-token.js +1 -1
  115. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  116. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  117. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  118. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  119. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  120. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  121. package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
  122. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  123. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  124. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  125. package/dist/modules/workflow/file-lock.service.js +30 -0
  126. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  127. package/dist/modules/workflow/git/git.service.d.ts +69 -0
  128. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  129. package/dist/modules/workflow/git/git.service.js +153 -2
  130. package/dist/modules/workflow/git/git.service.js.map +1 -1
  131. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  132. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  133. package/dist/modules/workflow/pending-commits.service.js +55 -0
  134. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  135. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  136. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  137. package/dist/modules/workflow/workflow-hooks.js +16 -1
  138. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  139. package/dist/modules/workflow/workflow.service.d.ts +60 -6
  140. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  141. package/dist/modules/workflow/workflow.service.js +111 -4
  142. package/dist/modules/workflow/workflow.service.js.map +1 -1
  143. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  144. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  145. package/dist/modules/workspace/agent-access.gate.js +123 -0
  146. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  147. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  148. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  149. package/dist/modules/workspace/routine-write-policy.js +5 -6
  150. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  151. package/dist/modules/workspace/session-sink.d.ts +5 -5
  152. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  153. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  154. package/dist/modules/workspace/set-aside-clone.js +92 -0
  155. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  156. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
  157. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  158. package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
  159. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  160. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  161. package/dist/modules/workspace/workspace.routes.js +92 -3
  162. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  163. package/dist/modules/workspace/workspace.service.d.ts +115 -5
  164. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  165. package/dist/modules/workspace/workspace.service.js +255 -25
  166. package/dist/modules/workspace/workspace.service.js.map +1 -1
  167. package/dist/modules/workspace/workspace.tools.d.ts +2 -2
  168. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  169. package/dist/modules/workspace/workspace.tools.js +309 -115
  170. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  171. package/dist/modules/write-access/write-access.d.ts +60 -0
  172. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  173. package/dist/modules/write-access/write-access.js +129 -0
  174. package/dist/modules/write-access/write-access.js.map +1 -0
  175. package/dist/shared/domain-errors.d.ts +25 -0
  176. package/dist/shared/domain-errors.d.ts.map +1 -1
  177. package/dist/shared/domain-errors.js +28 -0
  178. package/dist/shared/domain-errors.js.map +1 -1
  179. package/dist/shared/git.contract.d.ts +20 -0
  180. package/dist/shared/git.contract.d.ts.map +1 -1
  181. package/dist/shared/git.contract.js +26 -0
  182. package/dist/shared/git.contract.js.map +1 -1
  183. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  184. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  185. package/dist/tenancy/static-tenant-source.js +0 -2
  186. package/dist/tenancy/static-tenant-source.js.map +1 -1
  187. package/migrations/0014_change_request_closed_reason.sql +1 -0
  188. package/migrations/0015_account_deactivation.sql +3 -0
  189. package/migrations/meta/0014_snapshot.json +2265 -0
  190. package/migrations/meta/0015_snapshot.json +2271 -0
  191. package/migrations/meta/_journal.json +14 -0
  192. package/package.json +3 -3
  193. package/src/__tests__/kb-layout-config.test.ts +0 -2
  194. package/src/__tests__/retired-settings.test.ts +96 -0
  195. package/src/core/__tests__/gated-boot.test.ts +132 -0
  196. package/src/core/__tests__/lifecycle.test.ts +170 -1
  197. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  198. package/src/core/core-ports.ts +11 -1
  199. package/src/core/create-core-server.ts +32 -14
  200. package/src/core/create-core-services.ts +129 -23
  201. package/src/core/lifecycle.ts +98 -13
  202. package/src/core-config.ts +13 -14
  203. package/src/index.ts +12 -1
  204. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  205. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  206. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  207. package/src/modules/access/access-control.interface.ts +43 -12
  208. package/src/modules/access/access-control.service.ts +234 -36
  209. package/src/modules/access/access.routes.ts +13 -15
  210. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
  211. package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
  212. package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
  213. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  214. package/src/modules/auth/__tests__/auth.service.test.ts +1 -1
  215. package/src/modules/auth/account-admission.ts +78 -9
  216. package/src/modules/auth/account.routes.ts +63 -7
  217. package/src/modules/auth/auth.middleware.ts +19 -8
  218. package/src/modules/auth/auth.routes.ts +8 -0
  219. package/src/modules/auth/auth.service.ts +179 -6
  220. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  221. package/src/modules/database/core-schema.ts +35 -27
  222. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  223. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  224. package/src/modules/kb-fs/branch-name.ts +14 -1
  225. package/src/modules/kb-fs/repo-path.ts +85 -0
  226. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  227. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  228. package/src/modules/kb-sync/sync-auth.ts +2 -2
  229. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  230. package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
  231. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  232. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  233. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  234. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  235. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
  236. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
  237. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
  238. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  239. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  240. package/src/modules/settings/deployment-settings.service.ts +8 -3
  241. package/src/modules/settings/setup.routes.ts +263 -19
  242. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  243. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  244. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  245. package/src/modules/skills/skills.contract.ts +91 -18
  246. package/src/modules/skills/skills.service.ts +278 -41
  247. package/src/modules/skills/skills.tools.ts +80 -8
  248. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  249. package/src/modules/tool-auth/external-api-key.service.ts +4 -2
  250. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  251. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  252. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  253. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  254. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  255. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  256. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  257. package/src/modules/tool-helpers/validate-token.ts +1 -1
  258. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  259. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  260. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  261. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
  262. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  263. package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
  264. package/src/modules/workflow/file-lock.service.ts +31 -0
  265. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  266. package/src/modules/workflow/git/git.service.ts +168 -0
  267. package/src/modules/workflow/pending-commits.service.ts +59 -1
  268. package/src/modules/workflow/workflow-hooks.ts +64 -26
  269. package/src/modules/workflow/workflow.service.ts +121 -4
  270. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  271. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
  272. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  273. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  274. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  275. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  276. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  277. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  278. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
  279. package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
  280. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
  281. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +685 -14
  283. package/src/modules/workspace/agent-access.gate.ts +164 -0
  284. package/src/modules/workspace/routine-write-policy.ts +5 -6
  285. package/src/modules/workspace/session-sink.ts +5 -5
  286. package/src/modules/workspace/set-aside-clone.ts +96 -0
  287. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
  288. package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
  289. package/src/modules/workspace/workspace.routes.ts +95 -3
  290. package/src/modules/workspace/workspace.service.ts +256 -24
  291. package/src/modules/workspace/workspace.tools.ts +338 -119
  292. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  293. package/src/modules/write-access/write-access.ts +153 -0
  294. package/src/shared/domain-errors.ts +31 -0
  295. package/src/shared/git.contract.ts +27 -0
  296. package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
  297. package/src/tenancy/static-tenant-source.ts +0 -3
  298. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  299. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  300. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  301. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  302. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  303. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  304. package/dist/modules/workflow/session-ontology.service.js +0 -147
  305. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  306. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  307. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  308. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  309. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  310. package/dist/shared/kb-layout.d.ts +0 -39
  311. package/dist/shared/kb-layout.d.ts.map +0 -1
  312. package/dist/shared/kb-layout.js +0 -103
  313. package/dist/shared/kb-layout.js.map +0 -1
  314. package/dist/shared/kb-layout.test.d.ts +0 -2
  315. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  316. package/dist/shared/kb-layout.test.js +0 -75
  317. package/dist/shared/kb-layout.test.js.map +0 -1
  318. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  319. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  320. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  321. package/src/modules/workflow/session-ontology.service.ts +0 -183
  322. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  323. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  324. package/src/shared/kb-layout.test.ts +0 -98
  325. package/src/shared/kb-layout.ts +0 -102
@@ -0,0 +1,248 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import express from 'express';
3
+ import type { Server } from 'node:http';
4
+ import type { AddressInfo } from 'node:net';
5
+ import type { Request, Response } from 'express';
6
+ import {
7
+ alwaysWritable,
8
+ createWriteAccessRoutes,
9
+ createWriteGateMiddleware,
10
+ isAlwaysWritable,
11
+ READ_ONLY_CODE,
12
+ type IWriteAccess,
13
+ } from '../write-access.js';
14
+ import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
15
+ import type { ResolveToolContext } from '../../tool-helpers/tool-context.js';
16
+
17
+ const READ_ONLY_MESSAGE = 'This workspace has 5 people switched on and room for 3.';
18
+ const readOnly: IWriteAccess = { canWrite: async () => ({ ok: false, message: READ_ONLY_MESSAGE }) };
19
+
20
+ const servers: Server[] = [];
21
+ afterEach(async () => {
22
+ await Promise.all(servers.splice(0).map((s) => new Promise<void>((resolve) => s.close(() => resolve()))));
23
+ });
24
+
25
+ async function listen(app: express.Express): Promise<string> {
26
+ const server = app.listen(0);
27
+ servers.push(server);
28
+ await new Promise<void>((resolve) => server.once('listening', () => resolve()));
29
+ const { port } = server.address() as AddressInfo;
30
+ return `http://127.0.0.1:${port}`;
31
+ }
32
+
33
+ describe('isAlwaysWritable', () => {
34
+ it.each([
35
+ ['GET', '/api/workspace/w1/file'],
36
+ ['HEAD', '/api/workspace/w1/file'],
37
+ ['POST', '/health'],
38
+ ['POST', '/api/auth/login'],
39
+ ['POST', '/api/mcp'],
40
+ ['POST', '/api/mcp/external-api-keys'],
41
+ ['POST', '/api/agent/tools/write_file'],
42
+ ['POST', '/api/admin/accounts'],
43
+ ['POST', '/api/admin/accounts/u1/deactivate'],
44
+ ['DELETE', '/api/admin/accounts/u1'],
45
+ ['POST', '/api/sync'],
46
+ ['POST', '/api/setup/settings'],
47
+ ['POST', '/api/workspace/w1/access/batch'],
48
+ ['POST', '/api/workspace/w1/workflow/locks/heartbeat'],
49
+ ['POST', '/api/events/s1/focus'],
50
+ ])('lets %s %s through', (method, path) => {
51
+ expect(isAlwaysWritable(method, path)).toBe(true);
52
+ });
53
+
54
+ it.each([
55
+ ['PUT', '/api/workspace/w1/file'],
56
+ ['DELETE', '/api/workspace/w1/file'],
57
+ ['POST', '/api/workspace/w1/workflow/changes'],
58
+ ['POST', '/api/workflow/change-requests/7/merge'],
59
+ ['POST', '/api/workspace/w1/workflow/locks'],
60
+ ['POST', '/api/plugins'],
61
+ ['POST', '/api/admin/groups'],
62
+ // A look-alike of an allowed path is not that path.
63
+ ['POST', '/api/admin/accountsx'],
64
+ ['POST', '/api/workspace/w1/access/batch/extra'],
65
+ // A route nobody listed is refused by default.
66
+ ['POST', '/api/some-new-route'],
67
+ // The API root is inside the gated namespace too.
68
+ ['POST', '/api'],
69
+ ])('holds %s %s for the port', (method, path) => {
70
+ expect(isAlwaysWritable(method, path)).toBe(false);
71
+ });
72
+
73
+ it("lets the host's own paths through", () => {
74
+ expect(isAlwaysWritable('PUT', '/api/cloud/workspace/billing/seats', ['/api/cloud/workspace/billing'])).toBe(true);
75
+ expect(isAlwaysWritable('PUT', '/api/cloud/workspace/sign-in/domains', ['/api/cloud/workspace/billing'])).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe('createWriteGateMiddleware', () => {
80
+ function appWith(writeAccess: IWriteAccess) {
81
+ const app = express();
82
+ app.use(createWriteGateMiddleware(writeAccess));
83
+ app.all('/{*any}', (_req, res) => {
84
+ res.json({ reached: true });
85
+ });
86
+ return app;
87
+ }
88
+
89
+ it("refuses a change with the port's words and a code the app recognises", async () => {
90
+ const base = await listen(appWith(readOnly));
91
+ const res = await fetch(`${base}/api/workspace/w1/file`, { method: 'PUT' });
92
+ expect(res.status).toBe(403);
93
+ expect(await res.json()).toEqual({ error: READ_ONLY_MESSAGE, code: READ_ONLY_CODE });
94
+ });
95
+
96
+ /**
97
+ * The router matches a path whatever its letter case, so the gate has to
98
+ * read it the same way: here the route is mounted as the server mounts
99
+ * its own, under `/api`, and a spelling the gate did not recognise as
100
+ * that namespace would reach it.
101
+ */
102
+ it('refuses a change however the path is cased, since the router matches it either way', async () => {
103
+ const app = express();
104
+ app.use(createWriteGateMiddleware(readOnly));
105
+ const routes = express.Router();
106
+ const wrote = vi.fn();
107
+ routes.put('/workspace/:id/file', (_req, res) => {
108
+ wrote();
109
+ res.json({ wrote: true });
110
+ });
111
+ app.use('/api', routes);
112
+ const base = await listen(app);
113
+ for (const path of ['/api/workspace/w1/file', '/API/workspace/w1/file', '/Api/Workspace/w1/File']) {
114
+ const res = await fetch(`${base}${path}`, { method: 'PUT' });
115
+ expect({ path, status: res.status }).toEqual({ path, status: 403 });
116
+ }
117
+ expect(wrote).not.toHaveBeenCalled();
118
+ });
119
+
120
+ it('keeps an open route open however it is cased', async () => {
121
+ const base = await listen(appWith(readOnly));
122
+ expect((await fetch(`${base}/API/Auth/login`, { method: 'POST' })).status).toBe(200);
123
+ });
124
+
125
+ it('still serves reads, and the routes that sign people in and manage accounts', async () => {
126
+ const base = await listen(appWith(readOnly));
127
+ expect((await fetch(`${base}/api/workspace/w1/file`)).status).toBe(200);
128
+ expect((await fetch(`${base}/api/auth/login`, { method: 'POST' })).status).toBe(200);
129
+ expect((await fetch(`${base}/api/admin/accounts/u1/deactivate`, { method: 'POST' })).status).toBe(200);
130
+ });
131
+
132
+ it('does not ask the port about a request it lets through anyway', async () => {
133
+ const canWrite = vi.fn(readOnly.canWrite);
134
+ const base = await listen(appWith({ canWrite }));
135
+ await fetch(`${base}/api/workspace/w1/file`);
136
+ await fetch(`${base}/api/auth/login`, { method: 'POST' });
137
+ expect(canWrite).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('lets a change through while the port says yes', async () => {
141
+ const base = await listen(appWith({ canWrite: async () => ({ ok: true }) }));
142
+ expect((await fetch(`${base}/api/workspace/w1/file`, { method: 'PUT' })).status).toBe(200);
143
+ });
144
+
145
+ it('lets a change through when the port itself fails, rather than freezing the workspace', async () => {
146
+ const base = await listen(appWith({ canWrite: async () => Promise.reject(new Error('billing down')) }));
147
+ expect((await fetch(`${base}/api/workspace/w1/file`, { method: 'PUT' })).status).toBe(200);
148
+ });
149
+
150
+ it('is a no-op on core, which is always writable', async () => {
151
+ const base = await listen(appWith(alwaysWritable));
152
+ expect((await fetch(`${base}/api/workspace/w1/file`, { method: 'PUT' })).status).toBe(200);
153
+ });
154
+ });
155
+
156
+ describe('the tool layer', () => {
157
+ /**
158
+ * One tool, mounted the way the server mounts it and called over HTTP: a
159
+ * parsed body, then an auth step that does not wait for anything, then the
160
+ * handler. A request closes once its body has been read, so only a real
161
+ * request shows what the handler makes of that.
162
+ */
163
+ async function mounted(writeAccess: IWriteAccess, write: boolean) {
164
+ const resolve = (async () => ({})) as unknown as ResolveToolContext;
165
+ const handler = vi.fn(async () => ({ done: true }));
166
+ const closed = { count: 0 };
167
+ const app = express();
168
+ app.use(express.json());
169
+ app.use((req: Request, res: Response, next) => {
170
+ req.toolAuth = { source: 'external', userId: 'u1', scope: 'write' } as Request['toolAuth'];
171
+ res.on('close', () => {
172
+ closed.count += 1;
173
+ });
174
+ next();
175
+ });
176
+ app.post('/api/agent/tools/a_tool', createToolHandlerFactory(resolve, writeAccess)(handler as never, { write }));
177
+ const url = `${await listen(app)}/api/agent/tools/a_tool`;
178
+ const post = (signal?: AbortSignal) =>
179
+ fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}', signal });
180
+ return { post, handler, closed };
181
+ }
182
+
183
+ it('refuses a write tool while the deployment is read-only', async () => {
184
+ const { post, handler } = await mounted(readOnly, true);
185
+ const res = await post();
186
+ expect(res.status).toBe(403);
187
+ expect(await res.json()).toEqual({ error: READ_ONLY_MESSAGE, code: READ_ONLY_CODE });
188
+ expect(handler).not.toHaveBeenCalled();
189
+ });
190
+
191
+ /**
192
+ * The verdict is awaited, and by then the request has closed: its body was
193
+ * read. Taken for the client leaving, that left every write tool on a
194
+ * deployment with a port unanswered, for as long as the caller would wait.
195
+ */
196
+ it('answers a write tool the port allows, with the client still there', async () => {
197
+ const { post, handler } = await mounted({ canWrite: async () => ({ ok: true }) }, true);
198
+ const res = await post();
199
+ expect(res.status).toBe(200);
200
+ expect(await res.json()).toEqual({ done: true });
201
+ expect(handler).toHaveBeenCalledTimes(1);
202
+ });
203
+
204
+ it('does not run a write tool whose client left while the verdict was awaited', async () => {
205
+ const asked = { count: 0 };
206
+ let allow: () => void = () => undefined;
207
+ const { post, handler, closed } = await mounted(
208
+ {
209
+ canWrite: () => {
210
+ asked.count += 1;
211
+ return new Promise((resolve) => {
212
+ allow = () => resolve({ ok: true });
213
+ });
214
+ },
215
+ },
216
+ true,
217
+ );
218
+ const leaving = new AbortController();
219
+ const call = post(leaving.signal).catch(() => undefined);
220
+ await vi.waitFor(() => expect(asked.count).toBe(1));
221
+ leaving.abort();
222
+ await call;
223
+ await vi.waitFor(() => expect(closed.count).toBe(1));
224
+ allow();
225
+ await new Promise((resolve) => setTimeout(resolve, 20));
226
+ expect(handler).not.toHaveBeenCalled();
227
+ });
228
+
229
+ it('still runs a read tool', async () => {
230
+ const { post, handler } = await mounted(readOnly, false);
231
+ expect((await post()).status).toBe(200);
232
+ expect(handler).toHaveBeenCalledTimes(1);
233
+ });
234
+ });
235
+
236
+ describe('GET /write-access', () => {
237
+ it("says whether the deployment can be changed, and the port's words when not", async () => {
238
+ const app = express();
239
+ app.use('/api', createWriteAccessRoutes(readOnly));
240
+ const base = await listen(app);
241
+ expect(await (await fetch(`${base}/api/write-access`)).json()).toEqual({ writable: false, message: READ_ONLY_MESSAGE });
242
+
243
+ const open = express();
244
+ open.use('/api', createWriteAccessRoutes(alwaysWritable));
245
+ const openBase = await listen(open);
246
+ expect(await (await fetch(`${openBase}/api/write-access`)).json()).toEqual({ writable: true });
247
+ });
248
+ });
@@ -0,0 +1,153 @@
1
+ import express, { type RequestHandler } from 'express';
2
+ import { logger } from '../../shared/logging.js';
3
+
4
+ const log = logger('write-access');
5
+
6
+ /**
7
+ * Whether the deployment may be CHANGED right now. The seam a host that
8
+ * sells seats needs for a workspace that has more people on it than its plan
9
+ * allows: nobody is removed and everyone can still sign in and read, but
10
+ * nothing can be edited until an admin brings the count down or the plan up
11
+ * — the way Notion handles a lapsed workspace.
12
+ *
13
+ * Core is always writable. The port is asked on every request that would
14
+ * change something (a mutating HTTP route, a write tool), so a host answers
15
+ * from something cheap — its own cached view of the plan.
16
+ */
17
+ export type WriteAccessVerdict = { ok: true } | { ok: false; message: string };
18
+
19
+ export interface IWriteAccess {
20
+ canWrite(): Promise<WriteAccessVerdict>;
21
+ /**
22
+ * Paths of the host's own routes that must stay usable while the
23
+ * deployment is read-only — above all, the ones that END it (buying
24
+ * seats, the billing portal). Matched as prefixes of the request path.
25
+ */
26
+ readonly alwaysWritablePaths?: readonly string[];
27
+ }
28
+
29
+ /** Core's default: always writable. */
30
+ export const alwaysWritable: IWriteAccess = {
31
+ canWrite: async () => ({ ok: true }),
32
+ };
33
+
34
+ /** The `code` a refused write answers with, so a client can tell it from a permission error. */
35
+ export const READ_ONLY_CODE = 'workspace_read_only';
36
+
37
+ const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
38
+
39
+ /**
40
+ * The mutating routes that stay open while the deployment is read-only.
41
+ * EVERY OTHER mutating route under `/api` is refused, so a route added
42
+ * later is read-only until someone decides otherwise — the safe default for
43
+ * a list nobody re-reads when adding a route.
44
+ *
45
+ * What stays open, and why:
46
+ * - Signing in and out of everything: sessions, agent connections and
47
+ * their keys. A read-only workspace is still one people read.
48
+ * - Account administration: the way an admin brings the number of people
49
+ * down. Erasing an account with `removeFromAccess` commits to the
50
+ * knowledge base, and that commit is part of the same act.
51
+ * - The tool surface: every tool call is judged by the tool layer itself,
52
+ * which knows a read tool from a write tool (see `toolHandler`); the MCP
53
+ * endpoint only relays to it.
54
+ * - Deployment setup, tool secrets and pulling remote changes in: the
55
+ * deployment's configuration, not its content, and what an admin needs
56
+ * to keep it working.
57
+ * - Requests that are POST only because they carry a body: permission
58
+ * queries, previews, probes, focus and heartbeat signals, fetching
59
+ * remotes, accepting a review baseline. None of them changes content.
60
+ */
61
+ const ALWAYS_WRITABLE: readonly RegExp[] = [
62
+ /^\/api\/auth\//,
63
+ /^\/api\/mcp(\/|$)/,
64
+ /^\/api\/agent\//,
65
+ /^\/api\/admin\/accounts(\/|$)/,
66
+ /^\/api\/admin\/connection-keys\//,
67
+ /^\/api\/audit\//,
68
+ /^\/api\/sync(\/|$)/,
69
+ /^\/api\/setup\//,
70
+ /^\/api\/secrets\//,
71
+ /^\/api\/admin\/github-facade\//,
72
+ /^\/api\/workspace\/[^/]+\/access\/batch$/,
73
+ /^\/api\/workspace\/[^/]+\/review\/accept$/,
74
+ /^\/api\/workspace\/[^/]+\/flush$/,
75
+ /^\/api\/workspace\/[^/]+\/workflow\/refresh-remotes$/,
76
+ /^\/api\/workspace\/[^/]+\/workflow\/locks\/heartbeat$/,
77
+ /^\/api\/events\/[^/]+\/focus$/,
78
+ /^\/api\/tools\/preview$/,
79
+ ];
80
+
81
+ /**
82
+ * Whether a request may go ahead while the deployment is read-only, without asking.
83
+ *
84
+ * Judged on the path in LOWER CASE, because that is how the router reads
85
+ * it: routes match whatever the letter case, so `/API/workspace/…` reaches
86
+ * the same handler as `/api/workspace/…`. Judged as written, it would be
87
+ * outside the gated namespace and walk past the gate.
88
+ */
89
+ export function isAlwaysWritable(method: string, path: string, hostPaths: readonly string[] = []): boolean {
90
+ if (!MUTATING_METHODS.has(method.toUpperCase())) return true;
91
+ const asRouted = path.toLowerCase();
92
+ if (asRouted !== '/api' && !asRouted.startsWith('/api/')) return true;
93
+ return (
94
+ ALWAYS_WRITABLE.some((rule) => rule.test(asRouted)) ||
95
+ hostPaths.some((prefix) => asRouted.startsWith(prefix.toLowerCase()))
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Refuse every change to the deployment while the port says it is
101
+ * read-only, with a 403 the app recognises by {@link READ_ONLY_CODE}.
102
+ * Mounted ahead of every route under `/api`; a request the port is not
103
+ * asked about costs nothing. A port that fails is treated as writable —
104
+ * a billing lookup that is down must not freeze every workspace.
105
+ */
106
+ export function createWriteGateMiddleware(writeAccess: IWriteAccess): RequestHandler {
107
+ if (writeAccess === alwaysWritable) return (_req, _res, next) => next();
108
+ return async (req, res, next) => {
109
+ if (isAlwaysWritable(req.method, req.path, writeAccess.alwaysWritablePaths)) return next();
110
+ let verdict: WriteAccessVerdict;
111
+ try {
112
+ verdict = await writeAccess.canWrite();
113
+ } catch (err) {
114
+ log.warn('the write-access port failed; letting the request through:', { err });
115
+ return next();
116
+ }
117
+ if (verdict.ok) return next();
118
+ res.status(403).json({ error: verdict.message, code: READ_ONLY_CODE });
119
+ };
120
+ }
121
+
122
+ /**
123
+ * The verdict for a write tool, the tool layer's half of the gate: `null`
124
+ * when it may run, the refusal otherwise. Same failure rule as the HTTP gate.
125
+ */
126
+ export async function refuseWriteTool(writeAccess: IWriteAccess): Promise<string | null> {
127
+ if (writeAccess === alwaysWritable) return null;
128
+ try {
129
+ const verdict = await writeAccess.canWrite();
130
+ return verdict.ok ? null : verdict.message;
131
+ } catch (err) {
132
+ log.warn('the write-access port failed; letting the tool run:', { err });
133
+ return null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * GET /api/write-access — whether the deployment can be changed right now,
139
+ * and if not, the host's words for why. The app reads it to say so once, in
140
+ * a banner, instead of letting every save fail on its own.
141
+ */
142
+ export function createWriteAccessRoutes(writeAccess: IWriteAccess): express.Router {
143
+ const router = express.Router();
144
+ router.get('/write-access', async (_req, res) => {
145
+ try {
146
+ const verdict = await writeAccess.canWrite();
147
+ res.json(verdict.ok ? { writable: true } : { writable: false, message: verdict.message });
148
+ } catch {
149
+ res.json({ writable: true });
150
+ }
151
+ });
152
+ return router;
153
+ }
@@ -377,6 +377,37 @@ export class PathNotFoundError extends WorkflowDomainError {
377
377
  }
378
378
  }
379
379
 
380
+ /**
381
+ * The one sentence every read of a past save answers when that save is not in
382
+ * the history of the branch being viewed.
383
+ *
384
+ * Written once, here, because four surfaces have to give the same answer: the
385
+ * bytes route (`?ref=`), the patch route (`show-file`), the before/after
386
+ * contents route (`file-at-change`), and anything later that serves a file at
387
+ * a commit. A save the history panel listed came off `git log` on this
388
+ * workspace's own branch, so it can never be refused this way — what this
389
+ * refuses is a sha from someone else's branch, or one that was invented.
390
+ */
391
+ export const VERSION_NOT_ON_BRANCH_MESSAGE =
392
+ "This version is not in this file's history on this branch.";
393
+
394
+ /**
395
+ * The caller named a save that is not an ancestor of the branch this
396
+ * workspace has checked out.
397
+ *
398
+ * 404, and DELIBERATELY the same 404 for a sha that exists on another branch
399
+ * as for one that exists nowhere: telling the two apart would turn the route
400
+ * into an oracle for "does this commit exist in the repository", which is
401
+ * exactly the reading a branch-scoped rule exists to prevent.
402
+ */
403
+ export class VersionNotOnBranchError extends WorkflowDomainError {
404
+ readonly kind = 'version-not-on-branch' as const;
405
+ constructor() {
406
+ super(VERSION_NOT_ON_BRANCH_MESSAGE, 404, { kind: 'version-not-on-branch' });
407
+ this.name = 'VersionNotOnBranchError';
408
+ }
409
+ }
410
+
380
411
  /**
381
412
  * An archive the caller uploaded that the zip reader cannot open. 422, not
382
413
  * 400: the request is well-formed and the path is fine — the BYTES are not a
@@ -209,6 +209,33 @@ export class GitRunError extends Error {
209
209
  }
210
210
  }
211
211
 
212
+ /**
213
+ * Refuse a `cat-file --batch` spec that is not ONE line.
214
+ *
215
+ * `--batch` reads one object name per LINE, so a spec carrying a `\n` is two
216
+ * requests. For a single-spec read that means git answers about the part
217
+ * before the break — bytes the caller then presents as the whole path's,
218
+ * having had its access gates answered about a string that names nothing. For
219
+ * a batch it is worse: the extra answer shifts every result after it, and a
220
+ * walk that steps one header at a time hands each caller the object that
221
+ * belonged to the next one.
222
+ *
223
+ * A path is refused a control character long before this — by
224
+ * `assertValidRelativePath` and by `normalizeWorkspacePath`, which is where
225
+ * the rule belongs because it protects every route and tool at once. This is
226
+ * the fence AT the protocol, for a spec assembled from somewhere neither of
227
+ * those covers (a ref, a branch name, a path from a future caller). Nothing
228
+ * should ever trip it, so it throws rather than filtering: a request that
229
+ * cannot be expressed in this protocol has no correct truncation.
230
+ */
231
+ export function assertOneSpecPerLine(specs: readonly string[]): void {
232
+ for (const spec of specs) {
233
+ if (/[\r\n]/.test(spec)) {
234
+ throw new Error('git cat-file --batch spec contains a line break');
235
+ }
236
+ }
237
+ }
238
+
212
239
  /** Whether a failure came from the deadline rather than from git's own answer. */
213
240
  export function isGitTimeout(err: unknown): boolean {
214
241
  return err instanceof GitRunError && err.timedOut;
@@ -12,7 +12,6 @@ const settings: TenantHostSettings = {
12
12
  workspacesRoot: path.resolve('/srv/hexis/workspaces'),
13
13
  kbTemplateDir: '/srv/hexis/template',
14
14
  gitTimeoutMs: 120_000,
15
- ontologySessionBlock: true,
16
15
  updateCheckEnabled: false,
17
16
  masterKey: 'a-master-key-that-is-long-enough-to-count-as-random',
18
17
  };
@@ -23,7 +23,6 @@ export interface TenantHostSettings {
23
23
  readonly workspacesRoot: string;
24
24
  readonly kbTemplateDir: string;
25
25
  readonly gitTimeoutMs: number;
26
- readonly ontologySessionBlock: boolean;
27
26
  readonly updateCheckEnabled: boolean;
28
27
  /** See `tenant-secrets.ts`. */
29
28
  readonly masterKey: string;
@@ -182,7 +181,6 @@ export function tenantConfigFrom(record: TenantRecord, settings: TenantHostSetti
182
181
  gitUsername,
183
182
  gitToken: (record.gitToken ?? '').trim(),
184
183
  kbTemplateDir: settings.kbTemplateDir,
185
- ontologySessionBlock: settings.ontologySessionBlock,
186
184
  updateCheckEnabled: settings.updateCheckEnabled,
187
185
  allowedEmailDomains,
188
186
  trustProxy: settings.trustProxy,
@@ -289,7 +287,6 @@ export function tenantHostEnv(env: NodeJS.ProcessEnv = process.env): TenantHostE
289
287
  kbTemplateDir: env.KB_TEMPLATE_DIR || defaultKbTemplateDir(),
290
288
  gitTimeoutMs:
291
289
  Number.isFinite(gitTimeout) && gitTimeout > 0 && gitTimeout <= MAX_TIMER_MS ? gitTimeout : DEFAULT_GIT_TIMEOUT_MS,
292
- ontologySessionBlock: (env.ONTOLOGY_SESSION_BLOCK ?? 'true').trim().toLowerCase() !== 'false',
293
290
  updateCheckEnabled: (env.UPDATE_CHECK ?? 'true').trim().toLowerCase() !== 'false',
294
291
  idleMinutes: Number.isFinite(idle) && idle > 0 ? idle : 30,
295
292
  };
@@ -1,52 +0,0 @@
1
- /**
2
- * Ontology-session boundary — the PURE decision core (no IO, no DB).
3
- *
4
- * Model: each run/session accumulates the SET of named ontologies it has
5
- * touched (via any read or write). The rule is asymmetric:
6
- *
7
- * READS — always allowed. A read that touches a named ontology RECORDS it
8
- * into the session's touched set (so it can poison later writes).
9
- * WRITES — allowed only while the session is confined to a SINGLE named
10
- * ontology and the write targets that same ontology. The moment a
11
- * session has touched two or more named ontologies (typically by
12
- * reading across them), EVERY subsequent write is blocked — "read
13
- * across ontologies → you can't write anymore at all."
14
- *
15
- * Neutral touches (`ontologyOf` → null: root config, Skills, root-level
16
- * Knowledge, non-KB) never record and never block. See
17
- * `docs/rp4-ontology-boundary-plan-v2.md`.
18
- *
19
- * `SessionOntologyService` (Postgres-backed) persists the touched set and calls
20
- * these helpers; this module keeps the rule itself testable in isolation.
21
- */
22
- import type { Ontology } from '@bevel-software/platform-shared';
23
- /** Outcome of a write check against the session's touched-ontology set. */
24
- export type WriteDecision = {
25
- allow: true;
26
- } | {
27
- allow: false;
28
- reason: 'multi-ontology' | 'different-ontology';
29
- touched: string[];
30
- attempted: string;
31
- };
32
- /**
33
- * The set of NAMED ontologies a session has touched after this operation. A
34
- * neutral operation (`ontology === null`) adds nothing. Pure — returns a new
35
- * set, never mutates the input.
36
- */
37
- export declare function recordTouch(touched: ReadonlySet<string>, ontology: Ontology): Set<string>;
38
- /**
39
- * Decide whether a WRITE to `attempted` is allowed given the ontologies the
40
- * session has already touched (NOT counting this write yet).
41
- *
42
- * - A write to a neutral path (`attempted === null`) is always allowed.
43
- * - If the session has already touched two or more named ontologies, the
44
- * write is blocked (`multi-ontology`) — reading across ontologies poisoned
45
- * the session's write ability.
46
- * - If the session has touched exactly one named ontology and it differs from
47
- * the write target, the write is blocked (`different-ontology`).
48
- * - If the session is unpinned (touched none) or its single touched ontology
49
- * equals the target, the write is allowed.
50
- */
51
- export declare function decideWrite(touched: ReadonlySet<string>, attempted: Ontology): WriteDecision;
52
- //# sourceMappingURL=session-ontology.policy.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session-ontology.policy.d.ts","sourceRoot":"","sources":["../../../src/modules/workflow/session-ontology.policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAEhE,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GACrB;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,GAAG,oBAAoB,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5G;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAIzF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,QAAQ,GAAG,aAAa,CAgB5F"}
@@ -1,62 +0,0 @@
1
- /**
2
- * Ontology-session boundary — the PURE decision core (no IO, no DB).
3
- *
4
- * Model: each run/session accumulates the SET of named ontologies it has
5
- * touched (via any read or write). The rule is asymmetric:
6
- *
7
- * READS — always allowed. A read that touches a named ontology RECORDS it
8
- * into the session's touched set (so it can poison later writes).
9
- * WRITES — allowed only while the session is confined to a SINGLE named
10
- * ontology and the write targets that same ontology. The moment a
11
- * session has touched two or more named ontologies (typically by
12
- * reading across them), EVERY subsequent write is blocked — "read
13
- * across ontologies → you can't write anymore at all."
14
- *
15
- * Neutral touches (`ontologyOf` → null: root config, Skills, root-level
16
- * Knowledge, non-KB) never record and never block. See
17
- * `docs/rp4-ontology-boundary-plan-v2.md`.
18
- *
19
- * `SessionOntologyService` (Postgres-backed) persists the touched set and calls
20
- * these helpers; this module keeps the rule itself testable in isolation.
21
- */
22
- /**
23
- * The set of NAMED ontologies a session has touched after this operation. A
24
- * neutral operation (`ontology === null`) adds nothing. Pure — returns a new
25
- * set, never mutates the input.
26
- */
27
- export function recordTouch(touched, ontology) {
28
- const next = new Set(touched);
29
- if (ontology !== null)
30
- next.add(ontology);
31
- return next;
32
- }
33
- /**
34
- * Decide whether a WRITE to `attempted` is allowed given the ontologies the
35
- * session has already touched (NOT counting this write yet).
36
- *
37
- * - A write to a neutral path (`attempted === null`) is always allowed.
38
- * - If the session has already touched two or more named ontologies, the
39
- * write is blocked (`multi-ontology`) — reading across ontologies poisoned
40
- * the session's write ability.
41
- * - If the session has touched exactly one named ontology and it differs from
42
- * the write target, the write is blocked (`different-ontology`).
43
- * - If the session is unpinned (touched none) or its single touched ontology
44
- * equals the target, the write is allowed.
45
- */
46
- export function decideWrite(touched, attempted) {
47
- if (attempted === null)
48
- return { allow: true };
49
- // The set of named ontologies the session would span if this write lands.
50
- const spanned = new Set(touched);
51
- spanned.add(attempted);
52
- if (spanned.size <= 1)
53
- return { allow: true };
54
- // spanned.size >= 2 — the write would put the session across a boundary.
55
- const touchedList = [...touched].sort();
56
- if (touched.size >= 2) {
57
- return { allow: false, reason: 'multi-ontology', touched: touchedList, attempted };
58
- }
59
- // Exactly one prior ontology, and it isn't the write target.
60
- return { allow: false, reason: 'different-ontology', touched: touchedList, attempted };
61
- }
62
- //# sourceMappingURL=session-ontology.policy.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session-ontology.policy.js","sourceRoot":"","sources":["../../../src/modules/workflow/session-ontology.policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,OAA4B,EAAE,QAAkB;IAC1E,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,QAAQ,KAAK,IAAI;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,OAA4B,EAAE,SAAmB;IAC3E,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE/C,0EAA0E;IAC1E,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAEvB,IAAI,OAAO,CAAC,IAAI,IAAI,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE9C,yEAAyE;IACzE,MAAM,WAAW,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,IAAI,OAAO,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;IACrF,CAAC;IACD,6DAA6D;IAC7D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;AACzF,CAAC"}