@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.
- 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 +30 -18
- 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/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 +40 -20
- 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__/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/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/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
|
@@ -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, '
|
|
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 (
|
|
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('
|
|
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('
|
|
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('
|
|
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, '
|
|
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(['
|
|
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('
|
|
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, '
|
|
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('
|
|
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('
|
|
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(['
|
|
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('
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
* `
|
|
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. `
|
|
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] ===
|
|
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 `
|
|
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. `
|
|
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. `
|
|
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
|
|
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,
|
|
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
|
|
107
|
-
// failure (no workspace, no
|
|
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. `
|
|
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,
|
|
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
|
+
});
|