@bevel-software/platform-core-backend 0.7.5 → 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 +30 -18
- 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/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 +40 -20
- 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__/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__/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
package/src/index.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @bevel-software/platform-core-backend — the open-source core of the Bevel platform:
|
|
3
|
-
* git-backed knowledge workspace, workflow (branches / change requests /
|
|
4
|
-
* locks / SSE), skills, tool manuals + registry + auth, secrets vault, access
|
|
5
|
-
* control, diff/backup ledger, code-mode and the remote MCP surface.
|
|
6
|
-
*
|
|
7
|
-
* Composition entry points:
|
|
8
|
-
* - `new CoreConfig()` — env-driven core configuration
|
|
9
|
-
* - `createCoreServices(config, ports)` — construct every core service
|
|
10
|
-
* - `createCoreServer(core, ext, opts)` — the Express app (fixed mount order)
|
|
11
|
-
*
|
|
12
|
-
* Importing this package ALSO applies the global Express `Request`
|
|
13
|
-
* augmentation (`req.userId` / `req.userEmail` / token payloads) — the
|
|
14
|
-
* middleware modules re-exported below carry the `declare global` blocks, so
|
|
15
|
-
* consumers get the augmented types without a side-effect import of their own.
|
|
16
|
-
*
|
|
17
|
-
* Finer-grained internals are available via the `./modules/*` subpath exports.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export { CoreConfig } from './core-config.js';
|
|
21
|
-
export { createCoreServices, type CoreServices } from './core/create-core-services.js';
|
|
22
|
-
export {
|
|
23
|
-
createCoreServer,
|
|
24
|
-
type ServerExtensions,
|
|
25
|
-
type ToolSurfaceCtx,
|
|
26
|
-
} from './core/create-core-server.js';
|
|
27
|
-
export { noopRecoveryAgent, type CorePorts } from './core/core-ports.js';
|
|
28
|
-
|
|
29
|
-
// Packaged assets (migrations/, kb-template/) + the migration runners.
|
|
30
|
-
export { coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
|
|
31
|
-
export {
|
|
32
|
-
runCoreMigrations,
|
|
33
|
-
runEnterpriseMigrations,
|
|
34
|
-
} from './modules/database/migrate.js';
|
|
35
|
-
export { getDb, type Database } from './modules/database/connection.js';
|
|
36
|
-
|
|
37
|
-
// Build identity surfaced by GET /api/health.
|
|
38
|
-
export { GIT_SHA, resolveGitSha } from './version.js';
|
|
39
|
-
|
|
40
|
-
// Express `Request` augmentation carriers (userId/userEmail — auth.middleware;
|
|
41
|
-
// token payloads — tool-auth + external-api-key interface). Re-exported by
|
|
42
|
-
// VALUE/type so the `declare global` blocks land in this package's public
|
|
43
|
-
// declaration graph.
|
|
44
|
-
export { createAuthMiddleware, AUTH_COOKIE_NAME } from './modules/auth/auth.middleware.js';
|
|
45
|
-
export {
|
|
46
|
-
createToolAuthMiddleware,
|
|
47
|
-
createManualAuthMiddleware,
|
|
48
|
-
} from './modules/tool-auth/tool-auth.middleware.js';
|
|
49
|
-
export type {
|
|
50
|
-
IExternalApiKeyService,
|
|
51
|
-
ExternalApiKeySummary,
|
|
52
|
-
} from './modules/tool-auth/external-api-key.interface.js';
|
|
53
|
-
|
|
54
|
-
// Key port/seam types an overlay implements.
|
|
55
|
-
export type { ISessionSink } from './modules/workspace/session-sink.js';
|
|
56
|
-
export type {
|
|
57
|
-
ISystemNoticeSink,
|
|
58
|
-
RecoveryAgentRunner,
|
|
59
|
-
} from './modules/workflow/pending-commits.worker.js';
|
|
60
|
-
export type { ILlmUsageMeter } from './modules/tool-auth/llm-usage-meter.js';
|
|
61
|
-
export type { AuthProviderPlugin } from './modules/auth/auth.routes.js';
|
|
62
|
-
export type { IErasureParticipant } from './modules/auth/account-erasure.service.js';
|
|
63
|
-
export {
|
|
64
|
-
WorkflowHooks,
|
|
65
|
-
type CommitValidationHook,
|
|
66
|
-
type CommitValidationContext,
|
|
67
|
-
type PreWriteHook,
|
|
68
|
-
type PreWriteContext,
|
|
69
|
-
} from './modules/workflow/workflow-hooks.js';
|
|
1
|
+
/**
|
|
2
|
+
* @bevel-software/platform-core-backend — the open-source core of the Bevel platform:
|
|
3
|
+
* git-backed knowledge workspace, workflow (branches / change requests /
|
|
4
|
+
* locks / SSE), skills, tool manuals + registry + auth, secrets vault, access
|
|
5
|
+
* control, diff/backup ledger, code-mode and the remote MCP surface.
|
|
6
|
+
*
|
|
7
|
+
* Composition entry points:
|
|
8
|
+
* - `new CoreConfig()` — env-driven core configuration
|
|
9
|
+
* - `createCoreServices(config, ports)` — construct every core service
|
|
10
|
+
* - `createCoreServer(core, ext, opts)` — the Express app (fixed mount order)
|
|
11
|
+
*
|
|
12
|
+
* Importing this package ALSO applies the global Express `Request`
|
|
13
|
+
* augmentation (`req.userId` / `req.userEmail` / token payloads) — the
|
|
14
|
+
* middleware modules re-exported below carry the `declare global` blocks, so
|
|
15
|
+
* consumers get the augmented types without a side-effect import of their own.
|
|
16
|
+
*
|
|
17
|
+
* Finer-grained internals are available via the `./modules/*` subpath exports.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { CoreConfig } from './core-config.js';
|
|
21
|
+
export { createCoreServices, type CoreServices } from './core/create-core-services.js';
|
|
22
|
+
export {
|
|
23
|
+
createCoreServer,
|
|
24
|
+
type ServerExtensions,
|
|
25
|
+
type ToolSurfaceCtx,
|
|
26
|
+
} from './core/create-core-server.js';
|
|
27
|
+
export { noopRecoveryAgent, type CorePorts } from './core/core-ports.js';
|
|
28
|
+
|
|
29
|
+
// Packaged assets (migrations/, kb-template/) + the migration runners.
|
|
30
|
+
export { coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
|
|
31
|
+
export {
|
|
32
|
+
runCoreMigrations,
|
|
33
|
+
runEnterpriseMigrations,
|
|
34
|
+
} from './modules/database/migrate.js';
|
|
35
|
+
export { getDb, type Database } from './modules/database/connection.js';
|
|
36
|
+
|
|
37
|
+
// Build identity surfaced by GET /api/health.
|
|
38
|
+
export { GIT_SHA, resolveGitSha } from './version.js';
|
|
39
|
+
|
|
40
|
+
// Express `Request` augmentation carriers (userId/userEmail — auth.middleware;
|
|
41
|
+
// token payloads — tool-auth + external-api-key interface). Re-exported by
|
|
42
|
+
// VALUE/type so the `declare global` blocks land in this package's public
|
|
43
|
+
// declaration graph.
|
|
44
|
+
export { createAuthMiddleware, AUTH_COOKIE_NAME } from './modules/auth/auth.middleware.js';
|
|
45
|
+
export {
|
|
46
|
+
createToolAuthMiddleware,
|
|
47
|
+
createManualAuthMiddleware,
|
|
48
|
+
} from './modules/tool-auth/tool-auth.middleware.js';
|
|
49
|
+
export type {
|
|
50
|
+
IExternalApiKeyService,
|
|
51
|
+
ExternalApiKeySummary,
|
|
52
|
+
} from './modules/tool-auth/external-api-key.interface.js';
|
|
53
|
+
|
|
54
|
+
// Key port/seam types an overlay implements.
|
|
55
|
+
export type { ISessionSink } from './modules/workspace/session-sink.js';
|
|
56
|
+
export type {
|
|
57
|
+
ISystemNoticeSink,
|
|
58
|
+
RecoveryAgentRunner,
|
|
59
|
+
} from './modules/workflow/pending-commits.worker.js';
|
|
60
|
+
export type { ILlmUsageMeter } from './modules/tool-auth/llm-usage-meter.js';
|
|
61
|
+
export type { AuthProviderPlugin } from './modules/auth/auth.routes.js';
|
|
62
|
+
export type { IErasureParticipant } from './modules/auth/account-erasure.service.js';
|
|
63
|
+
export {
|
|
64
|
+
WorkflowHooks,
|
|
65
|
+
type CommitValidationHook,
|
|
66
|
+
type CommitValidationContext,
|
|
67
|
+
type PreWriteHook,
|
|
68
|
+
type PreWriteContext,
|
|
69
|
+
} from './modules/workflow/workflow-hooks.js';
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
|
-
import fs from 'node:fs/promises';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
|
|
8
|
-
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
9
|
-
import { AccessControlService } from '../access-control.service.js';
|
|
10
|
-
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
|
-
const PROCESS_MAP_DIR = 'knowledge-base';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* `canWriteBatchAtRef` / the batched at-ref own-entries read — backed by ONE
|
|
16
|
-
* `git cat-file --batch` process whose length-prefixed output we parse by
|
|
17
|
-
* hand, so exercise it against a REAL git repo: found blobs, a path missing
|
|
18
|
-
* at the ref (the `<spec> missing` line), a path with spaces, and frontmatter
|
|
19
|
-
* deny resolution — all in one batch, order-aligned.
|
|
20
|
-
*/
|
|
21
|
-
describe('AccessControlService — at-ref batch reads (git cat-file --batch)', () => {
|
|
22
|
-
let root: string;
|
|
23
|
-
let repo: string;
|
|
24
|
-
let svc: AccessControlService;
|
|
25
|
-
const workspaceId = 'ws-atref-1';
|
|
26
|
-
const admin = 'razvan@bevel.software';
|
|
27
|
-
|
|
28
|
-
async function git(...args: string[]): Promise<void> {
|
|
29
|
-
await execFileAsync('git', ['-C', repo, ...args]);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
beforeAll(async () => {
|
|
33
|
-
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-atref-'));
|
|
34
|
-
const workspaceDir = path.join(root, workspaceId);
|
|
35
|
-
repo = path.join(workspaceDir, PROCESS_MAP_DIR);
|
|
36
|
-
await fs.mkdir(path.join(repo, 'Knowledge'), { recursive: true });
|
|
37
|
-
|
|
38
|
-
await fs.writeFile(path.join(repo, 'roles.yaml'), 'roles:\n Admin:\n - razvan@bevel.software\n');
|
|
39
|
-
await fs.writeFile(path.join(repo, 'access.md'), '---\nwrite:\n - Admin\n---\n');
|
|
40
|
-
await fs.writeFile(path.join(repo, 'Knowledge/plain.md'), '# plain\n');
|
|
41
|
-
await fs.writeFile(
|
|
42
|
-
path.join(repo, 'Knowledge/denied.md'),
|
|
43
|
-
'---\nwrite:\n - deny razvan <razvan@bevel.software>\n---\n# denied\n',
|
|
44
|
-
);
|
|
45
|
-
await fs.writeFile(path.join(repo, 'Knowledge/with space.md'), '# spaced\n');
|
|
46
|
-
|
|
47
|
-
await execFileAsync('git', ['init', '-b', 'main', repo]);
|
|
48
|
-
await git('config', 'user.email', 'test@example.com');
|
|
49
|
-
await git('config', 'user.name', 'Test');
|
|
50
|
-
await git('add', '-A');
|
|
51
|
-
await git('commit', '-m', 'seed');
|
|
52
|
-
|
|
53
|
-
const stub = {
|
|
54
|
-
getWorkspacePath: async () => workspaceDir,
|
|
55
|
-
// Fixture repo has no remote; at-ref model loading fetches best-effort.
|
|
56
|
-
ensureRemotesFetched: async () => undefined,
|
|
57
|
-
} as unknown as WorkspaceService;
|
|
58
|
-
svc = new AccessControlService(stub, PROCESS_MAP_DIR);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
afterAll(async () => {
|
|
62
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('resolves a mixed batch in one pass: grants, frontmatter deny, spaces, and a missing path', async () => {
|
|
66
|
-
const paths = [
|
|
67
|
-
'Knowledge/plain.md', // dir-chain Admin grant → true
|
|
68
|
-
'Knowledge/denied.md', // own frontmatter deny → false
|
|
69
|
-
'Knowledge/with space.md', // found blob whose SPEC contains spaces → true
|
|
70
|
-
'Knowledge/not-there.md', // missing at the ref (`<spec> missing` line) → dir-chain verdict
|
|
71
|
-
];
|
|
72
|
-
const map = await svc.canWriteBatchAtRef(workspaceId, 'main', admin, paths);
|
|
73
|
-
expect(map).not.toBeNull();
|
|
74
|
-
expect(map!.get('Knowledge/plain.md')).toBe(true);
|
|
75
|
-
expect(map!.get('Knowledge/denied.md')).toBe(false);
|
|
76
|
-
expect(map!.get('Knowledge/with space.md')).toBe(true);
|
|
77
|
-
expect(map!.get('Knowledge/not-there.md')).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('matches the single-path at-ref resolution exactly', async () => {
|
|
81
|
-
// The batch is a pure performance change — verdicts must be identical to
|
|
82
|
-
// the per-path `git show` route the single-path helper still uses.
|
|
83
|
-
expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/denied.md')).toBe(false);
|
|
84
|
-
expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/plain.md')).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('eligibleWritersForPathsAtRef honours the same batched own-entries', async () => {
|
|
88
|
-
const map = await svc.eligibleWritersForPathsAtRef(workspaceId, 'main', [
|
|
89
|
-
'Knowledge/plain.md',
|
|
90
|
-
'Knowledge/denied.md',
|
|
91
|
-
]);
|
|
92
|
-
expect(map).not.toBeNull();
|
|
93
|
-
expect(map!.get('Knowledge/plain.md')!.roles).toContain('Admin');
|
|
94
|
-
// The per-user deny strips razvan from the expanded email set.
|
|
95
|
-
expect(map!.get('Knowledge/denied.md')!.emails.has(admin)).toBe(false);
|
|
96
|
-
expect(map!.get('Knowledge/plain.md')!.emails.has(admin)).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
|
|
8
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
9
|
+
import { AccessControlService } from '../access-control.service.js';
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const PROCESS_MAP_DIR = 'knowledge-base';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `canWriteBatchAtRef` / the batched at-ref own-entries read — backed by ONE
|
|
16
|
+
* `git cat-file --batch` process whose length-prefixed output we parse by
|
|
17
|
+
* hand, so exercise it against a REAL git repo: found blobs, a path missing
|
|
18
|
+
* at the ref (the `<spec> missing` line), a path with spaces, and frontmatter
|
|
19
|
+
* deny resolution — all in one batch, order-aligned.
|
|
20
|
+
*/
|
|
21
|
+
describe('AccessControlService — at-ref batch reads (git cat-file --batch)', () => {
|
|
22
|
+
let root: string;
|
|
23
|
+
let repo: string;
|
|
24
|
+
let svc: AccessControlService;
|
|
25
|
+
const workspaceId = 'ws-atref-1';
|
|
26
|
+
const admin = 'razvan@bevel.software';
|
|
27
|
+
|
|
28
|
+
async function git(...args: string[]): Promise<void> {
|
|
29
|
+
await execFileAsync('git', ['-C', repo, ...args]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-atref-'));
|
|
34
|
+
const workspaceDir = path.join(root, workspaceId);
|
|
35
|
+
repo = path.join(workspaceDir, PROCESS_MAP_DIR);
|
|
36
|
+
await fs.mkdir(path.join(repo, 'Knowledge'), { recursive: true });
|
|
37
|
+
|
|
38
|
+
await fs.writeFile(path.join(repo, 'roles.yaml'), 'roles:\n Admin:\n - razvan@bevel.software\n');
|
|
39
|
+
await fs.writeFile(path.join(repo, 'access.md'), '---\nwrite:\n - Admin\n---\n');
|
|
40
|
+
await fs.writeFile(path.join(repo, 'Knowledge/plain.md'), '# plain\n');
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.join(repo, 'Knowledge/denied.md'),
|
|
43
|
+
'---\nwrite:\n - deny razvan <razvan@bevel.software>\n---\n# denied\n',
|
|
44
|
+
);
|
|
45
|
+
await fs.writeFile(path.join(repo, 'Knowledge/with space.md'), '# spaced\n');
|
|
46
|
+
|
|
47
|
+
await execFileAsync('git', ['init', '-b', 'main', repo]);
|
|
48
|
+
await git('config', 'user.email', 'test@example.com');
|
|
49
|
+
await git('config', 'user.name', 'Test');
|
|
50
|
+
await git('add', '-A');
|
|
51
|
+
await git('commit', '-m', 'seed');
|
|
52
|
+
|
|
53
|
+
const stub = {
|
|
54
|
+
getWorkspacePath: async () => workspaceDir,
|
|
55
|
+
// Fixture repo has no remote; at-ref model loading fetches best-effort.
|
|
56
|
+
ensureRemotesFetched: async () => undefined,
|
|
57
|
+
} as unknown as WorkspaceService;
|
|
58
|
+
svc = new AccessControlService(stub, PROCESS_MAP_DIR);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterAll(async () => {
|
|
62
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('resolves a mixed batch in one pass: grants, frontmatter deny, spaces, and a missing path', async () => {
|
|
66
|
+
const paths = [
|
|
67
|
+
'Knowledge/plain.md', // dir-chain Admin grant → true
|
|
68
|
+
'Knowledge/denied.md', // own frontmatter deny → false
|
|
69
|
+
'Knowledge/with space.md', // found blob whose SPEC contains spaces → true
|
|
70
|
+
'Knowledge/not-there.md', // missing at the ref (`<spec> missing` line) → dir-chain verdict
|
|
71
|
+
];
|
|
72
|
+
const map = await svc.canWriteBatchAtRef(workspaceId, 'main', admin, paths);
|
|
73
|
+
expect(map).not.toBeNull();
|
|
74
|
+
expect(map!.get('Knowledge/plain.md')).toBe(true);
|
|
75
|
+
expect(map!.get('Knowledge/denied.md')).toBe(false);
|
|
76
|
+
expect(map!.get('Knowledge/with space.md')).toBe(true);
|
|
77
|
+
expect(map!.get('Knowledge/not-there.md')).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('matches the single-path at-ref resolution exactly', async () => {
|
|
81
|
+
// The batch is a pure performance change — verdicts must be identical to
|
|
82
|
+
// the per-path `git show` route the single-path helper still uses.
|
|
83
|
+
expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/denied.md')).toBe(false);
|
|
84
|
+
expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/plain.md')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('eligibleWritersForPathsAtRef honours the same batched own-entries', async () => {
|
|
88
|
+
const map = await svc.eligibleWritersForPathsAtRef(workspaceId, 'main', [
|
|
89
|
+
'Knowledge/plain.md',
|
|
90
|
+
'Knowledge/denied.md',
|
|
91
|
+
]);
|
|
92
|
+
expect(map).not.toBeNull();
|
|
93
|
+
expect(map!.get('Knowledge/plain.md')!.roles).toContain('Admin');
|
|
94
|
+
// The per-user deny strips razvan from the expanded email set.
|
|
95
|
+
expect(map!.get('Knowledge/denied.md')!.emails.has(admin)).toBe(false);
|
|
96
|
+
expect(map!.get('Knowledge/plain.md')!.emails.has(admin)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -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
|
|