@bevel-software/platform-core-backend 0.7.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +8 -1
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +75 -18
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +12 -5
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +69 -36
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -7
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +6 -6
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +5 -5
- package/dist/modules/access/access-declarations.js +3 -3
- package/dist/modules/access/access-mutation.service.d.ts +3 -3
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +3 -3
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.js +4 -4
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts +3 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +22 -20
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +2 -2
- package/dist/modules/access/creator-access.js +5 -5
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/render-roles-yaml.d.ts +22 -0
- package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
- package/dist/modules/access/render-roles-yaml.js +56 -0
- package/dist/modules/access/render-roles-yaml.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +33 -5
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +44 -19
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
- package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode-names.js +5 -27
- package/dist/modules/code-mode/code-mode-names.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +29 -7
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -52
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +33 -395
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +7 -0
- package/dist/modules/plugins/index.d.ts.map +1 -0
- package/dist/modules/plugins/index.js +6 -0
- package/dist/modules/plugins/index.js.map +1 -0
- package/dist/modules/plugins/join-proposals.d.ts +53 -0
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
- package/dist/modules/plugins/join-proposals.js +67 -0
- package/dist/modules/plugins/join-proposals.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts +81 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-requests.service.js +135 -0
- package/dist/modules/plugins/join-requests.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.js +344 -0
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +106 -0
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.contract.js +36 -0
- package/dist/modules/plugins/plugins.contract.js.map +1 -0
- package/dist/modules/plugins/plugins.routes.d.ts +42 -0
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.routes.js +379 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -0
- package/dist/modules/plugins/plugins.service.d.ts +60 -0
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.service.js +172 -0
- package/dist/modules/plugins/plugins.service.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +10 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +111 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts +2 -2
- package/dist/modules/skills/pending-skills.service.js +7 -7
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +4 -4
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.service.js +5 -5
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.js +2 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +43 -10
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/plugins-migration.d.ts +50 -0
- package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
- package/dist/modules/workspace/plugins-migration.js +379 -0
- package/dist/modules/workspace/plugins-migration.js.map +1 -0
- package/dist/modules/workspace/startup/kb-git.d.ts +23 -0
- package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-git.js +86 -0
- package/dist/modules/workspace/startup/kb-git.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.js +21 -0
- package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +0 -9
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +2 -30
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/kb-layout.test.js +3 -3
- package/dist/shared/kb-layout.test.js.map +1 -1
- package/dist/shared/utcp-namespace.d.ts +6 -27
- package/dist/shared/utcp-namespace.d.ts.map +1 -1
- package/dist/shared/utcp-namespace.js +6 -63
- package/dist/shared/utcp-namespace.js.map +1 -1
- package/dist/shared/variable-refs.d.ts +42 -0
- package/dist/shared/variable-refs.d.ts.map +1 -0
- package/dist/shared/variable-refs.js +60 -0
- package/dist/shared/variable-refs.js.map +1 -0
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +88 -35
- package/kb-template/KnowledgeBase/How to get started.md +10 -10
- package/kb-template/access.md +36 -36
- package/kb-template/gitignore.template +16 -0
- package/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +113 -106
- package/src/core/create-core-server.ts +85 -20
- package/src/core/create-core-services.ts +83 -42
- package/src/index.ts +69 -69
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
- package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
- package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
- package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
- package/src/modules/access/__tests__/access-splice.test.ts +2 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
- package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
- package/src/modules/access/__tests__/roles-admin.service.test.ts +36 -2
- package/src/modules/access/access-control.interface.ts +8 -7
- package/src/modules/access/access-control.service.ts +7 -7
- package/src/modules/access/access-declarations.ts +5 -5
- package/src/modules/access/access-mutation.service.ts +3 -3
- package/src/modules/access/access-splice.ts +4 -4
- package/src/modules/access/access.routes.ts +23 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/render-roles-yaml.ts +65 -0
- package/src/modules/access/roles-admin.service.ts +45 -16
- package/src/modules/admin/admin-access.routes.ts +29 -29
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
- package/src/modules/auth/rate-limit.ts +45 -45
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
- package/src/modules/code-mode/code-mode-names.ts +10 -36
- package/src/modules/code-mode/code-mode.tool.ts +27 -7
- package/src/modules/database/connection.ts +15 -15
- package/src/modules/database/schema.ts +11 -11
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
- package/src/modules/mcp/mcp.service.ts +57 -435
- package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
- package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
- package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
- package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
- package/src/modules/plugins/index.ts +17 -0
- package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
- package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
- package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
- package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
- package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
- package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
- package/src/modules/settings/__tests__/setup.routes.test.ts +124 -3
- package/src/modules/settings/setup.routes.ts +112 -5
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/pending-skills.service.ts +7 -7
- package/src/modules/skills/skills.contract.ts +4 -4
- package/src/modules/skills/skills.service.ts +5 -5
- package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
- package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
- package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
- package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/git.service.ts +2 -2
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/workflow-hooks.ts +101 -101
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
- package/src/modules/workspace/__tests__/workspace.service.test.ts +27 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
- package/src/modules/workspace/startup/kb-git.ts +94 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
- package/src/modules/workspace/startup/on-server-start.ts +97 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
- package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +561 -0
- package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
- package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +2 -35
- package/src/modules/workspace/workspace.tools.ts +922 -922
- package/src/shared/__tests__/join-request.test.ts +13 -13
- package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +3 -3
- package/src/shared/utcp-namespace.ts +10 -68
- package/src/shared/variable-refs.ts +64 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
- package/src/modules/groups/index.ts +0 -17
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +0 -444
- package/src/modules/workspace/kb-seed.interface.ts +0 -36
- package/src/modules/workspace/kb-seed.service.ts +0 -555
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
|
@@ -52,7 +52,7 @@ function makeWorkspace(files: Record<string, string>): IWorkspaceService {
|
|
|
52
52
|
} as unknown as IWorkspaceService;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const scan = (files: Record<string, string>, folder = '
|
|
55
|
+
const scan = (files: Record<string, string>, folder = 'Plugins/GTM') =>
|
|
56
56
|
listAccessDeclarationsUnder(makeWorkspace(files), WS, KB, folder);
|
|
57
57
|
|
|
58
58
|
const fm = (body: string) => `---\n${body}\n---\n\n# Body\n`;
|
|
@@ -60,14 +60,14 @@ const fm = (body: string) => `---\n${body}\n---\n\n# Body\n`;
|
|
|
60
60
|
describe('listAccessDeclarationsUnder', () => {
|
|
61
61
|
it('reports a descendant access.md against the directory it governs', async () => {
|
|
62
62
|
const { overrides, truncated } = await scan({
|
|
63
|
-
[`${KB}/
|
|
63
|
+
[`${KB}/Plugins/GTM/battlecards/access.md`]: fm('write:\n - GTM Team'),
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
expect(truncated).toBe(false);
|
|
67
67
|
expect(overrides).toEqual([
|
|
68
68
|
{
|
|
69
|
-
path: '
|
|
70
|
-
governs: '
|
|
69
|
+
path: 'Plugins/GTM/battlecards/access.md',
|
|
70
|
+
governs: 'Plugins/GTM/battlecards',
|
|
71
71
|
source: 'access-md',
|
|
72
72
|
entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'GTM Team' } }],
|
|
73
73
|
},
|
|
@@ -76,23 +76,23 @@ describe('listAccessDeclarationsUnder', () => {
|
|
|
76
76
|
|
|
77
77
|
it("excludes the target folder's own access.md — that is the summary, not an override", async () => {
|
|
78
78
|
const { overrides } = await scan({
|
|
79
|
-
[`${KB}/
|
|
80
|
-
[`${KB}/
|
|
79
|
+
[`${KB}/Plugins/GTM/access.md`]: fm('read:\n - GTM Team'),
|
|
80
|
+
[`${KB}/Plugins/GTM/battlecards/access.md`]: fm('read:\n - Developer'),
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
expect(overrides.map((o) => o.path)).toEqual(['
|
|
83
|
+
expect(overrides.map((o) => o.path)).toEqual(['Plugins/GTM/battlecards/access.md']);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
it('reads verbs out of SKILL.md and .tool frontmatter, governing the file itself', async () => {
|
|
87
87
|
const { overrides } = await scan({
|
|
88
|
-
[`${KB}/
|
|
89
|
-
[`${KB}/
|
|
88
|
+
[`${KB}/Plugins/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\nowner: Ali <ali@bevel.software>'),
|
|
89
|
+
[`${KB}/Plugins/GTM/slack.tool`]: fm('write:\n - Developer'),
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
expect(overrides).toEqual([
|
|
93
93
|
{
|
|
94
|
-
path: '
|
|
95
|
-
governs: '
|
|
94
|
+
path: 'Plugins/GTM/outreach/SKILL.md',
|
|
95
|
+
governs: 'Plugins/GTM/outreach/SKILL.md',
|
|
96
96
|
source: 'frontmatter',
|
|
97
97
|
entries: [
|
|
98
98
|
{
|
|
@@ -103,8 +103,8 @@ describe('listAccessDeclarationsUnder', () => {
|
|
|
103
103
|
],
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
|
-
path: '
|
|
107
|
-
governs: '
|
|
106
|
+
path: 'Plugins/GTM/slack.tool',
|
|
107
|
+
governs: 'Plugins/GTM/slack.tool',
|
|
108
108
|
source: 'frontmatter',
|
|
109
109
|
entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'Developer' } }],
|
|
110
110
|
},
|
|
@@ -113,7 +113,7 @@ describe('listAccessDeclarationsUnder', () => {
|
|
|
113
113
|
|
|
114
114
|
it('ignores nodes whose frontmatter declares no access verb', async () => {
|
|
115
115
|
const { overrides } = await scan({
|
|
116
|
-
[`${KB}/
|
|
116
|
+
[`${KB}/Plugins/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\ndescription: Runs outreach'),
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
expect(overrides).toEqual([]);
|
|
@@ -121,7 +121,7 @@ describe('listAccessDeclarationsUnder', () => {
|
|
|
121
121
|
|
|
122
122
|
it('preserves the deny prefix, collapses everyone, and carries user name + email', async () => {
|
|
123
123
|
const { overrides } = await scan({
|
|
124
|
-
[`${KB}/
|
|
124
|
+
[`${KB}/Plugins/GTM/battlecards/access.md`]: fm(
|
|
125
125
|
'read:\n - everyone\n - deny Bob Ruiz <bob@bevel.software>\nwrite:\n - deny GTM Team',
|
|
126
126
|
),
|
|
127
127
|
});
|
|
@@ -140,13 +140,13 @@ describe('listAccessDeclarationsUnder', () => {
|
|
|
140
140
|
it('surfaces an unparseable access.md and silently skips an unparseable node', async () => {
|
|
141
141
|
const { overrides } = await scan({
|
|
142
142
|
// `read:` must be a LIST in an access.md — this file controls nothing.
|
|
143
|
-
[`${KB}/
|
|
143
|
+
[`${KB}/Plugins/GTM/battlecards/access.md`]: fm('read: GTM Team'),
|
|
144
144
|
// Unterminated frontmatter on a node — forgiven, exactly like the resolver.
|
|
145
|
-
[`${KB}/
|
|
145
|
+
[`${KB}/Plugins/GTM/outreach/SKILL.md`]: '---\nowner: Ali <ali@bevel.software>\n\n# Body\n',
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
expect(overrides).toHaveLength(1);
|
|
149
|
-
expect(overrides[0].path).toBe('
|
|
149
|
+
expect(overrides[0].path).toBe('Plugins/GTM/battlecards/access.md');
|
|
150
150
|
expect(overrides[0].entries).toEqual([]);
|
|
151
151
|
expect(overrides[0].parseError).toContain("'read:' must be a list");
|
|
152
152
|
});
|
|
@@ -155,7 +155,7 @@ describe('listAccessDeclarationsUnder', () => {
|
|
|
155
155
|
const files: Record<string, string> = {};
|
|
156
156
|
for (let i = 0; i < DECLARATION_SCAN_FILE_CAP + 5; i++) {
|
|
157
157
|
const n = String(i).padStart(4, '0');
|
|
158
|
-
files[`${KB}/
|
|
158
|
+
files[`${KB}/Plugins/GTM/n${n}/SKILL.md`] = fm('read:\n - Developer');
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const { overrides, truncated } = await scan(files);
|
|
@@ -165,31 +165,31 @@ describe('listAccessDeclarationsUnder', () => {
|
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
it('returns an empty result for a folder with nothing in it', async () => {
|
|
168
|
-
expect(await scan({ [`${KB}/
|
|
168
|
+
expect(await scan({ [`${KB}/Plugins/Product/roadmap/SKILL.md`]: fm('read:\n - Developer') }))
|
|
169
169
|
.toEqual({ overrides: [], truncated: false });
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
it('returns an empty result for a folder that is not in the clone', async () => {
|
|
173
|
-
expect(await scan({}, '
|
|
173
|
+
expect(await scan({}, 'Plugins/Nope')).toEqual({ overrides: [], truncated: false });
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
it('refuses a target that is a file', async () => {
|
|
177
177
|
await expect(
|
|
178
|
-
scan({ [`${KB}/
|
|
178
|
+
scan({ [`${KB}/Plugins/GTM/slack.tool`]: fm('write:\n - Developer') }, 'Plugins/GTM/slack.tool'),
|
|
179
179
|
).rejects.toBeInstanceOf(AccessDeclarationsError);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
it('sorts rows by declaring path', async () => {
|
|
183
183
|
const { overrides } = await scan({
|
|
184
|
-
[`${KB}/
|
|
185
|
-
[`${KB}/
|
|
186
|
-
[`${KB}/
|
|
184
|
+
[`${KB}/Plugins/GTM/zeta.tool`]: fm('write:\n - Developer'),
|
|
185
|
+
[`${KB}/Plugins/GTM/alpha/access.md`]: fm('read:\n - Developer'),
|
|
186
|
+
[`${KB}/Plugins/GTM/mid/SKILL.md`]: fm('owner:\n - Developer'),
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
expect(overrides.map((o) => o.path)).toEqual([
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
'
|
|
190
|
+
'Plugins/GTM/alpha/access.md',
|
|
191
|
+
'Plugins/GTM/mid/SKILL.md',
|
|
192
|
+
'Plugins/GTM/zeta.tool',
|
|
193
193
|
]);
|
|
194
194
|
});
|
|
195
195
|
});
|
|
@@ -19,7 +19,7 @@ import { spliceGrant, spliceRevoke } from '../access-splice.js';
|
|
|
19
19
|
* govern itself (its readability follows the folder chain).
|
|
20
20
|
* - NEW: the body carries the folder's rules; the frontmatter — like every
|
|
21
21
|
* other file's — is about the FILE itself. `read: everyone` there makes
|
|
22
|
-
* the
|
|
22
|
+
* the plugin discoverable (anyone may open the access.md and see who runs
|
|
23
23
|
* the folder) while the folder's contents stay locked.
|
|
24
24
|
*
|
|
25
25
|
* Compat rule under test: a file whose body is NOT parsable as rules resolves
|
|
@@ -35,7 +35,7 @@ const ROLES_YAML = `roles:
|
|
|
35
35
|
- felix@example.com
|
|
36
36
|
`;
|
|
37
37
|
|
|
38
|
-
/** New-format
|
|
38
|
+
/** New-format plugin access.md: discoverable by everyone, readable by GTM Team. */
|
|
39
39
|
const NEW_FORMAT = `---
|
|
40
40
|
read:
|
|
41
41
|
- everyone
|
|
@@ -58,7 +58,7 @@ Some prose explaining this folder.
|
|
|
58
58
|
describe('access.md format detection + parsing', () => {
|
|
59
59
|
it('detects body rules (new format) and parses the BODY for the folder', () => {
|
|
60
60
|
expect(accessMdDeclaresBodyRules(NEW_FORMAT)).toBe(true);
|
|
61
|
-
const parsed = parseAccessFile(NEW_FORMAT, '
|
|
61
|
+
const parsed = parseAccessFile(NEW_FORMAT, 'Plugins/GTM/access.md');
|
|
62
62
|
expect(parsed.ok).toBe(true);
|
|
63
63
|
if (!parsed.ok) return;
|
|
64
64
|
expect(parsed.file.entries.read).toEqual([
|
|
@@ -89,7 +89,7 @@ describe('access.md format detection + parsing', () => {
|
|
|
89
89
|
// (possibly `read: everyone`) self-rules — a typo must fail loudly.
|
|
90
90
|
const typo = '---\nread:\n - everyone\n---\nread: GTM Team\n';
|
|
91
91
|
expect(accessMdDeclaresBodyRules(typo)).toBe(true);
|
|
92
|
-
const parsed = parseAccessFile(typo, '
|
|
92
|
+
const parsed = parseAccessFile(typo, 'Plugins/GTM/access.md');
|
|
93
93
|
expect(parsed.ok).toBe(false);
|
|
94
94
|
if (parsed.ok) return;
|
|
95
95
|
expect(parsed.errors.join(' ')).toContain("'read:' must be a list");
|
|
@@ -110,7 +110,7 @@ describe('access.md splice targeting', () => {
|
|
|
110
110
|
it("target 'folder' on a new-format file lands the grant in the BODY", () => {
|
|
111
111
|
const r = spliceGrant(NEW_FORMAT, 'read', FELIX, { target: 'folder' });
|
|
112
112
|
expect(r.changed).toBe(true);
|
|
113
|
-
const parsed = parseAccessFile(r.text, '
|
|
113
|
+
const parsed = parseAccessFile(r.text, 'Plugins/GTM/access.md');
|
|
114
114
|
expect(parsed.ok && parsed.file.entries.read.some((e) => e.kind === 'user' && e.email === 'ali@x.eu')).toBe(true);
|
|
115
115
|
// The self-frontmatter is untouched.
|
|
116
116
|
expect(accessMdSelfEntries(r.text)?.read).toEqual([
|
|
@@ -132,7 +132,7 @@ describe('access.md splice targeting', () => {
|
|
|
132
132
|
expect(r.changed).toBe(true);
|
|
133
133
|
expect(accessMdSelfEntries(r.text)?.read.some((e) => e.kind === 'user' && e.email === 'ali@x.eu')).toBe(true);
|
|
134
134
|
// Folder rules untouched.
|
|
135
|
-
const parsed = parseAccessFile(r.text, '
|
|
135
|
+
const parsed = parseAccessFile(r.text, 'Plugins/GTM/access.md');
|
|
136
136
|
expect(parsed.ok && parsed.file.entries.read.some((e) => e.kind === 'user')).toBe(false);
|
|
137
137
|
});
|
|
138
138
|
|
|
@@ -140,14 +140,14 @@ describe('access.md splice targeting', () => {
|
|
|
140
140
|
const granted = spliceGrant(NEW_FORMAT, 'read', FELIX, { target: 'folder' }).text;
|
|
141
141
|
const revoked = spliceRevoke(granted, 'read', FELIX, { target: 'folder' });
|
|
142
142
|
expect(revoked.changed).toBe(true);
|
|
143
|
-
const parsed = parseAccessFile(revoked.text, '
|
|
143
|
+
const parsed = parseAccessFile(revoked.text, 'Plugins/GTM/access.md');
|
|
144
144
|
expect(parsed.ok && parsed.file.entries.read.some((e) => e.kind === 'user')).toBe(false);
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
describe('end-to-end resolution over a new-format
|
|
148
|
+
describe('end-to-end resolution over a new-format plugin', () => {
|
|
149
149
|
let root: string;
|
|
150
|
-
const workspaceId = 'ws-
|
|
150
|
+
const workspaceId = 'ws-plugins-fmt';
|
|
151
151
|
|
|
152
152
|
beforeEach(async () => {
|
|
153
153
|
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-accessfmt-'));
|
|
@@ -159,12 +159,12 @@ describe('end-to-end resolution over a new-format group', () => {
|
|
|
159
159
|
async function seed(): Promise<AccessControlService> {
|
|
160
160
|
const workspaceDir = path.join(root, workspaceId);
|
|
161
161
|
const repo = path.join(workspaceDir, KB);
|
|
162
|
-
await fs.mkdir(path.join(repo, '
|
|
162
|
+
await fs.mkdir(path.join(repo, 'Plugins', 'GTM'), { recursive: true });
|
|
163
163
|
await fs.writeFile(path.join(repo, 'roles.yaml'), ROLES_YAML);
|
|
164
164
|
await fs.writeFile(path.join(repo, 'access.md'), '---\nwrite:\n - Admin\n---\n');
|
|
165
|
-
await fs.writeFile(path.join(repo, '
|
|
166
|
-
await fs.mkdir(path.join(repo, '
|
|
167
|
-
await fs.writeFile(path.join(repo, '
|
|
165
|
+
await fs.writeFile(path.join(repo, 'Plugins', 'GTM', 'access.md'), NEW_FORMAT);
|
|
166
|
+
await fs.mkdir(path.join(repo, 'Plugins', 'GTM', 'outreach'), { recursive: true });
|
|
167
|
+
await fs.writeFile(path.join(repo, 'Plugins', 'GTM', 'outreach', 'SKILL.md'), '# outreach\n');
|
|
168
168
|
const stub = {
|
|
169
169
|
getWorkspacePath: async (id: string) => {
|
|
170
170
|
if (id !== workspaceId) throw new Error(`unexpected workspace ${id}`);
|
|
@@ -177,15 +177,15 @@ describe('end-to-end resolution over a new-format group', () => {
|
|
|
177
177
|
it('a NON-member can read the access.md itself (discovery) but nothing inside the folder', async () => {
|
|
178
178
|
const svc = await seed();
|
|
179
179
|
const outsider = 'sara@example.com';
|
|
180
|
-
expect(await svc.canRead(workspaceId, outsider, '
|
|
181
|
-
expect(await svc.canRead(workspaceId, outsider, '
|
|
180
|
+
expect(await svc.canRead(workspaceId, outsider, 'Plugins/GTM/access.md')).toBe(true);
|
|
181
|
+
expect(await svc.canRead(workspaceId, outsider, 'Plugins/GTM/outreach/SKILL.md')).toBe(false);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
it('a member reads both; body rules (not frontmatter) decide the folder', async () => {
|
|
185
185
|
const svc = await seed();
|
|
186
186
|
const member = 'felix@example.com';
|
|
187
|
-
expect(await svc.canRead(workspaceId, member, '
|
|
188
|
-
expect(await svc.canRead(workspaceId, member, '
|
|
189
|
-
expect(await svc.canWrite(workspaceId, member, '
|
|
187
|
+
expect(await svc.canRead(workspaceId, member, 'Plugins/GTM/access.md')).toBe(true);
|
|
188
|
+
expect(await svc.canRead(workspaceId, member, 'Plugins/GTM/outreach/SKILL.md')).toBe(true);
|
|
189
|
+
expect(await svc.canWrite(workspaceId, member, 'Plugins/GTM/outreach/SKILL.md')).toBe(true);
|
|
190
190
|
});
|
|
191
191
|
});
|
|
@@ -80,11 +80,11 @@ describe('AccessMutationService', () => {
|
|
|
80
80
|
expect(text).toContain('# Sales folder');
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
it('kbPrincipals surfaces the built-in Everyone
|
|
84
|
-
const {
|
|
85
|
-
expect(
|
|
86
|
-
expect(
|
|
87
|
-
expect(
|
|
83
|
+
it('kbPrincipals surfaces the built-in Everyone plugin alongside declared roles', async () => {
|
|
84
|
+
const { plugins } = await access.kbPrincipals(WS);
|
|
85
|
+
expect(plugins).toContain('Everyone');
|
|
86
|
+
expect(plugins).toContain('Admin');
|
|
87
|
+
expect(plugins).toContain('Product Team');
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
it('grant everyone read makes the folder publicly readable (read: everyone)', async () => {
|
|
@@ -220,13 +220,13 @@ describe('validatePrincipal — injection guard', () => {
|
|
|
220
220
|
expect(p.kind === 'user' && p.displayName).toBe('Ali');
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
it('rejects a
|
|
223
|
+
it('rejects a plugin name with a colon (would forge a key)', () => {
|
|
224
224
|
expect(() => validatePrincipal({ kind: 'role', role: 'write:\n - everyone' })).toThrow(
|
|
225
225
|
AccessSpliceError,
|
|
226
226
|
);
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
it('rejects the reserved
|
|
229
|
+
it('rejects the reserved plugin name deny', () => {
|
|
230
230
|
expect(() => validatePrincipal({ kind: 'role', role: 'deny' })).toThrow(AccessSpliceError);
|
|
231
231
|
});
|
|
232
232
|
|
|
@@ -113,9 +113,9 @@ function close(s: Server): Promise<void> {
|
|
|
113
113
|
const fm = (body: string) => `---\n${body}\n---\n\n# Body\n`;
|
|
114
114
|
|
|
115
115
|
const FILES = {
|
|
116
|
-
[`${KB}/
|
|
117
|
-
[`${KB}/
|
|
118
|
-
[`${KB}/
|
|
116
|
+
[`${KB}/Plugins/GTM/access.md`]: fm('read:\n - GTM Team'),
|
|
117
|
+
[`${KB}/Plugins/GTM/battlecards/access.md`]: fm('read:\n - deny everyone'),
|
|
118
|
+
[`${KB}/Plugins/GTM/slack.tool`]: fm('owner: Ali <ali@bevel.software>'),
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
describe('GET /access/overrides', () => {
|
|
@@ -133,20 +133,20 @@ describe('GET /access/overrides', () => {
|
|
|
133
133
|
it('returns the declarations under a folder the caller can read', async () => {
|
|
134
134
|
h = await makeHarness({ files: FILES });
|
|
135
135
|
|
|
136
|
-
const res = await get('
|
|
136
|
+
const res = await get('Plugins/GTM');
|
|
137
137
|
expect(res.status).toBe(200);
|
|
138
138
|
expect(await res.json()).toEqual({
|
|
139
139
|
truncated: false,
|
|
140
140
|
overrides: [
|
|
141
141
|
{
|
|
142
|
-
path: '
|
|
143
|
-
governs: '
|
|
142
|
+
path: 'Plugins/GTM/battlecards/access.md',
|
|
143
|
+
governs: 'Plugins/GTM/battlecards',
|
|
144
144
|
source: 'access-md',
|
|
145
145
|
entries: [{ verb: 'read', deny: true, principal: { kind: 'everyone' } }],
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
|
-
path: '
|
|
149
|
-
governs: '
|
|
148
|
+
path: 'Plugins/GTM/slack.tool',
|
|
149
|
+
governs: 'Plugins/GTM/slack.tool',
|
|
150
150
|
source: 'frontmatter',
|
|
151
151
|
entries: [
|
|
152
152
|
{
|
|
@@ -163,7 +163,7 @@ describe('GET /access/overrides', () => {
|
|
|
163
163
|
it('403s a caller who cannot read the folder', async () => {
|
|
164
164
|
h = await makeHarness({ files: FILES, canRead: () => false });
|
|
165
165
|
|
|
166
|
-
const res = await get('
|
|
166
|
+
const res = await get('Plugins/GTM');
|
|
167
167
|
expect(res.status).toBe(403);
|
|
168
168
|
expect(await res.json()).toEqual({ error: 'You do not have access to this folder.' });
|
|
169
169
|
});
|
|
@@ -171,21 +171,21 @@ describe('GET /access/overrides', () => {
|
|
|
171
171
|
it('drops rows governing something the caller cannot read', async () => {
|
|
172
172
|
h = await makeHarness({
|
|
173
173
|
files: FILES,
|
|
174
|
-
rowReadable: (p) => p !== '
|
|
174
|
+
rowReadable: (p) => p !== 'Plugins/GTM/battlecards',
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
const res = await get('
|
|
177
|
+
const res = await get('Plugins/GTM');
|
|
178
178
|
const body = (await res.json()) as { overrides: { path: string }[] };
|
|
179
|
-
expect(body.overrides.map((o) => o.path)).toEqual(['
|
|
179
|
+
expect(body.overrides.map((o) => o.path)).toEqual(['Plugins/GTM/slack.tool']);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
it('accepts a workspace-relative path exactly like a repo-relative one', async () => {
|
|
183
183
|
h = await makeHarness({ files: FILES });
|
|
184
184
|
|
|
185
|
-
const repoRel = (await (await get('
|
|
186
|
-
const wsRel = (await (await get(`${KB}/
|
|
185
|
+
const repoRel = (await (await get('Plugins/GTM')).json()) as unknown;
|
|
186
|
+
const wsRel = (await (await get(`${KB}/Plugins/GTM`)).json()) as unknown;
|
|
187
187
|
expect(wsRel).toEqual(repoRel);
|
|
188
|
-
expect(h.canRead).toHaveBeenCalledWith(WS, USER.email, '
|
|
188
|
+
expect(h.canRead).toHaveBeenCalledWith(WS, USER.email, 'Plugins/GTM');
|
|
189
189
|
});
|
|
190
190
|
|
|
191
191
|
it('400s a path that escapes the KB repo', async () => {
|
|
@@ -198,7 +198,7 @@ describe('GET /access/overrides', () => {
|
|
|
198
198
|
it('400s a target that is a file, not a folder', async () => {
|
|
199
199
|
h = await makeHarness({ files: FILES });
|
|
200
200
|
|
|
201
|
-
const res = await get('
|
|
201
|
+
const res = await get('Plugins/GTM/slack.tool');
|
|
202
202
|
expect(res.status).toBe(400);
|
|
203
203
|
expect(await res.json()).toEqual({ error: 'path must be a folder' });
|
|
204
204
|
});
|
|
@@ -109,19 +109,19 @@ describe('AccessControlService.grantSources', () => {
|
|
|
109
109
|
expect(src.write).toEqual([{ kind: 'ancestor', path: 'Knowledge/access.md' }]);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
it('a USER who holds a verb only via a
|
|
112
|
+
it('a USER who holds a verb only via a plugin they belong to has NO source (the plugin row carries it)', async () => {
|
|
113
113
|
await writeFile(repo, 'access.md', '---\nwrite:\n - Engineer\n---\n');
|
|
114
|
-
// Ali is an Engineer (per roles.yaml) — his write flows through the
|
|
114
|
+
// Ali is an Engineer (per roles.yaml) — his write flows through the PLUGIN, not
|
|
115
115
|
// a per-user file entry. He isn't named in any file, so there's nothing to
|
|
116
|
-
// remove for HIM here; the Engineering
|
|
116
|
+
// remove for HIM here; the Engineering plugin is the removable principal.
|
|
117
117
|
const src = await svc.grantSources(workspaceId, 'folder', 'Knowledge', ALI);
|
|
118
118
|
expect(src.write).toBeUndefined();
|
|
119
119
|
expect(src).toEqual({}); // no verb resolves to a per-user file entry
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
it('the
|
|
122
|
+
it('the PLUGIN principal itself resolves to its own file scope (direct/ancestor)', async () => {
|
|
123
123
|
// The same Engineer grant, asked about as the ROLE principal, IS a removable
|
|
124
|
-
// file entry — the
|
|
124
|
+
// file entry — the plugin is named directly on Knowledge/access.md.
|
|
125
125
|
await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
|
|
126
126
|
await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - Engineer\n---\n');
|
|
127
127
|
const src = await svc.grantSources(workspaceId, 'folder', 'Knowledge', {
|
|
@@ -150,20 +150,20 @@ describe('AccessControlService.grantSources', () => {
|
|
|
150
150
|
expect(src).toEqual({});
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
it("a folder-target admin whose access is via the Admin
|
|
154
|
-
// Razvan's effective write comes from the `Admin`
|
|
153
|
+
it("a folder-target admin whose access is via the Admin PLUGIN has NO per-user source", async () => {
|
|
154
|
+
// Razvan's effective write comes from the `Admin` plugin grant, not a per-user
|
|
155
155
|
// entry — so as a USER principal he has no removable source here (the Admin
|
|
156
|
-
//
|
|
156
|
+
// plugin row carries it, exactly like any other plugin).
|
|
157
157
|
await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
|
|
158
158
|
const src = await svc.grantSources(workspaceId, 'folder', 'Knowledge', RAZVAN);
|
|
159
159
|
expect(src.write).toBeUndefined();
|
|
160
160
|
expect(src).toEqual({});
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
-
it("a user named DIRECTLY in a file keeps a source even if they're also in a
|
|
164
|
-
// Razvan is in the Admin
|
|
163
|
+
it("a user named DIRECTLY in a file keeps a source even if they're also in a plugin", async () => {
|
|
164
|
+
// Razvan is in the Admin plugin AND named inline on Knowledge — the inline
|
|
165
165
|
// entry is a removable file grant, so his source is direct (tier-1 email
|
|
166
|
-
// beats the
|
|
166
|
+
// beats the plugin at the same scope).
|
|
167
167
|
await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
|
|
168
168
|
await writeFile(
|
|
169
169
|
repo,
|
|
@@ -177,7 +177,7 @@ describe('AccessControlService.grantSources', () => {
|
|
|
177
177
|
it('a role principal is matched by its own scope entry, not a role indirection', async () => {
|
|
178
178
|
await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
|
|
179
179
|
await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - Engineer\n---\n');
|
|
180
|
-
// The Engineer
|
|
180
|
+
// The Engineer PLUGIN itself is granted on Knowledge — for the role principal that's a direct grant.
|
|
181
181
|
const src = await svc.grantSources(workspaceId, 'folder', 'Knowledge', {
|
|
182
182
|
kind: 'role',
|
|
183
183
|
role: 'Engineer',
|
|
@@ -160,7 +160,9 @@ describe('RolesAdminService', () => {
|
|
|
160
160
|
access = new AccessControlService(ws, KB);
|
|
161
161
|
workflow = stubWorkflow();
|
|
162
162
|
bus = stubEventBus();
|
|
163
|
-
svc = new RolesAdminService(ws, workflow.svc, access, KB, DEFAULT_BRANCH, bus.bus
|
|
163
|
+
svc = new RolesAdminService(ws, workflow.svc, access, KB, () => DEFAULT_BRANCH, bus.bus, [
|
|
164
|
+
'recovery-admin@example.com',
|
|
165
|
+
]);
|
|
164
166
|
});
|
|
165
167
|
|
|
166
168
|
afterEach(async () => {
|
|
@@ -174,6 +176,18 @@ describe('RolesAdminService', () => {
|
|
|
174
176
|
expect(roster.find((r) => r.canonical === 'sales')!.members).toEqual(['felix@example.com']);
|
|
175
177
|
});
|
|
176
178
|
|
|
179
|
+
it('resolves the default branch per call, not at construction', async () => {
|
|
180
|
+
// The deployment shape this guards: a service built during boot, before
|
|
181
|
+
// `configureBranchModel()` has applied the branch model, saw an empty
|
|
182
|
+
// `DEFAULT_BRANCH` and froze it — every roles route then failed branch
|
|
183
|
+
// validation with `Invalid branch name "": empty name` until a restart.
|
|
184
|
+
let branch = '';
|
|
185
|
+
const late = new RolesAdminService(ws, workflow.svc, access, KB, () => branch, bus.bus);
|
|
186
|
+
branch = DEFAULT_BRANCH;
|
|
187
|
+
const roster = await late.getRoster();
|
|
188
|
+
expect(roster.map((r) => r.canonical)).toEqual(['admin', 'sales']);
|
|
189
|
+
});
|
|
190
|
+
|
|
177
191
|
it('getHealth reports ok for a valid file and corrupted for a broken one', async () => {
|
|
178
192
|
expect(await svc.getHealth()).toEqual({ ok: true, errors: [] });
|
|
179
193
|
|
|
@@ -207,11 +221,31 @@ describe('RolesAdminService', () => {
|
|
|
207
221
|
const restored = await readRoles();
|
|
208
222
|
expect(access.validateRolesYaml(restored).ok).toBe(true);
|
|
209
223
|
expect(roster.find((r) => r.canonical === 'admin')?.isAdmin).toBe(true);
|
|
210
|
-
|
|
224
|
+
// The roster is THIS deployment's configured admin — never a list baked
|
|
225
|
+
// into the build (hard-coded emails would land one company's admins in
|
|
226
|
+
// every customer's recovered file).
|
|
227
|
+
expect(roster.find((r) => r.canonical === 'admin')!.members).toEqual([
|
|
228
|
+
'recovery-admin@example.com',
|
|
229
|
+
]);
|
|
230
|
+
expect(restored).not.toContain('bevel.software');
|
|
211
231
|
// And the file is now healthy.
|
|
212
232
|
expect(await svc.getHealth()).toEqual({ ok: true, errors: [] });
|
|
213
233
|
});
|
|
214
234
|
|
|
235
|
+
it('recover refuses when no admin is configured — an adminless roster is the disease, not the cure', async () => {
|
|
236
|
+
const corrupt = 'roles:\n Admin:\n - a@x.eu\n Admin:\n - b@x.eu\n';
|
|
237
|
+
await write(repo, 'roles.yaml', corrupt);
|
|
238
|
+
access.invalidate(WS);
|
|
239
|
+
const adminless = new RolesAdminService(ws, workflow.svc, access, KB, () => DEFAULT_BRANCH, bus.bus);
|
|
240
|
+
await expect(adminless.recover(ADMIN)).rejects.toMatchObject({
|
|
241
|
+
status: 500,
|
|
242
|
+
payload: { kind: 'no-recovery-admins' },
|
|
243
|
+
});
|
|
244
|
+
// Nothing parked, nothing overwritten.
|
|
245
|
+
await expect(fs.readFile(path.join(repo, 'old-roles.yaml'), 'utf-8')).rejects.toThrow();
|
|
246
|
+
expect(await fs.readFile(path.join(repo, 'roles.yaml'), 'utf-8')).toBe(corrupt);
|
|
247
|
+
});
|
|
248
|
+
|
|
215
249
|
it("getRoster's referencedBy is sound — finds node-frontmatter refs, not just folder access.md", async () => {
|
|
216
250
|
// A folder access.md grant (the only thing the old advisory scan saw)...
|
|
217
251
|
await write(repo, 'team/access.md', '---\nread:\n - Sales\n---\n');
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Only a principal NAMED in a file (a direct user grant `Name <email>`, or a
|
|
22
22
|
* group/role token) produces sources. A USER who merely RESOLVES to access via a
|
|
23
|
-
*
|
|
24
|
-
* per-target file entry and produces NO source (the
|
|
23
|
+
* plugin they belong to, the built-in `everyone`, or admin-rescue is NOT a
|
|
24
|
+
* per-target file entry and produces NO source (the plugin itself shows as its own
|
|
25
25
|
* row instead).
|
|
26
26
|
*/
|
|
27
27
|
export type GrantSource =
|
|
@@ -138,8 +138,8 @@ export interface IAccessControl {
|
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
140
|
* Batched `canOwner` — one config load per call, same `owner:`-lists-only
|
|
141
|
-
* resolution (no admin rescue). Used by
|
|
142
|
-
* of these folders does the caller own?" without one model load per
|
|
141
|
+
* resolution (no admin rescue). Used by plugin enumeration to answer "which
|
|
142
|
+
* of these folders does the caller own?" without one model load per plugin.
|
|
143
143
|
*/
|
|
144
144
|
canOwnerBatch(
|
|
145
145
|
workspaceId: string,
|
|
@@ -281,8 +281,9 @@ export interface IAccessControl {
|
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
283
|
* Enumerate the grantable principals known to the KB, for the share-dialog
|
|
284
|
-
* autocomplete. `
|
|
285
|
-
*
|
|
284
|
+
* autocomplete. `plugins` are the built-in `everyone` role plus the declared
|
|
285
|
+
* `roles.yaml` role display names (`everyone` is surfaced so the UI can
|
|
286
|
+
* grant public read; the grant route gates it to the `read` verb only).
|
|
286
287
|
* `people` are every email named in `roles.yaml` (name defaults to the local
|
|
287
288
|
* part) unioned with every `Name <email>` grant in any `access.md` (named).
|
|
288
289
|
* The login-only `users` table is unioned in by the caller — this method
|
|
@@ -290,7 +291,7 @@ export interface IAccessControl {
|
|
|
290
291
|
*/
|
|
291
292
|
kbPrincipals(
|
|
292
293
|
workspaceId: string,
|
|
293
|
-
): Promise<{
|
|
294
|
+
): Promise<{ plugins: string[]; people: { name: string; email: string }[] }>;
|
|
294
295
|
|
|
295
296
|
/**
|
|
296
297
|
* Reverse-lookup an email by its SHA-256 hash (per `hashEmail` semantics)
|
|
@@ -925,7 +925,7 @@ function scopeToGrantSource(
|
|
|
925
925
|
* returning the WHOLE list of removable entries rather than just the winner.
|
|
926
926
|
*
|
|
927
927
|
* Only the principal's OWN named entry yields a source — a `user` by their email,
|
|
928
|
-
* a `role` by its role token. A grant that reaches the user via a
|
|
928
|
+
* a `role` by its role token. A grant that reaches the user via a plugin they
|
|
929
929
|
* belong to, the built-in `everyone`, or admin-rescue is NOT their entry, so it
|
|
930
930
|
* never adds a source (the group/role shows as its own row instead). The list is:
|
|
931
931
|
* - `[]` (verb omitted by the caller) when the principal effectively holds no
|
|
@@ -941,7 +941,7 @@ function scopeToGrantSource(
|
|
|
941
941
|
* under closest-wins), and the revoke flow needs the inherited remainder that
|
|
942
942
|
* survives removing the direct entry. A group/everyone grant at some scope does
|
|
943
943
|
* not add a source AND does not hide a farther own-entry the principal is named
|
|
944
|
-
* in (removing it is still meaningful if the
|
|
944
|
+
* in (removing it is still meaningful if the plugin grant is later removed).
|
|
945
945
|
*/
|
|
946
946
|
function resolveGrantSourcesForVerb(
|
|
947
947
|
model: AccessModel,
|
|
@@ -1453,18 +1453,18 @@ export class AccessControlService implements IAccessControl {
|
|
|
1453
1453
|
|
|
1454
1454
|
async kbPrincipals(
|
|
1455
1455
|
workspaceId: string,
|
|
1456
|
-
): Promise<{
|
|
1456
|
+
): Promise<{ plugins: string[]; people: { name: string; email: string }[] }> {
|
|
1457
1457
|
let model: AccessModel;
|
|
1458
1458
|
try {
|
|
1459
1459
|
model = await this.loadModel(workspaceId);
|
|
1460
1460
|
} catch {
|
|
1461
|
-
return {
|
|
1461
|
+
return { plugins: [], people: [] };
|
|
1462
1462
|
}
|
|
1463
|
-
//
|
|
1463
|
+
// Plugins = the built-in `everyone` role plus every declared role's display
|
|
1464
1464
|
// name. `everyone` is surfaced so the share UI can grant public read; the
|
|
1465
1465
|
// grant route gates it to the `read` verb only (write/owner/download
|
|
1466
1466
|
// everyone stay a direct-access.md edit).
|
|
1467
|
-
const
|
|
1467
|
+
const plugins = [
|
|
1468
1468
|
EVERYONE_DISPLAY,
|
|
1469
1469
|
...[...model.roles.byCanonical.values()].map((r) => r.displayName),
|
|
1470
1470
|
];
|
|
@@ -1487,7 +1487,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
1487
1487
|
name: name || email.split('@')[0],
|
|
1488
1488
|
email,
|
|
1489
1489
|
}));
|
|
1490
|
-
return {
|
|
1490
|
+
return { plugins, people };
|
|
1491
1491
|
}
|
|
1492
1492
|
|
|
1493
1493
|
async findEmailByHash(
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
* WHERE access rules are DECLARED inside a folder — a read-only inventory, not
|
|
15
15
|
* a second resolver.
|
|
16
16
|
*
|
|
17
|
-
* A
|
|
17
|
+
* A plugin's `access.md` is never the whole story: resolution is closeness-first,
|
|
18
18
|
* so a rule written on a single skill or `.tool` inside the folder overrides the
|
|
19
|
-
* folder's rule for the principals it names. An admin looking at a
|
|
19
|
+
* folder's rule for the principals it names. An admin looking at a plugin's share
|
|
20
20
|
* list and seeing "GTM Team can read" would otherwise have no way to know that
|
|
21
21
|
* one battlecard underneath says `deny read: everyone`.
|
|
22
22
|
*
|
|
@@ -66,9 +66,9 @@ export interface AccessDeclaration {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* How many files one scan will read. A
|
|
69
|
+
* How many files one scan will read. A plugin folder is small by construction;
|
|
70
70
|
* this cap exists so a pathological folder (a whole ontology moved under
|
|
71
|
-
* `
|
|
71
|
+
* `Plugins/`) degrades into an honest partial answer instead of pinning the
|
|
72
72
|
* event loop. Callers surface `truncated` to the user.
|
|
73
73
|
*/
|
|
74
74
|
export const DECLARATION_SCAN_FILE_CAP = 400;
|
|
@@ -163,7 +163,7 @@ function toEntries(byVerb: Record<Verb, ParsedEntry[]>): DeclarationEntry[] {
|
|
|
163
163
|
*
|
|
164
164
|
* Failure modes are asymmetric on purpose, matching the resolver: a node whose
|
|
165
165
|
* frontmatter is malformed is skipped silently (a typo in a skill's `owner:`
|
|
166
|
-
* must not make the
|
|
166
|
+
* must not make the plugin's access page unreadable), while a malformed
|
|
167
167
|
* `access.md` is REPORTED with its `parseError` (that file was written to
|
|
168
168
|
* control access and currently controls nothing).
|
|
169
169
|
*
|