@bevel-software/platform-core-backend 0.7.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +51 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +5 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +38 -21
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -7
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +6 -6
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +5 -5
- package/dist/modules/access/access-declarations.js +3 -3
- package/dist/modules/access/access-mutation.service.d.ts +3 -3
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +3 -3
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.js +4 -4
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.js +19 -19
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +2 -2
- package/dist/modules/access/creator-access.js +5 -5
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +18 -4
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +14 -4
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
- package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode-names.js +5 -27
- package/dist/modules/code-mode/code-mode-names.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +29 -7
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -52
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +33 -395
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +7 -0
- package/dist/modules/plugins/index.d.ts.map +1 -0
- package/dist/modules/plugins/index.js +6 -0
- package/dist/modules/plugins/index.js.map +1 -0
- package/dist/modules/plugins/join-proposals.d.ts +53 -0
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
- package/dist/modules/plugins/join-proposals.js +67 -0
- package/dist/modules/plugins/join-proposals.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts +81 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-requests.service.js +135 -0
- package/dist/modules/plugins/join-requests.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.js +344 -0
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +106 -0
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.contract.js +36 -0
- package/dist/modules/plugins/plugins.contract.js.map +1 -0
- package/dist/modules/plugins/plugins.routes.d.ts +42 -0
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.routes.js +379 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -0
- package/dist/modules/plugins/plugins.service.d.ts +60 -0
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.service.js +172 -0
- package/dist/modules/plugins/plugins.service.js.map +1 -0
- package/dist/modules/skills/pending-skills.service.d.ts +2 -2
- package/dist/modules/skills/pending-skills.service.js +7 -7
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +4 -4
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.service.js +5 -5
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.js +2 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +37 -9
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +17 -4
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +63 -9
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +1 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +6 -1
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +43 -10
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/plugins-migration.d.ts +50 -0
- package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
- package/dist/modules/workspace/plugins-migration.js +379 -0
- package/dist/modules/workspace/plugins-migration.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +26 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +83 -12
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/kb-layout.test.js +3 -3
- package/dist/shared/kb-layout.test.js.map +1 -1
- package/dist/shared/utcp-namespace.d.ts +6 -27
- package/dist/shared/utcp-namespace.d.ts.map +1 -1
- package/dist/shared/utcp-namespace.js +6 -63
- package/dist/shared/utcp-namespace.js.map +1 -1
- package/dist/shared/variable-refs.d.ts +42 -0
- package/dist/shared/variable-refs.d.ts.map +1 -0
- package/dist/shared/variable-refs.js +60 -0
- package/dist/shared/variable-refs.js.map +1 -0
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +88 -35
- package/kb-template/KnowledgeBase/How to get started.md +10 -10
- package/kb-template/access.md +36 -36
- package/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +106 -106
- package/src/core/create-core-server.ts +55 -9
- package/src/core/create-core-services.ts +48 -24
- package/src/index.ts +69 -69
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
- package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
- package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
- package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
- package/src/modules/access/__tests__/access-splice.test.ts +2 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
- package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
- package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
- package/src/modules/access/access-control.interface.ts +8 -7
- package/src/modules/access/access-control.service.ts +7 -7
- package/src/modules/access/access-declarations.ts +5 -5
- package/src/modules/access/access-mutation.service.ts +3 -3
- package/src/modules/access/access-splice.ts +4 -4
- package/src/modules/access/access.routes.ts +20 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +13 -2
- package/src/modules/admin/admin-access.routes.ts +29 -29
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
- package/src/modules/auth/rate-limit.ts +45 -45
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
- package/src/modules/code-mode/code-mode-names.ts +10 -36
- package/src/modules/code-mode/code-mode.tool.ts +27 -7
- package/src/modules/database/connection.ts +15 -15
- package/src/modules/database/schema.ts +11 -11
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
- package/src/modules/mcp/mcp.service.ts +57 -435
- package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
- package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
- package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
- package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
- package/src/modules/plugins/index.ts +17 -0
- package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
- package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
- package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
- package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
- package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
- package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/pending-skills.service.ts +7 -7
- package/src/modules/skills/skills.contract.ts +4 -4
- package/src/modules/skills/skills.service.ts +5 -5
- package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
- package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
- package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
- package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +118 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/git.service.ts +2 -2
- package/src/modules/workflow/pending-commits.service.ts +48 -12
- package/src/modules/workflow/pending-commits.worker.ts +64 -8
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/workflow-hooks.ts +101 -101
- package/src/modules/workflow/workflow.service.ts +13 -2
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
- package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
- package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/kb-seed.service.ts +38 -9
- package/src/modules/workspace/plugins-migration.ts +479 -0
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +85 -14
- package/src/modules/workspace/workspace.tools.ts +922 -922
- package/src/shared/__tests__/join-request.test.ts +13 -13
- package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +3 -3
- package/src/shared/utcp-namespace.ts +10 -68
- package/src/shared/variable-refs.ts +64 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
- package/src/modules/groups/index.ts +0 -17
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
|
@@ -3,9 +3,9 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import type { Dirent } from 'node:fs';
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_BRANCH,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
PLUGINS_DIR,
|
|
7
|
+
pluginOfPath,
|
|
8
|
+
isPersonalPluginFolder,
|
|
9
9
|
} from '@bevel-software/platform-shared';
|
|
10
10
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
11
11
|
import { workspaceIdForBranch } from '../workspace/workspace.service.js';
|
|
@@ -13,37 +13,37 @@ import type { IAccessControl } from '../access/access-control.interface.js';
|
|
|
13
13
|
import type { ISkillService } from '../skills/skills.contract.js';
|
|
14
14
|
import type { IToolManualService } from '../tool-manuals/tool-manuals.contract.js';
|
|
15
15
|
import { TtlCache } from '../../shared/ttl-cache.js';
|
|
16
|
-
import type {
|
|
16
|
+
import type { PluginCatalogEntry, IPluginIndexService } from './plugins.contract.js';
|
|
17
17
|
|
|
18
18
|
const CACHE_TTL_MS = 60_000;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* The
|
|
21
|
+
* The plugin index: every plugin folder in the default-branch KB, with its
|
|
22
22
|
* caller-independent totals and access principals.
|
|
23
23
|
*
|
|
24
24
|
* Two decisions worth keeping straight:
|
|
25
25
|
*
|
|
26
|
-
* - **Existence is an `access.md`, counting is not.** A
|
|
26
|
+
* - **Existence is an `access.md`, counting is not.** A plugin EXISTS because
|
|
27
27
|
* its folder carries an `access.md` — the file the provisioning endpoint
|
|
28
28
|
* seeds and the one every per-caller verdict (member / manager /
|
|
29
|
-
* discoverable) resolves against. A bare directory under `
|
|
30
|
-
* a
|
|
29
|
+
* discoverable) resolves against. A bare directory under `Plugins/` is NOT
|
|
30
|
+
* a plugin: git cannot record an empty folder, so deleting a plugin's files
|
|
31
31
|
* leaves its directory behind on live checkouts, and enumerating by
|
|
32
32
|
* directory would resurrect every such ghost. Counts come from the
|
|
33
33
|
* already-cached global catalogs — `skillService.listSkills(undefined)` and
|
|
34
|
-
* `toolManualService.listAllSummaries()` — bucketed by `
|
|
34
|
+
* `toolManualService.listAllSummaries()` — bucketed by `pluginOfPath`. A
|
|
35
35
|
* second `walkFiles` pass would re-read the same tree and could disagree
|
|
36
36
|
* with the catalogs about what counts as a skill.
|
|
37
|
-
* - **Loose files are not
|
|
37
|
+
* - **Loose files are not plugins.** `Plugins/slack.tool` sits directly under
|
|
38
38
|
* the root, so it is a file, not a directory — the same ≥3-segment rule
|
|
39
|
-
* `
|
|
39
|
+
* `pluginOfPath` applies, arrived at from the other side.
|
|
40
40
|
*
|
|
41
41
|
* Cached for {@link CACHE_TTL_MS} and dropped by `invalidate()` from the
|
|
42
42
|
* file-change subscriber, so a grant committed on the default branch is
|
|
43
43
|
* reflected within one round-trip rather than one TTL.
|
|
44
44
|
*/
|
|
45
|
-
export class
|
|
46
|
-
private readonly cache: TtlCache<
|
|
45
|
+
export class PluginIndexService implements IPluginIndexService {
|
|
46
|
+
private readonly cache: TtlCache<PluginCatalogEntry[]>;
|
|
47
47
|
|
|
48
48
|
constructor(
|
|
49
49
|
private readonly workspaceService: WorkspaceService,
|
|
@@ -60,13 +60,13 @@ export class GroupIndexService implements IGroupIndexService {
|
|
|
60
60
|
this.cache.invalidate();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
async catalog(): Promise<
|
|
63
|
+
async catalog(): Promise<PluginCatalogEntry[]> {
|
|
64
64
|
const cached = this.cache.get();
|
|
65
65
|
if (cached) return cached;
|
|
66
66
|
const entries = await this.build();
|
|
67
|
-
// A failed scan and a KB with genuinely no
|
|
67
|
+
// A failed scan and a KB with genuinely no plugin folders both serve `[]`,
|
|
68
68
|
// but only the second is a fact worth holding for the TTL. Caching the
|
|
69
|
-
// failure would hide every
|
|
69
|
+
// failure would hide every plugin from every user for the full 60s AFTER
|
|
70
70
|
// the cause is gone — a clone mid-creation, a readdir that raced a
|
|
71
71
|
// checkout — and `invalidate()` only fires on a file-change event that may
|
|
72
72
|
// never arrive in that window. So a degraded read is served, not stored,
|
|
@@ -80,16 +80,16 @@ export class GroupIndexService implements IGroupIndexService {
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Degrades on ANY failure rather than throwing. The Library must never break
|
|
83
|
-
* because a
|
|
83
|
+
* because a plugin folder can't be read — same philosophy as the skill and
|
|
84
84
|
* tool-manual scanners, and the reason the route's 500 is reserved for the
|
|
85
85
|
* per-caller half.
|
|
86
86
|
*
|
|
87
87
|
* `null` is that degraded case and is deliberately NOT the same value as an
|
|
88
|
-
* empty array: a KB with no
|
|
88
|
+
* empty array: a KB with no plugin folders returns `[]`, which is true and
|
|
89
89
|
* cacheable, while a failure returns `null`, which `catalog()` serves as `[]`
|
|
90
90
|
* without storing it.
|
|
91
91
|
*/
|
|
92
|
-
private async build(): Promise<
|
|
92
|
+
private async build(): Promise<PluginCatalogEntry[] | null> {
|
|
93
93
|
try {
|
|
94
94
|
const wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
|
|
95
95
|
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
@@ -102,10 +102,10 @@ export class GroupIndexService implements IGroupIndexService {
|
|
|
102
102
|
this.countTools(),
|
|
103
103
|
]);
|
|
104
104
|
|
|
105
|
-
const entries:
|
|
106
|
-
for (const [name,
|
|
107
|
-
// One folder, one access boundary — the folder IS the
|
|
108
|
-
const [primary] =
|
|
105
|
+
const entries: PluginCatalogEntry[] = [];
|
|
106
|
+
for (const [name, pluginFolders] of folders) {
|
|
107
|
+
// One folder, one access boundary — the folder IS the plugin.
|
|
108
|
+
const [primary] = pluginFolders;
|
|
109
109
|
const [owners, writers, readers] = await Promise.all([
|
|
110
110
|
this.accessControl.eligibleOwners(wsId, primary),
|
|
111
111
|
this.accessControl.eligibleWriters(wsId, primary),
|
|
@@ -113,7 +113,7 @@ export class GroupIndexService implements IGroupIndexService {
|
|
|
113
113
|
]);
|
|
114
114
|
entries.push({
|
|
115
115
|
name,
|
|
116
|
-
folders:
|
|
116
|
+
folders: pluginFolders,
|
|
117
117
|
skillCount: skillCounts.get(name) ?? 0,
|
|
118
118
|
toolCount: toolCounts.get(name) ?? 0,
|
|
119
119
|
owners,
|
|
@@ -124,70 +124,70 @@ export class GroupIndexService implements IGroupIndexService {
|
|
|
124
124
|
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
125
125
|
} catch (err) {
|
|
126
126
|
console.warn(
|
|
127
|
-
`[
|
|
127
|
+
`[plugins] plugin index unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
128
128
|
);
|
|
129
129
|
return null;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/** name → repo-relative
|
|
133
|
+
/** name → repo-relative plugin folder (always a single-element list). */
|
|
134
134
|
private async scanFolders(kbRoot: string): Promise<Map<string, string[]>> {
|
|
135
135
|
const byName = new Map<string, string[]>();
|
|
136
136
|
let children: Dirent[];
|
|
137
137
|
try {
|
|
138
|
-
children = await fs.readdir(path.join(kbRoot,
|
|
138
|
+
children = await fs.readdir(path.join(kbRoot, PLUGINS_DIR), { withFileTypes: true });
|
|
139
139
|
} catch {
|
|
140
|
-
return byName; // a KB without a `
|
|
140
|
+
return byName; // a KB without a `Plugins/` root simply has no plugins
|
|
141
141
|
}
|
|
142
142
|
const candidates = children.filter(
|
|
143
143
|
(child) =>
|
|
144
144
|
child.isDirectory() &&
|
|
145
145
|
!child.name.startsWith('.') &&
|
|
146
|
-
// Personal folders live under
|
|
146
|
+
// Personal folders live under Plugins/ but are not plugins: one exists
|
|
147
147
|
// per person, private by construction, and listing them would put a
|
|
148
148
|
// locked row per employee in everyone's index.
|
|
149
|
-
!
|
|
149
|
+
!isPersonalPluginFolder(child.name),
|
|
150
150
|
);
|
|
151
|
-
// A
|
|
151
|
+
// A plugin exists exactly when its folder carries an `access.md` (see the
|
|
152
152
|
// class doc) — stat that file, don't trust the directory.
|
|
153
153
|
const verdicts = await Promise.all(
|
|
154
154
|
candidates.map(async (child) => {
|
|
155
155
|
try {
|
|
156
|
-
return (await fs.stat(path.join(kbRoot,
|
|
156
|
+
return (await fs.stat(path.join(kbRoot, PLUGINS_DIR, child.name, 'access.md'))).isFile();
|
|
157
157
|
} catch {
|
|
158
158
|
return false;
|
|
159
159
|
}
|
|
160
160
|
}),
|
|
161
161
|
);
|
|
162
162
|
candidates.forEach((child, i) => {
|
|
163
|
-
if (verdicts[i]) byName.set(child.name, [`${
|
|
163
|
+
if (verdicts[i]) byName.set(child.name, [`${PLUGINS_DIR}/${child.name}`]);
|
|
164
164
|
});
|
|
165
165
|
return byName;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
private async countSkills(): Promise<Map<string, number>> {
|
|
169
169
|
// `undefined` is the documented GLOBAL, unfiltered mode — counts are a
|
|
170
|
-
// property of the
|
|
171
|
-
return
|
|
170
|
+
// property of the plugin, not of who is asking.
|
|
171
|
+
return bucketByPlugin(await this.skillService.listSkills(undefined));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
private async countTools(): Promise<Map<string, number>> {
|
|
175
|
-
return
|
|
175
|
+
return bucketByPlugin(await this.toolManualService.listAllSummaries());
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/** Count items per
|
|
180
|
-
function
|
|
179
|
+
/** Count items per plugin folder name; ungrouped items (`null`) count nowhere. */
|
|
180
|
+
function bucketByPlugin(items: { path: string }[]): Map<string, number> {
|
|
181
181
|
const counts = new Map<string, number>();
|
|
182
182
|
for (const item of items) {
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
counts.set(
|
|
183
|
+
const plugin = pluginOfPath(item.path);
|
|
184
|
+
if (plugin === null) continue;
|
|
185
|
+
counts.set(plugin, (counts.get(plugin) ?? 0) + 1);
|
|
186
186
|
}
|
|
187
187
|
return counts;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
/** The default-branch workspace id every
|
|
191
|
-
export function
|
|
190
|
+
/** The default-branch workspace id every plugin resolution runs against. */
|
|
191
|
+
export function pluginsWorkspaceId(): string {
|
|
192
192
|
return workspaceIdForBranch(DEFAULT_BRANCH);
|
|
193
193
|
}
|
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
import type { Server as HttpServer } from 'node:http';
|
|
2
|
-
import express from 'express';
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { createSecretsVaultRoutes } from '../secrets-vault.routes.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* THE OWNER GATE: a tool's shared (admin) secrets — plain values and OAuth
|
|
8
|
-
* client secrets — are settable by whoever can WRITE that `.tool` FILE (its
|
|
9
|
-
* frontmatter `write:`/`owner:` verbs + the access.md chain), and by no one
|
|
10
|
-
* else. There is deliberately NO platform-role check here: a non-Admin who
|
|
11
|
-
* manages a `.tool` configures it; an Admin who can't write the file doesn't.
|
|
12
|
-
* The per-file resolution itself (frontmatter verbs on `.tool` files) is
|
|
13
|
-
* covered in access-control.service.test.ts — this locks the route gate to
|
|
14
|
-
* `canWrite(path)` so no role-based shortcut regresses it.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const TOOL_PATH = 'Tools/weather.tool';
|
|
18
|
-
const WRITER = 'writer@x.com';
|
|
19
|
-
const READER = 'reader@x.com';
|
|
20
|
-
|
|
21
|
-
const toolManualService = {
|
|
22
|
-
listAccessible: async () => [
|
|
23
|
-
{
|
|
24
|
-
slug: 'weather',
|
|
25
|
-
name: 'weather',
|
|
26
|
-
path: TOOL_PATH,
|
|
27
|
-
type: 'mcp' as const,
|
|
28
|
-
variables: [
|
|
29
|
-
{ name: 'SHARED_KEY', scope: 'admin' as const, label: 'Org key' },
|
|
30
|
-
{
|
|
31
|
-
name: 'SIGNIN',
|
|
32
|
-
scope: 'user' as const,
|
|
33
|
-
label: 'Weather sign-in',
|
|
34
|
-
oauth: {
|
|
35
|
-
authorizationUrl: 'https://auth.example.com/authorize',
|
|
36
|
-
tokenUrl: 'https://auth.example.com/token',
|
|
37
|
-
clientId: 'client-1',
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
} as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['toolManualService'];
|
|
44
|
-
|
|
45
|
-
const putSharedStatic = vi.fn(async () => ({ id: 's1' }));
|
|
46
|
-
const putSharedOAuthClientSecret = vi.fn(async () => {});
|
|
47
|
-
const secretsVault = {
|
|
48
|
-
putSharedStatic,
|
|
49
|
-
putSharedOAuthClientSecret,
|
|
50
|
-
} as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['secretsVault'];
|
|
51
|
-
|
|
52
|
-
// Per-FILE write: only WRITER may write THIS tool's path. No role concept at all.
|
|
53
|
-
const accessControl = {
|
|
54
|
-
canRead: async () => true,
|
|
55
|
-
canWrite: async (_ws: string, email: string, path: string) => email === WRITER && path === TOOL_PATH,
|
|
56
|
-
} as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['accessControl'];
|
|
57
|
-
|
|
58
|
-
let httpServer: HttpServer | undefined;
|
|
59
|
-
|
|
60
|
-
async function baseUrlAs(email: string): Promise<string> {
|
|
61
|
-
const app = express();
|
|
62
|
-
app.use(express.json());
|
|
63
|
-
app.use((req, _res, next) => {
|
|
64
|
-
req.userId = `id-${email}`;
|
|
65
|
-
req.userEmail = email;
|
|
66
|
-
next();
|
|
67
|
-
});
|
|
68
|
-
app.use(
|
|
69
|
-
'/api',
|
|
70
|
-
createSecretsVaultRoutes({
|
|
71
|
-
secretsVault,
|
|
72
|
-
toolManualService,
|
|
73
|
-
accessControl,
|
|
74
|
-
stateSecret: 'test-secret',
|
|
75
|
-
publicBackendUrl: 'http://localhost:3000',
|
|
76
|
-
publicFrontendUrl: 'http://localhost:5173',
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
httpServer = await new Promise<HttpServer>((resolve) => {
|
|
80
|
-
const s = app.listen(0, () => resolve(s));
|
|
81
|
-
});
|
|
82
|
-
const port = (httpServer.address() as { port: number }).port;
|
|
83
|
-
return `http://127.0.0.1:${port}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
afterEach(async () => {
|
|
87
|
-
if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
|
|
88
|
-
httpServer = undefined;
|
|
89
|
-
putSharedStatic.mockClear();
|
|
90
|
-
putSharedOAuthClientSecret.mockClear();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('tool owner gate — shared config requires WRITE on the `.tool` file', () => {
|
|
94
|
-
it('a writer of the file sets the shared admin value', async () => {
|
|
95
|
-
const base = await baseUrlAs(WRITER);
|
|
96
|
-
const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
|
|
97
|
-
method: 'PUT',
|
|
98
|
-
headers: { 'Content-Type': 'application/json' },
|
|
99
|
-
body: JSON.stringify({ value: 'k-123' }),
|
|
100
|
-
});
|
|
101
|
-
expect(res.status).toBe(201);
|
|
102
|
-
expect(putSharedStatic).toHaveBeenCalledWith(expect.objectContaining({ key: 'weather_SHARED_KEY' }));
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('a non-writer is refused (403), even though they can READ the tool', async () => {
|
|
106
|
-
const base = await baseUrlAs(READER);
|
|
107
|
-
const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
|
|
108
|
-
method: 'PUT',
|
|
109
|
-
headers: { 'Content-Type': 'application/json' },
|
|
110
|
-
body: JSON.stringify({ value: 'k-123' }),
|
|
111
|
-
});
|
|
112
|
-
expect(res.status).toBe(403);
|
|
113
|
-
expect(putSharedStatic).not.toHaveBeenCalled();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('a writer of the file sets the OAuth client secret, pinned to the declared provider', async () => {
|
|
117
|
-
const base = await baseUrlAs(WRITER);
|
|
118
|
-
const res = await fetch(`${base}/api/secrets/tools/weather/vars/SIGNIN/oauth/admin`, {
|
|
119
|
-
method: 'PUT',
|
|
120
|
-
headers: { 'Content-Type': 'application/json' },
|
|
121
|
-
body: JSON.stringify({ clientSecret: 'cs-123' }),
|
|
122
|
-
});
|
|
123
|
-
expect(res.status).toBe(201);
|
|
124
|
-
expect(putSharedOAuthClientSecret).toHaveBeenCalledWith(
|
|
125
|
-
expect.objectContaining({
|
|
126
|
-
key: 'weather_SIGNIN',
|
|
127
|
-
clientSecret: 'cs-123',
|
|
128
|
-
provider: expect.objectContaining({ clientId: 'client-1' }),
|
|
129
|
-
}),
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('a non-writer cannot set the client secret (403)', async () => {
|
|
134
|
-
const base = await baseUrlAs(READER);
|
|
135
|
-
const res = await fetch(`${base}/api/secrets/tools/weather/vars/SIGNIN/oauth/admin`, {
|
|
136
|
-
method: 'PUT',
|
|
137
|
-
headers: { 'Content-Type': 'application/json' },
|
|
138
|
-
body: JSON.stringify({ clientSecret: 'cs-123' }),
|
|
139
|
-
});
|
|
140
|
-
expect(res.status).toBe(403);
|
|
141
|
-
expect(putSharedOAuthClientSecret).not.toHaveBeenCalled();
|
|
142
|
-
});
|
|
143
|
-
});
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { createSecretsVaultRoutes } from '../secrets-vault.routes.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE OWNER GATE: a tool's shared (admin) secrets — plain values and OAuth
|
|
8
|
+
* client secrets — are settable by whoever can WRITE that `.tool` FILE (its
|
|
9
|
+
* frontmatter `write:`/`owner:` verbs + the access.md chain), and by no one
|
|
10
|
+
* else. There is deliberately NO platform-role check here: a non-Admin who
|
|
11
|
+
* manages a `.tool` configures it; an Admin who can't write the file doesn't.
|
|
12
|
+
* The per-file resolution itself (frontmatter verbs on `.tool` files) is
|
|
13
|
+
* covered in access-control.service.test.ts — this locks the route gate to
|
|
14
|
+
* `canWrite(path)` so no role-based shortcut regresses it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TOOL_PATH = 'Tools/weather.tool';
|
|
18
|
+
const WRITER = 'writer@x.com';
|
|
19
|
+
const READER = 'reader@x.com';
|
|
20
|
+
|
|
21
|
+
const toolManualService = {
|
|
22
|
+
listAccessible: async () => [
|
|
23
|
+
{
|
|
24
|
+
slug: 'weather',
|
|
25
|
+
name: 'weather',
|
|
26
|
+
path: TOOL_PATH,
|
|
27
|
+
type: 'mcp' as const,
|
|
28
|
+
variables: [
|
|
29
|
+
{ name: 'SHARED_KEY', scope: 'admin' as const, label: 'Org key' },
|
|
30
|
+
{
|
|
31
|
+
name: 'SIGNIN',
|
|
32
|
+
scope: 'user' as const,
|
|
33
|
+
label: 'Weather sign-in',
|
|
34
|
+
oauth: {
|
|
35
|
+
authorizationUrl: 'https://auth.example.com/authorize',
|
|
36
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
37
|
+
clientId: 'client-1',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
} as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['toolManualService'];
|
|
44
|
+
|
|
45
|
+
const putSharedStatic = vi.fn(async () => ({ id: 's1' }));
|
|
46
|
+
const putSharedOAuthClientSecret = vi.fn(async () => {});
|
|
47
|
+
const secretsVault = {
|
|
48
|
+
putSharedStatic,
|
|
49
|
+
putSharedOAuthClientSecret,
|
|
50
|
+
} as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['secretsVault'];
|
|
51
|
+
|
|
52
|
+
// Per-FILE write: only WRITER may write THIS tool's path. No role concept at all.
|
|
53
|
+
const accessControl = {
|
|
54
|
+
canRead: async () => true,
|
|
55
|
+
canWrite: async (_ws: string, email: string, path: string) => email === WRITER && path === TOOL_PATH,
|
|
56
|
+
} as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['accessControl'];
|
|
57
|
+
|
|
58
|
+
let httpServer: HttpServer | undefined;
|
|
59
|
+
|
|
60
|
+
async function baseUrlAs(email: string): Promise<string> {
|
|
61
|
+
const app = express();
|
|
62
|
+
app.use(express.json());
|
|
63
|
+
app.use((req, _res, next) => {
|
|
64
|
+
req.userId = `id-${email}`;
|
|
65
|
+
req.userEmail = email;
|
|
66
|
+
next();
|
|
67
|
+
});
|
|
68
|
+
app.use(
|
|
69
|
+
'/api',
|
|
70
|
+
createSecretsVaultRoutes({
|
|
71
|
+
secretsVault,
|
|
72
|
+
toolManualService,
|
|
73
|
+
accessControl,
|
|
74
|
+
stateSecret: 'test-secret',
|
|
75
|
+
publicBackendUrl: 'http://localhost:3000',
|
|
76
|
+
publicFrontendUrl: 'http://localhost:5173',
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
httpServer = await new Promise<HttpServer>((resolve) => {
|
|
80
|
+
const s = app.listen(0, () => resolve(s));
|
|
81
|
+
});
|
|
82
|
+
const port = (httpServer.address() as { port: number }).port;
|
|
83
|
+
return `http://127.0.0.1:${port}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
|
|
88
|
+
httpServer = undefined;
|
|
89
|
+
putSharedStatic.mockClear();
|
|
90
|
+
putSharedOAuthClientSecret.mockClear();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('tool owner gate — shared config requires WRITE on the `.tool` file', () => {
|
|
94
|
+
it('a writer of the file sets the shared admin value', async () => {
|
|
95
|
+
const base = await baseUrlAs(WRITER);
|
|
96
|
+
const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
|
|
97
|
+
method: 'PUT',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ value: 'k-123' }),
|
|
100
|
+
});
|
|
101
|
+
expect(res.status).toBe(201);
|
|
102
|
+
expect(putSharedStatic).toHaveBeenCalledWith(expect.objectContaining({ key: 'weather_SHARED_KEY' }));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('a non-writer is refused (403), even though they can READ the tool', async () => {
|
|
106
|
+
const base = await baseUrlAs(READER);
|
|
107
|
+
const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
|
|
108
|
+
method: 'PUT',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({ value: 'k-123' }),
|
|
111
|
+
});
|
|
112
|
+
expect(res.status).toBe(403);
|
|
113
|
+
expect(putSharedStatic).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('a writer of the file sets the OAuth client secret, pinned to the declared provider', async () => {
|
|
117
|
+
const base = await baseUrlAs(WRITER);
|
|
118
|
+
const res = await fetch(`${base}/api/secrets/tools/weather/vars/SIGNIN/oauth/admin`, {
|
|
119
|
+
method: 'PUT',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify({ clientSecret: 'cs-123' }),
|
|
122
|
+
});
|
|
123
|
+
expect(res.status).toBe(201);
|
|
124
|
+
expect(putSharedOAuthClientSecret).toHaveBeenCalledWith(
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
key: 'weather_SIGNIN',
|
|
127
|
+
clientSecret: 'cs-123',
|
|
128
|
+
provider: expect.objectContaining({ clientId: 'client-1' }),
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('a non-writer cannot set the client secret (403)', async () => {
|
|
134
|
+
const base = await baseUrlAs(READER);
|
|
135
|
+
const res = await fetch(`${base}/api/secrets/tools/weather/vars/SIGNIN/oauth/admin`, {
|
|
136
|
+
method: 'PUT',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({ clientSecret: 'cs-123' }),
|
|
139
|
+
});
|
|
140
|
+
expect(res.status).toBe(403);
|
|
141
|
+
expect(putSharedOAuthClientSecret).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -40,7 +40,7 @@ function cr(over: Partial<ChangeRequest> = {}): ChangeRequest {
|
|
|
40
40
|
base: 'main',
|
|
41
41
|
state: 'open',
|
|
42
42
|
createdAt: '2026-08-06T09:00:00.000Z',
|
|
43
|
-
touchedNodePaths: ['
|
|
43
|
+
touchedNodePaths: ['Plugins/Engineering/weekly-newsletter/SKILL.md'],
|
|
44
44
|
review: { approvals: 0, changesRequested: 0, pendingLogins: [] },
|
|
45
45
|
url: '/change-requests/7',
|
|
46
46
|
...over,
|
|
@@ -56,7 +56,7 @@ function harness(opts: {
|
|
|
56
56
|
writers?: Record<string, string[]>;
|
|
57
57
|
}) {
|
|
58
58
|
const branchFiles = opts.branchFiles ?? {
|
|
59
|
-
'agent/weekly-newsletter:
|
|
59
|
+
'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
|
|
60
60
|
};
|
|
61
61
|
const workspaceService = {
|
|
62
62
|
ensureRemotesFetched: async () => undefined,
|
|
@@ -81,7 +81,7 @@ function harness(opts: {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const ADMIN_WRITES = {
|
|
84
|
-
'
|
|
84
|
+
'Plugins/Engineering/weekly-newsletter/SKILL.md': [ADMIN],
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
describe('PendingSkillsService', () => {
|
|
@@ -93,7 +93,7 @@ describe('PendingSkillsService', () => {
|
|
|
93
93
|
name: 'weekly-newsletter',
|
|
94
94
|
description: 'Drafts the Friday newsletter.',
|
|
95
95
|
version: '0.1.0',
|
|
96
|
-
path: '
|
|
96
|
+
path: 'Plugins/Engineering/weekly-newsletter',
|
|
97
97
|
changeRequestNumber: 7,
|
|
98
98
|
branch: 'agent/weekly-newsletter',
|
|
99
99
|
authorName: 'Ali Raza',
|
|
@@ -110,7 +110,7 @@ describe('PendingSkillsService', () => {
|
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* The decision that shapes this surface: a proposal is between its author and
|
|
113
|
-
* the people who can approve it, and nobody else. A member of the
|
|
113
|
+
* the people who can approve it, and nobody else. A member of the plugin who
|
|
114
114
|
* cannot write the folder is not one of those people.
|
|
115
115
|
*/
|
|
116
116
|
test('hides it from everyone else', async () => {
|
|
@@ -143,7 +143,7 @@ describe('PendingSkillsService', () => {
|
|
|
143
143
|
{
|
|
144
144
|
name: 'weekly-newsletter',
|
|
145
145
|
description: 'Drafts the Friday newsletter.',
|
|
146
|
-
path: '
|
|
146
|
+
path: 'Plugins/Engineering/weekly-newsletter',
|
|
147
147
|
},
|
|
148
148
|
],
|
|
149
149
|
writers: ADMIN_WRITES,
|
|
@@ -156,9 +156,9 @@ describe('PendingSkillsService', () => {
|
|
|
156
156
|
crs: [
|
|
157
157
|
cr({
|
|
158
158
|
touchedNodePaths: [
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'
|
|
159
|
+
'Plugins/Engineering/access.md',
|
|
160
|
+
'Plugins/Engineering/SKILL.md', // a plugin folder is not a skill
|
|
161
|
+
'Plugins/Engineering/weekly-newsletter/reference.md',
|
|
162
162
|
],
|
|
163
163
|
}),
|
|
164
164
|
],
|
|
@@ -185,7 +185,7 @@ describe('PendingSkillsService', () => {
|
|
|
185
185
|
const svc = harness({
|
|
186
186
|
crs: [cr()],
|
|
187
187
|
branchFiles: {
|
|
188
|
-
'agent/weekly-newsletter:
|
|
188
|
+
'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md':
|
|
189
189
|
'---\ndescription: No name declared.\n---\n\n# Body\n',
|
|
190
190
|
},
|
|
191
191
|
writers: ADMIN_WRITES,
|
|
@@ -222,17 +222,17 @@ describe('PendingSkillsService', () => {
|
|
|
222
222
|
number: 3,
|
|
223
223
|
createdAt: '2026-08-01T12:00:00.000Z',
|
|
224
224
|
branch: 'agent/older',
|
|
225
|
-
touchedNodePaths: ['
|
|
225
|
+
touchedNodePaths: ['Plugins/Engineering/older/SKILL.md'],
|
|
226
226
|
}),
|
|
227
227
|
],
|
|
228
228
|
branchFiles: {
|
|
229
|
-
'agent/weekly-newsletter:
|
|
230
|
-
'agent/older:
|
|
229
|
+
'agent/weekly-newsletter:Plugins/Engineering/weekly-newsletter/SKILL.md': SKILL_MD,
|
|
230
|
+
'agent/older:Plugins/Engineering/older/SKILL.md':
|
|
231
231
|
'---\nname: older\ndescription: Older.\n---\n',
|
|
232
232
|
},
|
|
233
233
|
writers: {
|
|
234
234
|
...ADMIN_WRITES,
|
|
235
|
-
'
|
|
235
|
+
'Plugins/Engineering/older/SKILL.md': [ADMIN],
|
|
236
236
|
},
|
|
237
237
|
});
|
|
238
238
|
expect((await svc.listPendingSkills(ADMIN)).map((p) => p.changeRequestNumber)).toEqual([3, 9]);
|