@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
|
@@ -0,0 +1,346 @@
|
|
|
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 { DEFAULT_BRANCH, type AuthUser } from '@bevel-software/platform-shared';
|
|
6
|
+
import { workspaceIdForBranch } from '../../workspace/workspace.service.js';
|
|
7
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
8
|
+
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
9
|
+
import type { IToolManualService, ToolManualSummary } from '../tool-manuals.contract.js';
|
|
10
|
+
import { McpServerEditService } from '../mcp-server-edit.service.js';
|
|
11
|
+
|
|
12
|
+
const KB = 'knowledge-base';
|
|
13
|
+
const USER: AuthUser = { id: 'u-1', email: 'ali@example.com', name: 'Ali' } as AuthUser;
|
|
14
|
+
|
|
15
|
+
let root: string;
|
|
16
|
+
let repo: string;
|
|
17
|
+
let svc: McpServerEditService;
|
|
18
|
+
let commits: { runPendingCommit: ReturnType<typeof vi.fn> };
|
|
19
|
+
let canWrite: boolean;
|
|
20
|
+
let invalidated: number;
|
|
21
|
+
|
|
22
|
+
/** Summaries the locate step reads — mirrors what mcp.json discovery yields. */
|
|
23
|
+
const summaries: ToolManualSummary[] = [
|
|
24
|
+
{ slug: 'vendor', name: 'vendor', path: 'Plugins/GTM/mcp.json', type: 'mcp' },
|
|
25
|
+
{ slug: 'legacy', name: 'legacy', path: 'Plugins/GTM/software.bevel.hexis/tools/legacy.tool', type: 'mcp' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
async function write(rel: string, content: unknown): Promise<void> {
|
|
29
|
+
const abs = path.join(repo, rel);
|
|
30
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
31
|
+
await fs.writeFile(abs, JSON.stringify(content, null, 2), 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function readJson(rel: string): Promise<Record<string, unknown>> {
|
|
35
|
+
return JSON.parse(await fs.readFile(path.join(repo, rel), 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-mcp-edit-'));
|
|
40
|
+
const wsDir = path.join(root, workspaceIdForBranch(DEFAULT_BRANCH));
|
|
41
|
+
repo = path.join(wsDir, KB);
|
|
42
|
+
canWrite = true;
|
|
43
|
+
invalidated = 0;
|
|
44
|
+
commits = { runPendingCommit: vi.fn(async () => undefined) };
|
|
45
|
+
|
|
46
|
+
await write('Plugins/GTM/mcp.json', {
|
|
47
|
+
mcpServers: {
|
|
48
|
+
vendor: { type: 'streamable-http', url: 'https://v.example/mcp', headers: { 'X-V': '2' } },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
await write('Plugins/GTM/plugin.json', {
|
|
52
|
+
name: 'gtm',
|
|
53
|
+
extensions: {
|
|
54
|
+
'software.bevel.hexis': {
|
|
55
|
+
mcpServers: {
|
|
56
|
+
vendor: {
|
|
57
|
+
headers: { Authorization: 'Bearer ${VENDOR_KEY}' },
|
|
58
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const workspaceService = {
|
|
66
|
+
getOrCreateForBranch: vi.fn(async () => ({ id: workspaceIdForBranch(DEFAULT_BRANCH) })),
|
|
67
|
+
getWorkspacePath: vi.fn(async () => wsDir),
|
|
68
|
+
} as unknown as WorkspaceService;
|
|
69
|
+
const accessControl = {
|
|
70
|
+
canWrite: vi.fn(async () => canWrite),
|
|
71
|
+
} as unknown as IAccessControl;
|
|
72
|
+
const toolManuals = {
|
|
73
|
+
listAccessible: vi.fn(async () => summaries),
|
|
74
|
+
invalidate: vi.fn(() => {
|
|
75
|
+
invalidated += 1;
|
|
76
|
+
}),
|
|
77
|
+
} as unknown as IToolManualService;
|
|
78
|
+
|
|
79
|
+
svc = new McpServerEditService(workspaceService, commits, accessControl, toolManuals, KB);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(async () => {
|
|
83
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
84
|
+
vi.restoreAllMocks();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('getServer', () => {
|
|
88
|
+
it('merges both files into one view', async () => {
|
|
89
|
+
const view = await svc.getServer(USER.email, 'vendor');
|
|
90
|
+
expect(view).toMatchObject({
|
|
91
|
+
name: 'vendor',
|
|
92
|
+
transport: 'streamable-http',
|
|
93
|
+
url: 'https://v.example/mcp',
|
|
94
|
+
literalHeaders: { 'X-V': '2' },
|
|
95
|
+
authHeaders: { Authorization: 'Bearer ${VENDOR_KEY}' },
|
|
96
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
|
|
97
|
+
local: false,
|
|
98
|
+
canWrite: true,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('shape-checks hand-editable fields — malformed values read as empty, not as nonsense', async () => {
|
|
103
|
+
await write('Plugins/GTM/plugin.json', {
|
|
104
|
+
name: 'gtm',
|
|
105
|
+
extensions: {
|
|
106
|
+
'software.bevel.hexis': {
|
|
107
|
+
// A string where an array/object belongs — the form would `.map`
|
|
108
|
+
// and `Object.entries` these; they must arrive as empty instead.
|
|
109
|
+
mcpServers: { vendor: { headers: 'oops', variables: 'not-an-array' } },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const view = await svc.getServer(USER.email, 'vendor');
|
|
114
|
+
expect(view?.variables).toEqual([]);
|
|
115
|
+
expect(view?.authHeaders).toEqual({});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('answers null for an unknown slug and for a legacy .tool-backed manual alike', async () => {
|
|
119
|
+
expect(await svc.getServer(USER.email, 'nope')).toBeNull();
|
|
120
|
+
// A `.tool`-backed mcp manual predates the authoritative file; the form
|
|
121
|
+
// has no pair of files to edit for it, so it is not editable here.
|
|
122
|
+
expect(await svc.getServer(USER.email, 'legacy')).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('putServer', () => {
|
|
127
|
+
it('rewrites both files and commits the folder once', async () => {
|
|
128
|
+
await svc.putServer(USER, 'vendor', {
|
|
129
|
+
transport: 'streamable-http',
|
|
130
|
+
url: 'https://v2.example/mcp',
|
|
131
|
+
literalHeaders: { 'X-V': '3' },
|
|
132
|
+
authHeaders: { Authorization: 'Bearer ${VENDOR_KEY}' },
|
|
133
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
|
|
134
|
+
});
|
|
135
|
+
const mcp = await readJson('Plugins/GTM/mcp.json');
|
|
136
|
+
expect((mcp.mcpServers as Record<string, unknown>).vendor).toEqual({
|
|
137
|
+
type: 'streamable-http',
|
|
138
|
+
url: 'https://v2.example/mcp',
|
|
139
|
+
headers: { 'X-V': '3' },
|
|
140
|
+
});
|
|
141
|
+
expect(commits.runPendingCommit).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(commits.runPendingCommit).toHaveBeenCalledWith(
|
|
143
|
+
workspaceIdForBranch(DEFAULT_BRANCH),
|
|
144
|
+
DEFAULT_BRANCH,
|
|
145
|
+
`${KB}/Plugins/GTM`,
|
|
146
|
+
USER,
|
|
147
|
+
);
|
|
148
|
+
expect(invalidated).toBe(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('reroutes a ${VAR} that arrives in the literal headers — mcp.json never carries one', async () => {
|
|
152
|
+
await svc.putServer(USER, 'vendor', {
|
|
153
|
+
transport: 'streamable-http',
|
|
154
|
+
url: 'https://v.example/mcp',
|
|
155
|
+
literalHeaders: { 'X-Key': '${SNEAKY}' },
|
|
156
|
+
});
|
|
157
|
+
const mcp = await readJson('Plugins/GTM/mcp.json');
|
|
158
|
+
expect((mcp.mcpServers as Record<string, { headers?: unknown }>).vendor.headers).toBeUndefined();
|
|
159
|
+
const manifest = await readJson('Plugins/GTM/plugin.json');
|
|
160
|
+
const ext = (manifest.extensions as Record<string, { mcpServers: Record<string, { headers?: unknown }> }>)[
|
|
161
|
+
'software.bevel.hexis'
|
|
162
|
+
];
|
|
163
|
+
// The rerouted header lands BESIDE the stored auth (omitted, so kept) —
|
|
164
|
+
// rerouting must add to the extensions half, not replace it.
|
|
165
|
+
expect(ext.mcpServers.vendor.headers).toEqual({
|
|
166
|
+
Authorization: 'Bearer ${VENDOR_KEY}',
|
|
167
|
+
'X-Key': '${SNEAKY}',
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('renames across BOTH files, carrying every omitted field to the new key', async () => {
|
|
172
|
+
await svc.putServer(USER, 'vendor', {
|
|
173
|
+
newName: 'vendor_eu',
|
|
174
|
+
transport: 'streamable-http',
|
|
175
|
+
url: 'https://v.example/mcp',
|
|
176
|
+
authHeaders: { Authorization: 'Bearer ${VENDOR_KEY}' },
|
|
177
|
+
});
|
|
178
|
+
// FULL file contents, not just the key set: a rename that dropped the
|
|
179
|
+
// literal header or the variable declaration would still move the key.
|
|
180
|
+
const mcp = await readJson('Plugins/GTM/mcp.json');
|
|
181
|
+
expect(mcp.mcpServers).toEqual({
|
|
182
|
+
vendor_eu: {
|
|
183
|
+
type: 'streamable-http',
|
|
184
|
+
url: 'https://v.example/mcp',
|
|
185
|
+
headers: { 'X-V': '2' },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const manifest = await readJson('Plugins/GTM/plugin.json');
|
|
189
|
+
const servers = (
|
|
190
|
+
manifest.extensions as Record<string, { mcpServers: Record<string, unknown> }>
|
|
191
|
+
)['software.bevel.hexis'].mcpServers;
|
|
192
|
+
expect(servers).toEqual({
|
|
193
|
+
vendor_eu: {
|
|
194
|
+
headers: { Authorization: 'Bearer ${VENDOR_KEY}' },
|
|
195
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('preserves fields the write does not surface, and clears on explicit empties', async () => {
|
|
201
|
+
// Nothing surfaced beyond the transport: everything stored survives.
|
|
202
|
+
await svc.putServer(USER, 'vendor', { transport: 'streamable-http' });
|
|
203
|
+
let mcp = await readJson('Plugins/GTM/mcp.json');
|
|
204
|
+
expect((mcp.mcpServers as Record<string, unknown>).vendor).toEqual({
|
|
205
|
+
type: 'streamable-http',
|
|
206
|
+
url: 'https://v.example/mcp',
|
|
207
|
+
headers: { 'X-V': '2' },
|
|
208
|
+
});
|
|
209
|
+
let manifest = await readJson('Plugins/GTM/plugin.json');
|
|
210
|
+
let servers = (
|
|
211
|
+
manifest.extensions as Record<string, { mcpServers: Record<string, unknown> }>
|
|
212
|
+
)['software.bevel.hexis'].mcpServers;
|
|
213
|
+
expect(servers.vendor).toEqual({
|
|
214
|
+
headers: { Authorization: 'Bearer ${VENDOR_KEY}' },
|
|
215
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Present-but-empty is an explicit CLEAR, not an omission.
|
|
219
|
+
await svc.putServer(USER, 'vendor', {
|
|
220
|
+
transport: 'streamable-http',
|
|
221
|
+
literalHeaders: {},
|
|
222
|
+
authHeaders: {},
|
|
223
|
+
variables: [],
|
|
224
|
+
});
|
|
225
|
+
mcp = await readJson('Plugins/GTM/mcp.json');
|
|
226
|
+
expect((mcp.mcpServers as Record<string, unknown>).vendor).toEqual({
|
|
227
|
+
type: 'streamable-http',
|
|
228
|
+
url: 'https://v.example/mcp',
|
|
229
|
+
});
|
|
230
|
+
manifest = await readJson('Plugins/GTM/plugin.json');
|
|
231
|
+
servers = (
|
|
232
|
+
manifest.extensions as Record<string, { mcpServers: Record<string, unknown> }>
|
|
233
|
+
)['software.bevel.hexis'].mcpServers;
|
|
234
|
+
// An emptied extension entry is REMOVED, not left as `{}`.
|
|
235
|
+
expect(servers.vendor).toBeUndefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('refuses a rename onto a taken key with 409, committing nothing', async () => {
|
|
239
|
+
// A second server occupies the target name.
|
|
240
|
+
const mcp = await readJson('Plugins/GTM/mcp.json');
|
|
241
|
+
(mcp.mcpServers as Record<string, unknown>).vendor_eu = {
|
|
242
|
+
type: 'streamable-http',
|
|
243
|
+
url: 'https://eu.example/mcp',
|
|
244
|
+
};
|
|
245
|
+
await fs.writeFile(path.join(repo, 'Plugins/GTM/mcp.json'), JSON.stringify(mcp), 'utf-8');
|
|
246
|
+
await expect(
|
|
247
|
+
svc.putServer(USER, 'vendor', { newName: 'vendor_eu', transport: 'streamable-http', url: 'https://v.example/mcp' }),
|
|
248
|
+
).rejects.toMatchObject({ status: 409 });
|
|
249
|
+
expect(commits.runPendingCommit).not.toHaveBeenCalled();
|
|
250
|
+
// The original key survives untouched.
|
|
251
|
+
const after = await readJson('Plugins/GTM/mcp.json');
|
|
252
|
+
expect(Object.keys(after.mcpServers as object).sort()).toEqual(['vendor', 'vendor_eu']);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('refuses without write access, an invalid name, and a bad URL', async () => {
|
|
256
|
+
canWrite = false;
|
|
257
|
+
await expect(
|
|
258
|
+
svc.putServer(USER, 'vendor', { transport: 'streamable-http', url: 'https://v.example' }),
|
|
259
|
+
).rejects.toMatchObject({ status: 403 });
|
|
260
|
+
canWrite = true;
|
|
261
|
+
await expect(
|
|
262
|
+
svc.putServer(USER, 'vendor', { newName: 'Bad Name', transport: 'streamable-http', url: 'https://v.example' }),
|
|
263
|
+
).rejects.toMatchObject({ status: 422 });
|
|
264
|
+
await expect(
|
|
265
|
+
svc.putServer(USER, 'vendor', { transport: 'streamable-http', url: 'not-a-url' }),
|
|
266
|
+
).rejects.toMatchObject({ status: 422 });
|
|
267
|
+
expect(commits.runPendingCommit).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('422s a malformed variables declaration instead of persisting a server discovery will drop', async () => {
|
|
271
|
+
// Discovery's own validator, at save time: each of these shapes would
|
|
272
|
+
// save fine and then make the whole server vanish from the catalog on
|
|
273
|
+
// the next scan.
|
|
274
|
+
const bad: unknown[] = [
|
|
275
|
+
[{ name: 'bad name!', scope: 'user' }], // name outside [A-Za-z0-9_]+
|
|
276
|
+
[{ name: 'API_URL', scope: 'admin' }], // platform-seeded name re-declared
|
|
277
|
+
[{ name: 'K', scope: 'user' }, { name: 'K', scope: 'admin' }], // duplicate
|
|
278
|
+
[{ name: 'K', scope: 'team' }], // unknown scope
|
|
279
|
+
[
|
|
280
|
+
{
|
|
281
|
+
name: 'K',
|
|
282
|
+
scope: 'admin', // oauth demands per-caller scope
|
|
283
|
+
oauth: { authorizationUrl: 'https://v.example/auth', tokenUrl: 'https://v.example/token', clientId: 'c' },
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
[
|
|
287
|
+
{
|
|
288
|
+
name: 'K',
|
|
289
|
+
scope: 'user', // whitespace-only clientId — same rule as discovery
|
|
290
|
+
oauth: { authorizationUrl: 'https://v.example/auth', tokenUrl: 'https://v.example/token', clientId: ' ' },
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
];
|
|
294
|
+
const before = await readJson('Plugins/GTM/plugin.json');
|
|
295
|
+
for (const variables of bad) {
|
|
296
|
+
await expect(
|
|
297
|
+
svc.putServer(USER, 'vendor', {
|
|
298
|
+
transport: 'streamable-http',
|
|
299
|
+
url: 'https://v.example/mcp',
|
|
300
|
+
variables: variables as never,
|
|
301
|
+
}),
|
|
302
|
+
).rejects.toMatchObject({ status: 422 });
|
|
303
|
+
}
|
|
304
|
+
// Nothing landed: no commit, no file change.
|
|
305
|
+
expect(commits.runPendingCommit).not.toHaveBeenCalled();
|
|
306
|
+
expect(await readJson('Plugins/GTM/plugin.json')).toEqual(before);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('422s malformed STORED variables on a save that never touched them — not a silent clear', async () => {
|
|
310
|
+
// A hand-edited plugin.json whose declarations went bad: coercing them
|
|
311
|
+
// to [] before validation would let any unrelated save erase them.
|
|
312
|
+
await write('Plugins/GTM/plugin.json', {
|
|
313
|
+
name: 'gtm',
|
|
314
|
+
extensions: {
|
|
315
|
+
'software.bevel.hexis': { mcpServers: { vendor: { variables: 'corrupt' } } },
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const before = await readJson('Plugins/GTM/plugin.json');
|
|
319
|
+
await expect(
|
|
320
|
+
svc.putServer(USER, 'vendor', { transport: 'streamable-http', url: 'https://v.example/mcp' }),
|
|
321
|
+
).rejects.toMatchObject({ status: 422 });
|
|
322
|
+
expect(commits.runPendingCommit).not.toHaveBeenCalled();
|
|
323
|
+
expect(await readJson('Plugins/GTM/plugin.json')).toEqual(before);
|
|
324
|
+
// Supplying valid declarations IS the repair path — that save lands.
|
|
325
|
+
await svc.putServer(USER, 'vendor', {
|
|
326
|
+
transport: 'streamable-http',
|
|
327
|
+
url: 'https://v.example/mcp',
|
|
328
|
+
variables: [{ name: 'VENDOR_KEY', scope: 'user' }],
|
|
329
|
+
});
|
|
330
|
+
expect(commits.runPendingCommit).toHaveBeenCalledTimes(1);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('writes a stdio entry with command/args and no url', async () => {
|
|
334
|
+
await svc.putServer(USER, 'vendor', {
|
|
335
|
+
transport: 'stdio',
|
|
336
|
+
command: 'npx',
|
|
337
|
+
args: ['-y', 'indexer'],
|
|
338
|
+
});
|
|
339
|
+
const mcp = await readJson('Plugins/GTM/mcp.json');
|
|
340
|
+
expect((mcp.mcpServers as Record<string, unknown>).vendor).toEqual({
|
|
341
|
+
type: 'stdio',
|
|
342
|
+
command: 'npx',
|
|
343
|
+
args: ['-y', 'indexer'],
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
8
|
+
import { createToolManualsAgentRoutes } from '../tool-manuals.routes.js';
|
|
9
|
+
import { workspaceIdForBranch } from '../../workspace/workspace.service.js';
|
|
10
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
11
|
+
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
12
|
+
import type { IToolManualService } from '../tool-manuals.contract.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `GET /agent/plugins/:folder/archive` — error taxonomy of the plugin-folder
|
|
16
|
+
* probe. An ABSENT folder is a 404; a folder the process cannot lstat
|
|
17
|
+
* (EACCES, EIO) is a real failure that must surface as a 500, because a 404
|
|
18
|
+
* would report a permission problem as "this plugin does not exist" and the
|
|
19
|
+
* caller would delete its local copy of a plugin that is still there.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const KB = 'knowledge-base';
|
|
23
|
+
|
|
24
|
+
let httpServer: HttpServer | undefined;
|
|
25
|
+
let root: string;
|
|
26
|
+
let wsDir: string;
|
|
27
|
+
|
|
28
|
+
async function baseUrl(): Promise<string> {
|
|
29
|
+
const app = express();
|
|
30
|
+
const manualAuth: express.RequestHandler = (req, _res, next) => {
|
|
31
|
+
req.toolAuth = { userId: 'u-1' } as never;
|
|
32
|
+
next();
|
|
33
|
+
};
|
|
34
|
+
const workspaceService = {
|
|
35
|
+
getOrCreateForBranch: vi.fn(async () => ({ id: workspaceIdForBranch(DEFAULT_BRANCH) })),
|
|
36
|
+
getWorkspacePath: vi.fn(async () => wsDir),
|
|
37
|
+
} as unknown as WorkspaceService;
|
|
38
|
+
const accessControl = {
|
|
39
|
+
canReadBatch: vi.fn(async (_ws: string, _email: string, rels: string[]) => {
|
|
40
|
+
return new Map(rels.map((r) => [r, true]));
|
|
41
|
+
}),
|
|
42
|
+
} as unknown as IAccessControl;
|
|
43
|
+
app.use(
|
|
44
|
+
'/api',
|
|
45
|
+
createToolManualsAgentRoutes(
|
|
46
|
+
{} as unknown as IToolManualService,
|
|
47
|
+
manualAuth,
|
|
48
|
+
async () => 'ali@example.com',
|
|
49
|
+
{ workspaceService, accessControl, kbDirName: KB },
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
httpServer = await new Promise<HttpServer>((resolve) => {
|
|
53
|
+
const s = app.listen(0, () => resolve(s));
|
|
54
|
+
});
|
|
55
|
+
const port = (httpServer.address() as { port: number }).port;
|
|
56
|
+
return `http://127.0.0.1:${port}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-archive-'));
|
|
61
|
+
wsDir = path.join(root, workspaceIdForBranch(DEFAULT_BRANCH));
|
|
62
|
+
await fs.mkdir(wsDir, { recursive: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
|
|
67
|
+
httpServer = undefined;
|
|
68
|
+
vi.restoreAllMocks();
|
|
69
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('GET /agent/plugins/:folder/archive', () => {
|
|
73
|
+
it('zips a readable plugin folder', async () => {
|
|
74
|
+
const pluginDir = path.join(wsDir, KB, 'Plugins', 'GTM');
|
|
75
|
+
await fs.mkdir(pluginDir, { recursive: true });
|
|
76
|
+
await fs.writeFile(path.join(pluginDir, 'web-search.tool'), '{"name":"web_search"}', 'utf-8');
|
|
77
|
+
const base = await baseUrl();
|
|
78
|
+
const res = await fetch(`${base}/api/agent/plugins/GTM/archive`);
|
|
79
|
+
expect(res.status).toBe(200);
|
|
80
|
+
expect(res.headers.get('content-type')).toContain('application/zip');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('404s an absent plugin folder — ENOENT is an absence', async () => {
|
|
84
|
+
const base = await baseUrl();
|
|
85
|
+
const res = await fetch(`${base}/api/agent/plugins/Nope/archive`);
|
|
86
|
+
expect(res.status).toBe(404);
|
|
87
|
+
expect(await res.json()).toEqual({ error: 'Not found' });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('500s when the folder probe fails for a non-ENOENT reason', async () => {
|
|
91
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
92
|
+
const denied: NodeJS.ErrnoException = Object.assign(new Error('EACCES: permission denied'), {
|
|
93
|
+
code: 'EACCES',
|
|
94
|
+
});
|
|
95
|
+
vi.spyOn(fs, 'lstat').mockRejectedValueOnce(denied);
|
|
96
|
+
const base = await baseUrl();
|
|
97
|
+
const res = await fetch(`${base}/api/agent/plugins/GTM/archive`);
|
|
98
|
+
expect(res.status).toBe(500);
|
|
99
|
+
expect(await res.json()).toEqual({ error: 'Failed to archive plugin' });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -24,7 +24,7 @@ import type { IToolManualService, ToolManualDetail } from '../tool-manuals.contr
|
|
|
24
24
|
const DETAIL: ToolManualDetail = {
|
|
25
25
|
slug: 'github',
|
|
26
26
|
name: 'github',
|
|
27
|
-
path: '
|
|
27
|
+
path: 'Plugins/Engineering/github.tool',
|
|
28
28
|
type: 'inline',
|
|
29
29
|
description: 'Read and write GitHub issues and PRs.',
|
|
30
30
|
capabilities: [{ name: 'create_issue', description: 'Open an issue in a repo.' }],
|
|
@@ -165,7 +165,7 @@ describe('ToolManualService.getDetail — capabilities + access', () => {
|
|
|
165
165
|
/** Write one `.tool` into a fresh temp KB and return the service over it. */
|
|
166
166
|
async function withTool(file: string, content: string): Promise<void> {
|
|
167
167
|
root = await mkdtemp(join(tmpdir(), 'tooldetail-'));
|
|
168
|
-
const tools = join(root, wsId, KB_DIR, '
|
|
168
|
+
const tools = join(root, wsId, KB_DIR, 'Plugins');
|
|
169
169
|
await mkdir(tools, { recursive: true });
|
|
170
170
|
await writeFile(join(tools, file), content);
|
|
171
171
|
}
|
|
@@ -189,7 +189,7 @@ describe('ToolManualService.getDetail — capabilities + access', () => {
|
|
|
189
189
|
expect(detail).toMatchObject({
|
|
190
190
|
slug: 'github',
|
|
191
191
|
name: 'github',
|
|
192
|
-
path: '
|
|
192
|
+
path: 'Plugins/github.tool',
|
|
193
193
|
type: 'inline',
|
|
194
194
|
description: 'Read and write GitHub issues and PRs.',
|
|
195
195
|
capabilities: [
|
|
@@ -46,7 +46,7 @@ describe('ToolManualService — MCP OAuth auto-discovery decoration', () => {
|
|
|
46
46
|
|
|
47
47
|
beforeEach(async () => {
|
|
48
48
|
root = await mkdtemp(join(tmpdir(), 'tools-oauth-'));
|
|
49
|
-
const toolsDir = join(root, wsId, KB_DIR, '
|
|
49
|
+
const toolsDir = join(root, wsId, KB_DIR, 'Plugins');
|
|
50
50
|
await mkdir(toolsDir, { recursive: true });
|
|
51
51
|
await writeFile(join(toolsDir, 'notion.tool'), BARE_MCP_TOOL);
|
|
52
52
|
await writeFile(join(toolsDir, 'jira.tool'), KEYED_MCP_TOOL);
|
|
@@ -109,7 +109,7 @@ describe('ToolManualService — MCP OAuth auto-discovery decoration', () => {
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
test('referenced-but-undeclared ${VAR}s are auto-surfaced as admin keys (any tool type)', async () => {
|
|
112
|
-
const toolsDir = join(root, wsId, KB_DIR, '
|
|
112
|
+
const toolsDir = join(root, wsId, KB_DIR, 'Plugins');
|
|
113
113
|
await writeFile(
|
|
114
114
|
join(toolsDir, 'billing.tool'),
|
|
115
115
|
JSON.stringify({
|