@bevel-software/platform-core-backend 0.4.0 → 0.5.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 (305) hide show
  1. package/dist/core/core-ports.d.ts +15 -0
  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.map +1 -1
  5. package/dist/core/create-core-server.js +40 -6
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +17 -1
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +153 -29
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/core-config.d.ts +70 -21
  12. package/dist/core-config.d.ts.map +1 -1
  13. package/dist/core-config.js +134 -58
  14. package/dist/core-config.js.map +1 -1
  15. package/dist/modules/access/access-control.interface.d.ts +6 -0
  16. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.d.ts +74 -5
  18. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  19. package/dist/modules/access/access-control.service.js +116 -15
  20. package/dist/modules/access/access-control.service.js.map +1 -1
  21. package/dist/modules/access/access-declarations.d.ts +93 -0
  22. package/dist/modules/access/access-declarations.d.ts.map +1 -0
  23. package/dist/modules/access/access-declarations.js +168 -0
  24. package/dist/modules/access/access-declarations.js.map +1 -0
  25. package/dist/modules/access/access-mutation.service.js +4 -4
  26. package/dist/modules/access/access-mutation.service.js.map +1 -1
  27. package/dist/modules/access/access-splice.d.ts +16 -1
  28. package/dist/modules/access/access-splice.d.ts.map +1 -1
  29. package/dist/modules/access/access-splice.js +40 -21
  30. package/dist/modules/access/access-splice.js.map +1 -1
  31. package/dist/modules/access/access.routes.d.ts.map +1 -1
  32. package/dist/modules/access/access.routes.js +51 -0
  33. package/dist/modules/access/access.routes.js.map +1 -1
  34. package/dist/modules/access/creator-access.d.ts +8 -0
  35. package/dist/modules/access/creator-access.d.ts.map +1 -1
  36. package/dist/modules/access/creator-access.js +21 -2
  37. package/dist/modules/access/creator-access.js.map +1 -1
  38. package/dist/modules/admin/admin-access.service.d.ts +1 -1
  39. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  40. package/dist/modules/admin/admin-access.service.js +3 -2
  41. package/dist/modules/admin/admin-access.service.js.map +1 -1
  42. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  43. package/dist/modules/auth/auth.routes.js +50 -1
  44. package/dist/modules/auth/auth.routes.js.map +1 -1
  45. package/dist/modules/auth/auth.service.d.ts +39 -10
  46. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  47. package/dist/modules/auth/auth.service.js +38 -16
  48. package/dist/modules/auth/auth.service.js.map +1 -1
  49. package/dist/modules/database/core-schema.d.ts +124 -0
  50. package/dist/modules/database/core-schema.d.ts.map +1 -1
  51. package/dist/modules/database/core-schema.js +45 -0
  52. package/dist/modules/database/core-schema.js.map +1 -1
  53. package/dist/modules/groups/group-provision.service.d.ts +130 -0
  54. package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
  55. package/dist/modules/groups/group-provision.service.js +288 -0
  56. package/dist/modules/groups/group-provision.service.js.map +1 -0
  57. package/dist/modules/groups/groups.contract.d.ts +106 -0
  58. package/dist/modules/groups/groups.contract.d.ts.map +1 -0
  59. package/dist/modules/groups/groups.contract.js +36 -0
  60. package/dist/modules/groups/groups.contract.js.map +1 -0
  61. package/dist/modules/groups/groups.routes.d.ts +42 -0
  62. package/dist/modules/groups/groups.routes.d.ts.map +1 -0
  63. package/dist/modules/groups/groups.routes.js +379 -0
  64. package/dist/modules/groups/groups.routes.js.map +1 -0
  65. package/dist/modules/groups/groups.service.d.ts +60 -0
  66. package/dist/modules/groups/groups.service.d.ts.map +1 -0
  67. package/dist/modules/groups/groups.service.js +172 -0
  68. package/dist/modules/groups/groups.service.js.map +1 -0
  69. package/dist/modules/groups/index.d.ts +7 -0
  70. package/dist/modules/groups/index.d.ts.map +1 -0
  71. package/dist/modules/groups/index.js +6 -0
  72. package/dist/modules/groups/index.js.map +1 -0
  73. package/dist/modules/groups/join-proposals.d.ts +53 -0
  74. package/dist/modules/groups/join-proposals.d.ts.map +1 -0
  75. package/dist/modules/groups/join-proposals.js +67 -0
  76. package/dist/modules/groups/join-proposals.js.map +1 -0
  77. package/dist/modules/groups/join-requests.service.d.ts +81 -0
  78. package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
  79. package/dist/modules/groups/join-requests.service.js +135 -0
  80. package/dist/modules/groups/join-requests.service.js.map +1 -0
  81. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
  82. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  83. package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
  84. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  85. package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
  86. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
  87. package/dist/modules/settings/deployment-settings.service.js +368 -0
  88. package/dist/modules/settings/deployment-settings.service.js.map +1 -0
  89. package/dist/modules/settings/setup.routes.d.ts +49 -0
  90. package/dist/modules/settings/setup.routes.d.ts.map +1 -0
  91. package/dist/modules/settings/setup.routes.js +292 -0
  92. package/dist/modules/settings/setup.routes.js.map +1 -0
  93. package/dist/modules/skills/index.d.ts +2 -1
  94. package/dist/modules/skills/index.d.ts.map +1 -1
  95. package/dist/modules/skills/index.js +1 -0
  96. package/dist/modules/skills/index.js.map +1 -1
  97. package/dist/modules/skills/pending-skills.service.d.ts +40 -0
  98. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
  99. package/dist/modules/skills/pending-skills.service.js +147 -0
  100. package/dist/modules/skills/pending-skills.service.js.map +1 -0
  101. package/dist/modules/skills/skills.contract.d.ts +32 -3
  102. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  103. package/dist/modules/skills/skills.contract.js +1 -1
  104. package/dist/modules/skills/skills.routes.d.ts +3 -2
  105. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  106. package/dist/modules/skills/skills.routes.js +18 -1
  107. package/dist/modules/skills/skills.routes.js.map +1 -1
  108. package/dist/modules/skills/skills.service.d.ts +22 -0
  109. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  110. package/dist/modules/skills/skills.service.js +24 -12
  111. package/dist/modules/skills/skills.service.js.map +1 -1
  112. package/dist/modules/tool-manuals/index.d.ts +1 -1
  113. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  114. package/dist/modules/tool-manuals/index.js.map +1 -1
  115. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
  116. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  117. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  118. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
  119. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  120. package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
  121. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  122. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
  123. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  124. package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
  125. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  126. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  127. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
  128. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  129. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  130. package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
  131. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  132. package/dist/modules/workflow/event-bus.d.ts +11 -0
  133. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  134. package/dist/modules/workflow/event-bus.js +22 -0
  135. package/dist/modules/workflow/event-bus.js.map +1 -1
  136. package/dist/modules/workflow/file-lock.service.d.ts +9 -0
  137. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  138. package/dist/modules/workflow/file-lock.service.js +16 -0
  139. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  140. package/dist/modules/workflow/git/branch-name.d.ts +1 -1
  141. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
  142. package/dist/modules/workflow/git/branch-name.js +1 -1
  143. package/dist/modules/workflow/git/branch-name.js.map +1 -1
  144. package/dist/modules/workflow/git/git.service.d.ts +31 -2
  145. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  146. package/dist/modules/workflow/git/git.service.js +157 -54
  147. package/dist/modules/workflow/git/git.service.js.map +1 -1
  148. package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
  149. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  150. package/dist/modules/workflow/git/pull-request.service.js +19 -9
  151. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  152. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  153. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  154. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  155. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  156. package/dist/modules/workflow/workflow.routes.js +95 -14
  157. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  158. package/dist/modules/workflow/workflow.service.d.ts +97 -5
  159. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  160. package/dist/modules/workflow/workflow.service.js +393 -114
  161. package/dist/modules/workflow/workflow.service.js.map +1 -1
  162. package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
  163. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
  164. package/dist/modules/workspace/bevel-ignore.js +5 -1
  165. package/dist/modules/workspace/bevel-ignore.js.map +1 -1
  166. package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
  167. package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
  168. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  169. package/dist/modules/workspace/kb-seed.service.js +228 -51
  170. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  171. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  172. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  173. package/dist/modules/workspace/workspace.routes.js +51 -18
  174. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  175. package/dist/modules/workspace/workspace.service.d.ts +16 -4
  176. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  177. package/dist/modules/workspace/workspace.service.js +36 -9
  178. package/dist/modules/workspace/workspace.service.js.map +1 -1
  179. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  180. package/dist/modules/workspace/workspace.tools.js +8 -3
  181. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  182. package/dist/shared/kb-layout.test.js +2 -2
  183. package/dist/test-setup.d.ts +2 -0
  184. package/dist/test-setup.d.ts.map +1 -0
  185. package/dist/test-setup.js +18 -0
  186. package/dist/test-setup.js.map +1 -0
  187. package/kb-template/.bevelignore +6 -2
  188. package/kb-template/AGENTS.md +272 -0
  189. package/kb-template/KnowledgeBase/How to get started.md +65 -0
  190. package/kb-template/access.md +36 -36
  191. package/migrations/0002_user_onboarding_done.sql +1 -0
  192. package/migrations/0003_deployment_settings.sql +9 -0
  193. package/migrations/meta/0001_snapshot.json +1486 -0
  194. package/migrations/meta/0002_snapshot.json +1493 -0
  195. package/migrations/meta/0003_snapshot.json +1552 -0
  196. package/migrations/meta/_journal.json +15 -1
  197. package/package.json +3 -3
  198. package/src/__tests__/branch-model.test.ts +79 -0
  199. package/src/__tests__/core-config.admin.test.ts +114 -0
  200. package/src/__tests__/database-url.test.ts +70 -0
  201. package/src/core/core-ports.ts +15 -0
  202. package/src/core/create-core-server.ts +55 -6
  203. package/src/core/create-core-services.ts +201 -36
  204. package/src/core-config.ts +139 -59
  205. package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
  206. package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
  207. package/src/modules/access/__tests__/access-splice.test.ts +21 -0
  208. package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
  209. package/src/modules/access/access-control.interface.ts +11 -0
  210. package/src/modules/access/access-control.service.ts +113 -15
  211. package/src/modules/access/access-declarations.ts +251 -0
  212. package/src/modules/access/access-mutation.service.ts +4 -4
  213. package/src/modules/access/access-splice.ts +64 -21
  214. package/src/modules/access/access.routes.ts +61 -0
  215. package/src/modules/access/creator-access.ts +22 -2
  216. package/src/modules/admin/admin-access.service.ts +4 -3
  217. package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
  218. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
  219. package/src/modules/auth/auth.routes.ts +50 -1
  220. package/src/modules/auth/auth.service.ts +55 -17
  221. package/src/modules/database/core-schema.ts +46 -0
  222. package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
  223. package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
  224. package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
  225. package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
  226. package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
  227. package/src/modules/groups/group-provision.service.ts +328 -0
  228. package/src/modules/groups/groups.contract.ts +107 -0
  229. package/src/modules/groups/groups.routes.ts +433 -0
  230. package/src/modules/groups/groups.service.ts +193 -0
  231. package/src/modules/groups/index.ts +17 -0
  232. package/src/modules/groups/join-proposals.ts +114 -0
  233. package/src/modules/groups/join-requests.service.ts +185 -0
  234. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
  235. package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
  236. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
  237. package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
  238. package/src/modules/settings/deployment-settings.service.ts +419 -0
  239. package/src/modules/settings/setup.routes.ts +317 -0
  240. package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
  241. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  242. package/src/modules/skills/index.ts +3 -0
  243. package/src/modules/skills/pending-skills.service.ts +168 -0
  244. package/src/modules/skills/skills.contract.ts +34 -3
  245. package/src/modules/skills/skills.routes.ts +23 -2
  246. package/src/modules/skills/skills.service.ts +24 -16
  247. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
  248. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  249. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
  250. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
  251. package/src/modules/tool-manuals/index.ts +2 -0
  252. package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
  253. package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
  254. package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
  255. package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
  256. package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
  257. package/src/modules/workflow/__tests__/pending-commits.enqueue-if-absent.test.ts +82 -72
  258. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +295 -5
  259. package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
  260. package/src/modules/workflow/event-bus.ts +25 -0
  261. package/src/modules/workflow/file-lock.service.ts +17 -0
  262. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
  263. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
  264. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
  265. package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
  266. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
  267. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
  268. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +311 -306
  269. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
  270. package/src/modules/workflow/git/branch-name.ts +5 -1
  271. package/src/modules/workflow/git/git.service.ts +172 -64
  272. package/src/modules/workflow/git/pull-request.service.ts +22 -11
  273. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
  274. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  275. package/src/modules/workflow/workflow.routes.ts +91 -13
  276. package/src/modules/workflow/workflow.service.ts +365 -19
  277. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
  278. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
  279. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
  280. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
  281. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
  283. package/src/modules/workspace/bevel-ignore.ts +66 -62
  284. package/src/modules/workspace/kb-seed.interface.ts +1 -1
  285. package/src/modules/workspace/kb-seed.service.ts +247 -53
  286. package/src/modules/workspace/workspace.routes.ts +54 -18
  287. package/src/modules/workspace/workspace.service.ts +45 -7
  288. package/src/modules/workspace/workspace.tools.ts +922 -917
  289. package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
  290. package/src/shared/__tests__/join-request.test.ts +96 -0
  291. package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
  292. package/src/shared/kb-layout.test.ts +2 -2
  293. package/src/test-setup.ts +18 -0
  294. package/kb-template/Agents/README.md +0 -38
  295. package/kb-template/CLAUDE.md +0 -419
  296. package/kb-template/Data/.gitkeep +0 -0
  297. package/kb-template/Data/README.md +0 -39
  298. package/kb-template/KnowledgeBase/README.md +0 -37
  299. package/kb-template/Pipelines/.gitkeep +0 -0
  300. package/kb-template/Pipelines/README.md +0 -38
  301. package/kb-template/Skills/.gitkeep +0 -0
  302. package/kb-template/Skills/README.md +0 -32
  303. package/kb-template/Tools/.gitkeep +0 -0
  304. package/kb-template/Tools/README.md +0 -42
  305. /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
