@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
@@ -3,9 +3,9 @@ import fs from 'node:fs/promises';
3
3
  import type { Dirent } from 'node:fs';
4
4
  import {
5
5
  DEFAULT_BRANCH,
6
- GROUPS_DIR,
7
- groupOfPath,
8
- isPersonalGroupFolder,
6
+ PLUGINS_DIR,
7
+ pluginOfPath,
8
+ isPersonalPluginFolder,
9
9
  } from '@bevel-software/platform-shared';
10
10
  import type { WorkspaceService } from '../workspace/workspace.service.js';
11
11
  import { workspaceIdForBranch } from '../workspace/workspace.service.js';
@@ -13,37 +13,37 @@ import type { IAccessControl } from '../access/access-control.interface.js';
13
13
  import type { ISkillService } from '../skills/skills.contract.js';
14
14
  import type { IToolManualService } from '../tool-manuals/tool-manuals.contract.js';
15
15
  import { TtlCache } from '../../shared/ttl-cache.js';
16
- import type { GroupCatalogEntry, IGroupIndexService } from './groups.contract.js';
16
+ import type { PluginCatalogEntry, IPluginIndexService } from './plugins.contract.js';
17
17
 
18
18
  const CACHE_TTL_MS = 60_000;
19
19
 
20
20
  /**
21
- * The group index: every group folder in the default-branch KB, with its
21
+ * The plugin index: every plugin folder in the default-branch KB, with its
22
22
  * caller-independent totals and access principals.
23
23
  *
24
24
  * Two decisions worth keeping straight:
25
25
  *
26
- * - **Existence is an `access.md`, counting is not.** A group EXISTS because
26
+ * - **Existence is an `access.md`, counting is not.** A plugin EXISTS because
27
27
  * its folder carries an `access.md` — the file the provisioning endpoint
28
28
  * seeds and the one every per-caller verdict (member / manager /
29
- * discoverable) resolves against. A bare directory under `Groups/` is NOT
30
- * a group: git cannot record an empty folder, so deleting a group's files
29
+ * discoverable) resolves against. A bare directory under `Plugins/` is NOT
30
+ * a plugin: git cannot record an empty folder, so deleting a plugin's files
31
31
  * leaves its directory behind on live checkouts, and enumerating by
32
32
  * directory would resurrect every such ghost. Counts come from the
33
33
  * already-cached global catalogs — `skillService.listSkills(undefined)` and
34
- * `toolManualService.listAllSummaries()` — bucketed by `groupOfPath`. A
34
+ * `toolManualService.listAllSummaries()` — bucketed by `pluginOfPath`. A
35
35
  * second `walkFiles` pass would re-read the same tree and could disagree
36
36
  * with the catalogs about what counts as a skill.
37
- * - **Loose files are not groups.** `Groups/slack.tool` sits directly under
37
+ * - **Loose files are not plugins.** `Plugins/slack.tool` sits directly under
38
38
  * the root, so it is a file, not a directory — the same ≥3-segment rule
39
- * `groupOfPath` applies, arrived at from the other side.
39
+ * `pluginOfPath` applies, arrived at from the other side.
40
40
  *
41
41
  * Cached for {@link CACHE_TTL_MS} and dropped by `invalidate()` from the
42
42
  * file-change subscriber, so a grant committed on the default branch is
43
43
  * reflected within one round-trip rather than one TTL.
44
44
  */
