@bevel-software/platform-core-backend 0.7.4 → 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.
- package/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +51 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +5 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +38 -21
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -7
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +6 -6
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +5 -5
- package/dist/modules/access/access-declarations.js +3 -3
- package/dist/modules/access/access-mutation.service.d.ts +3 -3
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +3 -3
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.js +4 -4
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.js +19 -19
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +2 -2
- package/dist/modules/access/creator-access.js +5 -5
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +18 -4
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +14 -4
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
- package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode-names.js +5 -27
- package/dist/modules/code-mode/code-mode-names.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +29 -7
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -52
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +33 -395
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +7 -0
- package/dist/modules/plugins/index.d.ts.map +1 -0
- package/dist/modules/plugins/index.js +6 -0
- package/dist/modules/plugins/index.js.map +1 -0
- package/dist/modules/plugins/join-proposals.d.ts +53 -0
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
- package/dist/modules/plugins/join-proposals.js +67 -0
- package/dist/modules/plugins/join-proposals.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts +81 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-requests.service.js +135 -0
- package/dist/modules/plugins/join-requests.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.js +344 -0
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +106 -0
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.contract.js +36 -0
- package/dist/modules/plugins/plugins.contract.js.map +1 -0
- package/dist/modules/plugins/plugins.routes.d.ts +42 -0
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.routes.js +379 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -0
- package/dist/modules/plugins/plugins.service.d.ts +60 -0
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.service.js +172 -0
- package/dist/modules/plugins/plugins.service.js.map +1 -0
- package/dist/modules/skills/pending-skills.service.d.ts +2 -2
- package/dist/modules/skills/pending-skills.service.js +7 -7
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +4 -4
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.service.js +5 -5
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.js +2 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +37 -9
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +17 -4
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +63 -9
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +1 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +6 -1
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +43 -10
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/plugins-migration.d.ts +50 -0
- package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
- package/dist/modules/workspace/plugins-migration.js +379 -0
- package/dist/modules/workspace/plugins-migration.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +26 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +83 -12
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/kb-layout.test.js +3 -3
- package/dist/shared/kb-layout.test.js.map +1 -1
- package/dist/shared/utcp-namespace.d.ts +6 -27
- package/dist/shared/utcp-namespace.d.ts.map +1 -1
- package/dist/shared/utcp-namespace.js +6 -63
- package/dist/shared/utcp-namespace.js.map +1 -1
- package/dist/shared/variable-refs.d.ts +42 -0
- package/dist/shared/variable-refs.d.ts.map +1 -0
- package/dist/shared/variable-refs.js +60 -0
- package/dist/shared/variable-refs.js.map +1 -0
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +88 -35
- package/kb-template/KnowledgeBase/How to get started.md +10 -10
- package/kb-template/access.md +36 -36
- package/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +106 -106
- package/src/core/create-core-server.ts +55 -9
- package/src/core/create-core-services.ts +48 -24
- package/src/index.ts +69 -69
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
- package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
- package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
- package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
- package/src/modules/access/__tests__/access-splice.test.ts +2 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
- package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
- package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
- package/src/modules/access/access-control.interface.ts +8 -7
- package/src/modules/access/access-control.service.ts +7 -7
- package/src/modules/access/access-declarations.ts +5 -5
- package/src/modules/access/access-mutation.service.ts +3 -3
- package/src/modules/access/access-splice.ts +4 -4
- package/src/modules/access/access.routes.ts +20 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +13 -2
- package/src/modules/admin/admin-access.routes.ts +29 -29
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
- package/src/modules/auth/rate-limit.ts +45 -45
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
- package/src/modules/code-mode/code-mode-names.ts +10 -36
- package/src/modules/code-mode/code-mode.tool.ts +27 -7
- package/src/modules/database/connection.ts +15 -15
- package/src/modules/database/schema.ts +11 -11
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
- package/src/modules/mcp/mcp.service.ts +57 -435
- package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
- package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
- package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
- package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
- package/src/modules/plugins/index.ts +17 -0
- package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
- package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
- package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
- package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
- package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
- package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/pending-skills.service.ts +7 -7
- package/src/modules/skills/skills.contract.ts +4 -4
- package/src/modules/skills/skills.service.ts +5 -5
- package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
- package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
- package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
- package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +118 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/git.service.ts +2 -2
- package/src/modules/workflow/pending-commits.service.ts +48 -12
- package/src/modules/workflow/pending-commits.worker.ts +64 -8
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/workflow-hooks.ts +101 -101
- package/src/modules/workflow/workflow.service.ts +13 -2
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
- package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
- package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/kb-seed.service.ts +38 -9
- package/src/modules/workspace/plugins-migration.ts +479 -0
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +85 -14
- package/src/modules/workspace/workspace.tools.ts +922 -922
- package/src/shared/__tests__/join-request.test.ts +13 -13
- package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +3 -3
- package/src/shared/utcp-namespace.ts +10 -68
- package/src/shared/variable-refs.ts +64 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
- package/src/modules/groups/index.ts +0 -17
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
|
@@ -119,7 +119,7 @@ describe('KbSeedService', () => {
|
|
|
119
119
|
expect(await exists(path.join(dir, '.gitignore'))).toBe(true);
|
|
120
120
|
// Core's two roots are seeded (kept present via their .gitkeep).
|
|
121
121
|
expect(await exists(path.join(dir, 'KnowledgeBase/.gitkeep'))).toBe(true);
|
|
122
|
-
expect(await exists(path.join(dir, '
|
|
122
|
+
expect(await exists(path.join(dir, 'Plugins/.gitkeep'))).toBe(true);
|
|
123
123
|
// …and only those two. `Data/`, `Agents/` and `Pipelines/` scaffold an
|
|
124
124
|
// agentic execution layer this platform does not have, so seeding them
|
|
125
125
|
// would hand every operator three empty folders nothing can fill.
|
|
@@ -156,7 +156,7 @@ describe('KbSeedService', () => {
|
|
|
156
156
|
'.bevelignore': 'x',
|
|
157
157
|
'.gitignore': 'x',
|
|
158
158
|
'KnowledgeBase/Real/Knowledge/.gitkeep': '',
|
|
159
|
-
'
|
|
159
|
+
'Plugins/.gitkeep': '',
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
await makeSeeder(upstream).ensureRemoteSeeded();
|
|
@@ -172,7 +172,7 @@ describe('KbSeedService', () => {
|
|
|
172
172
|
'access.md': 'CUSTOM ACCESS RULES',
|
|
173
173
|
// AGENTS.md / .bevelignore / .gitignore are absent → should be added.
|
|
174
174
|
'KnowledgeBase/Real/Knowledge/.gitkeep': '',
|
|
175
|
-
'
|
|
175
|
+
'Plugins/.gitkeep': '',
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
// Simulate the app loading the branch: a fresh clone, then top-up on it.
|
|
@@ -201,7 +201,7 @@ describe('KbSeedService', () => {
|
|
|
201
201
|
'.gitignore': '',
|
|
202
202
|
'access.md': 'CUSTOM ACCESS RULES',
|
|
203
203
|
'KnowledgeBase/Real/Knowledge/.gitkeep': '',
|
|
204
|
-
'
|
|
204
|
+
'Plugins/.gitkeep': '',
|
|
205
205
|
});
|
|
206
206
|
|
|
207
207
|
const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
@@ -240,7 +240,7 @@ describe('KbSeedService', () => {
|
|
|
240
240
|
'.gitignore': '',
|
|
241
241
|
'access.md': 'CUSTOM ACCESS RULES',
|
|
242
242
|
'KnowledgeBase/Real/Knowledge/.gitkeep': '',
|
|
243
|
-
'
|
|
243
|
+
'Plugins/.gitkeep': '',
|
|
244
244
|
});
|
|
245
245
|
|
|
246
246
|
const first = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
@@ -254,7 +254,7 @@ describe('KbSeedService', () => {
|
|
|
254
254
|
const upstream = await seededUpstream(PROTECTED, {
|
|
255
255
|
'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
|
|
256
256
|
'KnowledgeBase/Real/Knowledge/.gitkeep': '',
|
|
257
|
-
'
|
|
257
|
+
'Plugins/.gitkeep': '',
|
|
258
258
|
});
|
|
259
259
|
|
|
260
260
|
const first = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
@@ -270,6 +270,74 @@ describe('KbSeedService', () => {
|
|
|
270
270
|
expect(after2).toBe(after1);
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
it('a note-only migration outcome stages nothing and raises no top-up warning', async () => {
|
|
274
|
+
const template = await fs.readFile(
|
|
275
|
+
path.join(__dirname, '../../../../kb-template/AGENTS.md'),
|
|
276
|
+
'utf8',
|
|
277
|
+
);
|
|
278
|
+
const upstream = await seededUpstream(PROTECTED, {
|
|
279
|
+
'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
|
|
280
|
+
'access.md': 'x',
|
|
281
|
+
'AGENTS.md': template,
|
|
282
|
+
'.bevelignore': 'AGENTS.md\nPlugins/\n',
|
|
283
|
+
'.gitignore': '',
|
|
284
|
+
'KnowledgeBase/Real/Knowledge/.gitkeep': '',
|
|
285
|
+
'Plugins/GTM/access.md': 'write:\n - Admin\n',
|
|
286
|
+
// An unparsable manifest keeps this manual a `.tool` — its variables
|
|
287
|
+
// have nowhere to go. The migration NOTES the refusal every run and
|
|
288
|
+
// changes nothing; staging on the note used to reach `git commit`
|
|
289
|
+
// with an empty index and warn about it on every boot.
|
|
290
|
+
'Plugins/GTM/plugin.json': '{ not json at all',
|
|
291
|
+
'Plugins/GTM/vendor.tool': JSON.stringify({
|
|
292
|
+
name: 'vendor',
|
|
293
|
+
type: 'mcp',
|
|
294
|
+
url: 'https://mcp.vendor.example/mcp',
|
|
295
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
299
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
300
|
+
try {
|
|
301
|
+
await makeSeeder(upstream).topUpWorkspace(repoDir, DEFAULT_BRANCH);
|
|
302
|
+
expect(warn.mock.calls.flat().join(' ')).not.toMatch(/top-up/i);
|
|
303
|
+
} finally {
|
|
304
|
+
warn.mockRestore();
|
|
305
|
+
}
|
|
306
|
+
// Only the pre-seed commit — a note is not a change.
|
|
307
|
+
expect(await headCommitCount(root, upstream, DEFAULT_BRANCH)).toBe(1);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('the root rename rewrites the stale .bevelignore rule and commits it along', async () => {
|
|
311
|
+
const template = await fs.readFile(
|
|
312
|
+
path.join(__dirname, '../../../../kb-template/AGENTS.md'),
|
|
313
|
+
'utf8',
|
|
314
|
+
);
|
|
315
|
+
const upstream = await seededUpstream(PROTECTED, {
|
|
316
|
+
'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
|
|
317
|
+
'access.md': 'x',
|
|
318
|
+
'AGENTS.md': template,
|
|
319
|
+
// The pre-rename rule: without the rewrite the migrated KB has a rule
|
|
320
|
+
// for a root that no longer exists and none for the one that does.
|
|
321
|
+
'.bevelignore': 'AGENTS.md\nGroups/\n',
|
|
322
|
+
'.gitignore': '',
|
|
323
|
+
'KnowledgeBase/Real/Knowledge/.gitkeep': '',
|
|
324
|
+
'Groups/GTM/access.md': 'write:\n - Admin\n',
|
|
325
|
+
});
|
|
326
|
+
const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
327
|
+
await makeSeeder(upstream).topUpWorkspace(repoDir, DEFAULT_BRANCH);
|
|
328
|
+
|
|
329
|
+
// An independent clone proves the rewrite was STAGED (the ignore file
|
|
330
|
+
// is a repo-root path outside the two roots' pathspecs) and pushed.
|
|
331
|
+
const dir = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
332
|
+
expect(await exists(path.join(dir, 'Plugins/GTM/access.md'))).toBe(true);
|
|
333
|
+
const ignore = await fs.readFile(path.join(dir, '.bevelignore'), 'utf8');
|
|
334
|
+
expect(ignore.split('\n').map((l) => l.trim())).toContain('Plugins/');
|
|
335
|
+
expect(ignore).not.toContain('Groups/');
|
|
336
|
+
expect(ignore).toContain('AGENTS.md');
|
|
337
|
+
const log = await git(dir, ['log', '-1', '--pretty=%s']);
|
|
338
|
+
expect(log.trim()).toBe('Move Groups/ to Plugins/ (Agent Plugins layout)');
|
|
339
|
+
});
|
|
340
|
+
|
|
273
341
|
});
|
|
274
342
|
|
|
275
343
|
/**
|
|
@@ -292,7 +360,7 @@ describe('KbSeedService', () => {
|
|
|
292
360
|
describe('the pre-rename ignore file', () => {
|
|
293
361
|
const legacy = (ignore: string): Record<string, string> => ({
|
|
294
362
|
'KnowledgeBase/.gitkeep': '',
|
|
295
|
-
'
|
|
363
|
+
'Plugins/.gitkeep': '',
|
|
296
364
|
'access.md': '---\nwrite:\n - Admin\n---\n',
|
|
297
365
|
'roles.yaml': 'roles:\n Admin:\n - a@example.com\n',
|
|
298
366
|
'.gitignore': '',
|
|
@@ -335,7 +403,7 @@ describe('KbSeedService', () => {
|
|
|
335
403
|
const dir = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
336
404
|
// Core's two, still there.
|
|
337
405
|
expect(await exists(path.join(dir, 'KnowledgeBase/.gitkeep'))).toBe(true);
|
|
338
|
-
expect(await exists(path.join(dir, '
|
|
406
|
+
expect(await exists(path.join(dir, 'Plugins/.gitkeep'))).toBe(true);
|
|
339
407
|
// …and the three this distribution asked for.
|
|
340
408
|
for (const claimed of ['Data', 'Agents', 'Pipelines']) {
|
|
341
409
|
expect(await exists(path.join(dir, `${claimed}/.gitkeep`))).toBe(true);
|
|
@@ -379,7 +447,7 @@ describe('KbSeedService', () => {
|
|
|
379
447
|
});
|
|
380
448
|
|
|
381
449
|
/**
|
|
382
|
-
* A FILE named `
|
|
450
|
+
* A FILE named `Plugins` used to satisfy the presence check — `fs.access`
|
|
383
451
|
* answers "is there something here?" — so the seeder skipped it, said
|
|
384
452
|
* nothing, and left a knowledge base permanently missing a root it claims
|
|
385
453
|
* to guarantee. Now it says so.
|
|
@@ -396,18 +464,18 @@ describe('KbSeedService', () => {
|
|
|
396
464
|
'.gitignore': '',
|
|
397
465
|
'KnowledgeBase/.gitkeep': '',
|
|
398
466
|
// Not a folder.
|
|
399
|
-
|
|
467
|
+
Plugins: 'this is a file, not the plugins root',
|
|
400
468
|
});
|
|
401
469
|
const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
402
470
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
403
471
|
try {
|
|
404
472
|
await makeSeeder(upstream).topUpWorkspace(repoDir, DEFAULT_BRANCH);
|
|
405
|
-
expect(warn.mock.calls.flat().join(' ')).toMatch(/
|
|
473
|
+
expect(warn.mock.calls.flat().join(' ')).toMatch(/Plugins.*not a directory/);
|
|
406
474
|
} finally {
|
|
407
475
|
warn.mockRestore();
|
|
408
476
|
}
|
|
409
477
|
// …and it did not quietly replace the file either.
|
|
410
|
-
expect((await fs.lstat(path.join(repoDir, '
|
|
478
|
+
expect((await fs.lstat(path.join(repoDir, 'Plugins'))).isFile()).toBe(true);
|
|
411
479
|
});
|
|
412
480
|
|
|
413
481
|
/**
|
|
@@ -422,7 +490,7 @@ describe('KbSeedService', () => {
|
|
|
422
490
|
const upstream = await seededUpstream(PROTECTED, {
|
|
423
491
|
'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
|
|
424
492
|
'KnowledgeBase/Real/Knowledge/.gitkeep': '',
|
|
425
|
-
'
|
|
493
|
+
'Plugins/.gitkeep': '',
|
|
426
494
|
});
|
|
427
495
|
// Branch a suggestion off the default and load it.
|
|
428
496
|
const seed = await checkout(root, upstream, DEFAULT_BRANCH);
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { migrateGroupsToPlugins } from '../plugins-migration.js';
|
|
6
|
+
|
|
7
|
+
let repo: string;
|
|
8
|
+
|
|
9
|
+
async function write(rel: string, content: string): Promise<void> {
|
|
10
|
+
const abs = path.join(repo, rel);
|
|
11
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
12
|
+
await fs.writeFile(abs, content, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function read(rel: string): Promise<string> {
|
|
16
|
+
return fs.readFile(path.join(repo, rel), 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function exists(rel: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
await fs.stat(path.join(repo, rel));
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A knowledge base as every deployment looks today, pre-migration. */
|
|
29
|
+
async function seedLegacyKb(): Promise<void> {
|
|
30
|
+
await write('Groups/GTM/access.md', '---\nread:\n - everyone\n---\nwrite:\n - Admin\n');
|
|
31
|
+
await write('Groups/GTM/outreach/SKILL.md', '---\ndescription: Outreach.\n---\n# Outreach\n');
|
|
32
|
+
await write('Groups/GTM/outreach/scripts/run.py', 'print("hi")\n');
|
|
33
|
+
await write(
|
|
34
|
+
'Groups/GTM/notion.tool',
|
|
35
|
+
JSON.stringify({ name: 'notion', type: 'mcp', url: 'https://mcp.notion.com/mcp' }, null, 2),
|
|
36
|
+
);
|
|
37
|
+
await write(
|
|
38
|
+
'Groups/GTM/web-search.tool',
|
|
39
|
+
JSON.stringify(
|
|
40
|
+
{ name: 'web_search', type: 'inline', tools: [{ name: 'search' }] },
|
|
41
|
+
null,
|
|
42
|
+
2,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
repo = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-plugins-'));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await fs.rm(repo, { recursive: true, force: true });
|
|
53
|
+
vi.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('migrateGroupsToPlugins', () => {
|
|
57
|
+
it('does nothing to a knowledge base that has neither root', async () => {
|
|
58
|
+
await write('KnowledgeBase/.gitkeep', '');
|
|
59
|
+
expect(await migrateGroupsToPlugins(repo)).toEqual({
|
|
60
|
+
migrated: false,
|
|
61
|
+
renamed: false,
|
|
62
|
+
ignoreRewritten: false,
|
|
63
|
+
notes: [],
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('moves the root, the skills and the tools, and writes the manifest', async () => {
|
|
68
|
+
await seedLegacyKb();
|
|
69
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
70
|
+
expect(result.migrated).toBe(true);
|
|
71
|
+
|
|
72
|
+
expect(await exists('Groups')).toBe(false);
|
|
73
|
+
// access.md stays at the PLUGIN ROOT — one level down it would govern only
|
|
74
|
+
// its own subtree, silently narrowing what it protects.
|
|
75
|
+
expect(await exists('Plugins/GTM/access.md')).toBe(true);
|
|
76
|
+
expect(await exists('Plugins/GTM/skills/outreach/SKILL.md')).toBe(true);
|
|
77
|
+
// A skill folder moves whole; its bundled assets are part of the skill.
|
|
78
|
+
expect(await exists('Plugins/GTM/skills/outreach/scripts/run.py')).toBe(true);
|
|
79
|
+
// The mcp manual CONVERTED (mcp.json is authoritative); http/inline moved.
|
|
80
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/notion.tool')).toBe(false);
|
|
81
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/web-search.tool')).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('writes a manifest a conformant client accepts', async () => {
|
|
85
|
+
await seedLegacyKb();
|
|
86
|
+
await migrateGroupsToPlugins(repo);
|
|
87
|
+
const manifest = JSON.parse(await read('Plugins/GTM/plugin.json'));
|
|
88
|
+
expect(manifest.$schema).toBe('https://agent-plugins.org/schemas/1.0.0/plugin.schema.json');
|
|
89
|
+
expect(manifest.name).toBe('gtm');
|
|
90
|
+
// The closed field set: anything else is a top-level field the spec does
|
|
91
|
+
// not permit, and inventing metadata about a folder is asserting things
|
|
92
|
+
// nobody said.
|
|
93
|
+
expect(Object.keys(manifest).sort()).toEqual(['$schema', 'name']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('converts mcp-type manuals into mcp.json entries and leaves the others out of it', async () => {
|
|
97
|
+
await seedLegacyKb();
|
|
98
|
+
await migrateGroupsToPlugins(repo);
|
|
99
|
+
const mcp = JSON.parse(await read('Plugins/GTM/mcp.json'));
|
|
100
|
+
expect(mcp.$schema).toBe('https://agent-plugins.org/schemas/1.0.0/mcp.schema.json');
|
|
101
|
+
expect(mcp.mcpServers).toEqual({
|
|
102
|
+
notion: { type: 'streamable-http', url: 'https://mcp.notion.com/mcp' },
|
|
103
|
+
});
|
|
104
|
+
// `inline` has no representation in the spec, so it exists only as a
|
|
105
|
+
// `.tool` under our namespace.
|
|
106
|
+
expect(mcp.mcpServers.web_search).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('splits headers: literals into mcp.json, credential references into plugin.json extensions', async () => {
|
|
110
|
+
await write('Groups/GTM/access.md', 'write:\n - Admin\n');
|
|
111
|
+
await write(
|
|
112
|
+
'Groups/GTM/vendor.tool',
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
name: 'vendor',
|
|
115
|
+
type: 'mcp',
|
|
116
|
+
url: 'https://mcp.vendor.example/mcp',
|
|
117
|
+
headers: { Authorization: 'Bearer ${VENDOR_KEY}', 'X-Api-Version': '2' },
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
await migrateGroupsToPlugins(repo);
|
|
121
|
+
const mcp = JSON.parse(await read('Plugins/GTM/mcp.json'));
|
|
122
|
+
// The spec forbids credentials in `headers` and forbids expanding anything
|
|
123
|
+
// but ${PLUGIN_ROOT}/${PLUGIN_DATA} — a copied ${VENDOR_KEY} would be sent
|
|
124
|
+
// literally by a conformant client.
|
|
125
|
+
expect(mcp.mcpServers.vendor.headers).toEqual({ 'X-Api-Version': '2' });
|
|
126
|
+
// The reference lives on in the extensions block, which is ours to interpret.
|
|
127
|
+
const manifest = JSON.parse(await read('Plugins/GTM/plugin.json'));
|
|
128
|
+
expect(manifest.extensions['software.bevel.hexis'].mcpServers.vendor.headers).toEqual({
|
|
129
|
+
Authorization: 'Bearer ${VENDOR_KEY}',
|
|
130
|
+
});
|
|
131
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/vendor.tool')).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('omits headers entirely when every one of them was a credential reference', async () => {
|
|
135
|
+
await write('Groups/GTM/access.md', 'write:\n - Admin\n');
|
|
136
|
+
await write(
|
|
137
|
+
'Groups/GTM/vendor.tool',
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
name: 'vendor',
|
|
140
|
+
type: 'mcp',
|
|
141
|
+
url: 'https://mcp.vendor.example/mcp',
|
|
142
|
+
headers: { Authorization: 'Bearer ${VENDOR_KEY}' },
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
await migrateGroupsToPlugins(repo);
|
|
146
|
+
const mcp = JSON.parse(await read('Plugins/GTM/mcp.json'));
|
|
147
|
+
// An empty `headers` object would assert "this server needs no auth".
|
|
148
|
+
// Saying nothing is the honest shape: where it is, not how to reach it.
|
|
149
|
+
expect(mcp.mcpServers.vendor).toEqual({
|
|
150
|
+
type: 'streamable-http',
|
|
151
|
+
url: 'https://mcp.vendor.example/mcp',
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('converts an mcp .tool a previous run parked in the extension dir, second sweep', async () => {
|
|
156
|
+
// The earlier migration shape MOVED mcp .tools here and projected mcp.json;
|
|
157
|
+
// now that mcp.json is authoritative, a parked twin must convert away.
|
|
158
|
+
await write('Plugins/GTM/access.md', 'write:\n - Admin\n');
|
|
159
|
+
await write('Plugins/GTM/plugin.json', '{ "name": "gtm" }');
|
|
160
|
+
await write(
|
|
161
|
+
'Plugins/GTM/software.bevel.hexis/tools/notion.tool',
|
|
162
|
+
JSON.stringify({ name: 'notion', type: 'mcp', url: 'https://mcp.notion.com/mcp' }),
|
|
163
|
+
);
|
|
164
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
165
|
+
expect(result.migrated).toBe(true);
|
|
166
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/notion.tool')).toBe(false);
|
|
167
|
+
const mcp = JSON.parse(await read('Plugins/GTM/mcp.json'));
|
|
168
|
+
expect(mcp.mcpServers.notion.url).toBe('https://mcp.notion.com/mcp');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('moves declared variables and the local flag into the plugin.json extensions block', async () => {
|
|
172
|
+
await write('Plugins/GTM/access.md', 'write:\n - Admin\n');
|
|
173
|
+
await write('Plugins/GTM/plugin.json', '{ "name": "gtm" }');
|
|
174
|
+
await write(
|
|
175
|
+
'Plugins/GTM/vendor.tool',
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
name: 'vendor',
|
|
178
|
+
type: 'mcp',
|
|
179
|
+
url: 'http://localhost:9000/mcp',
|
|
180
|
+
remote: false,
|
|
181
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user', label: 'Your key' }],
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
await migrateGroupsToPlugins(repo);
|
|
185
|
+
const manifest = JSON.parse(await read('Plugins/GTM/plugin.json'));
|
|
186
|
+
expect(manifest.extensions['software.bevel.hexis'].mcpServers.vendor).toEqual({
|
|
187
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user', label: 'Your key' }],
|
|
188
|
+
local: true,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('is idempotent — a second run changes nothing', async () => {
|
|
193
|
+
await seedLegacyKb();
|
|
194
|
+
await migrateGroupsToPlugins(repo);
|
|
195
|
+
const before = await read('Plugins/GTM/plugin.json');
|
|
196
|
+
expect(await migrateGroupsToPlugins(repo)).toEqual({
|
|
197
|
+
migrated: false,
|
|
198
|
+
renamed: false,
|
|
199
|
+
ignoreRewritten: false,
|
|
200
|
+
notes: [],
|
|
201
|
+
});
|
|
202
|
+
expect(await read('Plugins/GTM/plugin.json')).toBe(before);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('finishes a half-done migration rather than skipping it', async () => {
|
|
206
|
+
// The rename landed but the process died before the reorganisation.
|
|
207
|
+
await write('Plugins/GTM/access.md', 'write:\n - Admin\n');
|
|
208
|
+
await write('Plugins/GTM/outreach/SKILL.md', '# Outreach\n');
|
|
209
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
210
|
+
expect(result.migrated).toBe(true);
|
|
211
|
+
expect(await exists('Plugins/GTM/skills/outreach/SKILL.md')).toBe(true);
|
|
212
|
+
expect(await exists('Plugins/GTM/plugin.json')).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('refuses to guess when both roots exist, and says so', async () => {
|
|
216
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
217
|
+
await write('Groups/GTM/access.md', 'write:\n - Admin\n');
|
|
218
|
+
await write('Plugins/GTM/access.md', 'write:\n - Admin\n');
|
|
219
|
+
expect(await migrateGroupsToPlugins(repo)).toEqual({
|
|
220
|
+
migrated: false,
|
|
221
|
+
renamed: false,
|
|
222
|
+
ignoreRewritten: false,
|
|
223
|
+
notes: [],
|
|
224
|
+
});
|
|
225
|
+
// Both left exactly as they were — merging would pick a winner nobody chose.
|
|
226
|
+
expect(await exists('Groups/GTM/access.md')).toBe(true);
|
|
227
|
+
expect(await exists('Plugins/GTM/access.md')).toBe(true);
|
|
228
|
+
expect(warn.mock.calls.flat().join(' ')).toMatch(/both .*exist/i);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('moves a malformed .tool without letting it stop the migration', async () => {
|
|
232
|
+
await write('Groups/GTM/access.md', 'write:\n - Admin\n');
|
|
233
|
+
await write('Groups/GTM/broken.tool', '{ not json at all');
|
|
234
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
235
|
+
expect(result.migrated).toBe(true);
|
|
236
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/broken.tool')).toBe(true);
|
|
237
|
+
// Nothing to project, so no mcp.json is invented.
|
|
238
|
+
expect(await exists('Plugins/GTM/mcp.json')).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('merges into an existing mcp.json without clobbering what is already there', async () => {
|
|
242
|
+
await seedLegacyKb();
|
|
243
|
+
await write(
|
|
244
|
+
'Groups/GTM/mcp.json',
|
|
245
|
+
JSON.stringify({ mcpServers: { notion: { type: 'streamable-http', url: 'https://hand.example/mcp' } } }),
|
|
246
|
+
);
|
|
247
|
+
await migrateGroupsToPlugins(repo);
|
|
248
|
+
const mcp = JSON.parse(await read('Plugins/GTM/mcp.json'));
|
|
249
|
+
// The hand-written notion entry WINS; the converted .tool is gone either way.
|
|
250
|
+
expect(mcp.mcpServers.notion.url).toBe('https://hand.example/mcp');
|
|
251
|
+
expect(await exists('Plugins/GTM/notion.tool')).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('leaves a personal folder a valid plugin', async () => {
|
|
255
|
+
await write('Groups/personal-u-123/access.md', 'write:\n - Ali <ali@x.com>\n');
|
|
256
|
+
await migrateGroupsToPlugins(repo);
|
|
257
|
+
const manifest = JSON.parse(await read('Plugins/personal-u-123/plugin.json'));
|
|
258
|
+
expect(manifest.name).toMatch(/^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('routes anything the substitutor would expand to the extensions block — non-name ${…} stays literal', async () => {
|
|
262
|
+
await write('Groups/GTM/access.md', 'write:\n - Admin\n');
|
|
263
|
+
await write(
|
|
264
|
+
'Groups/GTM/vendor.tool',
|
|
265
|
+
JSON.stringify({
|
|
266
|
+
name: 'vendor',
|
|
267
|
+
type: 'mcp',
|
|
268
|
+
url: 'https://mcp.vendor.example/mcp',
|
|
269
|
+
// The substitutor's grammar decides: bare `$VENDOR_KEY` and the
|
|
270
|
+
// digit-leading `$5` in the price BOTH expand, so both route to the
|
|
271
|
+
// non-portable half; `${not-a-name}` is not expandable and stays.
|
|
272
|
+
headers: {
|
|
273
|
+
Authorization: 'Bearer $VENDOR_KEY',
|
|
274
|
+
'X-Price': '$5 per call',
|
|
275
|
+
'X-Tag': 'v ${not-a-name}',
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
await migrateGroupsToPlugins(repo);
|
|
280
|
+
const mcp = JSON.parse(await read('Plugins/GTM/mcp.json'));
|
|
281
|
+
expect(mcp.mcpServers.vendor.headers).toEqual({ 'X-Tag': 'v ${not-a-name}' });
|
|
282
|
+
const manifest = JSON.parse(await read('Plugins/GTM/plugin.json'));
|
|
283
|
+
expect(manifest.extensions['software.bevel.hexis'].mcpServers.vendor.headers).toEqual({
|
|
284
|
+
Authorization: 'Bearer $VENDOR_KEY',
|
|
285
|
+
'X-Price': '$5 per call',
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('does not follow a symlinked extension tools dir in the second sweep', async () => {
|
|
290
|
+
// The second sweep DELETES what it converts — following a link would
|
|
291
|
+
// convert-and-delete `.tool` files from wherever the link really points.
|
|
292
|
+
await write('Plugins/GTM/access.md', 'write:\n - Admin\n');
|
|
293
|
+
await write('Plugins/GTM/plugin.json', '{ "name": "gtm" }');
|
|
294
|
+
await write(
|
|
295
|
+
'elsewhere/tools/notion.tool',
|
|
296
|
+
JSON.stringify({ name: 'notion', type: 'mcp', url: 'https://mcp.notion.com/mcp' }),
|
|
297
|
+
);
|
|
298
|
+
await fs.mkdir(path.join(repo, 'Plugins/GTM/software.bevel.hexis'), { recursive: true });
|
|
299
|
+
// 'junction' so the link is creatable without privileges on Windows; on
|
|
300
|
+
// POSIX the type argument is ignored and a plain dir symlink is made.
|
|
301
|
+
await fs.symlink(
|
|
302
|
+
path.join(repo, 'elsewhere/tools'),
|
|
303
|
+
path.join(repo, 'Plugins/GTM/software.bevel.hexis/tools'),
|
|
304
|
+
'junction',
|
|
305
|
+
);
|
|
306
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
307
|
+
// The linked-to file is untouched — not converted, not deleted.
|
|
308
|
+
expect(await exists('elsewhere/tools/notion.tool')).toBe(true);
|
|
309
|
+
expect(await exists('Plugins/GTM/mcp.json')).toBe(false);
|
|
310
|
+
expect(result.notes.join(' ')).not.toContain('notion');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('refuses to convert an mcp .tool whose url is not directly parseable http(s)', async () => {
|
|
314
|
+
// A templated url (`${VENDOR_BASE}/mcp`) is legal in a `.tool`, where the
|
|
315
|
+
// substitutor expands it — but the mcp.json loader validates `new URL`
|
|
316
|
+
// and skips the entry, so converting would delete the source and write a
|
|
317
|
+
// dead entry: the integration would simply vanish.
|
|
318
|
+
await write('Groups/GTM/access.md', 'write:\n - Admin\n');
|
|
319
|
+
await write(
|
|
320
|
+
'Groups/GTM/vendor.tool',
|
|
321
|
+
JSON.stringify({ name: 'vendor', type: 'mcp', url: '${VENDOR_BASE}/mcp' }),
|
|
322
|
+
);
|
|
323
|
+
await write(
|
|
324
|
+
'Groups/GTM/socket.tool',
|
|
325
|
+
JSON.stringify({ name: 'socket', type: 'mcp', url: 'wss://mcp.vendor.example/mcp' }),
|
|
326
|
+
);
|
|
327
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
328
|
+
// Both stay `.tool` files (moved with the other unconvertibles), and no
|
|
329
|
+
// mcp.json is invented for them.
|
|
330
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/vendor.tool')).toBe(true);
|
|
331
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/socket.tool')).toBe(true);
|
|
332
|
+
expect(await exists('Plugins/GTM/mcp.json')).toBe(false);
|
|
333
|
+
// The refusal is NAMED in the log — an operator must be able to tell a
|
|
334
|
+
// deliberately-retained integration from one that silently failed.
|
|
335
|
+
// (socket.tool never reaches the named refusals: its wss url fails
|
|
336
|
+
// `.tool` normalization itself, the not-a-candidate path.)
|
|
337
|
+
expect(result.notes.some((n) => /vendor\.tool.*NOT converted/.test(n))).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('refuses to convert an mcp .tool whose url carries userinfo', async () => {
|
|
341
|
+
// `https://user:pass@…` copied into mcp.json would put a credential in
|
|
342
|
+
// the PORTABLE file — the exact thing the header split exists to prevent
|
|
343
|
+
// — and stripping it would break the server. The manual keeps its
|
|
344
|
+
// `.tool` form, where the credential stays platform-internal.
|
|
345
|
+
await write('Groups/GTM/access.md', 'write:\n - Admin\n');
|
|
346
|
+
await write(
|
|
347
|
+
'Groups/GTM/vendor.tool',
|
|
348
|
+
JSON.stringify({ name: 'vendor', type: 'mcp', url: 'https://user:pass@mcp.vendor.example/mcp' }),
|
|
349
|
+
);
|
|
350
|
+
await migrateGroupsToPlugins(repo);
|
|
351
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/vendor.tool')).toBe(true);
|
|
352
|
+
expect(await exists('Plugins/GTM/mcp.json')).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('refuses to convert an mcp .tool that gates itself with frontmatter access verbs', async () => {
|
|
356
|
+
// The access resolver reads a `.tool`'s own verbs from the file itself;
|
|
357
|
+
// an mcp.json entry has no per-server home for them, so converting would
|
|
358
|
+
// silently widen who may configure and run the server.
|
|
359
|
+
await write('Groups/GTM/access.md', 'write:\n - Admin\n');
|
|
360
|
+
await write(
|
|
361
|
+
'Groups/GTM/gated.tool',
|
|
362
|
+
'---\nname: gated\ntype: mcp\nurl: https://mcp.vendor.example/mcp\nwrite:\n - Product Team\n---\n',
|
|
363
|
+
);
|
|
364
|
+
await migrateGroupsToPlugins(repo);
|
|
365
|
+
expect(await exists('Plugins/GTM/software.bevel.hexis/tools/gated.tool')).toBe(true);
|
|
366
|
+
expect(await exists('Plugins/GTM/mcp.json')).toBe(false);
|
|
367
|
+
// The verbs travel with the file — the moved copy still declares them.
|
|
368
|
+
expect(await read('Plugins/GTM/software.bevel.hexis/tools/gated.tool')).toContain('Product Team');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('reports a note-only run as not migrated — there is nothing to commit', async () => {
|
|
372
|
+
// An unparsable manifest blocks conversion of a manual that carries
|
|
373
|
+
// extension data; the refusal is a NOTE, not a file change, and saying
|
|
374
|
+
// `migrated` anyway would send the caller into an empty commit every boot.
|
|
375
|
+
await write('Plugins/GTM/access.md', 'write:\n - Admin\n');
|
|
376
|
+
await write('Plugins/GTM/plugin.json', '{ not json at all');
|
|
377
|
+
await write(
|
|
378
|
+
'Plugins/GTM/vendor.tool',
|
|
379
|
+
JSON.stringify({
|
|
380
|
+
name: 'vendor',
|
|
381
|
+
type: 'mcp',
|
|
382
|
+
url: 'https://mcp.vendor.example/mcp',
|
|
383
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
387
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
388
|
+
warn.mockRestore();
|
|
389
|
+
expect(result.migrated).toBe(false);
|
|
390
|
+
expect(result.notes.join(' ')).toContain('NOT converted');
|
|
391
|
+
// The source stays, ready for the run after the manifest is fixed.
|
|
392
|
+
expect(await exists('Plugins/GTM/vendor.tool')).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe('the .bevelignore root rule', () => {
|
|
396
|
+
it('follows the rename, preserving every other line', async () => {
|
|
397
|
+
await seedLegacyKb();
|
|
398
|
+
await write('.bevelignore', '# mine\nGroups/\nMy-Own-Rule/\n');
|
|
399
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
400
|
+
expect(result.ignoreRewritten).toBe(true);
|
|
401
|
+
const ignore = await read('.bevelignore');
|
|
402
|
+
expect(ignore.split('\n')).toContain('Plugins/');
|
|
403
|
+
expect(ignore).not.toContain('Groups/');
|
|
404
|
+
expect(ignore).toContain('# mine');
|
|
405
|
+
expect(ignore).toContain('My-Own-Rule/');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('is left alone when Plugins/ is already listed', async () => {
|
|
409
|
+
await seedLegacyKb();
|
|
410
|
+
await write('.bevelignore', 'Groups/\nPlugins/\n');
|
|
411
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
412
|
+
expect(result.ignoreRewritten).toBe(false);
|
|
413
|
+
// The stale line is harmlessly dead; deleting it would be editing the
|
|
414
|
+
// operator's file beyond what the rename made stale.
|
|
415
|
+
expect(await read('.bevelignore')).toBe('Groups/\nPlugins/\n');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('is not touched by a run that does not rename', async () => {
|
|
419
|
+
await write('Plugins/GTM/access.md', 'write:\n - Admin\n');
|
|
420
|
+
await write('Plugins/GTM/outreach/SKILL.md', '# Outreach\n');
|
|
421
|
+
await write('.bevelignore', 'Groups/\n');
|
|
422
|
+
const result = await migrateGroupsToPlugins(repo);
|
|
423
|
+
expect(result.ignoreRewritten).toBe(false);
|
|
424
|
+
expect(await read('.bevelignore')).toBe('Groups/\n');
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|