@@ -1,5 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { IGNORE_FILENAME } from './bevel-ignore.js';
4
+ import type { IAdminAccessService } from '../admin/admin.interface.js';
3
5
  import express from 'express';
4
6
  import type { AuthUser, IWorkflowService } from '@bevel-software/platform-shared';
5
7
  import {
@@ -9,8 +11,7 @@ import {
9
11
  KNOWLEDGE_BASE_DIR,
10
12
  KNOWLEDGE_DIR,
11
13
  PIPELINES_DIR,
12
- SKILLS_DIR,
13
- TOOLS_DIR,
14
+ GROUPS_DIR,
14
15
  } from '@bevel-software/platform-shared';
15
16
  import { branchForWorkspaceId, FolderTooLargeError, type ReadTreeFilter } from './workspace.service.js';
16
17
  import type { WorkspaceService } from './workspace.service.js';
@@ -48,6 +49,7 @@ export function createWorkspaceRoutes(
48
49
  accessControl: IAccessControl,
49
50
  kbDirName: string,
50
51
  creatorAccess: ICreatorAccess,
52
+ adminAccess: IAdminAccessService,
51
53
  ): express.Router {
52
54
  const router = express.Router();
53
55
 
@@ -463,26 +465,45 @@ export function createWorkspaceRoutes(
463
465
  // The structural top-level folders are always shown as folders, even to
464
466
  // a user who can't read into them. Their existence isn't sensitive
465
467
  // (every KB has them), and keeping them visible lets the explorer render
466
- // its Knowledge/Skills section view instead of collapsing to an empty
467
- // flat tree. `KnowledgeBase/`, `Data/`, `Agents/`, `Pipelines/`,
468
- // `Skills/` + `Tools/` are the current layout; a ROOT-LEVEL `Knowledge/`
469
- // is the legacy pre-split layout's knowledge root (kb-layout.ts calls it
470
- // the neutral bucket) and gets the same treatment so legacy clones don't
471
- // collapse. Only the folders themselves are forced visible — their
472
- // contents stay gated by the verdict above.
468
+ // its Knowledge/Groups section view instead of collapsing to an empty
469
+ // flat tree. A ROOT-LEVEL `Knowledge/` is the legacy pre-split layout's
470
+ // knowledge root (kb-layout.ts calls it the neutral bucket) and gets the
471
+ // same treatment so legacy clones don't collapse. Only the folders
472
+ // themselves are forced visible — their contents stay gated by the
473
+ // verdict above.
473
474
  const structuralRoots = new Set([
474
475
  KNOWLEDGE_BASE_DIR,
475
476
  DATA_DIR,
476
477
  AGENTS_DIR,
477
478
  PIPELINES_DIR,
478
- SKILLS_DIR,
479
- TOOLS_DIR,
479
+ GROUPS_DIR,
480
480
  KNOWLEDGE_DIR,
481
481
  ]);
482
482
  for (const wp of wsRelPaths) {
483
483
  const rel = toKbRelative(wp, kbDirName);
484
484
  if (rel !== null && structuralRoots.has(rel)) verdict.set(wp, true);
485
485
  }
486
+
487
+ // `.bevelignore` is ADMIN-ONLY. It is the file that decides what the file
488
+ // tree and the agent view show at all, so it is deployment configuration
489
+ // rather than knowledge — and it sits alone in being visible: its
490
+ // siblings (`.gitignore`, `roles.yaml`, `access.md`, `AGENTS.md`) are
491
+ // already hidden from every reader by the shipped ignore rules. Somebody
492
+ // who cannot act on it gains a puzzle; somebody who can needs to reach it.
493
+ //
494
+ // Read permission is deliberately NOT the lever. Everyone can read it —
495
+ // it has to be readable to be applied — so hiding it is a listing
496
+ // decision, made here, rather than an ACL fiction maintained in a file.
497
+ //
498
+ // Resolved through `AdminAccessService` rather than by asking whether
499
+ // this caller can write `roles.yaml` in THIS workspace: admin is settled
500
+ // on the default branch precisely so that editing `roles.yaml` on your
501
+ // own branch cannot promote you, and the same reasoning applies to
502
+ // anything gated on being an admin.
503
+ const ignoreFiles = wsRelPaths.filter((wp) => path.basename(wp) === IGNORE_FILENAME);
504
+ if (ignoreFiles.length > 0 && !(await adminAccess.isAdmin(userEmail))) {
505
+ for (const wp of ignoreFiles) verdict.set(wp, false);
506
+ }
486
507
  return verdict;
487
508
  };
488
509
  }
@@ -565,16 +586,25 @@ export function createWorkspaceRoutes(
565
586
  '.xlsx':
566
587
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
567
588
  };
568
- // SVG is active web content (it can carry <script>). Inline view
569
- // already trusts the source (the workspace owns its bytes), but a
570
- // saved-to-disk SVG that's later re-opened in a browser tab would
571
- // execute its scripts under the file:// origin — so when the user
572
- // explicitly downloads, force octet-stream to defang it.
589
+ // SVG is active web content (it can carry <script>), and it is active in
590
+ // BOTH directions: a saved-to-disk SVG re-opened later runs its scripts
591
+ // under the file:// origin, so a download is forced to octet-stream.
573
592
  const downloadMime = ext === '.svg' ? 'application/octet-stream' : (mimeTypes[ext] || 'application/octet-stream');
574
593
  res.setHeader('Content-Type', wantsDownload ? downloadMime : (mimeTypes[ext] || 'application/octet-stream'));
575
594
  // Block MIME-sniffing so a misdeclared file can't be promoted to
576
595
  // active content by the browser.
577
596
  res.setHeader('X-Content-Type-Options', 'nosniff');
597
+ // …and inline is a DOCUMENT the moment somebody opens this URL in a tab
598
+ // directly, where those scripts would run under THIS
599
+ // origin with this user's session — stored XSS for anyone who can write
600
+ // a file into the workspace. `sandbox` drops the document into a unique
601
+ // origin with scripting off. It applies to documents only, so the
602
+ // renderers' fetch → blob → <img> path is untouched, and that path is
603
+ // why the type stays `image/svg+xml`: browsers do not sniff SVG, and an
604
+ // octet-stream blob would simply not render.
605
+ if (!wantsDownload && ext === '.svg') {
606
+ res.setHeader('Content-Security-Policy', 'sandbox');
607
+ }
578
608
  if (wantsDownload) {
579
609
  // RFC 5987 UTF-8 filename encoding so unicode + spaces round-trip;
580
610
  // CR/LF stripped to block header injection via a crafted path.
@@ -840,7 +870,7 @@ export function createWorkspaceRoutes(
840
870
  res.status(400).json({ error: 'path query parameter is required' });
841
871
  return;
842
872
  }
843
- const { content } = req.body as { content?: string };
873
+ const { content, ifAbsent } = req.body as { content?: string; ifAbsent?: boolean };
844
874
  if (content === undefined) {
845
875
  res.status(400).json({ error: 'content is required in body' });
846
876
  return;
@@ -861,7 +891,13 @@ export function createWorkspaceRoutes(
861
891
  const plan = await creatorAccess.planForCreate(id, user, filePath, 'file');
862
892
  if (plan?.kind === 'seed-access-md') await seedCreatorAccessMd(id, user, plan);
863
893
  const toWrite = plan?.kind === 'frontmatter' ? plan.apply(content) : content;
864
- await withLock(id, user, filePath, () => workspaceService.writeFile(id, filePath, toWrite));
894
+ // `ifAbsent` = exclusive create: the service's `wx` write turns a
895
+ // concurrent or stale create against an existing file into a 409
896
+ // instead of a silent replace. `withLock`'s failure arm releases
897
+ // without committing, so the refusal leaves no trace.
898
+ await withLock(id, user, filePath, () =>
899
+ workspaceService.writeFile(id, filePath, toWrite, { failIfExists: ifAbsent === true }),
900
+ );
865
901
  res.json({ status: 'written' });
866
902
  } catch (err) {
867
903
  sendError(res, err);
@@ -147,10 +147,25 @@ export class WorkspaceService implements IWorkspaceService {
147
147
 
148
148
  constructor(
149
149
  private readonly workspacesRoot: string,
150
- private readonly kbRepoUrl: string,
150
+ /**
151
+ * The KB remote. A GETTER is read per-clone, so a repository supplied
152
+ * through the setup screen takes effect without restarting the process; a
153
+ * plain string is still accepted for callers that have nothing to vary.
154
+ *
155
+ * `kbDirName` stays a plain string on purpose — it is threaded into a
156
+ * dozen other services at construction, so making it live only here would
157
+ * buy an inconsistency rather than a feature.
158
+ */
159
+ kbRepoUrl: string | (() => string),
151
160
  private readonly kbDirName: string,
152
- private readonly gitUsername: string = 'x-access-token',
153
- ) {}
161
+ gitUsername: string | (() => string) = 'x-access-token',
162
+ ) {
163
+ this.kbRepoUrl = typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl;
164
+ this.gitUsername = typeof gitUsername === 'function' ? gitUsername : () => gitUsername;
165
+ }
166
+
167
+ private readonly kbRepoUrl: () => string;
168
+ private readonly gitUsername: () => string;
154
169
 
155
170
  setDiffService(diffService: IDiffService): void {
156
171
  this.diffService = diffService;
@@ -185,10 +200,10 @@ export class WorkspaceService implements IWorkspaceService {
185
200
  // Username is provider-specific (GitHub `x-access-token`, GitLab `oauth2`, …); the token is
186
201
  // always the Basic-auth password, which every major host accepts.
187
202
  args.push(
188
- '-c', `credential.helper=!f() { echo "username=${this.gitUsername}"; echo "password=$GITHUB_TOKEN"; }; f`,
203
+ '-c', `credential.helper=!f() { echo "username=${this.gitUsername()}"; echo "password=$GITHUB_TOKEN"; }; f`,
189
204
  );
190
205
  }
191
- args.push(this.kbRepoUrl, targetDir);
206
+ args.push(this.kbRepoUrl(), targetDir);
192
207
  return args;
193
208
  }
194
209
 
@@ -819,13 +834,36 @@ export class WorkspaceService implements IWorkspaceService {
819
834
  }
820
835
  }
821
836
 
822
- async writeFile(workspaceId: string, relativePath: string, content: string): Promise<void> {
837
+ async writeFile(
838
+ workspaceId: string,
839
+ relativePath: string,
840
+ content: string,
841
+ options?: { failIfExists?: boolean },
842
+ ): Promise<void> {
823
843
  assertValidRelativePath(relativePath);
824
844
  const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
825
845
  const absolutePath = path.resolve(workspaceDir, relativePath);
826
846
  this.assertWithinWorkspace(absolutePath, workspaceDir);
827
847
  await fs.mkdir(path.dirname(absolutePath), { recursive: true });
828
- await fs.writeFile(absolutePath, content, 'utf-8');
848
+ try {
849
+ // `wx` makes create-if-absent ATOMIC at the fs level — an exists-check
850
+ // followed by a plain write would let two concurrent creators (or a
851
+ // stale client whose file list predates the file) both pass the check
852
+ // and silently replace each other's content.
853
+ await fs.writeFile(absolutePath, content, {
854
+ encoding: 'utf-8',
855
+ flag: options?.failIfExists ? 'wx' : 'w',
856
+ });
857
+ } catch (err) {
858
+ if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
859
+ const conflict: Error & { status?: number } = new Error(
860
+ `"${relativePath}" already exists.`,
861
+ );
862
+ conflict.status = 409;
863
+ throw conflict;
864
+ }
865
+ throw err;
866
+ }
829
867
  await this.diffService?.syncFromDisk(workspaceId, relativePath);
830
868
  }
831
869