45
- export class GroupIndexService implements IGroupIndexService {
46
- private readonly cache: TtlCache<GroupCatalogEntry[]>;
45
+ export class PluginIndexService implements IPluginIndexService {
46
+ private readonly cache: TtlCache<PluginCatalogEntry[]>;
47
47
 
48
48
  constructor(
49
49
  private readonly workspaceService: WorkspaceService,
@@ -60,13 +60,13 @@ export class GroupIndexService implements IGroupIndexService {
60
60
  this.cache.invalidate();
61
61
  }
62
62
 
63
- async catalog(): Promise<GroupCatalogEntry[]> {
63
+ async catalog(): Promise<PluginCatalogEntry[]> {
64
64
  const cached = this.cache.get();
65
65
  if (cached) return cached;
66
66
  const entries = await this.build();
67
- // A failed scan and a KB with genuinely no group folders both serve `[]`,
67
+ // A failed scan and a KB with genuinely no plugin folders both serve `[]`,
68
68
  // but only the second is a fact worth holding for the TTL. Caching the
69
- // failure would hide every group from every user for the full 60s AFTER
69
+ // failure would hide every plugin from every user for the full 60s AFTER
70
70
  // the cause is gone — a clone mid-creation, a readdir that raced a
71
71
  // checkout — and `invalidate()` only fires on a file-change event that may
72
72
  // never arrive in that window. So a degraded read is served, not stored,
@@ -80,16 +80,16 @@ export class GroupIndexService implements IGroupIndexService {
80
80
 
81
81
  /**
82
82
  * Degrades on ANY failure rather than throwing. The Library must never break
83
- * because a group folder can't be read — same philosophy as the skill and
83
+ * because a plugin folder can't be read — same philosophy as the skill and
84
84
  * tool-manual scanners, and the reason the route's 500 is reserved for the
85
85
  * per-caller half.
86
86
  *
87
87
  * `null` is that degraded case and is deliberately NOT the same value as an
88
- * empty array: a KB with no group folders returns `[]`, which is true and
88
+ * empty array: a KB with no plugin folders returns `[]`, which is true and
89
89
  * cacheable, while a failure returns `null`, which `catalog()` serves as `[]`
90
90
  * without storing it.
91
91
  */
92
- private async build(): Promise<GroupCatalogEntry[] | null> {
92
+ private async build(): Promise<PluginCatalogEntry[] | null> {
93
93
  try {
94
94
  const wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
95
95
  const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
@@ -102,10 +102,10 @@ export class GroupIndexService implements IGroupIndexService {
102
102
  this.countTools(),
103
103
  ]);
104
104
 
105
- const entries: GroupCatalogEntry[] = [];
106
- for (const [name, groupFolders] of folders) {
107
- // One folder, one access boundary — the folder IS the group.
108
- const [primary] = groupFolders;
105
+ const entries: PluginCatalogEntry[] = [];
106
+ for (const [name, pluginFolders] of folders) {
107
+ // One folder, one access boundary — the folder IS the plugin.
108
+ const [primary] = pluginFolders;
109
109
  const [owners, writers, readers] = await Promise.all([
110
110
  this.accessControl.eligibleOwners(wsId, primary),
111
111
  this.accessControl.eligibleWriters(wsId, primary),
@@ -113,7 +113,7 @@ export class GroupIndexService implements IGroupIndexService {
113
113
  ]);
114
114
  entries.push({
115
115
  name,
116
- folders: groupFolders,
116
+ folders: pluginFolders,
117
117
  skillCount: skillCounts.get(name) ?? 0,
118
118
  toolCount: toolCounts.get(name) ?? 0,
119
119
  owners,
@@ -124,70 +124,70 @@ export class GroupIndexService implements IGroupIndexService {
124
124
  return entries.sort((a, b) => a.name.localeCompare(b.name));
125
125
  } catch (err) {
126
126
  console.warn(
127
- `[groups] group index unavailable: ${err instanceof Error ? err.message : String(err)}`,
127
+ `[plugins] plugin index unavailable: ${err instanceof Error ? err.message : String(err)}`,
128
128
  );
129
129
  return null;
130
130
  }
131
131
  }
132
132
 
133
- /** name → repo-relative group folder (always a single-element list). */
133
+ /** name → repo-relative plugin folder (always a single-element list). */
134
134
  private async scanFolders(kbRoot: string): Promise<Map<string, string[]>> {
135
135
  const byName = new Map<string, string[]>();
136
136
  let children: Dirent[];
137
137
  try {
138
- children = await fs.readdir(path.join(kbRoot, GROUPS_DIR), { withFileTypes: true });
138
+ children = await fs.readdir(path.join(kbRoot, PLUGINS_DIR), { withFileTypes: true });
139
139
  } catch {
140
- return byName; // a KB without a `Groups/` root simply has no groups
140
+ return byName; // a KB without a `Plugins/` root simply has no plugins
141
141
  }
142
142
  const candidates = children.filter(
143
143
  (child) =>
144
144
  child.isDirectory() &&
145
145
  !child.name.startsWith('.') &&
146
- // Personal folders live under Groups/ but are not groups: one exists
146
+ // Personal folders live under Plugins/ but are not plugins: one exists
147
147
  // per person, private by construction, and listing them would put a
148
148
  // locked row per employee in everyone's index.
149
- !isPersonalGroupFolder(child.name),
149
+ !isPersonalPluginFolder(child.name),
150
150
  );
151
- // A group exists exactly when its folder carries an `access.md` (see the
151
+ // A plugin exists exactly when its folder carries an `access.md` (see the
152
152
  // class doc) — stat that file, don't trust the directory.
153
153
  const verdicts = await Promise.all(
154
154
  candidates.map(async (child) => {
155
155
  try {
156
- return (await fs.stat(path.join(kbRoot, GROUPS_DIR, child.name, 'access.md'))).isFile();
156
+ return (await fs.stat(path.join(kbRoot, PLUGINS_DIR, child.name, 'access.md'))).isFile();
157
157
  } catch {
158
158
  return false;
159
159
  }
160
160
  }),
161
161
  );
162
162
  candidates.forEach((child, i) => {
163
- if (verdicts[i]) byName.set(child.name, [`${GROUPS_DIR}/${child.name}`]);
163
+ if (verdicts[i]) byName.set(child.name, [`${PLUGINS_DIR}/${child.name}`]);
164
164
  });
165
165
  return byName;
166
166
  }
167
167
 
168
168
  private async countSkills(): Promise<Map<string, number>> {
169
169
  // `undefined` is the documented GLOBAL, unfiltered mode — counts are a
170
- // property of the group, not of who is asking.
171
- return bucketByGroup(await this.skillService.listSkills(undefined));
170
+ // property of the plugin, not of who is asking.
171
+ return bucketByPlugin(await this.skillService.listSkills(undefined));
172
172
  }
173
173
 
174
174
  private async countTools(): Promise<Map<string, number>> {
175
- return bucketByGroup(await this.toolManualService.listAllSummaries());
175
+ return bucketByPlugin(await this.toolManualService.listAllSummaries());
176
176
  }
177
177
  }
178
178
 
179
- /** Count items per group folder name; ungrouped items (`null`) count nowhere. */
180
- function bucketByGroup(items: { path: string }[]): Map<string, number> {
179
+ /** Count items per plugin folder name; ungrouped items (`null`) count nowhere. */
180
+ function bucketByPlugin(items: { path: string }[]): Map<string, number> {
181
181
  const counts = new Map<string, number>();
182
182
  for (const item of items) {
183
- const group = groupOfPath(item.path);
184
- if (group === null) continue;
185
- counts.set(group, (counts.get(group) ?? 0) + 1);
183
+ const plugin = pluginOfPath(item.path);
184
+ if (plugin === null) continue;
185
+ counts.set(plugin, (counts.get(plugin) ?? 0) + 1);
186
186
  }
187
187
  return counts;
188
188
  }
189
189
 
190
- /** The default-branch workspace id every group resolution runs against. */
191
- export function groupsWorkspaceId(): string {
190
+ /** The default-branch workspace id every plugin resolution runs against. */
191
+ export function pluginsWorkspaceId(): string {
192
192
  return workspaceIdForBranch(DEFAULT_BRANCH);
193
193
  }
@@ -1,143 +1,143 @@
1
- import type { Server as HttpServer } from 'node:http';
2
- import express from 'express';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
4
- import { createSecretsVaultRoutes } from '../secrets-vault.routes.js';
5
-
6
- /**
7
- * THE OWNER GATE: a tool's shared (admin) secrets — plain values and OAuth
8
- * client secrets — are settable by whoever can WRITE that `.tool` FILE (its
9
- * frontmatter `write:`/`owner:` verbs + the access.md chain), and by no one
10
- * else. There is deliberately NO platform-role check here: a non-Admin who
11
- * manages a `.tool` configures it; an Admin who can't write the file doesn't.
12
- * The per-file resolution itself (frontmatter verbs on `.tool` files) is
13
- * covered in access-control.service.test.ts — this locks the route gate to
14
- * `canWrite(path)` so no role-based shortcut regresses it.
15
- */
16
-
17
- const TOOL_PATH = 'Tools/weather.tool';
18
- const WRITER = 'writer@x.com';
19
- const READER = 'reader@x.com';
20
-
21
- const toolManualService = {
22
- listAccessible: async () => [
23
- {
24
- slug: 'weather',
25
- name: 'weather',
26
- path: TOOL_PATH,
27
- type: 'mcp' as const,
28
- variables: [
29
- { name: 'SHARED_KEY', scope: 'admin' as const, label: 'Org key' },
30
- {
31
- name: 'SIGNIN',
32
- scope: 'user' as const,
33
- label: 'Weather sign-in',
34
- oauth: {
35
- authorizationUrl: 'https://auth.example.com/authorize',
36
- tokenUrl: 'https://auth.example.com/token',
37
- clientId: 'client-1',
38
- },
39
- },
40
- ],
41
- },
42
- ],
43
- } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['toolManualService'];
44
-
45
- const putSharedStatic = vi.fn(async () => ({ id: 's1' }));
46
- const putSharedOAuthClientSecret = vi.fn(async () => {});
47
- const secretsVault = {
48
- putSharedStatic,
49
- putSharedOAuthClientSecret,
50
- } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['secretsVault'];
51
-
52
- // Per-FILE write: only WRITER may write THIS tool's path. No role concept at all.
53
- const accessControl = {
54
- canRead: async () => true,
55
- canWrite: async (_ws: string, email: string, path: string) => email === WRITER && path === TOOL_PATH,
56
- } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['accessControl'];
57
-
58
- let httpServer: HttpServer | undefined;
59
-
60
- async function baseUrlAs(email: string): Promise<string> {
61
- const app = express();
62
- app.use(express.json());
63
- app.use((req, _res, next) => {
64
- req.userId = `id-${email}`;
65
- req.userEmail = email;
66
- next();
67
- });
68
- app.use(
69
- '/api',
70
- createSecretsVaultRoutes({
71
- secretsVault,
72
- toolManualService,
73
- accessControl,
74
- stateSecret: 'test-secret',
75
- publicBackendUrl: 'http://localhost:3000',
76
- publicFrontendUrl: 'http://localhost:5173',
77
- }),
78
- );
79
- httpServer = await new Promise<HttpServer>((resolve) => {
80
- const s = app.listen(0, () => resolve(s));
81
- });
82
- const port = (httpServer.address() as { port: number }).port;
83
- return `http://127.0.0.1:${port}`;
84
- }
85
-
86
- afterEach(async () => {
87
- if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
88
- httpServer = undefined;
89
- putSharedStatic.mockClear();
90
- putSharedOAuthClientSecret.mockClear();
91
- });
92
-
93
- describe('tool owner gate — shared config requires WRITE on the `.tool` file', () => {
94
- it('a writer of the file sets the shared admin value', async () => {
95
- const base = await baseUrlAs(WRITER);
96
- const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
97
- method: 'PUT',
98
- headers: { 'Content-Type': 'application/json' },
99
- body: JSON.stringify({ value: 'k-123' }),
100
- });
101
- expect(res.status).toBe(201);
102
- expect(putSharedStatic).toHaveBeenCalledWith(expect.objectContaining({ key: 'weather_SHARED_KEY' }));
103
- });
104
-
105
- it('a non-writer is refused (403), even though they can READ the tool', async () => {
106
- const base = await baseUrlAs(READER);
107
- const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
108
- method: 'PUT',
109
- headers: { 'Content-Type': 'application/json' },
110
- body: JSON.stringify({ value: 'k-123' }),
111
- });
112
- expect(res.status).toBe(403);
113
- expect(putSharedStatic).not.toHaveBeenCalled();
114
- });
115
-
116
- it('a writer of the file sets the OAuth client secret, pinned to the declared provider', async () => {
117
- const base = await baseUrlAs(WRITER);
118
- const res = await fetch(`${base}/api/secrets/tools/weather/vars/SIGNIN/oauth/admin`, {
119
- method: 'PUT',
120
- headers: { 'Content-Type': 'application/json' },
121
- body: JSON.stringify({ clientSecret: 'cs-123' }),
122
- });
123
- expect(res.status).toBe(201);
124
- expect(putSharedOAuthClientSecret).toHaveBeenCalledWith(
125
- expect.objectContaining({
126
- key: 'weather_SIGNIN',
127
- clientSecret: 'cs-123',
128
- provider: expect.objectContaining({ clientId: 'client-1' }),
129
- }),
130
- );
131
- });
132
-
133
- it('a non-writer cannot set the client secret (403)', async () => {
134
- const base = await baseUrlAs(READER);
135
- const res = await fetch(`${base}/api/secrets/tools/weather/vars/SIGNIN/oauth/admin`, {
136
- method: 'PUT',
137
- headers: { 'Content-Type': 'application/json' },
138
- body: JSON.stringify({ clientSecret: 'cs-123' }),
139
- });
140
- expect(res.status).toBe(403);
141
- expect(putSharedOAuthClientSecret).not.toHaveBeenCalled();
142
- });
143
- });
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import express from 'express';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { createSecretsVaultRoutes } from '../secrets-vault.routes.js';
5
+
6
+ /**
7
+ * THE OWNER GATE: a tool's shared (admin) secrets — plain values and OAuth
8
+ * client secrets — are settable by whoever can WRITE that `.tool` FILE (its
9
+ * frontmatter `write:`/`owner:` verbs + the access.md chain), and by no one
10
+ * else. There is deliberately NO platform-role check here: a non-Admin who
11
+ * manages a `.tool` configures it; an Admin who can't write the file doesn't.
12
+ * The per-file resolution itself (frontmatter verbs on `.tool` files) is
13
+ * covered in access-control.service.test.ts — this locks the route gate to
14
+ * `canWrite(path)` so no role-based shortcut regresses it.
15
+ */
16
+
17
+ const TOOL_PATH = 'Tools/weather.tool';
18
+ const WRITER = 'writer@x.com';
19
+ const READER = 'reader@x.com';
20
+
21
+ const toolManualService = {
22
+ listAccessible: async () => [
23
+ {
24
+ slug: 'weather',
25
+ name: 'weather',
26
+ path: TOOL_PATH,
27
+ type: 'mcp' as const,
28
+ variables: [
29
+ { name: 'SHARED_KEY', scope: 'admin' as const, label: 'Org key' },
30
+ {
31
+ name: 'SIGNIN',
32
+ scope: 'user' as const,
33
+ label: 'Weather sign-in',
34
+ oauth: {
35
+ authorizationUrl: 'https://auth.example.com/authorize',
36
+ tokenUrl: 'https://auth.example.com/token',
37
+ clientId: 'client-1',
38
+ },
39
+ },
40
+ ],
41
+ },
42
+ ],
43
+ } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['toolManualService'];
44
+
45
+ const putSharedStatic = vi.fn(async () => ({ id: 's1' }));
46
+ const putSharedOAuthClientSecret = vi.fn(async () => {});
47
+ const secretsVault = {
48
+ putSharedStatic,
49
+ putSharedOAuthClientSecret,
50
+ } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['secretsVault'];
51
+
52
+ // Per-FILE write: only WRITER may write THIS tool's path. No role concept at all.
53
+ const accessControl = {
54
+ canRead: async () => true,
55
+ canWrite: async (_ws: string, email: string, path: string) => email === WRITER && path === TOOL_PATH,
56
+ } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['accessControl'];
57
+
58
+ let httpServer: HttpServer | undefined;
59
+
60
+ async function baseUrlAs(email: string): Promise<string> {
61
+ const app = express();
62
+ app.use(express.json());
63
+ app.use((req, _res, next) => {
64
+ req.userId = `id-${email}`;
65
+ req.userEmail = email;
66
+ next();
67
+ });
68
+ app.use(
69
+ '/api',
70
+ createSecretsVaultRoutes({
71
+ secretsVault,
72
+ toolManualService,
73
+ accessControl,
74
+ stateSecret: 'test-secret',
75
+ publicBackendUrl: 'http://localhost:3000',
76
+ publicFrontendUrl: 'http://localhost:5173',
77
+ }),
78
+ );
79
+ httpServer = await new Promise<HttpServer>((resolve) => {
80
+ const s = app.listen(0, () => resolve(s));
81
+ });
82
+ const port = (httpServer.address() as { port: number }).port;
83
+ return `http://127.0.0.1:${port}`;
84
+ }
85
+
86
+ afterEach(async () => {
87
+ if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
88
+ httpServer = undefined;
89
+ putSharedStatic.mockClear();
90
+ putSharedOAuthClientSecret.mockClear();
91
+ });
92
+
93
+ describe('tool owner gate — shared config requires WRITE on the `.tool` file', () => {
94
+ it('a writer of the file sets the shared admin value', async () => {
95
+ const base = await baseUrlAs(WRITER);
96
+ const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
97
+ method: 'PUT',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify({ value: 'k-123' }),
100
+ });
101
+ expect(res.status).toBe(201);
102
+ expect(putSharedStatic).toHaveBeenCalledWith(expect.objectContaining({ key: 'weather_SHARED_KEY' }));
103
+ });
104
+
105
+ it('a non-writer is refused (403), even though they can READ the tool', async () => {
106
+ const base = await baseUrlAs(READER);
107
+ const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
108
+ method: 'PUT',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ value: 'k-123' }),
111
+ });
112
+ expect(res.status).toBe(403);
113
+ expect(putSharedStatic).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it('a writer of the file sets the OAuth client secret, pinned to the declared provider', async () => {
117
+ const base = await baseUrlAs(WRITER);
118
+ const res = await fetch(`${base}/api/secrets/tools/weather/vars/SIGNIN/oauth/admin`, {
119
+ method: 'PUT',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify({ clientSecret: 'cs-123' }),
122
+ });
123
+ expect(res.status).toBe(201);
124
+ expect(putSharedOAuthClientSecret).toHaveBeenCalledWith(
125
+ expect.objectContaining({
126
+ key: 'weather_SIGNIN',
127
+ clientSecret: 'cs-123',
128
+ provider: expect.objectContaining({ clientId: 'client-1' }),
129
+ }),
130
+ );
131
+ });
132
+
133
+ it('a non-writer cannot set the client secret (403)', async () => {
134
+ const base = await baseUrlAs(READER);
135
+ const res = await fetch(`${base}/api/secrets/tools/weather/vars/SIGNIN/oauth/admin`, {
136
+ method: 'PUT',
137
+ headers: { 'Content-Type': 'application/json' },
138
+ body: JSON.stringify({ clientSecret: 'cs-123' }),
139
+ });
140
+ expect(res.status).toBe(403);
141
+ expect(putSharedOAuthClientSecret).not.toHaveBeenCalled();
142
+ });
143
+ });
@@ -40,7 +40,7 @@ function cr(over: Partial<ChangeRequest> = {}): ChangeRequest {
40
40
  base: 'main',
41
41
  state: 'open',
42
42
  createdAt: '2026-08-06T09:00:00.000Z',
43
- touchedNodePaths: ['Groups/Engineering/weekly-newsletter/SKILL.md'],
43
+ touchedNodePaths: ['Plugins/Engineering/weekly-newsletter/SKILL.md'],
44
44
  review: { approvals: 0, changesRequested: 0, pendingLogins: [] },
45
45
  url: '/change-requests/7',
46
46
  ...over,
@@ -56,7 +56,7 @@ function harness(opts: {
56
56
  writers?: Record<string, string[]>;
57
57
  }) {
58
58
  const branchFiles = opts.branchFiles ?? {
59
- 'agent/weekly-newsletter:Groups/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
59
+ 'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
60
60
  };
61
61
  const workspaceService = {
62
62
  ensureRemotesFetched: async () => undefined,
@@ -81,7 +81,7 @@ function harness(opts: {
81
81
  }
82
82
 
83
83
  const ADMIN_WRITES = {
84
- 'Groups/Engineering/weekly-newsletter/SKILL.md': [ADMIN],
84
+ 'Plugins/Engineering/weekly-newsletter/SKILL.md': [ADMIN],
85
85
  };
86
86
 
87
87
  describe('PendingSkillsService', () => {
@@ -93,7 +93,7 @@ describe('PendingSkillsService', () => {
93
93
  name: 'weekly-newsletter',
94
94
  description: 'Drafts the Friday newsletter.',
95
95
  version: '0.1.0',
96
- path: 'Groups/Engineering/weekly-newsletter',
96
+ path: 'Plugins/Engineering/weekly-newsletter',
97
97
  changeRequestNumber: 7,
98
98
  branch: 'agent/weekly-newsletter',
99
99
  authorName: 'Ali Raza',
@@ -110,7 +110,7 @@ describe('PendingSkillsService', () => {
110
110
 
111
111
  /**
112
112
  * The decision that shapes this surface: a proposal is between its author and
113
- * the people who can approve it, and nobody else. A member of the group who
113
+ * the people who can approve it, and nobody else. A member of the plugin who
114
114
  * cannot write the folder is not one of those people.
115
115
  */
116
116
  test('hides it from everyone else', async () => {
@@ -143,7 +143,7 @@ describe('PendingSkillsService', () => {
143
143
  {
144
144
  name: 'weekly-newsletter',
145
145
  description: 'Drafts the Friday newsletter.',
146
- path: 'Groups/Engineering/weekly-newsletter',
146
+ path: 'Plugins/Engineering/weekly-newsletter',
147
147
  },
148
148
  ],
149
149
  writers: ADMIN_WRITES,
@@ -156,9 +156,9 @@ describe('PendingSkillsService', () => {
156
156
  crs: [
157
157
  cr({
158
158
  touchedNodePaths: [
159
- 'Groups/Engineering/access.md',
160
- 'Groups/Engineering/SKILL.md', // a group folder is not a skill
161
- 'Groups/Engineering/weekly-newsletter/reference.md',
159
+ 'Plugins/Engineering/access.md',
160
+ 'Plugins/Engineering/SKILL.md', // a plugin folder is not a skill
161
+ 'Plugins/Engineering/weekly-newsletter/reference.md',
162
162
  ],
163
163
  }),
164
164
  ],
@@ -185,7 +185,7 @@ describe('PendingSkillsService', () => {
185
185
  const svc = harness({
186
186
  crs: [cr()],
187
187
  branchFiles: {
188
- 'agent/weekly-newsletter:Groups/Engineering/weekly-newsletter/SKILL.md':
188
+ 'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md':
189
189
  '---\ndescription: No name declared.\n---\n\n# Body\n',
190
190
  },
191
191
  writers: ADMIN_WRITES,
@@ -222,17 +222,17 @@ describe('PendingSkillsService', () => {
222
222
  number: 3,
223
223
  createdAt: '2026-08-01T12:00:00.000Z',
224
224
  branch: 'agent/older',
225
- touchedNodePaths: ['Groups/Engineering/older/SKILL.md'],
225
+ touchedNodePaths: ['Plugins/Engineering/older/SKILL.md'],
226
226
  }),
227
227
  ],
228
228
  branchFiles: {
229
- 'agent/weekly-newsletter:Groups/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
230
- 'agent/older:Groups/Engineering/older/SKILL.md':
229
+ 'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
230
+ 'agent/older:Plugins/Engineering/older/SKILL.md':
231
231
  '---\nname: older\ndescription: Older.\n---\n',
232
232
  },
233
233
  writers: {
234
234
  ...ADMIN_WRITES,
235
- 'Groups/Engineering/older/SKILL.md': [ADMIN],
235
+ 'Plugins/Engineering/older/SKILL.md': [ADMIN],
236
236
  },
237
237
  });
238
238
  expect((await svc.listPendingSkills(ADMIN)).map((p) => p.changeRequestNumber)).toEqual([3, 9]);