@bevel-software/platform-core-backend 0.7.5 → 0.8.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 (230) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +1 -1
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +51 -7
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +5 -3
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +30 -18
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/modules/access/access-control.interface.d.ts +8 -7
  12. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  13. package/dist/modules/access/access-control.service.d.ts +1 -1
  14. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.js +6 -6
  16. package/dist/modules/access/access-control.service.js.map +1 -1
  17. package/dist/modules/access/access-declarations.d.ts +5 -5
  18. package/dist/modules/access/access-declarations.js +3 -3
  19. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  20. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  21. package/dist/modules/access/access-mutation.service.js +3 -3
  22. package/dist/modules/access/access-mutation.service.js.map +1 -1
  23. package/dist/modules/access/access-splice.js +4 -4
  24. package/dist/modules/access/access-splice.js.map +1 -1
  25. package/dist/modules/access/access.routes.js +19 -19
  26. package/dist/modules/access/access.routes.js.map +1 -1
  27. package/dist/modules/access/creator-access.d.ts +2 -2
  28. package/dist/modules/access/creator-access.js +5 -5
  29. package/dist/modules/access/creator-access.js.map +1 -1
  30. package/dist/modules/access/roles-admin.service.d.ts +18 -4
  31. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  32. package/dist/modules/access/roles-admin.service.js +14 -4
  33. package/dist/modules/access/roles-admin.service.js.map +1 -1
  34. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  35. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  36. package/dist/modules/code-mode/code-mode-names.js +5 -27
  37. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  38. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  39. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  40. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  41. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  42. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  43. package/dist/modules/mcp/mcp.service.js +33 -395
  44. package/dist/modules/mcp/mcp.service.js.map +1 -1
  45. package/dist/modules/plugins/index.d.ts +7 -0
  46. package/dist/modules/plugins/index.d.ts.map +1 -0
  47. package/dist/modules/plugins/index.js +6 -0
  48. package/dist/modules/plugins/index.js.map +1 -0
  49. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  50. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  51. package/dist/modules/plugins/join-proposals.js +67 -0
  52. package/dist/modules/plugins/join-proposals.js.map +1 -0
  53. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  54. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  55. package/dist/modules/plugins/join-requests.service.js +135 -0
  56. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  57. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  58. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  59. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  60. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  61. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  62. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  63. package/dist/modules/plugins/plugins.contract.js +36 -0
  64. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  65. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  66. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugins.routes.js +379 -0
  68. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  69. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  70. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.service.js +172 -0
  72. package/dist/modules/plugins/plugins.service.js.map +1 -0
  73. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  74. package/dist/modules/skills/pending-skills.service.js +7 -7
  75. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  76. package/dist/modules/skills/skills.contract.d.ts +4 -4
  77. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  78. package/dist/modules/skills/skills.contract.js +1 -1
  79. package/dist/modules/skills/skills.service.js +5 -5
  80. package/dist/modules/skills/skills.service.js.map +1 -1
  81. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  82. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  83. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  84. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  85. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  86. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  87. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  88. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  89. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  90. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  91. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  92. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  93. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  94. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  95. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  96. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  97. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  98. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  99. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  100. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  101. package/dist/modules/workflow/git/git.service.js +2 -2
  102. package/dist/modules/workflow/git/git.service.js.map +1 -1
  103. package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
  104. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  105. package/dist/modules/workspace/kb-seed.service.js +43 -10
  106. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  107. package/dist/modules/workspace/plugins-migration.d.ts +50 -0
  108. package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
  109. package/dist/modules/workspace/plugins-migration.js +379 -0
  110. package/dist/modules/workspace/plugins-migration.js.map +1 -0
  111. package/dist/modules/workspace/workspace.routes.js +3 -3
  112. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  113. package/dist/modules/workspace/workspace.service.d.ts +26 -0
  114. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  115. package/dist/modules/workspace/workspace.service.js +83 -12
  116. package/dist/modules/workspace/workspace.service.js.map +1 -1
  117. package/dist/shared/kb-layout.test.js +3 -3
  118. package/dist/shared/kb-layout.test.js.map +1 -1
  119. package/dist/shared/utcp-namespace.d.ts +6 -27
  120. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  121. package/dist/shared/utcp-namespace.js +6 -63
  122. package/dist/shared/utcp-namespace.js.map +1 -1
  123. package/dist/shared/variable-refs.d.ts +42 -0
  124. package/dist/shared/variable-refs.d.ts.map +1 -0
  125. package/dist/shared/variable-refs.js +60 -0
  126. package/dist/shared/variable-refs.js.map +1 -0
  127. package/kb-template/.bevelignore +1 -1
  128. package/kb-template/AGENTS.md +88 -35
  129. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  130. package/kb-template/access.md +36 -36
  131. package/migrations/meta/0000_snapshot.json +1479 -1479
  132. package/package.json +5 -4
  133. package/src/assets.ts +25 -25
  134. package/src/core/core-ports.ts +106 -106
  135. package/src/core/create-core-server.ts +55 -9
  136. package/src/core/create-core-services.ts +40 -20
  137. package/src/index.ts +69 -69
  138. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  139. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  140. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  141. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  142. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  143. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  144. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  145. package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
  146. package/src/modules/access/access-control.interface.ts +8 -7
  147. package/src/modules/access/access-control.service.ts +7 -7
  148. package/src/modules/access/access-declarations.ts +5 -5
  149. package/src/modules/access/access-mutation.service.ts +3 -3
  150. package/src/modules/access/access-splice.ts +4 -4
  151. package/src/modules/access/access.routes.ts +20 -20
  152. package/src/modules/access/creator-access.ts +5 -5
  153. package/src/modules/access/roles-admin.service.ts +13 -2
  154. package/src/modules/admin/admin-access.routes.ts +29 -29
  155. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  156. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  157. package/src/modules/auth/rate-limit.ts +45 -45
  158. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  159. package/src/modules/code-mode/code-mode-names.ts +10 -36
  160. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  161. package/src/modules/database/connection.ts +15 -15
  162. package/src/modules/database/schema.ts +11 -11
  163. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  164. package/src/modules/mcp/mcp.service.ts +57 -435
  165. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  166. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  167. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  168. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  169. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  170. package/src/modules/plugins/index.ts +17 -0
  171. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  172. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  173. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  174. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  175. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  176. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  177. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  178. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  179. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  180. package/src/modules/skills/pending-skills.service.ts +7 -7
  181. package/src/modules/skills/skills.contract.ts +4 -4
  182. package/src/modules/skills/skills.service.ts +5 -5
  183. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  184. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  185. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  186. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  187. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  188. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  189. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  190. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  191. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  192. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  193. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  194. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  195. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  196. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  197. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  198. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  199. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  200. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  201. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  202. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  203. package/src/modules/workflow/git/git.service.ts +2 -2
  204. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  205. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  206. package/src/modules/workflow/workflow-hooks.ts +101 -101
  207. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
  208. package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
  209. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  210. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  211. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  212. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  213. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  214. package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
  215. package/src/modules/workspace/bevel-ignore.ts +66 -66
  216. package/src/modules/workspace/kb-seed.service.ts +38 -9
  217. package/src/modules/workspace/plugins-migration.ts +479 -0
  218. package/src/modules/workspace/session-sink.ts +25 -25
  219. package/src/modules/workspace/workspace.routes.ts +3 -3
  220. package/src/modules/workspace/workspace.service.ts +85 -14
  221. package/src/modules/workspace/workspace.tools.ts +922 -922
  222. package/src/shared/__tests__/join-request.test.ts +13 -13
  223. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  224. package/src/shared/kb-layout.test.ts +3 -3
  225. package/src/shared/utcp-namespace.ts +10 -68
  226. package/src/shared/variable-refs.ts +64 -0
  227. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  228. package/src/modules/groups/index.ts +0 -17
  229. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  230. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
