@bevel-software/platform-core-backend 0.25.2 → 0.26.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/agent-guide/access-control.md +234 -0
- package/agent-guide/conventions.md +27 -0
- package/agent-guide/directory-structure.md +145 -0
- package/agent-guide/finding-things.md +7 -0
- package/agent-guide/introduction.md +27 -0
- package/agent-guide/skills.md +47 -0
- package/agent-guide/tool-manuals.md +217 -0
- package/agent-guide/where-a-new-file-goes.md +36 -0
- package/dist/assets.d.ts +7 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +9 -0
- package/dist/assets.js.map +1 -1
- package/dist/core/core-ports.d.ts +11 -0
- 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 +13 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +9 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +14 -4
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +9 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +16 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +139 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.js +191 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +4 -0
- package/dist/modules/agent-guide/index.d.ts.map +1 -0
- package/dist/modules/agent-guide/index.js +4 -0
- package/dist/modules/agent-guide/index.js.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +9 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +9 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +1 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +1 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +32 -85
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +29 -2
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +113 -16
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
- package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
- package/dist/modules/mcp/tool-schema-guard.js +171 -0
- package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
- package/dist/modules/plugins/plugins.tools.d.ts +36 -2
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.tools.js +71 -14
- package/dist/modules/plugins/plugins.tools.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +0 -7
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +14 -53
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +3 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +58 -16
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +66 -36
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +14 -14
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +24 -26
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +23 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
- package/dist/modules/tool-registry/guide-first.js +32 -0
- package/dist/modules/tool-registry/guide-first.js.map +1 -0
- package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
- package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool-registry.js +9 -2
- package/dist/modules/tool-registry/tool-registry.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +73 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +582 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +9 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +210 -13
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +456 -91
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
- package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
- package/dist/modules/workflow/git/merge-commit.js +89 -0
- package/dist/modules/workflow/git/merge-commit.js.map +1 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +332 -37
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +178 -12
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +6 -2
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +7 -2
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +4 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +3 -0
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +22 -27
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +5 -3
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +10 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +211 -18
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +11 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +14 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/hidden-tools.d.ts +44 -0
- package/dist/shared/hidden-tools.d.ts.map +1 -0
- package/dist/shared/hidden-tools.js +13 -0
- package/dist/shared/hidden-tools.js.map +1 -0
- package/kb-template/.bevelignore +0 -5
- package/package.json +4 -3
- package/src/__tests__/kb-layout-config.test.ts +10 -100
- package/src/__tests__/packaged-assets-ship.test.ts +54 -0
- package/src/assets.ts +10 -0
- package/src/core/core-ports.ts +11 -0
- package/src/core/create-core-server.ts +13 -2
- package/src/core/create-core-services.ts +28 -4
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
- package/src/modules/access/access-control.interface.ts +15 -0
- package/src/modules/access/access-control.service.ts +21 -0
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +328 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +189 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
- package/src/modules/agent-guide/agent-guide.ts +291 -0
- package/src/modules/agent-guide/index.ts +21 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +28 -121
- package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
- package/src/modules/agent-instructions/compose.ts +9 -6
- package/src/modules/agent-instructions/index.ts +0 -3
- package/src/modules/agent-instructions/shared-file-rules.ts +31 -93
- package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +250 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +31 -23
- package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
- package/src/modules/mcp/mcp.service.ts +137 -19
- package/src/modules/mcp/tool-schema-guard.ts +196 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +154 -4
- package/src/modules/plugins/plugins.tools.ts +75 -15
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +26 -55
- package/src/modules/settings/deployment-settings.service.ts +13 -54
- package/src/modules/settings/setup.routes.ts +3 -6
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +91 -0
- package/src/modules/skills/skills.tools.ts +62 -16
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
- package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +74 -36
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +160 -0
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +61 -59
- package/src/modules/tool-registry/description-length.ts +24 -26
- package/src/modules/tool-registry/guide-first.ts +34 -0
- package/src/modules/tool-registry/tool-registry.ts +9 -2
- package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
- package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1518 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +23 -2
- package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +724 -0
- package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
- package/src/modules/workflow/agent-tools/workflow.tools.ts +8 -0
- package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
- package/src/modules/workflow/git/git.service.ts +537 -94
- package/src/modules/workflow/git/merge-commit.ts +88 -0
- package/src/modules/workflow/git/pull-request.service.ts +380 -54
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
- package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +189 -11
- package/src/modules/workflow/workflow.routes.ts +7 -2
- package/src/modules/workflow/workflow.service.ts +7 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +255 -22
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
- package/src/modules/workspace/startup/steps/seed-tree.ts +21 -27
- package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
- package/src/modules/workspace/startup/steps/template-source.ts +5 -3
- package/src/modules/workspace/workspace.tools.ts +226 -16
- package/src/shared/domain-errors.ts +15 -0
- package/src/shared/hidden-tools.ts +45 -0
- package/kb-template/AGENTS.md +0 -730
|
@@ -14,24 +14,24 @@
|
|
|
14
14
|
*
|
|
15
15
|
* - the MCP `instructions` of the initialize handshake (see `compose.ts`),
|
|
16
16
|
* which Claude Code, Claude Desktop and Cursor put in the system prompt;
|
|
17
|
-
* - the platform
|
|
18
|
-
*
|
|
19
|
-
* on the web, the Agent SDK and Cline drop `instructions`, and a
|
|
20
|
-
* agent is told to read before its first action is always
|
|
17
|
+
* - the platform's agent guide, which `get_agent_guide` returns and a
|
|
18
|
+
* `read_file` of the guide's name serves (see `modules/agent-guide`) —
|
|
19
|
+
* claude.ai on the web, the Agent SDK and Cline drop `instructions`, and a
|
|
20
|
+
* guide the agent is told to read before its first action is always
|
|
21
|
+
* available.
|
|
21
22
|
*
|
|
22
23
|
* Both places get the SAME string, from {@link sharedFileRulesSection} — not
|
|
23
24
|
* two hand-mirrored copies. A rule written twice is a rule that drifts, and a
|
|
24
25
|
* drifted rule is worse than a repeated one, because the agent cannot tell
|
|
25
|
-
* which copy is current. Each description
|
|
26
|
-
*
|
|
26
|
+
* which copy is current. Each description opens instead with the one
|
|
27
|
+
* sentence sending the agent to the guide (`tool-registry/guide-first.ts`).
|
|
27
28
|
*
|
|
28
|
-
* Pure text, a function of the layout only: the guide's
|
|
29
|
-
* deployment
|
|
29
|
+
* Pure text, a function of the layout only: the guide's name is a deployment
|
|
30
|
+
* setting (an alias a deployment chose before the guide left the disk), so
|
|
31
|
+
* nothing here may snapshot `AGENTS.md`.
|
|
30
32
|
*/
|
|
31
33
|
|
|
32
34
|
import {
|
|
33
|
-
DEFAULT_KB_LAYOUT,
|
|
34
|
-
agentsFileOf,
|
|
35
35
|
LEGACY_AGENTS_FILE,
|
|
36
36
|
platformFilesByDepth,
|
|
37
37
|
type KbLayout,
|
|
@@ -43,9 +43,9 @@ export const SHARED_RULES_SECTION = 'Working with files';
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* The guide's name on a knowledge base seeded before it was renamed to
|
|
46
|
-
* {@link LEGACY_AGENTS_FILE}
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* {@link LEGACY_AGENTS_FILE}, back when the guide was a file. The platform's
|
|
47
|
+
* own copy is taken out at startup now (see template-files.step.ts); one that
|
|
48
|
+
* stays is a file the organisation edited, so it is theirs and still named.
|
|
49
49
|
*/
|
|
50
50
|
const PRE_RENAME_AGENTS_FILE = 'CLAUDE.md';
|
|
51
51
|
|
|
@@ -84,12 +84,11 @@ export interface SharedFileRule {
|
|
|
84
84
|
* ones that said "this tool" or "this call" name the tools instead.
|
|
85
85
|
*/
|
|
86
86
|
export function sharedFileRules(layout: KbLayout): readonly SharedFileRule[] {
|
|
87
|
-
const agentsFile = agentsFileOf(layout);
|
|
88
87
|
return [
|
|
89
88
|
{
|
|
90
89
|
id: 'agent-guide',
|
|
91
90
|
heading: "This knowledge base's own conventions",
|
|
92
|
-
body: conventionsRule(
|
|
91
|
+
body: conventionsRule(),
|
|
93
92
|
},
|
|
94
93
|
{
|
|
95
94
|
id: 'content-kinds',
|
|
@@ -204,15 +203,14 @@ export function sharedFileRules(layout: KbLayout): readonly SharedFileRule[] {
|
|
|
204
203
|
|
|
205
204
|
/**
|
|
206
205
|
* The platform files as the rules list them — from the one function that knows
|
|
207
|
-
* which they are, so the list cannot drift from what actually refuses a move
|
|
208
|
-
* and the guide appears under this deployment's name for it.
|
|
206
|
+
* which they are, so the list cannot drift from what actually refuses a move.
|
|
209
207
|
*
|
|
210
208
|
* WITH THE DEPTH each name counts at, because the name alone is half the rule:
|
|
211
209
|
* `access.md` governs the folder it sits in and `.bevelignore` layers, so both
|
|
212
|
-
* are platform files wherever they are; `roles.yaml`
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
210
|
+
* are platform files wherever they are; `roles.yaml` is read from the
|
|
211
|
+
* repository root only, so a nested copy is ordinary content that moves and
|
|
212
|
+
* deletes like any page. An agent told only the names refuses a rename it may
|
|
213
|
+
* make, and trusts a nested `access.md` it may not.
|
|
216
214
|
*/
|
|
217
215
|
function platformFileList(layout: KbLayout): string {
|
|
218
216
|
const { anyDepth, rootOnly } = platformFilesByDepth(layout);
|
|
@@ -221,30 +219,21 @@ function platformFileList(layout: KbLayout): string {
|
|
|
221
219
|
}
|
|
222
220
|
|
|
223
221
|
/**
|
|
224
|
-
* The conventions reminder —
|
|
225
|
-
*
|
|
222
|
+
* The conventions reminder — where the platform's guide is, that the
|
|
223
|
+
* organisation's own conventions file comes with it, and to read both first.
|
|
226
224
|
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* checkout. Under the default name the wording collapses to the one file it
|
|
233
|
-
* has always named.
|
|
225
|
+
* The guide is read by name at the repository root, where coding agents look
|
|
226
|
+
* for an `AGENTS.md` by convention, and `get_agent_guide` returns it alone.
|
|
227
|
+
* One name on every deployment, so the rule takes no layout. `CLAUDE.md` is
|
|
228
|
+
* named as a fallback because a knowledge base seeded before the rename may
|
|
229
|
+
* still carry one its people edited.
|
|
234
230
|
*/
|
|
235
|
-
function conventionsRule(
|
|
236
|
-
if (agentsFile === LEGACY_AGENTS_FILE) {
|
|
237
|
-
return (
|
|
238
|
-
`Before your first read or change in a workspace, read \`${LEGACY_AGENTS_FILE}\` at the KB root — or ` +
|
|
239
|
-
`\`${PRE_RENAME_AGENTS_FILE}\` on a knowledge base seeded before it was renamed — if either exists: it holds ` +
|
|
240
|
-
"the author's conventions for this knowledge base, and you should follow them."
|
|
241
|
-
);
|
|
242
|
-
}
|
|
231
|
+
function conventionsRule(): string {
|
|
243
232
|
return (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
233
|
+
"Before your first read or change in a workspace, call `get_agent_guide` and read the platform's guide " +
|
|
234
|
+
`(whole, or one section). read_file on \`${LEGACY_AGENTS_FILE}\` at the KB root answers with the same ` +
|
|
235
|
+
"guide, after the organisation's own conventions file of that name when it has one: follow both. A " +
|
|
236
|
+
`\`${PRE_RENAME_AGENTS_FILE}\` at the KB root is the organisation's own too; read it if it exists.`
|
|
248
237
|
);
|
|
249
238
|
}
|
|
250
239
|
|
|
@@ -260,55 +249,4 @@ export function sharedFileRulesSection(layout: KbLayout): string {
|
|
|
260
249
|
return `## ${SHARED_RULES_SECTION}\n\n${body}`;
|
|
261
250
|
}
|
|
262
251
|
|
|
263
|
-
/**
|
|
264
|
-
* The longest guide file name the pointer sentence spells out. Beyond this it
|
|
265
|
-
* names the guide by its ROLE instead (see {@link sharedRulesPointer}).
|
|
266
|
-
*
|
|
267
|
-
* There has to be a bound somewhere, because the pointer rides on every file
|
|
268
|
-
* tool and a file name is not a fixed cost: `validateFilename` allows a name
|
|
269
|
-
* of up to 255 bytes, so an unbounded pointer could reach 318 characters and
|
|
270
|
-
* push `file_stat` to 1,428 — over the description cap, recreating on a
|
|
271
|
-
* renamed deployment exactly the truncation this module exists to prevent, and
|
|
272
|
-
* invisibly, because every measurement is taken under the default layout.
|
|
273
|
-
*
|
|
274
|
-
* 40 is well past any name a deployment plausibly picks
|
|
275
|
-
* (`ENGINEERING-AGENT-CONVENTIONS.md` is 32) and the fallback below is only
|
|
276
|
-
* reachable past it.
|
|
277
|
-
*/
|
|
278
|
-
export const POINTER_GUIDE_NAME_BUDGET = 40;
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* The one sentence a tool description ends with, in place of the paragraphs it
|
|
282
|
-
* used to carry. Short on purpose: it costs every description the same ~100
|
|
283
|
-
* characters at worst, and its whole job is to name the section and the file
|
|
284
|
-
* to read.
|
|
285
|
-
*
|
|
286
|
-
* BOUNDED BY CONSTRUCTION, which is what lets the description cap mean
|
|
287
|
-
* something on a deployment that renamed its guide: a name within
|
|
288
|
-
* {@link POINTER_GUIDE_NAME_BUDGET} is spelled out, and a longer one gets the
|
|
289
|
-
* generic wording. Naming the file is the better sentence and wins whenever it
|
|
290
|
-
* fits; a name past the budget is pathological, and there the choice is between
|
|
291
|
-
* a sentence that says where to look and a catalog entry the client cuts. The
|
|
292
|
-
* guide's own name is still in the section's first rule either way.
|
|
293
|
-
*
|
|
294
|
-
* An absent layout means the default one, as it does in
|
|
295
|
-
* `composeAgentInstructions`: a caller reading the layout from configuration
|
|
296
|
-
* gets `undefined` when none is set, and the pointer must still name a file.
|
|
297
|
-
*/
|
|
298
|
-
export function sharedRulesPointer(layout: KbLayout = DEFAULT_KB_LAYOUT): string {
|
|
299
|
-
const agentsFile = agentsFileOf(layout);
|
|
300
|
-
const where =
|
|
301
|
-
agentsFile.length <= POINTER_GUIDE_NAME_BUDGET ? agentsFile : 'the agent guide at the KB root';
|
|
302
|
-
return ` Shared rules for all file tools: see "${SHARED_RULES_SECTION}" in ${where}.`;
|
|
303
|
-
}
|
|
304
252
|
|
|
305
|
-
/**
|
|
306
|
-
* The most the pointer can ever cost a description, over every layout. What the
|
|
307
|
-
* description cap is measured against, the way the tool prefix is measured at
|
|
308
|
-
* ITS cap rather than at whatever the current admin wrote: a description that
|
|
309
|
-
* only fits beside the short default guide name does not really fit.
|
|
310
|
-
*/
|
|
311
|
-
export const SHARED_RULES_POINTER_MAX = Math.max(
|
|
312
|
-
sharedRulesPointer({ ...DEFAULT_KB_LAYOUT, agentsFile: `${'x'.repeat(POINTER_GUIDE_NAME_BUDGET - 3)}.md` }).length,
|
|
313
|
-
sharedRulesPointer({ ...DEFAULT_KB_LAYOUT, agentsFile: `${'x'.repeat(POINTER_GUIDE_NAME_BUDGET + 10)}.md` }).length,
|
|
314
|
-
);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ListToolsRequestSchema,
|
|
9
9
|
isInitializeRequest,
|
|
10
10
|
type CallToolResult,
|
|
11
|
+
type ListToolsResult,
|
|
11
12
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -44,6 +45,13 @@ export interface FakeDownstreamOptions {
|
|
|
44
45
|
* revoked or expired token with.
|
|
45
46
|
*/
|
|
46
47
|
acceptsToken?: (token: string) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* What `tools/list` answers, in place of the single `echo` tool. A FUNCTION,
|
|
50
|
+
* read per request, so a test can have the server correct a schema between
|
|
51
|
+
* two discoveries the way a vendor's fix reaches us — through the next load,
|
|
52
|
+
* with nothing restarted here.
|
|
53
|
+
*/
|
|
54
|
+
tools?: () => Array<{ name: string; description?: string; inputSchema: unknown }>;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
export async function startFakeDownstreamMcpServer(opts: FakeDownstreamOptions = {}): Promise<FakeDownstreamMcpServer> {
|
|
@@ -55,13 +63,16 @@ export async function startFakeDownstreamMcpServer(opts: FakeDownstreamOptions =
|
|
|
55
63
|
function buildServer(): Server {
|
|
56
64
|
const server = new Server({ name: 'downstream', version: '0.0.0' }, { capabilities: { tools: {} } });
|
|
57
65
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
58
|
-
|
|
66
|
+
// Cast because a test may deliberately advertise a schema that is NOT
|
|
67
|
+
// valid JSON Schema, which is precisely what the SDK's type forbids and
|
|
68
|
+
// what a real server is free to send.
|
|
69
|
+
tools: (opts.tools?.() ?? [
|
|
59
70
|
{
|
|
60
71
|
name: 'echo',
|
|
61
72
|
description: 'Return the text it was given.',
|
|
62
|
-
inputSchema: { type: 'object'
|
|
73
|
+
inputSchema: { type: 'object', properties: { text: { type: 'string' } } },
|
|
63
74
|
},
|
|
64
|
-
],
|
|
75
|
+
]) as unknown as ListToolsResult['tools'],
|
|
65
76
|
}));
|
|
66
77
|
server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
|
|
67
78
|
executions += 1;
|
|
@@ -1147,3 +1147,253 @@ describe('the first call on a fresh connection, fifty at once', () => {
|
|
|
1147
1147
|
expect(report.distinctSessionIds).toBe(50);
|
|
1148
1148
|
}, 120_000);
|
|
1149
1149
|
});
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* A connected server's input schemas, end to end: what the server sends is
|
|
1154
|
+
* what a client is offered, and a tool whose schema is genuinely invalid is
|
|
1155
|
+
* kept off every agent surface with a reason its owner can read.
|
|
1156
|
+
*
|
|
1157
|
+
* Over the real transport, because this is the layer where the three
|
|
1158
|
+
* silently-dropped tools would have been caught. The proxy had been corrupting
|
|
1159
|
+
* deeply nested schemas on the way OUT, after every parse in the chain had
|
|
1160
|
+
* approved them, so nothing short of a client's own refusal said so.
|
|
1161
|
+
*
|
|
1162
|
+
* One boundary this suite also pins, because it decides what the check can
|
|
1163
|
+
* ever see: a tool whose schema breaks the shape `@modelcontextprotocol/sdk`
|
|
1164
|
+
* models at the ROOT (`type`, `properties`, `required`) makes the SDK client
|
|
1165
|
+
* reject the WHOLE `tools/list` response, so that server's manual fails to
|
|
1166
|
+
* register and Hexis is handed none of its tools. See the last test.
|
|
1167
|
+
*/
|
|
1168
|
+
describe('a connected tool whose schema is invalid is not offered to agents', () => {
|
|
1169
|
+
let downstream: FakeDownstreamMcpServer | undefined;
|
|
1170
|
+
afterEach(async () => {
|
|
1171
|
+
await downstream?.stop();
|
|
1172
|
+
downstream = undefined;
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
const manualAt = (server: FakeDownstreamMcpServer) => () => [
|
|
1176
|
+
{ name: 'notion', call_template_type: 'mcp', config: { mcpServers: { srv: { transport: 'http', url: server.url } } } },
|
|
1177
|
+
];
|
|
1178
|
+
|
|
1179
|
+
/** The constructs the dropped tools carried, nested as deep as theirs were. */
|
|
1180
|
+
function richSchema(levels: number): Record<string, unknown> {
|
|
1181
|
+
let node: Record<string, unknown> = {
|
|
1182
|
+
type: 'object',
|
|
1183
|
+
properties: {
|
|
1184
|
+
socialLinks: { type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'null' }] } },
|
|
1185
|
+
value: { anyOf: [{ type: 'object', required: ['id', 'name'] }, { type: 'null' }] },
|
|
1186
|
+
table: { type: 'object', properties: { rows: { type: 'array', items: { type: 'string' } } } },
|
|
1187
|
+
},
|
|
1188
|
+
required: ['value'],
|
|
1189
|
+
};
|
|
1190
|
+
for (let i = levels; i > 0; i -= 1) {
|
|
1191
|
+
node = { type: 'object', properties: { [`level${i}`]: node }, required: [`level${i}`] };
|
|
1192
|
+
}
|
|
1193
|
+
return node;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* The Notion refusal, as reported: `required` holding a number, in an
|
|
1198
|
+
* `anyOf` branch. `anyOf` is a keyword nothing between the server and Hexis
|
|
1199
|
+
* models, which is why a tool carrying this arrives intact and is ours to
|
|
1200
|
+
* screen — unlike the same mistake at the root (last test).
|
|
1201
|
+
*/
|
|
1202
|
+
const INVALID_SCHEMA = {
|
|
1203
|
+
type: 'object',
|
|
1204
|
+
properties: { value: { anyOf: [{ type: 'object', required: [7] }] } },
|
|
1205
|
+
};
|
|
1206
|
+
const INVALID_AT = '/properties/value/anyOf/0/required/0';
|
|
1207
|
+
const INVALID_BECAUSE = 'must be a string';
|
|
1208
|
+
|
|
1209
|
+
const goodTool = (name: string) => ({
|
|
1210
|
+
name,
|
|
1211
|
+
description: `does ${name}`,
|
|
1212
|
+
inputSchema: { type: 'object', properties: { text: { type: 'string' } } },
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
it('offers an `anyOf` list, a `required` list and nested `items` exactly as the server sent them', async () => {
|
|
1216
|
+
// Twelve levels deep: past the point at which the proxy used to replace
|
|
1217
|
+
// whatever node it had reached with `{}`, which is how `anyOf: {}`,
|
|
1218
|
+
// `required: [{}]` and `type: {}` reached clients and cost three real
|
|
1219
|
+
// tools their place in the agent's toolset.
|
|
1220
|
+
const sent = richSchema(12);
|
|
1221
|
+
downstream = await startFakeDownstreamMcpServer({
|
|
1222
|
+
tools: () => [{ name: 'query', description: 'query it', inputSchema: sent }],
|
|
1223
|
+
});
|
|
1224
|
+
const { baseUrl } = await startPlatform({ manualsFor: manualAt(downstream) });
|
|
1225
|
+
const { client } = await connectSdkClient(baseUrl);
|
|
1226
|
+
const offered = (await client.listTools()).tools.find((t) => t.name === 'notion_srv_query');
|
|
1227
|
+
expect(offered).toBeDefined();
|
|
1228
|
+
expect(offered!.inputSchema).toEqual(sent);
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it('hides the tool whose schema is invalid, keeps its nine siblings, and marks it for the owner', async () => {
|
|
1232
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1233
|
+
downstream = await startFakeDownstreamMcpServer({
|
|
1234
|
+
tools: () => [
|
|
1235
|
+
...Array.from({ length: 9 }, (_, i) => goodTool(`good${i}`)),
|
|
1236
|
+
{ name: 'broken', description: 'broken', inputSchema: INVALID_SCHEMA },
|
|
1237
|
+
],
|
|
1238
|
+
});
|
|
1239
|
+
const platform = await startPlatform({ manualsFor: manualAt(downstream) });
|
|
1240
|
+
const { client } = await connectSdkClient(platform.baseUrl);
|
|
1241
|
+
const names = (await client.listTools()).tools.map((t) => t.name);
|
|
1242
|
+
|
|
1243
|
+
expect(names).not.toContain('notion_srv_broken');
|
|
1244
|
+
for (let i = 0; i < 9; i += 1) expect(names).toContain(`notion_srv_good${i}`);
|
|
1245
|
+
|
|
1246
|
+
// The owner's side of the same fact, by the manual's catalog name.
|
|
1247
|
+
expect(platform.service.hiddenTools.hiddenFor('notion')).toEqual([
|
|
1248
|
+
{
|
|
1249
|
+
manual: 'notion',
|
|
1250
|
+
name: 'notion_srv_broken',
|
|
1251
|
+
path: INVALID_AT,
|
|
1252
|
+
reason: INVALID_BECAUSE,
|
|
1253
|
+
marker: `Hidden from agents: its schema is invalid at ${INVALID_AT} (${INVALID_BECAUSE}).`,
|
|
1254
|
+
},
|
|
1255
|
+
]);
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
it('keeps the hidden tool out of `list_tools` and out of the tool chain', async () => {
|
|
1259
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1260
|
+
downstream = await startFakeDownstreamMcpServer({
|
|
1261
|
+
tools: () => [goodTool('fine'), { name: 'broken', description: 'broken', inputSchema: INVALID_SCHEMA }],
|
|
1262
|
+
});
|
|
1263
|
+
const { baseUrl } = await startPlatform({ manualsFor: manualAt(downstream) });
|
|
1264
|
+
const { client } = await connectSdkClient(baseUrl);
|
|
1265
|
+
// `list_tools` and the chain read the SAME tool repository the listing is
|
|
1266
|
+
// built from, which is why taking the tool off it covers all three.
|
|
1267
|
+
const listed = toolText(await client.callTool({ name: 'list_tools', arguments: {} }));
|
|
1268
|
+
expect(listed).toContain('notion.srv_fine');
|
|
1269
|
+
expect(listed).not.toContain('broken');
|
|
1270
|
+
|
|
1271
|
+
// The chain's view of the catalog is the same repository: it can describe
|
|
1272
|
+
// the sibling and knows nothing of the hidden tool.
|
|
1273
|
+
const info = JSON.parse(
|
|
1274
|
+
toolText(
|
|
1275
|
+
await client.callTool({
|
|
1276
|
+
name: 'tools_info',
|
|
1277
|
+
arguments: { tool_names: ['notion.srv_fine', 'notion.srv_broken'] },
|
|
1278
|
+
}),
|
|
1279
|
+
),
|
|
1280
|
+
) as { interfaces: string; not_found: string[] };
|
|
1281
|
+
expect(info.interfaces).toContain('fine');
|
|
1282
|
+
expect(info.not_found).toEqual(['notion.srv_broken']);
|
|
1283
|
+
|
|
1284
|
+
// And a chain that calls it dies on a tool that is not there — the
|
|
1285
|
+
// namespace is bound, the member is simply absent, so the isolate's own
|
|
1286
|
+
// reason NAMES it. Asserted on that reason rather than on a prefix: the
|
|
1287
|
+
// chain reports a failure as `isError` with the thrown text, and a test
|
|
1288
|
+
// pinned to the wrapper's wording passes while the tool is still bound.
|
|
1289
|
+
const chain = await client.callTool({
|
|
1290
|
+
name: 'call_tool_chain',
|
|
1291
|
+
arguments: { code: 'return notion.srv_broken({ body: {} });' },
|
|
1292
|
+
});
|
|
1293
|
+
expect(chain.isError).toBe(true);
|
|
1294
|
+
expect(toolText(chain)).toContain('notion.srv_broken is not a function');
|
|
1295
|
+
|
|
1296
|
+
// The same chain, same namespace, on the sibling: bound and callable. This
|
|
1297
|
+
// is what makes the line above a statement about the HIDDEN tool rather
|
|
1298
|
+
// than about a namespace the chain could not reach at all.
|
|
1299
|
+
const sibling = await client.callTool({
|
|
1300
|
+
name: 'call_tool_chain',
|
|
1301
|
+
arguments: { code: 'return notion.srv_fine({ body: {} });' },
|
|
1302
|
+
});
|
|
1303
|
+
expect(sibling.isError).toBeFalsy();
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
it('answers an agent that calls the hidden tool by name, without quoting the schema to it', async () => {
|
|
1307
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1308
|
+
downstream = await startFakeDownstreamMcpServer({
|
|
1309
|
+
tools: () => [{ name: 'broken', description: 'broken', inputSchema: INVALID_SCHEMA }],
|
|
1310
|
+
});
|
|
1311
|
+
const { baseUrl } = await startPlatform({ manualsFor: manualAt(downstream) });
|
|
1312
|
+
const { client } = await connectSdkClient(baseUrl);
|
|
1313
|
+
await client.listTools(); // the load that screens the server's tools
|
|
1314
|
+
const res = await client.callTool({ name: 'notion_srv_broken', arguments: {} });
|
|
1315
|
+
expect(res.isError).toBe(true);
|
|
1316
|
+
const text = toolText(res);
|
|
1317
|
+
expect(text).toContain('hidden from agents because its schema is invalid');
|
|
1318
|
+
expect(text).toContain('list_tool_setup');
|
|
1319
|
+
// The place and the reason are the owner's to read, not the agent's.
|
|
1320
|
+
expect(text).not.toContain(INVALID_AT);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it('offers the tool again, with no marker, once the server sends a corrected schema', async () => {
|
|
1324
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1325
|
+
let required: unknown = [7];
|
|
1326
|
+
downstream = await startFakeDownstreamMcpServer({
|
|
1327
|
+
tools: () => [
|
|
1328
|
+
{
|
|
1329
|
+
name: 'query',
|
|
1330
|
+
description: 'query it',
|
|
1331
|
+
inputSchema: { type: 'object', properties: { value: { anyOf: [{ type: 'object', required }] } } },
|
|
1332
|
+
},
|
|
1333
|
+
],
|
|
1334
|
+
});
|
|
1335
|
+
const platform = await startPlatform({ manualsFor: manualAt(downstream) });
|
|
1336
|
+
const { client } = await connectSdkClient(platform.baseUrl);
|
|
1337
|
+
expect((await client.listTools()).tools.map((t) => t.name)).not.toContain('notion_srv_query');
|
|
1338
|
+
|
|
1339
|
+
required = ['id']; // the vendor fixes it
|
|
1340
|
+
// Dropping the pooled connection is what a refresh IS: the next request
|
|
1341
|
+
// re-dials and re-reads the tools. Nothing is restarted, and no marker has
|
|
1342
|
+
// to be cleared by hand.
|
|
1343
|
+
platform.service.onSecretsChanged(null);
|
|
1344
|
+
|
|
1345
|
+
expect((await client.listTools()).tools.map((t) => t.name)).toContain('notion_srv_query');
|
|
1346
|
+
expect(platform.service.hiddenTools.hiddenFor('notion')).toEqual([]);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
it('a tool call does not re-run the check, and does not hide a tool it ran on before', async () => {
|
|
1350
|
+
downstream = await startFakeDownstreamMcpServer({ tools: () => [goodTool('echo')] });
|
|
1351
|
+
const { baseUrl } = await startPlatform({ manualsFor: manualAt(downstream) });
|
|
1352
|
+
const { client } = await connectSdkClient(baseUrl);
|
|
1353
|
+
await client.listTools();
|
|
1354
|
+
// The surface is rebuilt per request, so a call sees the same schemas
|
|
1355
|
+
// again; what it must not do is check them again. That the check runs once
|
|
1356
|
+
// per distinct schema is counted in the guard's own unit test — here the
|
|
1357
|
+
// point is that calling costs nothing and changes nothing.
|
|
1358
|
+
for (const text of ['a', 'b', 'c']) {
|
|
1359
|
+
expect((await client.callTool({ name: 'notion_srv_echo', arguments: { text } })).isError).toBeFalsy();
|
|
1360
|
+
}
|
|
1361
|
+
expect((await client.listTools()).tools.map((t) => t.name)).toContain('notion_srv_echo');
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* The boundary, pinned so nobody has to rediscover it: at the ROOT of a
|
|
1366
|
+
* tool's input schema, `type`, `properties` and `required` are the three
|
|
1367
|
+
* fields `@modelcontextprotocol/sdk` models itself
|
|
1368
|
+
* (`ToolSchema.inputSchema`, with `required: z.array(z.string())`), and the
|
|
1369
|
+
* client rejects the ENTIRE `tools/list` response when one tool breaks them.
|
|
1370
|
+
*
|
|
1371
|
+
* So for that class of defect Hexis is handed NOTHING — not the bad tool, not
|
|
1372
|
+
* the good ones — and cannot hide one tool or mark it: the whole manual fails
|
|
1373
|
+
* to register, which is what it did before this change too. The reason is
|
|
1374
|
+
* logged, with the SDK's own message naming the tool's index.
|
|
1375
|
+
*/
|
|
1376
|
+
it('cannot single out a root-level defect: the MCP SDK refuses the whole tools/list response', async () => {
|
|
1377
|
+
const warnings: string[] = [];
|
|
1378
|
+
vi.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => {
|
|
1379
|
+
warnings.push(args.map(String).join(' '));
|
|
1380
|
+
});
|
|
1381
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1382
|
+
downstream = await startFakeDownstreamMcpServer({
|
|
1383
|
+
tools: () => [
|
|
1384
|
+
goodTool('fine'),
|
|
1385
|
+
{ name: 'broken', description: 'broken', inputSchema: { type: 'object', properties: {}, required: [7] } },
|
|
1386
|
+
],
|
|
1387
|
+
});
|
|
1388
|
+
const platform = await startPlatform({ manualsFor: manualAt(downstream) });
|
|
1389
|
+
const { client } = await connectSdkClient(platform.baseUrl);
|
|
1390
|
+
const names = (await client.listTools()).tools.map((t) => t.name);
|
|
1391
|
+
|
|
1392
|
+
expect(names).not.toContain('notion_srv_broken');
|
|
1393
|
+
// Its sibling goes with it, and the marker cannot name what never arrived.
|
|
1394
|
+
expect(names).not.toContain('notion_srv_fine');
|
|
1395
|
+
expect(platform.service.hiddenTools.hiddenFor('notion')).toEqual([]);
|
|
1396
|
+
expect(warnings.join('\n')).toContain('skipping manual "notion"');
|
|
1397
|
+
expect(warnings.join('\n')).toContain('required');
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
@@ -12,7 +12,9 @@ import { createManualRoutes } from '../../tool-registry/manual.routes.js';
|
|
|
12
12
|
import { ToolRegistry } from '../../tool-registry/tool-registry.js';
|
|
13
13
|
import { toolDef } from '../../tool-helpers/tool-def.js';
|
|
14
14
|
import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
|
|
15
|
-
import { TOOL_PREFIX_LINE, platformInstructions
|
|
15
|
+
import { TOOL_PREFIX_LINE, platformInstructions } from '../../agent-instructions/index.js';
|
|
16
|
+
import { GUIDE_FIRST_SENTENCE } from '../../tool-registry/guide-first.js';
|
|
17
|
+
import { GUIDE_FIRST_SENTENCE } from '../../tool-registry/guide-first.js';
|
|
16
18
|
import type { AgentEventInput, IAgentEventRecorder } from '../../audit/audit.contract.js';
|
|
17
19
|
|
|
18
20
|
/** The platform-owned part of the handshake text: the header plus the shared file rules. */
|
|
@@ -266,23 +268,23 @@ describe('McpService (UTCP→MCP proxy)', () => {
|
|
|
266
268
|
expect(askSchema.properties.body?.properties?.prompt).toBeDefined();
|
|
267
269
|
});
|
|
268
270
|
|
|
269
|
-
it('
|
|
270
|
-
// What a chained read does to an IMAGE
|
|
271
|
-
// share, so it is stated once — in the
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
271
|
+
it('opens every served meta-tool with the guide-first sentence, and states the chain rules nowhere on the chain', async () => {
|
|
272
|
+
// What a chained read does to an IMAGE, a failure or a large result is one
|
|
273
|
+
// of the rules the file tools share, so it is stated once — in the
|
|
274
|
+
// handshake instructions and in the guide — and each meta-tool, like every
|
|
275
|
+
// tool of the platform's own, opens with the one sentence saying where.
|
|
276
|
+
// The clients that drop `instructions` have only descriptions to go on, so
|
|
277
|
+
// that sentence is their way to the rules.
|
|
275
278
|
const client = await setup();
|
|
276
279
|
const { tools } = await client.listTools();
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// than a file.
|
|
282
|
-
expect(chain.description!.split(pointer)).toHaveLength(2);
|
|
283
|
-
for (const name of ['list_tools', 'tools_info']) {
|
|
284
|
-
expect(tools.find((t) => t.name === name)!.description, name).not.toContain(pointer);
|
|
280
|
+
for (const name of ['call_tool_chain', 'list_tools', 'tools_info']) {
|
|
281
|
+
const served = tools.find((t) => t.name === name)!;
|
|
282
|
+
expect(served.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `), name).toBe(true);
|
|
283
|
+
expect(served.description!.split(GUIDE_FIRST_SENTENCE), name).toHaveLength(2);
|
|
285
284
|
}
|
|
285
|
+
const chain = tools.find((t) => t.name === 'call_tool_chain')!;
|
|
286
|
+
expect(chain.description).not.toContain('image_omitted');
|
|
287
|
+
expect(chain.description).not.toContain('Shared rules for all file tools');
|
|
286
288
|
});
|
|
287
289
|
|
|
288
290
|
it('a $defs/$ref tool schema survives tools/list and a real MCP client accepts it', async () => {
|
|
@@ -612,27 +614,33 @@ describe('McpService — agent instructions', () => {
|
|
|
612
614
|
expect.stringContaining('mcp-description.md'),
|
|
613
615
|
expect.objectContaining({ message: 'disk' }),
|
|
614
616
|
);
|
|
615
|
-
// And the four tools carry the fixed line alone
|
|
617
|
+
// And the four tools carry the fixed line alone, ahead of the guide-first
|
|
618
|
+
// opening every listed tool has.
|
|
616
619
|
const { tools } = await client.listTools();
|
|
617
620
|
for (const name of KB_TOOLS) {
|
|
618
|
-
expect(tools.find((t) => t.name === name)?.description).toBe(
|
|
621
|
+
expect(tools.find((t) => t.name === name)?.description).toBe(
|
|
622
|
+
`${TOOL_PREFIX_LINE}\n\n${GUIDE_FIRST_SENTENCE} original ${name} description`,
|
|
623
|
+
);
|
|
619
624
|
}
|
|
620
625
|
});
|
|
621
626
|
|
|
622
|
-
it('prefixes exactly the four knowledge-base tools; every other description is
|
|
627
|
+
it('prefixes exactly the four knowledge-base tools; every other description is as the catalog lists it', async () => {
|
|
623
628
|
const client = await setup({ readAgentPreamble: async () => 'Acme builds solar farms.', extraTools: KB_TOOLS });
|
|
624
629
|
const { tools } = await client.listTools();
|
|
625
630
|
const byName = Object.fromEntries(tools.map((t) => [t.name, t.description]));
|
|
626
631
|
const prefix = `${TOOL_PREFIX_LINE} Acme builds solar farms.`;
|
|
627
632
|
for (const name of KB_TOOLS) {
|
|
628
|
-
expect(byName[name], name).toBe(`${prefix}\n\
|
|
633
|
+
expect(byName[name], name).toBe(`${prefix}\n\n${GUIDE_FIRST_SENTENCE} original ${name} description`);
|
|
629
634
|
}
|
|
630
|
-
// Regression: the rest
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
expect(byName.
|
|
635
|
+
// Regression: the rest carries only what the catalog gives every tool —
|
|
636
|
+
// the guide-first opening — and never the purpose prefix; the meta-tools
|
|
637
|
+
// open with that sentence too.
|
|
638
|
+
expect(byName.ask).toBe(`${GUIDE_FIRST_SENTENCE} echo the prompt`);
|
|
639
|
+
expect(byName.boom).toBe(`${GUIDE_FIRST_SENTENCE} always errors`);
|
|
640
|
+
expect(byName.refy).toBe(`${GUIDE_FIRST_SENTENCE} has $defs/$ref in its schema`);
|
|
634
641
|
for (const meta of ['call_tool_chain', 'list_tools', 'tools_info']) {
|
|
635
642
|
expect(byName[meta], meta).not.toContain(TOOL_PREFIX_LINE);
|
|
643
|
+
expect(byName[meta]!.startsWith(`${GUIDE_FIRST_SENTENCE} `), meta).toBe(true);
|
|
636
644
|
}
|
|
637
645
|
});
|
|
638
646
|
|