@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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connected tool Hexis keeps off every agent surface because its input
|
|
3
|
+
* schema is not valid JSON Schema as its server sent it — and the read port
|
|
4
|
+
* the surfaces that SHOW that to the server's owner use.
|
|
5
|
+
*
|
|
6
|
+
* Here rather than in either module because two of them meet over it: the MCP
|
|
7
|
+
* proxy finds these when a server's tools are loaded (`modules/mcp`), and the
|
|
8
|
+
* tool catalog reports them to the people who manage the server
|
|
9
|
+
* (`modules/tool-manuals`, on the tool page and in `list_tool_setup`). Neither
|
|
10
|
+
* has any business importing the other.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=hidden-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hidden-tools.js","sourceRoot":"","sources":["../../src/shared/hidden-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
package/kb-template/.bevelignore
CHANGED
|
@@ -11,13 +11,8 @@
|
|
|
11
11
|
# Repo hygiene files that clutter the tree without being node content.
|
|
12
12
|
.gitignore
|
|
13
13
|
.gitattributes
|
|
14
|
-
# The platform's agent guide.
|
|
15
|
-
{{agentsFile}}
|
|
16
14
|
# The deployment preamble is edited from External agent access, not as a KB page.
|
|
17
15
|
/mcp-description.md
|
|
18
|
-
# The pre-rename name. Listed so a knowledge base carrying both files hides
|
|
19
|
-
# both — top-up adds AGENTS.md but never deletes the CLAUDE.md beside it.
|
|
20
|
-
CLAUDE.md
|
|
21
16
|
|
|
22
17
|
# Empty-folder placeholders used only for committing empty dirs.
|
|
23
18
|
**/.gitkeep
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bevel-software/platform-core-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, workflow (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"src",
|
|
24
24
|
"migrations",
|
|
25
25
|
"kb-template",
|
|
26
|
+
"agent-guide",
|
|
26
27
|
"THIRD-PARTY-NOTICES.md"
|
|
27
28
|
],
|
|
28
29
|
"engines": {
|
|
@@ -52,8 +53,8 @@
|
|
|
52
53
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
53
54
|
"yaml": "^2.9.0",
|
|
54
55
|
"zod": "^3.24.0",
|
|
55
|
-
"@bevel-software/platform-mcp-core": "0.
|
|
56
|
-
"@bevel-software/platform-shared": "0.
|
|
56
|
+
"@bevel-software/platform-mcp-core": "0.26.0",
|
|
57
|
+
"@bevel-software/platform-shared": "0.26.0"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@types/adm-zip": "^0.5.8",
|
|
@@ -5,15 +5,12 @@ import {
|
|
|
5
5
|
KNOWLEDGE_BASE_DIR,
|
|
6
6
|
PLUGINS_DIR,
|
|
7
7
|
SKILLS_DIR,
|
|
8
|
-
agentsFilePointerSentence,
|
|
9
8
|
configureKbLayout,
|
|
10
9
|
currentKbLayout,
|
|
11
10
|
isPlatformFile,
|
|
12
|
-
mentionsAgentsFile,
|
|
13
11
|
platformFileNames,
|
|
14
12
|
pluginOfPath,
|
|
15
13
|
renderKbLayoutPlaceholders,
|
|
16
|
-
retargetAgentsFilePointer,
|
|
17
14
|
reservedRootDirNames,
|
|
18
15
|
validateAgentsFileName,
|
|
19
16
|
validateKbLayout,
|
|
@@ -193,106 +190,19 @@ describe('KB layout — the agent guide\'s file name', () => {
|
|
|
193
190
|
expect(renderKbLayoutPlaceholders('see {{agentsFile}}', DEFAULT_KB_LAYOUT)).toBe('see AGENTS.md');
|
|
194
191
|
});
|
|
195
192
|
|
|
196
|
-
test('the platform
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
'.bevelignore',
|
|
201
|
-
'AGENTS.md',
|
|
202
|
-
]);
|
|
203
|
-
expect(platformFile('AGENTS.md')).toBe(true);
|
|
204
|
-
|
|
205
|
-
configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
|
|
206
|
-
expect(platformFileNames(currentKbLayout())).toEqual(['access.md', 'roles.yaml', '.bevelignore', 'HEXIS.md']);
|
|
207
|
-
// The guide the platform writes is immovable and undeletable…
|
|
208
|
-
expect(platformFile('HEXIS.md')).toBe(true);
|
|
209
|
-
// …and the customer's own AGENTS.md is a page like any other.
|
|
193
|
+
test('the guide is no platform file under any name: a root AGENTS.md is the organisation\'s own page', () => {
|
|
194
|
+
// The platform's guide is served from code and never written to the
|
|
195
|
+
// repository, so nothing under its name is the platform's to protect.
|
|
196
|
+
expect(platformFileNames(DEFAULT_KB_LAYOUT)).toEqual(['access.md', 'roles.yaml', '.bevelignore']);
|
|
210
197
|
expect(platformFile('AGENTS.md')).toBe(false);
|
|
211
|
-
|
|
212
|
-
expect(platformFile('KnowledgeBase/HEXIS.md')).toBe(false);
|
|
213
|
-
});
|
|
198
|
+
expect(platformFile('roles.yaml')).toBe(true);
|
|
214
199
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
'Read [HEXIS.md](./HEXIS.md) before working in this knowledge base — ' +
|
|
218
|
-
"it is the platform's guide to its layout, files and rules.",
|
|
219
|
-
);
|
|
200
|
+
// A name a deployment saved for the written guide is a read alias now,
|
|
201
|
+
// not a file, and makes no platform file either.
|
|
220
202
|
configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
|
|
221
|
-
expect(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* A guide name is a FILE NAME: spaces, brackets, parentheses, `#` and `%`
|
|
226
|
-
* all pass `validateFilename`, and every one of them means something in an
|
|
227
|
-
* inline link. The label must not end early and the destination must still
|
|
228
|
-
* point at the file.
|
|
229
|
-
*/
|
|
230
|
-
test('the pointer sentence links correctly for a name full of markdown punctuation', () => {
|
|
231
|
-
const name = 'Our [Agent] Guide (v2).md';
|
|
232
|
-
expect(validateGuideName(name)).toBeNull();
|
|
233
|
-
const sentence = agentsFilePointerSentence(name);
|
|
234
|
-
// The label cannot end early: the brackets in it are escaped…
|
|
235
|
-
expect(sentence).toContain('[Our \\[Agent\\] Guide (v2).md]');
|
|
236
|
-
// …and the destination is percent-encoded, parentheses included — a bare
|
|
237
|
-
// `)` would close the link half way through the name.
|
|
238
|
-
expect(sentence).toContain('(./Our%20%5BAgent%5D%20Guide%20%28v2%29.md)');
|
|
239
|
-
// The ordinary name reads as it always has — no escapes, nothing encoded.
|
|
240
|
-
expect(agentsFilePointerSentence('AGENTS.md')).toContain('[AGENTS.md](./AGENTS.md)');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('the pointer sentence encodes a name that would otherwise open a URL fragment', () => {
|
|
244
|
-
// `#` is legal in a filename and opens a fragment in a URL: `./#2 Guide.md`
|
|
245
|
-
// links to the customer's OWN file with a fragment, not to the guide.
|
|
246
|
-
expect(validateGuideName('#2 Guide.md')).toBeNull();
|
|
247
|
-
expect(agentsFilePointerSentence('#2 Guide.md')).toContain('(./%232%20Guide.md)');
|
|
248
|
-
// `%` is legal too, and an unencoded one is a malformed escape.
|
|
249
|
-
expect(agentsFilePointerSentence('100%.md')).toContain('(./100%25.md)');
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* The startup step asks this before appending, and it has to recognise the
|
|
254
|
-
* sentence the LAST boot wrote — whose spelling of the name is escaped in
|
|
255
|
-
* the label and encoded in the destination. Asking for the raw name alone
|
|
256
|
-
* would append a second copy on every boot after the first.
|
|
257
|
-
*/
|
|
258
|
-
test('a file already carrying the pointer sentence counts as mentioning the guide', () => {
|
|
259
|
-
for (const name of ['AGENTS.md', 'Our [Agent] Guide (v2).md', '#2 Guide.md', '100%.md']) {
|
|
260
|
-
const appended = `# Acme\n\n${agentsFilePointerSentence(name)}\n`;
|
|
261
|
-
expect(mentionsAgentsFile(appended, name), name).toBe(true);
|
|
262
|
-
}
|
|
263
|
-
// A file that says nothing about the guide still reads as silent…
|
|
264
|
-
expect(mentionsAgentsFile('# Acme\n\nWrite tickets in the present tense.\n', 'HEXIS.md')).toBe(false);
|
|
265
|
-
// …and the customer's own plain mention counts, in their own words.
|
|
266
|
-
expect(mentionsAgentsFile('See HEXIS.md for the platform.', 'HEXIS.md')).toBe(true);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* A second rename. The sentence written for the previous guide points at a
|
|
271
|
-
* file that is no longer there, and the new name appears nowhere in the
|
|
272
|
-
* text — so it has to be recognised by its SHAPE and aimed again.
|
|
273
|
-
*/
|
|
274
|
-
test('a pointer sentence the platform wrote is retargeted, whatever guide it named', () => {
|
|
275
|
-
for (const before of ['HEXIS.md', 'Our [Agent] Guide (v2).md', '#2 Guide.md']) {
|
|
276
|
-
const text = `# Acme\n\n${agentsFilePointerSentence(before)}\n`;
|
|
277
|
-
expect(retargetAgentsFilePointer(text, 'GUIDE.md'), before).toBe(
|
|
278
|
-
`# Acme\n\n${agentsFilePointerSentence('GUIDE.md')}\n`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
// Already aimed right: ours, and unchanged — which is not the same answer
|
|
282
|
-
// as "none of ours here", and the caller tells them apart.
|
|
283
|
-
const current = `# Acme\n\n${agentsFilePointerSentence('GUIDE.md')}\n`;
|
|
284
|
-
expect(retargetAgentsFilePointer(current, 'GUIDE.md')).toBe(current);
|
|
203
|
+
expect(platformFileNames(currentKbLayout())).toEqual(['access.md', 'roles.yaml', '.bevelignore']);
|
|
204
|
+
expect(platformFile('HEXIS.md')).toBe(false);
|
|
205
|
+
expect(platformFile('AGENTS.md')).toBe(false);
|
|
285
206
|
});
|
|
286
207
|
|
|
287
|
-
test('leaves a file holding nothing of the platform\'s alone', () => {
|
|
288
|
-
// Null, not the text: there is nothing of ours to aim, so the caller goes
|
|
289
|
-
// on to ask whether the customer mentioned the guide themselves.
|
|
290
|
-
expect(retargetAgentsFilePointer('# Acme\n\nWrite tickets in the present tense.\n', 'GUIDE.md')).toBeNull();
|
|
291
|
-
// Their own link to their own file is not our sentence.
|
|
292
|
-
expect(retargetAgentsFilePointer('Read [notes](./notes.md) before working here.', 'GUIDE.md')).toBeNull();
|
|
293
|
-
// Our opening words, their sentence.
|
|
294
|
-
expect(
|
|
295
|
-
retargetAgentsFilePointer('Read [HEXIS.md](./HEXIS.md) when you have a moment.', 'GUIDE.md'),
|
|
296
|
-
).toBeNull();
|
|
297
|
-
});
|
|
298
208
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { agentGuideDir, coreMigrationsDir, defaultKbTemplateDir } from '../assets.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every asset folder this package reads at run time has to reach the places
|
|
9
|
+
* the package is shipped from: the npm tarball (`files` in package.json) and
|
|
10
|
+
* the Docker image (the runtime stage of the repository's Dockerfile, which
|
|
11
|
+
* copies each folder by name).
|
|
12
|
+
*
|
|
13
|
+
* The second list is the one nothing else checks. `agent-guide/` was added to
|
|
14
|
+
* `assets.ts` and to `files`, but not to the Dockerfile, and no test or CI job
|
|
15
|
+
* builds the image: the folder was simply absent from every image built from
|
|
16
|
+
* dev. It is read while the tool list is composed, so the first request for
|
|
17
|
+
* any tool on the deployment failed, and every agent surface with it
|
|
18
|
+
* (2026-10-06). A folder named here and missing from either list fails this
|
|
19
|
+
* test instead.
|
|
20
|
+
*/
|
|
21
|
+
const packageRoot = fileURLToPath(new URL('../..', import.meta.url));
|
|
22
|
+
const repoRoot = path.resolve(packageRoot, '..', '..');
|
|
23
|
+
|
|
24
|
+
/** The folders assets.ts locates, as names under the package root. */
|
|
25
|
+
const ASSET_DIRS = [coreMigrationsDir(), defaultKbTemplateDir(), agentGuideDir()].map((dir) =>
|
|
26
|
+
path.basename(dir),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
describe('the asset folders this package reads at run time', () => {
|
|
30
|
+
it('are the three assets.ts names', () => {
|
|
31
|
+
expect(ASSET_DIRS.sort()).toEqual(['agent-guide', 'kb-template', 'migrations']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('exist in the source tree', () => {
|
|
35
|
+
for (const dir of ASSET_DIRS) {
|
|
36
|
+
expect(fs.statSync(path.join(packageRoot, dir)).isDirectory(), dir).toBe(true);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('are in the npm tarball', () => {
|
|
41
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')) as { files: string[] };
|
|
42
|
+
for (const dir of ASSET_DIRS) {
|
|
43
|
+
expect(pkg.files, `${dir} missing from package.json "files"`).toContain(dir);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('are copied into the Docker image by the runtime stage', () => {
|
|
48
|
+
const dockerfile = fs.readFileSync(path.join(repoRoot, 'Dockerfile'), 'utf8');
|
|
49
|
+
for (const dir of ASSET_DIRS) {
|
|
50
|
+
const line = `COPY --from=builder /app/packages/core-backend/${dir} packages/core-backend/${dir}`;
|
|
51
|
+
expect(dockerfile, `Dockerfile does not copy ${dir} into the image`).toContain(line);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/assets.ts
CHANGED
|
@@ -23,3 +23,13 @@ export function coreMigrationsDir(): string {
|
|
|
23
23
|
export function defaultKbTemplateDir(): string {
|
|
24
24
|
return path.join(packageRoot(), 'kb-template');
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The sections of the platform's agent guide, one markdown file each — the
|
|
29
|
+
* text `get_agent_guide` and a `read_file` of the guide's name serve, composed
|
|
30
|
+
* by `modules/agent-guide`. Beside `kb-template/` rather than under `src/`,
|
|
31
|
+
* so a build and the published source find it at the same place.
|
|
32
|
+
*/
|
|
33
|
+
export function agentGuideDir(): string {
|
|
34
|
+
return path.join(packageRoot(), 'agent-guide');
|
|
35
|
+
}
|
package/src/core/core-ports.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { IErasureParticipant } from '../modules/auth/account-erasure.servic
|
|
|
9
9
|
import type { IAccountAdmission } from '../modules/auth/account-admission.js';
|
|
10
10
|
import type { IWriteAccess } from '../modules/write-access/write-access.js';
|
|
11
11
|
import type { OnServerStart } from '../modules/workspace/startup/on-server-start.js';
|
|
12
|
+
import type { AgentGuideHook } from '../modules/agent-guide/agent-guide.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Every seam the enterprise overlay can fill in the CORE composition
|
|
@@ -100,6 +101,16 @@ export interface CorePorts {
|
|
|
100
101
|
* contract each step signs up to). Core default: `[]`.
|
|
101
102
|
*/
|
|
102
103
|
kbStartupSteps?: readonly OnServerStart[];
|
|
104
|
+
/**
|
|
105
|
+
* THIS distribution's say over the agent guide — the text `get_agent_guide`
|
|
106
|
+
* and a `read_file` of the guide's name serve. Called with core's sections
|
|
107
|
+
* and the layout in effect on every composition; returns the sections the
|
|
108
|
+
* guide is made of, so a distribution appends its own, replaces one of
|
|
109
|
+
* core's by id, or drops one, and every change core makes to the rest still
|
|
110
|
+
* reaches it. Core default: core's sections as they are. See
|
|
111
|
+
* `modules/agent-guide`.
|
|
112
|
+
*/
|
|
113
|
+
agentGuide?: AgentGuideHook;
|
|
103
114
|
/**
|
|
104
115
|
* Mirror this composition's branch model and knowledge-base layout onto the
|
|
105
116
|
* shared package's PROCESS-WIDE live bindings (`DEFAULT_BRANCH`,
|
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
registerToolManualsTools,
|
|
27
27
|
} from '../modules/tool-manuals/index.js';
|
|
28
28
|
import { registerWorkflowTools } from '../modules/workflow/agent-tools/workflow.tools.js';
|
|
29
|
+
import { registerChangeRequestReadTools } from '../modules/workflow/agent-tools/change-request-read.tools.js';
|
|
29
30
|
import { registerWorkspaceTools } from '../modules/workspace/workspace.tools.js';
|
|
31
|
+
import { registerAgentGuideTool } from '../modules/agent-guide/index.js';
|
|
30
32
|
import { RECOVERY_BOT_EMAIL } from '../modules/workflow/recovery-bot.js';
|
|
31
33
|
import {
|
|
32
34
|
registerSkillsTools,
|
|
@@ -456,7 +458,13 @@ export async function createCoreServer(
|
|
|
456
458
|
// `get_skill`. Warnings only; it never refuses a save.
|
|
457
459
|
const allowedToolsChecker = new AllowedToolsChecker(core.toolRegistry, core.toolManualService, core.kb);
|
|
458
460
|
registerWorkflowTools(core.toolRegistry, toolsRouter, ta, th, core.kb);
|
|
459
|
-
|
|
461
|
+
// The five read tools over change requests. Separate from the workflow tools
|
|
462
|
+
// because they are the only ones that gate their whole payload on the
|
|
463
|
+
// caller's read access, so they take the access service and nothing else.
|
|
464
|
+
registerChangeRequestReadTools(core.toolRegistry, toolsRouter, ta, th, core.accessControl, core.kb);
|
|
465
|
+
registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate, core.agentUploadStore, core.agentGuide);
|
|
466
|
+
// The guide on its own, beside the file tools that serve it by name.
|
|
467
|
+
registerAgentGuideTool(core.toolRegistry, toolsRouter, ta, th, core.agentGuideSections);
|
|
460
468
|
// The agent upload route, on the same router as the tool endpoints so it
|
|
461
469
|
// mounts ahead of the JWT `/api` mounts below — but WITHOUT `toolAuth`: its
|
|
462
470
|
// whole credential is the single-use token in its path, which is the point
|
|
@@ -467,13 +475,16 @@ export async function createCoreServer(
|
|
|
467
475
|
registerSkillsTools(core.toolRegistry, toolsRouter, ta, th, core.skillService, allowedToolsChecker);
|
|
468
476
|
// Definitions only: the endpoints they describe are the app's own plugin
|
|
469
477
|
// creation routes, mounted below behind the key-or-session gate.
|
|
470
|
-
registerPluginsTools(core.toolRegistry);
|
|
478
|
+
registerPluginsTools(core.toolRegistry, core.kb);
|
|
471
479
|
registerToolManualsTools(core.toolRegistry, toolsRouter, ta, th, core.toolManualService, {
|
|
472
480
|
accessControl: core.accessControl,
|
|
473
481
|
// The vault satisfies the module's local VariableStatusPort — `list_tool_setup`
|
|
474
482
|
// reports configuration booleans only; secret values never ride through tools.
|
|
475
483
|
variableStatus: core.secretsVaultService,
|
|
476
484
|
kb: core.kb,
|
|
485
|
+
// What the proxy's schema check found when each server's tools were last
|
|
486
|
+
// loaded — reported to a caller who may write the tool, nobody else.
|
|
487
|
+
hiddenTools: core.mcpService.hiddenTools,
|
|
477
488
|
});
|
|
478
489
|
// Overlay tool registrations (defs + module-hosted endpoints).
|
|
479
490
|
ext.tools?.({
|
|
@@ -138,6 +138,12 @@ import { unmeteredLlmUsage, type ILlmUsageMeter } from '../modules/tool-auth/llm
|
|
|
138
138
|
import { McpService } from '../modules/mcp/mcp.service.js';
|
|
139
139
|
import { AgentAuditService, retentionDaysFrom } from '../modules/audit/agent-audit.service.js';
|
|
140
140
|
import { readAgentPreamble, type AgentPreambleReader } from '../modules/agent-instructions/index.js';
|
|
141
|
+
import {
|
|
142
|
+
agentGuideSections,
|
|
143
|
+
joinGuideSections,
|
|
144
|
+
type AgentGuideReader,
|
|
145
|
+
type AgentGuideSectionsReader,
|
|
146
|
+
} from '../modules/agent-guide/index.js';
|
|
141
147
|
import { createMcpAuthMiddleware } from '../modules/mcp/mcp-auth.middleware.js';
|
|
142
148
|
import { BevelOAuthProvider } from '../modules/mcp/oauth/bevel-oauth-provider.js';
|
|
143
149
|
import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
|
@@ -239,6 +245,14 @@ export interface CoreServices {
|
|
|
239
245
|
* and `GET /api/agent/instructions`. See modules/agent-instructions.
|
|
240
246
|
*/
|
|
241
247
|
readAgentPreamble: AgentPreambleReader;
|
|
248
|
+
/**
|
|
249
|
+
* Composes the platform's agent guide for the layout in effect, through
|
|
250
|
+
* the distribution's hook when it passed one: what `get_agent_guide` and a
|
|
251
|
+
* `read_file` of the guide's name answer. See modules/agent-guide.
|
|
252
|
+
*/
|
|
253
|
+
agentGuide: AgentGuideReader;
|
|
254
|
+
/** The same guide as its sections, each with its title — what `get_agent_guide` lists and serves one of. */
|
|
255
|
+
agentGuideSections: AgentGuideSectionsReader;
|
|
242
256
|
mcpServerEditService: McpServerEditService;
|
|
243
257
|
/** Deleting one tool — the owner's verb (see ToolDeleteService). */
|
|
244
258
|
toolDeleteService: ToolDeleteService;
|
|
@@ -606,10 +620,7 @@ export async function createCoreServices(
|
|
|
606
620
|
// backfill would walk a tree still missing those manifests.
|
|
607
621
|
new PluginDisplayNamesStep(disk, kb),
|
|
608
622
|
new PersonalSpacesStep(disk, kb),
|
|
609
|
-
|
|
610
|
-
// still hold the defaults, and the save that completes first-run setup
|
|
611
|
-
// applies the admin's answer afterwards.
|
|
612
|
-
new TemplateFilesStep(disk, kb, extraDirs, () => settings.resolveAgentsFileLink()),
|
|
623
|
+
new TemplateFilesStep(disk, kb, extraDirs),
|
|
613
624
|
new RolesYamlStep(disk, [config.adminEmail]),
|
|
614
625
|
...(ports.kbStartupSteps ?? []),
|
|
615
626
|
];
|
|
@@ -1168,6 +1179,12 @@ export async function createCoreServices(
|
|
|
1168
1179
|
// the proxy below composes in-process per request; the agent-facing route
|
|
1169
1180
|
// serves the same composition to the local bridge and the frontend card.
|
|
1170
1181
|
const readPreamble: AgentPreambleReader = () => readAgentPreamble(workspaceService, kb, disk);
|
|
1182
|
+
// The guide every agent is told to read first, composed when asked for —
|
|
1183
|
+
// the layout is read per call, so a name the setup save applies lands
|
|
1184
|
+
// without a restart, and the distribution's hook sees every composition.
|
|
1185
|
+
const agentGuideSectionsReader: AgentGuideSectionsReader = () =>
|
|
1186
|
+
agentGuideSections(kb.layout, ports.agentGuide, { kbDirName });
|
|
1187
|
+
const agentGuide: AgentGuideReader = async () => joinGuideSections(await agentGuideSectionsReader());
|
|
1171
1188
|
// The Audit log. Records through the proxy below (every call an external
|
|
1172
1189
|
// agent makes), reads keys through the key service so their shape is
|
|
1173
1190
|
// defined once, and prunes past the retention setting — read per sweep, so
|
|
@@ -1214,6 +1231,11 @@ export async function createCoreServices(
|
|
|
1214
1231
|
// so a just-repaired credential is retried on the very next request instead
|
|
1215
1232
|
// of waiting out the failure memo's TTL, and pooled downstream connections.
|
|
1216
1233
|
secretsVaultService.onMutation((changedUserId) => mcpService.onSecretsChanged(changedUserId));
|
|
1234
|
+
// The proxy is the one place a connected server's tools are loaded, so it is
|
|
1235
|
+
// the one place their schemas are checked — and the tool catalog is where the
|
|
1236
|
+
// people who manage a server read what it found. Setter injection, like the
|
|
1237
|
+
// OAuth discovery above: the proxy is constructed after the catalog.
|
|
1238
|
+
toolManualService.setHiddenTools(mcpService.hiddenTools);
|
|
1217
1239
|
// MCP OAuth 2.1 authorization server (our own AS): lets MCP clients with no
|
|
1218
1240
|
// pre-shared connection key connect via the standard 401 → discovery → DCR →
|
|
1219
1241
|
// authorize (PKCE) flow. The authorize step routes the browser to /connect
|
|
@@ -1440,6 +1462,8 @@ export async function createCoreServices(
|
|
|
1440
1462
|
toolManualService,
|
|
1441
1463
|
pendingToolsService,
|
|
1442
1464
|
readAgentPreamble: readPreamble,
|
|
1465
|
+
agentGuide,
|
|
1466
|
+
agentGuideSections: agentGuideSectionsReader,
|
|
1443
1467
|
pluginIndexService,
|
|
1444
1468
|
pluginProvisionService,
|
|
1445
1469
|
joinRequestsService,
|
package/src/index.ts
CHANGED
|
@@ -47,8 +47,8 @@ export {
|
|
|
47
47
|
type LeasedWorker,
|
|
48
48
|
} from './core/lifecycle.js';
|
|
49
49
|
|
|
50
|
-
// Packaged assets (migrations/, kb-template/) + the migration runners.
|
|
51
|
-
export { coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
|
|
50
|
+
// Packaged assets (migrations/, kb-template/, agent-guide/) + the migration runners.
|
|
51
|
+
export { agentGuideDir, coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
|
|
52
52
|
export {
|
|
53
53
|
runCoreMigrations,
|
|
54
54
|
runEnterpriseMigrations,
|
|
@@ -25,6 +25,8 @@ describe('AccessControlService — at-ref batch reads (git cat-file --batch)', (
|
|
|
25
25
|
let svc: AccessControlService;
|
|
26
26
|
const workspaceId = 'ws-atref-1';
|
|
27
27
|
const admin = 'razvan@bevel.software';
|
|
28
|
+
/** In no role and named nowhere — read is default-deny for them. */
|
|
29
|
+
const stranger = 'mia@bevel.software';
|
|
28
30
|
|
|
29
31
|
async function git(...args: string[]): Promise<void> {
|
|
30
32
|
await execFileAsync('git', ['-C', repo, ...args]);
|
|
@@ -44,6 +46,10 @@ describe('AccessControlService — at-ref batch reads (git cat-file --batch)', (
|
|
|
44
46
|
'---\nwrite:\n - deny razvan <razvan@bevel.software>\n---\n# denied\n',
|
|
45
47
|
);
|
|
46
48
|
await fs.writeFile(path.join(repo, 'Knowledge/with space.md'), '# spaced\n');
|
|
49
|
+
// For the read batch: a folder the whole organisation may open.
|
|
50
|
+
await fs.mkdir(path.join(repo, 'Knowledge/shared'), { recursive: true });
|
|
51
|
+
await fs.writeFile(path.join(repo, 'Knowledge/shared/access.md'), '---\nread:\n - everyone\n---\n');
|
|
52
|
+
await fs.writeFile(path.join(repo, 'Knowledge/shared/note.md'), '# shared\n');
|
|
47
53
|
|
|
48
54
|
await execFileAsync('git', ['init', '-b', 'main', repo]);
|
|
49
55
|
await git('config', 'user.email', 'test@example.com');
|
|
@@ -96,4 +102,56 @@ describe('AccessControlService — at-ref batch reads (git cat-file --batch)', (
|
|
|
96
102
|
expect(map!.get('Knowledge/denied.md')!.emails.has(admin)).toBe(false);
|
|
97
103
|
expect(map!.get('Knowledge/plain.md')!.emails.has(admin)).toBe(true);
|
|
98
104
|
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The read side of the same batch, used by the change-request read tools to
|
|
108
|
+
* decide which of a request's files their caller may see. The verdicts must
|
|
109
|
+
* be identical to the single-path `canReadAtRef` the app's own content routes
|
|
110
|
+
* gate on — a batch that answered even one path differently would show an
|
|
111
|
+
* agent a file the app refuses to open.
|
|
112
|
+
*/
|
|
113
|
+
it('canReadBatchAtRef answers default-deny, an `everyone` grant, and write-implies-read in one pass', async () => {
|
|
114
|
+
const paths = [
|
|
115
|
+
'Knowledge/plain.md',
|
|
116
|
+
'Knowledge/shared/note.md',
|
|
117
|
+
'Knowledge/with space.md',
|
|
118
|
+
'Knowledge/not-there.md',
|
|
119
|
+
];
|
|
120
|
+
const forStranger = await svc.canReadBatchAtRef(workspaceId, 'main', stranger, paths);
|
|
121
|
+
expect(forStranger).not.toBeNull();
|
|
122
|
+
// Nothing names them, so read is refused — except where the folder says everyone.
|
|
123
|
+
expect(forStranger!.get('Knowledge/plain.md')).toBe(false);
|
|
124
|
+
expect(forStranger!.get('Knowledge/shared/note.md')).toBe(true);
|
|
125
|
+
expect(forStranger!.get('Knowledge/with space.md')).toBe(false);
|
|
126
|
+
expect(forStranger!.get('Knowledge/not-there.md')).toBe(false);
|
|
127
|
+
|
|
128
|
+
// The Admin write grant folds into read.
|
|
129
|
+
const forAdmin = await svc.canReadBatchAtRef(workspaceId, 'main', admin, paths);
|
|
130
|
+
expect([...forAdmin!.values()]).toEqual([true, true, true, true]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('canReadBatchAtRef matches the single-path read verdict on every path', async () => {
|
|
134
|
+
const paths = [
|
|
135
|
+
'Knowledge/plain.md',
|
|
136
|
+
'Knowledge/shared/note.md',
|
|
137
|
+
'Knowledge/with space.md',
|
|
138
|
+
'Knowledge/not-there.md',
|
|
139
|
+
];
|
|
140
|
+
for (const who of [admin, stranger]) {
|
|
141
|
+
const batch = await svc.canReadBatchAtRef(workspaceId, 'main', who, paths);
|
|
142
|
+
for (const path of paths) {
|
|
143
|
+
const single = await svc.canReadAtRef(workspaceId, 'main', who, path);
|
|
144
|
+
expect(batch!.get(path), `${who} on ${path}`).toBe(single);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('canReadBatchAtRef answers an empty map for no paths, and null for a ref it cannot resolve', async () => {
|
|
150
|
+
expect(await svc.canReadBatchAtRef(workspaceId, 'main', admin, [])).toEqual(new Map());
|
|
151
|
+
expect(await svc.canReadBatchAtRef(workspaceId, 'no-such-ref', admin, ['Knowledge/plain.md'])).toBeNull();
|
|
152
|
+
// The null-semantics hold for the empty set too: the ref is validated
|
|
153
|
+
// whatever was asked about, so no caller can read an empty map as proof
|
|
154
|
+
// that the ref resolved.
|
|
155
|
+
expect(await svc.canReadBatchAtRef(workspaceId, 'no-such-ref', admin, [])).toBeNull();
|
|
156
|
+
});
|
|
99
157
|
});
|
|
@@ -67,9 +67,10 @@ describe('restoring a platform file', () => {
|
|
|
67
67
|
// only because it is the file that says who the admin is — the repository
|
|
68
68
|
// that lost that one too is the last case in this file.
|
|
69
69
|
const svc = service();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
expect(await svc.canRestorePlatformFile(WS, ADMIN, '.bevelignore')).toBe(true);
|
|
71
|
+
// The agent guide is not a platform file: it is served from code, never
|
|
72
|
+
// written to the root, so a root `AGENTS.md` is content and no restore.
|
|
73
|
+
expect(await svc.canRestorePlatformFile(WS, ADMIN, 'AGENTS.md')).toBe(false);
|
|
73
74
|
// Nothing is missing at `roles.yaml`: the root has one. A move is a rename
|
|
74
75
|
// on disk, so landing another there would REPLACE the admin model rather
|
|
75
76
|
// than put it back, and no exception carries that.
|
|
@@ -95,9 +96,9 @@ describe('restoring a platform file', () => {
|
|
|
95
96
|
expect(await svc.canRestorePlatformFile(WS, ENGINEER, 'Sales/Legal/access.md')).toBe(false);
|
|
96
97
|
});
|
|
97
98
|
|
|
98
|
-
it('the
|
|
99
|
+
it('the root-only names are a restore at the root and nowhere else', async () => {
|
|
99
100
|
const svc = service();
|
|
100
|
-
for (const dest of ['Sales/roles.yaml', 'Sales/.bevelignore'
|
|
101
|
+
for (const dest of ['Sales/roles.yaml', 'Sales/.bevelignore']) {
|
|
101
102
|
expect(await svc.canRestorePlatformFile(WS, ADMIN, dest)).toBe(false);
|
|
102
103
|
}
|
|
103
104
|
});
|
|
@@ -113,14 +114,14 @@ describe('restoring a platform file', () => {
|
|
|
113
114
|
|
|
114
115
|
it('a non-admin never gets the exception, wherever it would land', async () => {
|
|
115
116
|
const svc = service();
|
|
116
|
-
for (const dest of ['roles.yaml', '.bevelignore', '
|
|
117
|
+
for (const dest of ['roles.yaml', '.bevelignore', 'access.md', 'Legal/access.md']) {
|
|
117
118
|
expect(await svc.canRestorePlatformFile(WS, ENGINEER, dest)).toBe(false);
|
|
118
119
|
}
|
|
119
120
|
});
|
|
120
121
|
|
|
121
122
|
it('the deployment owner is an admin for it, and an ordinary destination is not a restore for anyone', async () => {
|
|
122
123
|
const svc = service();
|
|
123
|
-
expect(await svc.canRestorePlatformFile(WS, OWNER, '
|
|
124
|
+
expect(await svc.canRestorePlatformFile(WS, OWNER, '.bevelignore')).toBe(true);
|
|
124
125
|
for (const email of [ADMIN, OWNER]) {
|
|
125
126
|
expect(await svc.canRestorePlatformFile(WS, email, 'Sales/deal.md')).toBe(false);
|
|
126
127
|
expect(await svc.canRestorePlatformFile(WS, email, 'Sales/notes.md')).toBe(false);
|
|
@@ -7,7 +7,6 @@ import os from 'node:os';
|
|
|
7
7
|
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
8
8
|
import { AccessControlService } from '../access-control.service.js';
|
|
9
9
|
import { personalAccessMd, pluginAccessMd } from '../../plugins/plugin-provision.service.js';
|
|
10
|
-
import { defaultKbTemplateDir } from '../../../assets.js';
|
|
11
10
|
import { closePersonalSpaceRules } from '../../workspace/startup/steps/personal-spaces.step.js';
|
|
12
11
|
|
|
13
12
|
/**
|
|
@@ -17,10 +16,7 @@ import { closePersonalSpaceRules } from '../../workspace/startup/steps/personal-
|
|
|
17
16
|
* - a personal space must stay private even after an administrator opens
|
|
18
17
|
* the repository root with `read: everyone` (the usual way to let a new
|
|
19
18
|
* joiner read anything), while its owner keeps reading it — and Admin,
|
|
20
|
-
* who reads everything else, does not
|
|
21
|
-
* - the managed AGENTS.md must be readable by every signed-in person even
|
|
22
|
-
* when the root grants read to nobody, because agents are told to read
|
|
23
|
-
* it before their first action.
|
|
19
|
+
* who reads everything else, does not.
|
|
24
20
|
*/
|
|
25
21
|
|
|
26
22
|
const KB_DIR = 'knowledge-base';
|
|
@@ -137,17 +133,4 @@ describe('the seeded access templates, resolved', () => {
|
|
|
137
133
|
expect(await svc.canRead(workspaceId, 'admin@x.io', skill)).toBe(false);
|
|
138
134
|
});
|
|
139
135
|
|
|
140
|
-
it('the packaged AGENTS.md is readable by a non-admin even when the root grants read to nobody', async () => {
|
|
141
|
-
const template = await fs.readFile(path.join(defaultKbTemplateDir(), 'AGENTS.md'), 'utf8');
|
|
142
|
-
const svc = await makeService({
|
|
143
|
-
'roles.yaml': ROLES_YAML,
|
|
144
|
-
'access.md': '---\nowner:\n - Admin\n---\nwrite:\n - Admin\n',
|
|
145
|
-
'AGENTS.md': template,
|
|
146
|
-
'KnowledgeBase/Notes.md': '# Notes\n',
|
|
147
|
-
});
|
|
148
|
-
const bob = await svc.canReadBatch(workspaceId, 'bob@x.io', ['AGENTS.md', 'KnowledgeBase/Notes.md']);
|
|
149
|
-
expect(bob.get('AGENTS.md')).toBe(true);
|
|
150
|
-
// The grant is the FILE's own, not a widening of the root.
|
|
151
|
-
expect(bob.get('KnowledgeBase/Notes.md')).toBe(false);
|
|
152
|
-
});
|
|
153
136
|
});
|
|
@@ -631,6 +631,21 @@ export interface IAccessControl {
|
|
|
631
631
|
relativePaths: string[],
|
|
632
632
|
): Promise<Map<string, boolean> | null>;
|
|
633
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Batched variant of `canReadAtRef`, with the same null-semantics (the whole
|
|
636
|
+
* call returns null when the ref or its `roles.yaml` can't be resolved, and
|
|
637
|
+
* the caller must read that as deny). One access-tree load and one
|
|
638
|
+
* `git cat-file --batch` for the path set, the way `canWriteBatchAtRef`
|
|
639
|
+
* does it — a change request with forty files would otherwise cost forty
|
|
640
|
+
* single-path lookups to decide which of them its reader may see.
|
|
641
|
+
*/
|
|
642
|
+
canReadBatchAtRef(
|
|
643
|
+
workspaceId: string,
|
|
644
|
+
ref: string,
|
|
645
|
+
userEmail: string,
|
|
646
|
+
relativePaths: string[],
|
|
647
|
+
): Promise<Map<string, boolean> | null>;
|
|
648
|
+
|
|
634
649
|
/**
|
|
635
650
|
* The set of principals with `write` on this path as of `ref`. Used by PR
|
|
636
651
|
* reviewer routing and the per-file approval UI rendered against the PR
|
|
@@ -2103,6 +2103,27 @@ export class AccessControlService implements IAccessControl {
|
|
|
2103
2103
|
return result;
|
|
2104
2104
|
}
|
|
2105
2105
|
|
|
2106
|
+
async canReadBatchAtRef(
|
|
2107
|
+
workspaceId: string,
|
|
2108
|
+
ref: string,
|
|
2109
|
+
userEmail: string,
|
|
2110
|
+
relativePaths: string[],
|
|
2111
|
+
): Promise<Map<string, boolean> | null> {
|
|
2112
|
+
// No shortcut for an empty path set: the null-semantics are the contract,
|
|
2113
|
+
// so an unresolvable ref must answer null however many paths were asked
|
|
2114
|
+
// about. Returning an empty map early would tell a caller the ref resolved.
|
|
2115
|
+
const loaded = await this.loadModelAtRef(workspaceId, ref);
|
|
2116
|
+
if (!loaded) return null;
|
|
2117
|
+
const repoDir = await this.repoDir(workspaceId);
|
|
2118
|
+
// One `git cat-file --batch` for the whole set — see canWriteBatchAtRef.
|
|
2119
|
+
const owns = await this.readOwnEntriesAtRefBatch(repoDir, loaded.resolvedRef, relativePaths);
|
|
2120
|
+
const result = new Map<string, boolean>();
|
|
2121
|
+
for (const p of relativePaths) {
|
|
2122
|
+
result.set(p, canReadResolved(loaded.model, userEmail, p, owns.get(p) ?? null));
|
|
2123
|
+
}
|
|
2124
|
+
return result;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2106
2127
|
async eligibleWritersAtRef(
|
|
2107
2128
|
workspaceId: string,
|
|
2108
2129
|
ref: string,
|