@bevel-software/platform-core-backend 0.7.5 → 0.9.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 +8 -1
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +75 -18
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +12 -5
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +69 -36
- 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.d.ts +3 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +22 -20
- 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/render-roles-yaml.d.ts +22 -0
- package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
- package/dist/modules/access/render-roles-yaml.js +56 -0
- package/dist/modules/access/render-roles-yaml.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +33 -5
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +44 -19
- 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/settings/setup.routes.d.ts +10 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +111 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- 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/startup/kb-git.d.ts +23 -0
- package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-git.js +86 -0
- package/dist/modules/workspace/startup/kb-git.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.js +21 -0
- package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
- package/dist/modules/workspace/startup/steps/template-files.step.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 +0 -9
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +2 -30
- 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/kb-template/gitignore.template +16 -0
- 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 +113 -106
- package/src/core/create-core-server.ts +85 -20
- package/src/core/create-core-services.ts +83 -42
- 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 +36 -2
- 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 +23 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/render-roles-yaml.ts +65 -0
- package/src/modules/access/roles-admin.service.ts +45 -16
- 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/settings/__tests__/setup.routes.test.ts +124 -3
- package/src/modules/settings/setup.routes.ts +112 -5
- 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__/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 +27 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
- package/src/modules/workspace/startup/kb-git.ts +94 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
- package/src/modules/workspace/startup/on-server-start.ts +97 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
- package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +561 -0
- package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
- package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +2 -35
- 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/modules/workspace/__tests__/kb-seed.service.test.ts +0 -444
- package/src/modules/workspace/kb-seed.interface.ts +0 -36
- package/src/modules/workspace/kb-seed.service.ts +0 -555
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import express, { type Request, type RequestHandler } from 'express';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import AdmZip from 'adm-zip';
|
|
5
|
+
import { DEFAULT_BRANCH, PLUGINS_DIR } from '@bevel-software/platform-shared';
|
|
6
|
+
import { workspaceIdForBranch, type WorkspaceService } from '../workspace/workspace.service.js';
|
|
7
|
+
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
2
8
|
import '@utcp/http'; // side effect: register the 'http' call-template type
|
|
3
9
|
import { CallTemplateSerializer, type CallTemplate } from '@utcp/sdk';
|
|
4
10
|
import { type IToolManualService, EXTERNAL_KB_MANUAL_NAME } from './tool-manuals.contract.js';
|
|
11
|
+
import { McpServerEditError, type McpServerEditService, type McpServerWrite } from './mcp-server-edit.service.js';
|
|
12
|
+
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
5
13
|
import '../auth/auth.middleware.js'; // Express Request augmentation (req.userId / req.userEmail)
|
|
6
14
|
import '../tool-auth/tool-auth.middleware.js'; // Express Request augmentation (req.toolAuth)
|
|
7
15
|
|
|
@@ -35,6 +43,7 @@ export function createToolManualsAgentRoutes(
|
|
|
35
43
|
toolManualService: IToolManualService,
|
|
36
44
|
manualAuth: RequestHandler,
|
|
37
45
|
resolveUserEmail: ResolveUserEmail,
|
|
46
|
+
archiveDeps?: { workspaceService: WorkspaceService; accessControl: IAccessControl; kbDirName: string },
|
|
38
47
|
): express.Router {
|
|
39
48
|
const router = express.Router();
|
|
40
49
|
|
|
@@ -63,6 +72,173 @@ export function createToolManualsAgentRoutes(
|
|
|
63
72
|
}
|
|
64
73
|
});
|
|
65
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The whole plugin, byte-for-byte, as a zip — the materialization surface
|
|
77
|
+
* for the local MCP server. `read_file` is the agent's READING tool (text,
|
|
78
|
+
* one file at a time); this exists because a stdio server's plugin must
|
|
79
|
+
* land on the user's disk exactly as it is, binaries included. Access is
|
|
80
|
+
* per-file and the caller's own: every entry is filtered through the same
|
|
81
|
+
* read verdicts `list_files` uses, so a file the key cannot read is a file
|
|
82
|
+
* that is not in the archive — not an error, an absence.
|
|
83
|
+
*/
|
|
84
|
+
router.get('/agent/plugins/:folder/archive', manualAuth, async (req, res) => {
|
|
85
|
+
if (!archiveDeps) return void res.status(404).json({ error: 'Not available' });
|
|
86
|
+
try {
|
|
87
|
+
const email = await callerEmail(req);
|
|
88
|
+
if (!email) return void res.status(403).json({ error: 'Forbidden' });
|
|
89
|
+
const folder = String(Array.isArray(req.params.folder) ? req.params.folder[0] : req.params.folder);
|
|
90
|
+
if (!folder || folder === '.' || folder === '..' || /[/\\]/.test(folder)) {
|
|
91
|
+
return void res.status(422).json({ error: 'Not a plugin folder name' });
|
|
92
|
+
}
|
|
93
|
+
const { workspaceService, accessControl, kbDirName } = archiveDeps;
|
|
94
|
+
const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
|
|
95
|
+
await workspaceService.getOrCreateForBranch(DEFAULT_BRANCH);
|
|
96
|
+
const wsDir = await workspaceService.getWorkspacePath(wsId);
|
|
97
|
+
const pluginDir = path.join(wsDir, kbDirName, PLUGINS_DIR, folder);
|
|
98
|
+
// SYMLINKS ARE NOT SUPPORTED IN PLUGINS, anywhere. Access control
|
|
99
|
+
// resolves rules by path, and a symlink is a second path to the same
|
|
100
|
+
// content — a standing invitation for the spelling the ACL judged and
|
|
101
|
+
// the bytes served to diverge. The platform's own write paths never
|
|
102
|
+
// create one (they arrive only via direct git pushes), so every symlink
|
|
103
|
+
// is skipped rather than resolved: no target following, no cycle
|
|
104
|
+
// guards, no read-time re-resolution — complexity that existed only to
|
|
105
|
+
// support what the platform has no use for. Starting with the plugin
|
|
106
|
+
// folder itself: a symlinked `Plugins/<folder>` is not a plugin.
|
|
107
|
+
// Only ENOENT is an absence; an EACCES/EIO answered with 404 would
|
|
108
|
+
// dress a real read problem up as a missing plugin (the same contract
|
|
109
|
+
// as the realpath/open probes below).
|
|
110
|
+
const folderStat = await fs.lstat(pluginDir).catch((err: NodeJS.ErrnoException) => {
|
|
111
|
+
if (err.code === 'ENOENT') return null;
|
|
112
|
+
throw err;
|
|
113
|
+
});
|
|
114
|
+
if (folderStat === null || !folderStat.isDirectory()) {
|
|
115
|
+
return void res.status(404).json({ error: 'Not found' });
|
|
116
|
+
}
|
|
117
|
+
const rels: string[] = [];
|
|
118
|
+
const walk = async (dir: string, rel: string): Promise<void> => {
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// Only an absent directory is a non-event; anything else (EACCES,
|
|
124
|
+
// EIO) silently missing from the archive would hand the client an
|
|
125
|
+
// incomplete plugin stamped as success.
|
|
126
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
for (const e of entries) {
|
|
130
|
+
if (e.name === '.git') continue;
|
|
131
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
132
|
+
// Dirent's isDirectory/isFile are both false for a symlink, so a
|
|
133
|
+
// link is never walked and never listed — but say so, once, because
|
|
134
|
+
// an author who committed one deserves to know it went nowhere.
|
|
135
|
+
if (e.isDirectory()) await walk(path.join(dir, e.name), childRel);
|
|
136
|
+
else if (e.isFile()) rels.push(childRel);
|
|
137
|
+
else if (e.isSymbolicLink()) {
|
|
138
|
+
console.warn(`[tool-manuals] archive of "${folder}": ${childRel} is a symlink — not supported in plugins, skipped.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
await walk(pluginDir, '');
|
|
143
|
+
if (rels.length === 0) return void res.status(404).json({ error: 'Not found' });
|
|
144
|
+
const verdicts = await accessControl.canReadBatch(
|
|
145
|
+
wsId,
|
|
146
|
+
email,
|
|
147
|
+
rels.map((r) => `${PLUGINS_DIR}/${folder}/${r}`),
|
|
148
|
+
);
|
|
149
|
+
const zip = new AdmZip();
|
|
150
|
+
let included = 0;
|
|
151
|
+
let bytes = 0;
|
|
152
|
+
// The zip is built in memory; without a ceiling one plugin full of large
|
|
153
|
+
// assets is a backend OOM any key holder can trigger.
|
|
154
|
+
const MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
|
|
155
|
+
// Canonical base for the read-time identity check below. The folder was
|
|
156
|
+
// lstat-verified a real directory above, so this is normalization
|
|
157
|
+
// (case, 8.3 names, drive spelling), not link resolution — but that
|
|
158
|
+
// verification is a moment old, and a plugin deleted since (a workspace
|
|
159
|
+
// reset, a merged deletion) is an ABSENCE, the same 404 it would have
|
|
160
|
+
// been a moment earlier, not an internal error.
|
|
161
|
+
const pluginRealBase = await fs.realpath(pluginDir).catch((err: NodeJS.ErrnoException) => {
|
|
162
|
+
if (err.code === 'ENOENT') return null;
|
|
163
|
+
throw err;
|
|
164
|
+
});
|
|
165
|
+
if (pluginRealBase === null) return void res.status(404).json({ error: 'Not found' });
|
|
166
|
+
for (const rel of rels) {
|
|
167
|
+
// Fail closed, per file — only an explicit `true` verdict is included.
|
|
168
|
+
if (verdicts.get(`${PLUGINS_DIR}/${folder}/${rel}`) !== true) continue;
|
|
169
|
+
const abs = path.join(pluginDir, ...rel.split('/'));
|
|
170
|
+
// The no-symlink rule, re-checked at read time over the WHOLE path.
|
|
171
|
+
// The open below guards only the final component — a PARENT directory
|
|
172
|
+
// swapped for a link between walk and read makes `abs` traverse the
|
|
173
|
+
// link with the final component still a regular file. realpath
|
|
174
|
+
// resolves every component, so demanding it equal the spelled path is
|
|
175
|
+
// exactly "no component is a link": identity, not mere containment.
|
|
176
|
+
const realNow = await fs.realpath(abs).catch((err: NodeJS.ErrnoException) => {
|
|
177
|
+
if (err.code === 'ENOENT') return null; // deleted since the walk — an absence, not a failure
|
|
178
|
+
throw err;
|
|
179
|
+
});
|
|
180
|
+
if (realNow === null || realNow !== path.join(pluginRealBase, ...rel.split('/'))) {
|
|
181
|
+
if (realNow !== null) {
|
|
182
|
+
console.warn(`[tool-manuals] archive of "${folder}": ${rel} no longer resolves to itself — skipped.`);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// Check and read through ONE file handle, so the bytes that land in
|
|
187
|
+
// the zip come from the very inode the checks passed — a path-based
|
|
188
|
+
// stat-then-readFile pair leaves a window where the path is swapped
|
|
189
|
+
// between the two and the archive ships bytes the ACL never judged.
|
|
190
|
+
// O_NOFOLLOW makes a final-component symlink fail the open itself
|
|
191
|
+
// where the platform defines it (Linux — production; Windows test
|
|
192
|
+
// runs fall back to the realpath identity check above alone). Only an
|
|
193
|
+
// ENOENT is a skip; any other failure is a real read problem that
|
|
194
|
+
// must surface as a 500, not ship as a silently partial archive.
|
|
195
|
+
const handle = await fs
|
|
196
|
+
.open(abs, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0))
|
|
197
|
+
.catch((err: NodeJS.ErrnoException) => {
|
|
198
|
+
if (err.code === 'ENOENT') return null; // deleted since the walk — an absence
|
|
199
|
+
if (err.code === 'ELOOP') {
|
|
200
|
+
// O_NOFOLLOW's spelling of "the final component is a symlink".
|
|
201
|
+
console.warn(`[tool-manuals] archive of "${folder}": ${rel} is a symlink — not supported in plugins, skipped.`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
throw err;
|
|
205
|
+
});
|
|
206
|
+
if (handle === null) continue;
|
|
207
|
+
try {
|
|
208
|
+
// fstat on the handle types and sizes the OPENED file — the size
|
|
209
|
+
// still gates BEFORE content, because rejecting after the read
|
|
210
|
+
// would already have spiked memory by exactly the payload the
|
|
211
|
+
// limit exists to refuse.
|
|
212
|
+
const stat = await handle.stat();
|
|
213
|
+
if (!stat.isFile()) continue;
|
|
214
|
+
bytes += stat.size;
|
|
215
|
+
if (bytes > MAX_ARCHIVE_BYTES) {
|
|
216
|
+
return void res.status(413).json({
|
|
217
|
+
error: `Plugin "${folder}" exceeds the ${MAX_ARCHIVE_BYTES / (1024 * 1024)}MB archive limit.`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// Unix mode rides in the zip attrs so a `./`-command stdio server is
|
|
221
|
+
// still executable after materialization (0 on Windows — harmless).
|
|
222
|
+
// PLAIN mode, no shifting: adm-zip masks a numeric attr with 0xfff and
|
|
223
|
+
// positions it into the external-attribute high bits itself — a
|
|
224
|
+
// pre-shifted value is destroyed by that mask.
|
|
225
|
+
zip.addFile(rel, await handle.readFile(), '', stat.mode & 0o7777);
|
|
226
|
+
included += 1;
|
|
227
|
+
} finally {
|
|
228
|
+
await handle.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// An all-filtered plugin looks exactly like an absent one — a 404 must
|
|
232
|
+
// not confirm to a keyless caller that the folder exists.
|
|
233
|
+
if (included === 0) return void res.status(404).json({ error: 'Not found' });
|
|
234
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
235
|
+
res.send(zip.toBuffer());
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error('[tool-manuals] plugin archive failed:', err instanceof Error ? err.message : err);
|
|
238
|
+
res.status(500).json({ error: 'Failed to archive plugin' });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
66
242
|
router.get('/tools/:slug/manual', manualAuth, async (req, res) => {
|
|
67
243
|
try {
|
|
68
244
|
const email = await callerEmail(req);
|
|
@@ -89,9 +265,54 @@ export function createToolManualsAgentRoutes(
|
|
|
89
265
|
* literal sibling is shadowed by the param segment;
|
|
90
266
|
* `preview` is POST-only, so it never collides.
|
|
91
267
|
*/
|
|
92
|
-
export function createToolManualsBrowserRoutes(
|
|
268
|
+
export function createToolManualsBrowserRoutes(
|
|
269
|
+
toolManualService: IToolManualService,
|
|
270
|
+
serverEdit?: { service: McpServerEditService; getUser: (userId: string) => Promise<AuthUser | undefined> },
|
|
271
|
+
): express.Router {
|
|
93
272
|
const router = express.Router();
|
|
94
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Server-scoped read/write of one MCP server — the tool page's edit form.
|
|
276
|
+
* One server's truth spans mcp.json AND plugin.json's extensions block; a
|
|
277
|
+
* raw file editor shows a writer half of it at best, so the form talks to
|
|
278
|
+
* this pair instead. PUT commits both files together and refuses the states
|
|
279
|
+
* that would strand a half (see McpServerEditService).
|
|
280
|
+
*/
|
|
281
|
+
router.get('/tools/:slug/server', async (req, res) => {
|
|
282
|
+
if (!serverEdit) return void res.status(404).json({ error: 'Not available' });
|
|
283
|
+
const email = req.userEmail;
|
|
284
|
+
if (!email) return void res.status(401).json({ error: 'Not authenticated' });
|
|
285
|
+
try {
|
|
286
|
+
const view = await serverEdit.service.getServer(email, String(req.params.slug));
|
|
287
|
+
if (!view) return void res.status(404).json({ error: 'Not found' });
|
|
288
|
+
res.json(view);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error('[tool-manuals] server read failed:', err instanceof Error ? err.message : err);
|
|
291
|
+
res.status(500).json({ error: 'Failed to read the server' });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
router.put('/tools/:slug/server', async (req, res) => {
|
|
296
|
+
if (!serverEdit) return void res.status(404).json({ error: 'Not available' });
|
|
297
|
+
if (!req.userId) return void res.status(401).json({ error: 'Not authenticated' });
|
|
298
|
+
try {
|
|
299
|
+
const user = await serverEdit.getUser(req.userId);
|
|
300
|
+
if (!user) return void res.status(401).json({ error: 'Not authenticated' });
|
|
301
|
+
const result = await serverEdit.service.putServer(
|
|
302
|
+
user,
|
|
303
|
+
String(req.params.slug),
|
|
304
|
+
(req.body ?? {}) as McpServerWrite,
|
|
305
|
+
);
|
|
306
|
+
res.json(result);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (err instanceof McpServerEditError) {
|
|
309
|
+
return void res.status(err.status).json({ error: err.message });
|
|
310
|
+
}
|
|
311
|
+
console.error('[tool-manuals] server write failed:', err instanceof Error ? err.message : err);
|
|
312
|
+
res.status(500).json({ error: 'Failed to save the server' });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
95
316
|
router.get('/tools', async (req, res) => {
|
|
96
317
|
const email = req.userEmail;
|
|
97
318
|
if (!email) return void res.status(401).json({ error: 'Not authenticated' });
|
|
@@ -9,11 +9,18 @@ import {
|
|
|
9
9
|
DefaultVariableSubstitutor,
|
|
10
10
|
type CallTemplate,
|
|
11
11
|
} from '@utcp/sdk';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_BRANCH,
|
|
14
|
+
PLUGINS_DIR,
|
|
15
|
+
PLUGIN_MANIFEST_FILE,
|
|
16
|
+
PLUGIN_MCP_FILE,
|
|
17
|
+
} from '@bevel-software/platform-shared';
|
|
18
|
+
import { descriptorsFromMcpJson } from './mcp-json-discovery.js';
|
|
13
19
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
14
20
|
import { workspaceIdForBranch } from '../workspace/workspace.service.js';
|
|
15
21
|
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
16
22
|
import { assertSafeFetchUrl } from '../../shared/ssrf.js';
|
|
23
|
+
import { RESERVED_VARIABLE_NAMES, findReservedVariableRef } from '../../shared/variable-refs.js';
|
|
17
24
|
import { extractFrontmatter, resolveDeclaredId, isValidId, dedupeById } from '../../shared/frontmatter-id.js';
|
|
18
25
|
import { walkFiles } from '../../shared/fs-walk.js';
|
|
19
26
|
import { TtlCache } from '../../shared/ttl-cache.js';
|
|
@@ -27,6 +34,7 @@ import {
|
|
|
27
34
|
EXTERNAL_KB_MANUAL_NAME,
|
|
28
35
|
type IToolManualService,
|
|
29
36
|
type ToolManualDescriptor,
|
|
37
|
+
type ToolManualDescriptorBase,
|
|
30
38
|
type ToolManualSummary,
|
|
31
39
|
type ToolManualDetail,
|
|
32
40
|
type ToolCapability,
|
|
@@ -60,46 +68,26 @@ const MAX_CAPABILITIES = 100;
|
|
|
60
68
|
const RESERVED_TOOL_NAMESPACES = [INTERNAL_MANUAL_NAME, EXTERNAL_KB_MANUAL_NAME].map((n) => n.toLowerCase());
|
|
61
69
|
|
|
62
70
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
71
|
+
* No user `.tool` may REFERENCE the platform-seeded variables (`${API_URL}` /
|
|
72
|
+
* `$API_URL`), in any `.tool` type: the only seeded user namespace is an
|
|
73
|
+
* inline `.tool`'s (its discovery template is platform-served), and a
|
|
74
|
+
* reference inside author-written content would resolve platform creds into a
|
|
75
|
+
* request the author shaped. Refusing every `.tool` at the producing boundary
|
|
76
|
+
* makes "user tools never carry platform credentials" structural rather than
|
|
77
|
+
* dependent on which namespaces happen to be seeded. The names and the
|
|
78
|
+
* reference grammar live in `shared/variable-refs.ts` — one definition for
|
|
79
|
+
* every boundary that classifies references.
|
|
72
80
|
*/
|
|
73
|
-
const RESERVED_VARIABLE_NAMES: readonly string[] = ['API_URL', 'CONNECTION_KEY'];
|
|
74
|
-
|
|
75
|
-
/** The SDK substitutor's reference grammar, exactly: `${VAR}` or `$VAR`. */
|
|
76
|
-
const VARIABLE_REFERENCE_RE = /\$\{([a-zA-Z0-9_]+)\}|\$([a-zA-Z0-9_]+)/g;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* A REFERENCE is reserved by SUFFIX, not by exact name: the substitutor looks a
|
|
80
|
-
* variable up under the manual's UTCP namespace first, so `${<ns>_CONNECTION_KEY}`
|
|
81
|
-
* resolves the very same seeded value the bare `${CONNECTION_KEY}` does. Matching
|
|
82
|
-
* the bare name only would let a `.tool` reach the platform bearer just by
|
|
83
|
-
* spelling the namespace out. Suffix matching also refuses harmless-looking
|
|
84
|
-
* near-misses (`MY_API_URL`) — deliberately fail-closed: a `.tool` author who
|
|
85
|
-
* wants their own base URL has the whole namespace minus two suffixes.
|
|
86
|
-
*/
|
|
87
|
-
function isReservedVariableRef(varName: string): boolean {
|
|
88
|
-
return RESERVED_VARIABLE_NAMES.some((reserved) => varName.endsWith(reserved));
|
|
89
|
-
}
|
|
90
81
|
|
|
91
82
|
/** Throw if any string in the `.tool` document references a reserved variable. */
|
|
92
83
|
function assertNoReservedVariableRefs(doc: unknown, name: string): void {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'are seeded by the platform for its own manuals and may not appear anywhere in a `.tool`.',
|
|
101
|
-
);
|
|
102
|
-
}
|
|
84
|
+
const ref = findReservedVariableRef(doc);
|
|
85
|
+
if (ref !== null) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`\`.tool\` "${name}" references the reserved variable "${ref}" — ` +
|
|
88
|
+
'API_URL and CONNECTION_KEY (bare or namespaced, e.g. `<namespace>_CONNECTION_KEY`) ' +
|
|
89
|
+
'are seeded by the platform for its own manuals and may not appear anywhere in a `.tool`.',
|
|
90
|
+
);
|
|
103
91
|
}
|
|
104
92
|
}
|
|
105
93
|
|
|
@@ -270,7 +258,7 @@ export class ToolManualService implements IToolManualService {
|
|
|
270
258
|
async preview(content: string): Promise<ToolManualPreview> {
|
|
271
259
|
let descriptor: ToolManualDescriptor;
|
|
272
260
|
try {
|
|
273
|
-
descriptor = normalizeToolManual('draft', '
|
|
261
|
+
descriptor = normalizeToolManual('draft', 'Plugins/draft.tool', content);
|
|
274
262
|
} catch (err) {
|
|
275
263
|
return { ok: false, errors: [err instanceof Error ? err.message : String(err)] };
|
|
276
264
|
}
|
|
@@ -316,6 +304,28 @@ export class ToolManualService implements IToolManualService {
|
|
|
316
304
|
headers: { Authorization: 'Bearer ${CONNECTION_KEY}' },
|
|
317
305
|
};
|
|
318
306
|
}
|
|
307
|
+
if (m.type === 'mcp' && m.stdio) {
|
|
308
|
+
// A stdio server, for LOCAL consumers only (`remote: false` is implied
|
|
309
|
+
// at discovery). Command/args/env/cwd pass through verbatim — the Agent
|
|
310
|
+
// Plugins placeholders (`${PLUGIN_ROOT}`/`${PLUGIN_DATA}`) are expanded
|
|
311
|
+
// by the LOCAL runtime against its materialized plugin copy; this
|
|
312
|
+
// process has no such paths and must not guess them.
|
|
313
|
+
return {
|
|
314
|
+
name: m.name,
|
|
315
|
+
call_template_type: 'mcp',
|
|
316
|
+
config: {
|
|
317
|
+
mcpServers: {
|
|
318
|
+
[m.name]: {
|
|
319
|
+
transport: 'stdio',
|
|
320
|
+
command: m.stdio.command,
|
|
321
|
+
args: m.stdio.args,
|
|
322
|
+
...(m.stdio.env ? { env: m.stdio.env } : {}),
|
|
323
|
+
...(m.stdio.cwd ? { cwd: m.stdio.cwd } : {}),
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
319
329
|
if (m.type === 'mcp') {
|
|
320
330
|
// Remote (HTTP/streamable) MCP server. Exact plugin field shape is
|
|
321
331
|
// finalized in Phase 4 (native `@utcp/mcp`); the proxy try/catches
|
|
@@ -487,14 +497,41 @@ export class ToolManualService implements IToolManualService {
|
|
|
487
497
|
}
|
|
488
498
|
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
489
499
|
|
|
490
|
-
// A `.tool` sits under `
|
|
500
|
+
// A `.tool` sits under `Plugins/`, beside the skills that use it.
|
|
491
501
|
const files: { abs: string; rel: string }[] = [];
|
|
492
|
-
const root = path.join(kbRoot,
|
|
502
|
+
const root = path.join(kbRoot, PLUGINS_DIR);
|
|
493
503
|
for (const rel of await walkFiles(root, (n) => n.toLowerCase().endsWith('.tool'))) {
|
|
494
|
-
files.push({ abs: path.join(root, rel), rel: `${
|
|
504
|
+
files.push({ abs: path.join(root, rel), rel: `${PLUGINS_DIR}/${rel}` });
|
|
495
505
|
}
|
|
496
506
|
|
|
507
|
+
// MCP servers come from each plugin's mcp.json — the AUTHORITATIVE source
|
|
508
|
+
// (the Agent Plugins fixed location), synthesized into the same descriptor
|
|
509
|
+
// shape. Listed BEFORE the `.tool` files: on a name collision (a legacy
|
|
510
|
+
// mcp `.tool` the migration has not converted yet), the shared dedup keeps
|
|
511
|
+
// the first occurrence, and the authoritative source must be the one kept.
|
|
497
512
|
const parsed: ToolManualDescriptor[] = [];
|
|
513
|
+
let pluginFolders: string[] = [];
|
|
514
|
+
try {
|
|
515
|
+
pluginFolders = (await fs.readdir(root, { withFileTypes: true }))
|
|
516
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
517
|
+
.map((e) => e.name)
|
|
518
|
+
.sort();
|
|
519
|
+
} catch {
|
|
520
|
+
/* no Plugins/ root — nothing to scan */
|
|
521
|
+
}
|
|
522
|
+
for (const folder of pluginFolders) {
|
|
523
|
+
let mcpJson: string;
|
|
524
|
+
try {
|
|
525
|
+
mcpJson = await fs.readFile(path.join(root, folder, PLUGIN_MCP_FILE), 'utf-8');
|
|
526
|
+
} catch {
|
|
527
|
+
continue; // no mcp.json is the common case, not an error
|
|
528
|
+
}
|
|
529
|
+
const pluginJson = await fs
|
|
530
|
+
.readFile(path.join(root, folder, PLUGIN_MANIFEST_FILE), 'utf-8')
|
|
531
|
+
.catch(() => null);
|
|
532
|
+
parsed.push(...descriptorsFromMcpJson(folder, mcpJson, pluginJson));
|
|
533
|
+
}
|
|
534
|
+
|
|
498
535
|
for (const f of files) {
|
|
499
536
|
let content: string;
|
|
500
537
|
try {
|
|
@@ -654,7 +691,10 @@ export function normalizeToolManual(
|
|
|
654
691
|
// reference the platform-seeded variables.
|
|
655
692
|
assertNoReservedVariableRefs(obj, name);
|
|
656
693
|
|
|
657
|
-
|
|
694
|
+
// The non-stdio constituent of the union, by name: `.tool` parsing can
|
|
695
|
+
// never produce a spawn spec, and the stdio side pins `remote: false`,
|
|
696
|
+
// which this builder must stay free to set from the file's own `remote:`.
|
|
697
|
+
const descriptor: ToolManualDescriptorBase & { remote?: boolean; stdio?: undefined } = {
|
|
658
698
|
slug: provisionalSlug,
|
|
659
699
|
name,
|
|
660
700
|
path: repoPath,
|
|
@@ -179,9 +179,12 @@ async function buildListLocalToolsDef(svc: IToolManualService, userEmail?: strin
|
|
|
179
179
|
description:
|
|
180
180
|
'List tools your workspace admins configured that run ONLY in a local environment ' +
|
|
181
181
|
'(e.g. a self-hosted MCP server on localhost) and therefore cannot be called through this ' +
|
|
182
|
-
'remote endpoint.
|
|
183
|
-
'
|
|
184
|
-
'
|
|
182
|
+
'remote endpoint. To CALL them, run the workspace as a local MCP server instead of this one: ' +
|
|
183
|
+
'`npx @bevel-software/hexis-mcp --url <workspace-url> --key <connection-key>` serves every tool ' +
|
|
184
|
+
'you have here plus these, because it runs where they exist (their own credentials come from ' +
|
|
185
|
+
'that process\'s environment). Otherwise each entry gives the tool’s name and its `.tool` file ' +
|
|
186
|
+
'path in the knowledge base — read that file with `read_file` and wire the tool into your local ' +
|
|
187
|
+
'setup by hand. ' +
|
|
185
188
|
(await localToolsLine(svc, userEmail)),
|
|
186
189
|
path: '/api/agent/tools/list_local_tools',
|
|
187
190
|
inputs: { type: 'object', properties: {}, additionalProperties: false },
|
|
@@ -93,7 +93,7 @@ function makeAccessControl(): IAccessControl {
|
|
|
93
93
|
grantSources: vi.fn().mockResolvedValue({}),
|
|
94
94
|
invalidate: vi.fn(),
|
|
95
95
|
findEmailByHash: vi.fn().mockResolvedValue(null),
|
|
96
|
-
kbPrincipals: vi.fn().mockResolvedValue({
|
|
96
|
+
kbPrincipals: vi.fn().mockResolvedValue({ plugins: [], people: [] }),
|
|
97
97
|
validateRolesYaml: vi.fn().mockReturnValue({ ok: true }),
|
|
98
98
|
referencesToRole: vi.fn().mockResolvedValue([]),
|
|
99
99
|
canWriteAtRef: vi.fn().mockResolvedValue(null),
|
|
@@ -49,9 +49,9 @@ describe('assertValidRelativePath', () => {
|
|
|
49
49
|
// now sets `GIT_LITERAL_PATHSPECS=1` on every git subprocess, so KB files
|
|
50
50
|
// arriving with bracketed prefixes (`[Approved]`, `[New]`, `[Updated …]`)
|
|
51
51
|
// or other glob metacharacters round-trip through commit cleanly.
|
|
52
|
-
'[Approved]
|
|
53
|
-
'
|
|
54
|
-
'
|
|
52
|
+
'[Approved] Handbook_Order archive.docx',
|
|
53
|
+
'Handbook/[New] Handbook_E-Invoicing DE.docx',
|
|
54
|
+
'Handbook/[Updated 03.09.2025] Handbook_How to search_Detail page.docx',
|
|
55
55
|
'docs/wildcard*name.md',
|
|
56
56
|
'docs/question?name.md',
|
|
57
57
|
'docs/bang!name.md',
|
|
@@ -124,7 +124,7 @@ function recordingAccessControl(opts: {
|
|
|
124
124
|
eligibleWritersForPathsAtRef: async (_w, _ref, paths) =>
|
|
125
125
|
new Map(paths.map((p) => [p, { roles: [], users: [], emails: new Set<string>() }])),
|
|
126
126
|
findEmailByHash: async () => null,
|
|
127
|
-
kbPrincipals: async () => ({
|
|
127
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
128
128
|
validateRolesYaml: () => ({ ok: true }),
|
|
129
129
|
referencesToRole: async () => [],
|
|
130
130
|
};
|
|
@@ -279,8 +279,8 @@ describe('GitService — push gate uses origin/<branch> (not HEAD or working tre
|
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
it('systemAuthorized skips the gate entirely, and the push still lands', async () => {
|
|
282
|
-
// The
|
|
283
|
-
// signed-in user may claim an unused name under
|
|
282
|
+
// The plugin-provisioning path: its endpoint IS the authorization (any
|
|
283
|
+
// signed-in user may claim an unused name under Plugins/), and the gate
|
|
284
284
|
// could only read the new folder's chain at origin as `write: Admin`.
|
|
285
285
|
const { svc, calls } = await setupWithOrigin();
|
|
286
286
|
await svc.push(workspaceId, USER, { systemAuthorized: true });
|
|
@@ -139,8 +139,10 @@ describe('GitService.commitFile — skipValidator', () => {
|
|
|
139
139
|
'knowledge-base',
|
|
140
140
|
);
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// Shape mirrors the reported filename (bracketed status prefix carrying a
|
|
143
|
+
// date, underscores, spaces) without reproducing anyone's actual content.
|
|
144
|
+
const relPath = 'Handbook/[Updated 03.09.2025] Handbook_How to search_Detail page.md';
|
|
145
|
+
await fs.mkdir(path.join(repo, 'Handbook'), { recursive: true });
|
|
144
146
|
await fs.writeFile(path.join(repo, relPath), 'content\n');
|
|
145
147
|
|
|
146
148
|
const change = await svc.commitFile(workspaceId, USER, relPath);
|
|
@@ -168,15 +170,18 @@ describe('GitService.commitFile — skipValidator', () => {
|
|
|
168
170
|
'knowledge-base',
|
|
169
171
|
);
|
|
170
172
|
|
|
173
|
+
// Synthetic segments matching the SHAPE of real deep-KB naming (numbered
|
|
174
|
+
// prefixes, parenthesised abbreviations, kebab tails) — the property under
|
|
175
|
+
// test is only the total length, asserted below.
|
|
171
176
|
const deepDir = path.join(
|
|
172
|
-
'
|
|
177
|
+
'Handbook',
|
|
173
178
|
'Knowledge',
|
|
174
|
-
'1.
|
|
175
|
-
'1.1 In -
|
|
176
|
-
'0-
|
|
177
|
-
'0.5-
|
|
179
|
+
'1. First-Level Section With A Long Name',
|
|
180
|
+
'1.1 In - Second-Level Subsection (SL)',
|
|
181
|
+
'0-Reference',
|
|
182
|
+
'0.5-Third-Level-Reference-Naming-Convention',
|
|
178
183
|
);
|
|
179
|
-
const fileName = '0.5.5-
|
|
184
|
+
const fileName = '0.5.5-A-Deeply-Nested-Reference-Document-With-A-Long-Name.md';
|
|
180
185
|
const relPath = path.posix.join(deepDir.split(path.sep).join('/'), fileName);
|
|
181
186
|
expect(`Update ${relPath}`.length).toBeGreaterThan(200);
|
|
182
187
|
|
|
@@ -134,7 +134,7 @@ function fakeAccessControl(adminEmails: string[]): IAccessControl {
|
|
|
134
134
|
canOwner: async () => notImpl(),
|
|
135
135
|
invalidate: () => {},
|
|
136
136
|
findEmailByHash: async () => null,
|
|
137
|
-
kbPrincipals: async () => ({
|
|
137
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
138
138
|
validateRolesYaml: () => ({ ok: true }),
|
|
139
139
|
referencesToRole: async () => [],
|
|
140
140
|
canWriteAtRef: async () => null,
|
|
@@ -64,7 +64,7 @@ function makeAccessControl(byRef: WritersByRefAndPath): IAccessControl {
|
|
|
64
64
|
eligibleWritersAtRef: async () => null,
|
|
65
65
|
eligibleWritersForPathsAtRef: async () => null,
|
|
66
66
|
findEmailByHash: async () => null,
|
|
67
|
-
kbPrincipals: async () => ({
|
|
67
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
68
68
|
validateRolesYaml: () => ({ ok: true }),
|
|
69
69
|
referencesToRole: async () => [],
|
|
70
70
|
};
|
|
@@ -1047,12 +1047,12 @@ export class GitService implements IGitService {
|
|
|
1047
1047
|
// protected pushes; that's defence-in-depth against local rebases or
|
|
1048
1048
|
// `commit-tree` shenanigans that might bypass the commit-time gate.
|
|
1049
1049
|
// `systemAuthorized` skips the gate for flows whose ENDPOINT is the
|
|
1050
|
-
// authorization —
|
|
1050
|
+
// authorization — plugin provisioning commits an access.md into a
|
|
1051
1051
|
// folder that does not exist at origin yet, which this gate can only
|
|
1052
1052
|
// ever read as "write: Admin". The provisioning service has already
|
|
1053
1053
|
// decided the write is legitimate (unused name, exclusive create);
|
|
1054
1054
|
// gating it here again just refuses every non-admin the product
|
|
1055
|
-
// promised a
|
|
1055
|
+
// promised a plugin to.
|
|
1056
1056
|
if (isProtectedBranch(branch) && !opts?.systemAuthorized) {
|
|
1057
1057
|
const touched = await this.unpushedTouchedPaths(cwd);
|
|
1058
1058
|
await this.assertCanWriteAtRef(
|
|
@@ -164,7 +164,7 @@ function makeAccessControl(byPath: EligibilityByPath): IAccessControl {
|
|
|
164
164
|
return result;
|
|
165
165
|
},
|
|
166
166
|
findEmailByHash: async () => null,
|
|
167
|
-
kbPrincipals: async () => ({
|
|
167
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
168
168
|
validateRolesYaml: () => ({ ok: true }),
|
|
169
169
|
referencesToRole: async () => [],
|
|
170
170
|
};
|
|
@@ -53,7 +53,7 @@ function makeAccessControl(adminEmails: string[] = []): IAccessControl {
|
|
|
53
53
|
eligibleWritersAtRef: async () => null,
|
|
54
54
|
eligibleWritersForPathsAtRef: async () => null,
|
|
55
55
|
findEmailByHash: async () => null,
|
|
56
|
-
kbPrincipals: async () => ({
|
|
56
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
57
57
|
validateRolesYaml: () => ({ ok: true }),
|
|
58
58
|
referencesToRole: async () => [],
|
|
59
59
|
};
|