@@ -50,14 +50,14 @@ describe('SkillService', () => {
50
50
 
51
51
  beforeEach(async () => {
52
52
  root = await mkdtemp(join(tmpdir(), 'skills-'));
53
- const skills = join(root, wsId, KB_DIR, 'Groups');
53
+ const skills = join(root, wsId, KB_DIR, 'Plugins');
54
54
  await mkdir(join(skills, 'rfi', 'scripts'), { recursive: true });
55
55
  await writeFile(join(skills, 'rfi', 'SKILL.md'), RFI_SKILL);
56
56
  await writeFile(join(skills, 'rfi', 'scripts', 'build_xlsx.py'), 'print("xlsx")\n');
57
57
  // A folder with no SKILL.md is not a skill.
58
58
  await mkdir(join(skills, 'not-a-skill'), { recursive: true });
59
59
  await writeFile(join(skills, 'not-a-skill', 'README.md'), '# nope\n');
60
- // A skill nested in a category subfolder (Groups/Development/coding-guidelines).
60
+ // A skill nested in a category subfolder (Plugins/Development/coding-guidelines).
61
61
  await mkdir(join(skills, 'Development', 'coding-guidelines'), { recursive: true });
62
62
  await writeFile(
63
63
  join(skills, 'Development', 'coding-guidelines', 'SKILL.md'),
@@ -72,7 +72,7 @@ describe('SkillService', () => {
72
72
  expect(skills).toHaveLength(2);
73
73
  const rfi = skills.find((s) => s.name === 'rfi')!;
74
74
  expect(rfi).toBeDefined();
75
- expect(rfi.path).toBe('Groups/rfi');
75
+ expect(rfi.path).toBe('Plugins/rfi');
76
76
  expect(rfi.version).toBe('1.4.0');
77
77
  expect(rfi.description).not.toBe('|');
78
78
  expect(rfi.description).toContain('Specialist RFI responder');
@@ -83,19 +83,19 @@ describe('SkillService', () => {
83
83
  const skills = await svc().listSkills();
84
84
  const cg = skills.find((s) => s.name === 'coding-guidelines')!;
85
85
  expect(cg).toBeDefined();
86
- expect(cg.path).toBe('Groups/Development/coding-guidelines');
86
+ expect(cg.path).toBe('Plugins/Development/coding-guidelines');
87
87
  expect(cg.description).toBe('Coding guidelines.');
88
88
 
89
89
  const res = await svc().getSkill('user@x.eu', 'coding-guidelines');
90
90
  expect(res.ok).toBe(true);
91
91
  if (res.ok && res.kind === 'skill') {
92
- expect(res.skill.path).toBe('Groups/Development/coding-guidelines');
92
+ expect(res.skill.path).toBe('Plugins/Development/coding-guidelines');
93
93
  expect(res.skill.body).toContain('# Guidelines');
94
94
  }
95
95
  });
96
96
 
97
97
  test('refuses a colliding id (no auto-suffix) — the shared dedup rule', async () => {
98
- const skills = join(root, wsId, KB_DIR, 'Groups');
98
+ const skills = join(root, wsId, KB_DIR, 'Plugins');
99
99
  // Two skills resolve to the same id `dup` (no frontmatter id/name → folder name).
100
100
  await mkdir(join(skills, 'Zeta', 'dup'), { recursive: true });
101
101
  await writeFile(join(skills, 'Zeta', 'dup', 'SKILL.md'), '---\ndescription: zeta dup\n---\n\n# Z\n');
@@ -104,28 +104,28 @@ describe('SkillService', () => {
104
104
 
105
105
  const list = await svc().listSkills();
106
106
  // Only the first (smallest path) survives; the duplicate is dropped, not suffixed.
107
- expect(list.filter((s) => s.name === 'dup').map((s) => s.path)).toEqual(['Groups/Alpha/dup']);
107
+ expect(list.filter((s) => s.name === 'dup').map((s) => s.path)).toEqual(['Plugins/Alpha/dup']);
108
108
  expect(list.find((s) => s.name === 'dup2')).toBeUndefined();
109
109
 
110
110
  const a = await svc().getSkill('user@x.eu', 'dup');
111
- expect(a.ok && a.kind === 'skill' && a.skill.path).toBe('Groups/Alpha/dup');
111
+ expect(a.ok && a.kind === 'skill' && a.skill.path).toBe('Plugins/Alpha/dup');
112
112
  });
113
113
 
114
114
  test('frontmatter `id`/`name` overrides the folder name for identity', async () => {
115
- const skills = join(root, wsId, KB_DIR, 'Groups');
115
+ const skills = join(root, wsId, KB_DIR, 'Plugins');
116
116
  await mkdir(join(skills, 'folderx'), { recursive: true });
117
117
  await writeFile(join(skills, 'folderx', 'SKILL.md'), '---\nid: my_skill\ndescription: d\n---\n\n# X\n');
118
118
  const list = await svc().listSkills();
119
- expect(list.find((s) => s.name === 'my_skill')?.path).toBe('Groups/folderx');
119
+ expect(list.find((s) => s.name === 'my_skill')?.path).toBe('Plugins/folderx');
120
120
  });
121
121
 
122
122
  test('getSkill returns body, folder path, files and allowed-tools', async () => {
123
123
  const res = await svc().getSkill('user@x.eu', 'rfi');
124
124
  expect(res.ok).toBe(true);
125
125
  if (res.ok && res.kind === 'skill') {
126
- expect(res.skill.path).toBe('Groups/rfi');
126
+ expect(res.skill.path).toBe('Plugins/rfi');
127
127
  expect(res.skill.body).toContain('# /rfi: Specialist RFI Responder');
128
- expect(res.skill.files).toEqual(['Groups/rfi/scripts/build_xlsx.py']);
128
+ expect(res.skill.files).toEqual(['Plugins/rfi/scripts/build_xlsx.py']);
129
129
  expect(res.skill.allowedTools).toEqual(['Bash', 'Read']);
130
130
  }
131
131
  });
@@ -134,7 +134,7 @@ describe('SkillService', () => {
134
134
  const res = await svc().getSkill('user@x.eu', 'rfi', 'scripts/build_xlsx.py');
135
135
  expect(res.ok).toBe(true);
136
136
  if (res.ok && res.kind === 'file') {
137
- expect(res.file.path).toBe('Groups/rfi/scripts/build_xlsx.py');
137
+ expect(res.file.path).toBe('Plugins/rfi/scripts/build_xlsx.py');
138
138
  expect(res.file.content).toContain('print("xlsx")');
139
139
  }
140
140
  });
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DEFAULT_BRANCH,
3
- GROUPS_DIR,
3
+ PLUGINS_DIR,
4
4
  type ChangeRequest,
5
5
  type IWorkflowService,
6
6
  } from '@bevel-software/platform-shared';
@@ -27,9 +27,9 @@ const SKILL_DOC = 'SKILL.md';
27
27
  *
28
28
  * WHO MAY SEE ONE is narrower than who may see the catalog: the author, and
29
29
  * whoever could approve it. "Could approve it" is not re-stated here as a
30
- * group-admin check — it is `canWrite` on the SKILL.md the request would
30
+ * plugin-admin check — it is `canWrite` on the SKILL.md the request would
31
31
  * create. The new folder carries no `access.md` of its own, so that verdict
32
- * inherits from the group folder, which IS the group's admin; asking the access
32
+ * inherits from the plugin folder, which IS the plugin's admin; asking the access
33
33
  * tree keeps this surface and the merge gate answering with one voice, and it
34
34
  * keeps working for skills nested in a category subfolder that carries rules of
35
35
  * its own.
@@ -80,7 +80,7 @@ export class PendingSkillsService implements IPendingSkillService {
80
80
  const authorHash = hashEmail(email);
81
81
 
82
82
  // One access load for every candidate. Resolved on the DEFAULT branch —
83
- // the tree as it stands is what says who owns the group the skill is
83
+ // the tree as it stands is what says who owns the plugin the skill is
84
84
  // arriving into; the branch's own copy is written by the proposer and must
85
85
  // not get a say in who reviews them.
86
86
  const writable = await this.accessControl
@@ -148,16 +148,16 @@ export class PendingSkillsService implements IPendingSkillService {
148
148
  /**
149
149
  * Is this touched path a skill's own SKILL.md?
150
150
  *
151
- * `Groups/<group>/<skill>/SKILL.md` is the shallowest shape, hence four
151
+ * `Plugins/<plugin>/<skill>/SKILL.md` is the shallowest shape, hence four
152
152
  * segments; deeper ones are skills nested in a category subfolder, which the
153
- * catalog already supports. `Groups/<group>/SKILL.md` is NOT a skill — a group
153
+ * catalog already supports. `Plugins/<plugin>/SKILL.md` is NOT a skill — a plugin
154
154
  * folder is not itself one.
155
155
  */
156
156
  function isSkillDoc(repoRelPath: string): boolean {
157
157
  const segments = repoRelPath.split('/');
158
158
  return (
159
159
  segments.length >= 4 &&
160
- segments[0] === GROUPS_DIR &&
160
+ segments[0] === PLUGINS_DIR &&
161
161
  segments[segments.length - 1] === SKILL_DOC
162
162
  );
163
163
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Skills are reusable specialist instructions living under `Groups/<group>/<name>/SKILL.md`
2
+ * Skills are reusable specialist instructions living under `Plugins/<plugin>/<name>/SKILL.md`
3
3
  * in the KB repo. Discovery/loading is pinned to the DEFAULT branch — the catalog
4
4
  * is a single global, released set (a skill on a draft isn't discoverable until
5
5
  * merged). Progressive disclosure: `listSkills` = name + description (level 1),
@@ -12,7 +12,7 @@ export interface SkillSummary {
12
12
  name: string;
13
13
  description: string;
14
14
  version?: string;
15
- /** Repo-root-relative skill folder, e.g. `Groups/Everyone/rfi`. */
15
+ /** Repo-root-relative skill folder, e.g. `Plugins/Everyone/rfi`. */
16
16
  path: string;
17
17
  }
18
18
 
@@ -21,7 +21,7 @@ export interface Skill extends SkillSummary {
21
21
  body: string;
22
22
  /** Pre-approved tools from the `allowed-tools` frontmatter, if declared. */
23
23
  allowedTools?: string[];
24
- /** Repo-root-relative bundled file paths, e.g. `Groups/Everyone/rfi/scripts/build_xlsx.py`. */
24
+ /** Repo-root-relative bundled file paths, e.g. `Plugins/Everyone/rfi/scripts/build_xlsx.py`. */
25
25
  files: string[];
26
26
  }
27
27
 
@@ -48,7 +48,7 @@ export type GetSkillResult =
48
48
  *
49
49
  * Who may see one is narrower than who may see the catalog: its author, and
50
50
  * whoever could approve it. See `PendingSkillsService` for why that predicate
51
- * is the access tree's answer rather than a group-admin check spelled out
51
+ * is the access tree's answer rather than a plugin-admin check spelled out
52
52
  * again here.
53
53
  */
54
54
  export interface PendingSkill extends SkillSummary {
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import { parseDocument } from 'yaml';
4
- import { DEFAULT_BRANCH, GROUPS_DIR } from '@bevel-software/platform-shared';
4
+ import { DEFAULT_BRANCH, PLUGINS_DIR } from '@bevel-software/platform-shared';
5
5
  import type { WorkspaceService } from '../workspace/workspace.service.js';
6
6
  import { workspaceIdForBranch } from '../workspace/workspace.service.js';
7
7
  import type { IAccessControl } from '../access/access-control.interface.js';
@@ -103,8 +103,8 @@ export class SkillService implements ISkillService {
103
103
  }
104
104
 
105
105
  private async scanDisk(): Promise<ParsedSkill[]> {
106
- // Ensure the default-branch clone exists, then scan its Groups/ dir. Any
107
- // failure (no workspace, no Groups/ dir) degrades to an empty catalog — the
106
+ // Ensure the default-branch clone exists, then scan its Plugins/ dir. Any
107
+ // failure (no workspace, no Plugins/ dir) degrades to an empty catalog — the
108
108
  // manual/tools must never break because skills can't be read.
109
109
  let wsId: string;
110
110
  try {
@@ -119,7 +119,7 @@ export class SkillService implements ISkillService {
119
119
  // tree and treat every folder that directly contains a SKILL.md as a skill;
120
120
  // don't descend past it — its inner files are bundled assets, not nested
121
121
  // skills. The skill name is the leaf folder name; its path is the full
122
- // repo-relative folder (e.g. `Groups/Development/coding-guidelines`).
122
+ // repo-relative folder (e.g. `Plugins/Development/coding-guidelines`).
123
123
  const out: ParsedSkill[] = [];
124
124
  const walk = async (dir: string, relFolder: string): Promise<void> => {
125
125
  let entries: import('node:fs').Dirent[];
@@ -156,7 +156,7 @@ export class SkillService implements ISkillService {
156
156
  await walk(path.join(dir, entry.name), `${relFolder}/${entry.name}`);
157
157
  }
158
158
  };
159
- await walk(path.join(kbRoot, GROUPS_DIR), GROUPS_DIR);
159
+ await walk(path.join(kbRoot, PLUGINS_DIR), PLUGINS_DIR);
160
160
  // A skill's id (frontmatter `id`/`name`, else folder name) is how getSkill()
161
161
  // resolves it, so it must be unique. Sort by (name, path) for a deterministic
162
162
  // winner, then REFUSE later duplicates via the shared dedup — the same rule
@@ -1,19 +1,19 @@
1
- /**
2
- * Metering view over connection-key LLM usage, consumed by the connection-key
3
- * management routes (mcp) to show each key's spend against the daily budget.
4
- * The enterprise LLM proxy's `LlmUsageService` satisfies this structurally; a
5
- * core-only deployment (no LLM proxy, nothing to meter) passes
6
- * {@link unmeteredLlmUsage}.
7
- */
8
- export interface ILlmUsageMeter {
9
- /** Daily per-key token cap shown in the UI, or null when unmetered. */
10
- readonly dailyCap: number | null;
11
- /** Today's usage per key id, as `{ [tokenId]: tokens }` — missing keys mean 0. */
12
- usageForTokens(tokenIds: string[]): Promise<Record<string, number>>;
13
- }
14
-
15
- /** Core default: no proxy, no metering — every key reads as unused/uncapped. */
16
- export const unmeteredLlmUsage: ILlmUsageMeter = {
17
- dailyCap: null,
18
- usageForTokens: async () => ({}),
19
- };
1
+ /**
2
+ * Metering view over connection-key LLM usage, consumed by the connection-key
3
+ * management routes (mcp) to show each key's spend against the daily budget.
4
+ * The enterprise LLM proxy's `LlmUsageService` satisfies this structurally; a
5
+ * core-only deployment (no LLM proxy, nothing to meter) passes
6
+ * {@link unmeteredLlmUsage}.
7
+ */
8
+ export interface ILlmUsageMeter {
9
+ /** Daily per-key token cap shown in the UI, or null when unmetered. */
10
+ readonly dailyCap: number | null;
11
+ /** Today's usage per key id, as `{ [tokenId]: tokens }` — missing keys mean 0. */
12
+ usageForTokens(tokenIds: string[]): Promise<Record<string, number>>;
13
+ }
14
+
15
+ /** Core default: no proxy, no metering — every key reads as unused/uncapped. */
16
+ export const unmeteredLlmUsage: ILlmUsageMeter = {
17
+ dailyCap: null,
18
+ usageForTokens: async () => ({}),
19
+ };
@@ -0,0 +1,198 @@
1
+ import { describe, expect, it, vi, afterEach } from 'vitest';
2
+ import { descriptorsFromMcpJson } from '../mcp-json-discovery.js';
3
+
4
+ afterEach(() => vi.restoreAllMocks());
5
+
6
+ const MANIFEST = JSON.stringify({
7
+ name: 'gtm',
8
+ extensions: {
9
+ 'software.bevel.hexis': {
10
+ mcpServers: {
11
+ vendor: {
12
+ headers: { Authorization: 'Bearer ${VENDOR_KEY}' },
13
+ variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
14
+ description: 'Vendor API',
15
+ },
16
+ localbox: { local: true },
17
+ },
18
+ },
19
+ },
20
+ });
21
+
22
+ describe('descriptorsFromMcpJson', () => {
23
+ it('synthesizes descriptors keyed by server name, path at mcp.json', () => {
24
+ const out = descriptorsFromMcpJson(
25
+ 'GTM',
26
+ JSON.stringify({ mcpServers: { notion: { type: 'streamable-http', url: 'https://mcp.notion.com/mcp' } } }),
27
+ null,
28
+ );
29
+ expect(out).toEqual([
30
+ {
31
+ slug: 'notion',
32
+ name: 'notion',
33
+ path: 'Plugins/GTM/mcp.json',
34
+ type: 'mcp',
35
+ url: 'https://mcp.notion.com/mcp',
36
+ },
37
+ ]);
38
+ });
39
+
40
+ it('merges extension auth over mcp.json literals and carries variables + description', () => {
41
+ const out = descriptorsFromMcpJson(
42
+ 'GTM',
43
+ JSON.stringify({
44
+ mcpServers: { vendor: { type: 'streamable-http', url: 'https://v.example/mcp', headers: { 'X-V': '2' } } },
45
+ }),
46
+ MANIFEST,
47
+ );
48
+ expect(out[0]).toMatchObject({
49
+ headers: { 'X-V': '2', Authorization: 'Bearer ${VENDOR_KEY}' },
50
+ variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
51
+ description: 'Vendor API',
52
+ });
53
+ });
54
+
55
+ it('marks extension-flagged servers and every stdio server local-only', () => {
56
+ const out = descriptorsFromMcpJson(
57
+ 'GTM',
58
+ JSON.stringify({
59
+ mcpServers: {
60
+ localbox: { type: 'streamable-http', url: 'http://localhost:9000/mcp' },
61
+ indexer: { type: 'stdio', command: 'npx', args: ['-y', 'indexer'] },
62
+ },
63
+ }),
64
+ MANIFEST,
65
+ );
66
+ expect(out.find((d) => d.name === 'localbox')?.remote).toBe(false);
67
+ const stdio = out.find((d) => d.name === 'indexer');
68
+ expect(stdio?.remote).toBe(false);
69
+ expect(stdio?.stdio).toEqual({ command: 'npx', args: ['-y', 'indexer'], env: undefined, cwd: undefined });
70
+ });
71
+
72
+ it('skips a malformed entry without losing its siblings', () => {
73
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
74
+ const out = descriptorsFromMcpJson(
75
+ 'GTM',
76
+ JSON.stringify({
77
+ mcpServers: {
78
+ 'Bad Name!': { type: 'streamable-http', url: 'https://x.example' },
79
+ nourl: { type: 'streamable-http' },
80
+ odd: { type: 'carrier-pigeon' },
81
+ ok: { type: 'streamable-http', url: 'https://ok.example/mcp' },
82
+ },
83
+ }),
84
+ null,
85
+ );
86
+ expect(out.map((d) => d.name)).toEqual(['ok']);
87
+ });
88
+
89
+ it('ignores malformed extension headers instead of spreading them into keys', () => {
90
+ // A string spread into the merge would scatter its indices ('0', '1', …)
91
+ // into header names; a malformed extension must cost its own data only.
92
+ const out = descriptorsFromMcpJson(
93
+ 'GTM',
94
+ JSON.stringify({
95
+ mcpServers: { vendor: { type: 'streamable-http', url: 'https://v.example/mcp', headers: { 'X-V': '2' } } },
96
+ }),
97
+ JSON.stringify({
98
+ name: 'gtm',
99
+ extensions: { 'software.bevel.hexis': { mcpServers: { vendor: { headers: 'Bearer oops' } } } },
100
+ }),
101
+ );
102
+ expect(out).toHaveLength(1);
103
+ expect(out[0].headers).toEqual({ 'X-V': '2' });
104
+ });
105
+
106
+ it('skips a server whose variables declaration is malformed, keeping siblings', () => {
107
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
108
+ // A dropped declaration would silently re-scope a credential (undeclared
109
+ // defaults to the shared admin row), so a bad entry takes the SERVER
110
+ // offline — never just the entry, and never its siblings.
111
+ const out = descriptorsFromMcpJson(
112
+ 'GTM',
113
+ JSON.stringify({
114
+ mcpServers: {
115
+ vendor: { type: 'streamable-http', url: 'https://v.example/mcp' },
116
+ ok: { type: 'streamable-http', url: 'https://ok.example/mcp' },
117
+ },
118
+ }),
119
+ JSON.stringify({
120
+ name: 'gtm',
121
+ extensions: {
122
+ 'software.bevel.hexis': {
123
+ mcpServers: { vendor: { variables: [{ name: 'bad name!', scope: 'user' }] } },
124
+ },
125
+ },
126
+ }),
127
+ );
128
+ expect(out.map((d) => d.name)).toEqual(['ok']);
129
+ });
130
+
131
+ it('applies the .tool declaration rules: no reserved names, no duplicates, gated OAuth URLs, string authParams', () => {
132
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
133
+ const declare = (variables: unknown) =>
134
+ descriptorsFromMcpJson(
135
+ 'GTM',
136
+ JSON.stringify({ mcpServers: { vendor: { type: 'streamable-http', url: 'https://v.example/mcp' } } }),
137
+ JSON.stringify({
138
+ name: 'gtm',
139
+ extensions: { 'software.bevel.hexis': { mcpServers: { vendor: { variables } } } },
140
+ }),
141
+ ).map((d) => d.name);
142
+ // A re-declared platform-seeded name would shadow the seeding; a
143
+ // duplicate makes scope resolution order-dependent.
144
+ expect(declare([{ name: 'CONNECTION_KEY', scope: 'user' }])).toEqual([]);
145
+ expect(declare([{ name: 'K', scope: 'user' }, { name: 'K', scope: 'admin' }])).toEqual([]);
146
+ const oauth = { tokenUrl: 'https://v.example/token', clientId: 'c' };
147
+ // Same https + SSRF gate the .tool parser runs on OAuth endpoints.
148
+ expect(declare([{ name: 'T', scope: 'user', oauth: { ...oauth, authorizationUrl: 'http://v.example/auth' } }])).toEqual([]);
149
+ expect(declare([{ name: 'T', scope: 'user', oauth: { ...oauth, authorizationUrl: 'https://169.254.169.254/auth' } }])).toEqual([]);
150
+ // authParams travel verbatim as query params — non-strings are malformed.
151
+ expect(
152
+ declare([{ name: 'T', scope: 'user', oauth: { ...oauth, authorizationUrl: 'https://v.example/auth', authParams: { p: 1 } } }]),
153
+ ).toEqual([]);
154
+ expect(
155
+ declare([{ name: 'T', scope: 'user', oauth: { ...oauth, authorizationUrl: 'https://v.example/auth', authParams: { p: 'x' } } }]),
156
+ ).toEqual(['vendor']);
157
+ // `clientId` must be non-empty after trimming, as in the `.tool` parser:
158
+ // a whitespace-only value would pass discovery only to fail the owner's
159
+ // client-secret setup later with "clientId is required".
160
+ expect(
161
+ declare([{ name: 'T', scope: 'user', oauth: { tokenUrl: 'https://v.example/token', authorizationUrl: 'https://v.example/auth', clientId: ' ' } }]),
162
+ ).toEqual([]);
163
+ });
164
+
165
+ it('trims a padded oauth clientId like the .tool parser does', () => {
166
+ const out = descriptorsFromMcpJson(
167
+ 'GTM',
168
+ JSON.stringify({ mcpServers: { vendor: { type: 'streamable-http', url: 'https://v.example/mcp' } } }),
169
+ JSON.stringify({
170
+ name: 'gtm',
171
+ extensions: {
172
+ 'software.bevel.hexis': {
173
+ mcpServers: {
174
+ vendor: {
175
+ variables: [
176
+ {
177
+ name: 'T',
178
+ scope: 'user',
179
+ oauth: { authorizationUrl: 'https://v.example/auth', tokenUrl: 'https://v.example/token', clientId: ' c-1 ' },
180
+ },
181
+ ],
182
+ },
183
+ },
184
+ },
185
+ },
186
+ }),
187
+ );
188
+ expect(out[0]?.variables?.[0]?.oauth?.clientId).toBe('c-1');
189
+ });
190
+
191
+ it('yields nothing for an unparsable file, quietly for an absent extensions block', () => {
192
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
193
+ expect(descriptorsFromMcpJson('GTM', '{ not json', null)).toEqual([]);
194
+ expect(
195
+ descriptorsFromMcpJson('GTM', JSON.stringify({ mcpServers: {} }), '{ also not json'),
196
+ ).toEqual([]);
197
+ });
198
+ });