@bevel-software/platform-core-backend 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-guide/access-control.md +19 -8
- package/agent-guide/html-views.md +231 -5
- package/agent-guide/tool-manuals.md +1 -1
- package/dist/core/catalog-revision.d.ts +12 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +23 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/core-ports.d.ts +41 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +14 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +16 -5
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +37 -12
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +12 -2
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +17 -9
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +12 -2
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +164 -75
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +22 -24
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +48 -51
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-view.d.ts.map +1 -1
- package/dist/modules/access/access-view.js +12 -11
- package/dist/modules/access/access-view.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts +3 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +35 -29
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
- package/dist/modules/access/groups-admin.routes.js +4 -2
- package/dist/modules/access/groups-admin.routes.js.map +1 -1
- package/dist/modules/access/groups-admin.service.d.ts +44 -13
- package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
- package/dist/modules/access/groups-admin.service.js +80 -38
- package/dist/modules/access/groups-admin.service.js.map +1 -1
- package/dist/modules/access/groups-edit.d.ts +1 -1
- package/dist/modules/access/groups-edit.d.ts.map +1 -1
- package/dist/modules/access/groups-edit.js +1 -1
- package/dist/modules/access/groups-edit.js.map +1 -1
- package/dist/modules/access/reference-scan.d.ts +116 -24
- package/dist/modules/access/reference-scan.d.ts.map +1 -1
- package/dist/modules/access/reference-scan.js +305 -44
- package/dist/modules/access/reference-scan.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +17 -8
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +32 -23
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +35 -10
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +60 -20
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/access-splice.d.ts +22 -18
- package/dist/modules/access-model/access-splice.d.ts.map +1 -1
- package/dist/modules/access-model/access-splice.js +20 -14
- package/dist/modules/access-model/access-splice.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts +1 -0
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +5 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.js +13 -3
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +3 -1
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +3 -1
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +16 -1
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
- package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
- package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
- package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
- package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
- package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
- package/dist/modules/agent-scope/connection-scope.js +165 -0
- package/dist/modules/agent-scope/connection-scope.js.map +1 -0
- package/dist/modules/agent-scope/index.d.ts +3 -0
- package/dist/modules/agent-scope/index.d.ts.map +1 -0
- package/dist/modules/agent-scope/index.js +3 -0
- package/dist/modules/agent-scope/index.js.map +1 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +2 -0
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/audit.contract.d.ts +6 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +12 -0
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +15 -0
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +27 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +42 -4
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts +13 -9
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +35 -29
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +55 -6
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +128 -34
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/index.d.ts +1 -1
- package/dist/modules/auth/index.d.ts.map +1 -1
- package/dist/modules/auth/index.js +1 -1
- package/dist/modules/auth/index.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +2 -9
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +68 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +33 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
- package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
- package/dist/modules/embed/embed.tools.d.ts.map +1 -1
- package/dist/modules/embed/embed.tools.js +5 -0
- package/dist/modules/embed/embed.tools.js.map +1 -1
- package/dist/modules/kb-fs/index.d.ts +2 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -1
- package/dist/modules/kb-fs/index.js +1 -0
- package/dist/modules/kb-fs/index.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +7 -1
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +26 -6
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +22 -7
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
- package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
- package/dist/modules/kb-fs/tmp-ref.js +76 -0
- package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +7 -1
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +39 -4
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +68 -13
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +1 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +18 -2
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-links.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +5 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +32 -5
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +15 -3
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/skills/index.d.ts +3 -3
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +0 -2
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +16 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +6 -1
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +24 -12
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +4 -1
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/index.d.ts +1 -1
- package/dist/modules/tool-auth/index.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
- package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.js +1 -0
- package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +3 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +19 -0
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
- package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
- package/dist/modules/tool-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/tool-manuals/index.d.ts +3 -3
- package/dist/modules/tool-manuals/index.d.ts.map +1 -1
- package/dist/modules/tool-manuals/index.js +0 -2
- package/dist/modules/tool-manuals/index.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +19 -6
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +13 -6
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +23 -15
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +25 -10
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
- package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
- package/dist/modules/tool-registry/manual.routes.js +27 -7
- package/dist/modules/tool-registry/manual.routes.js.map +1 -1
- package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
- package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool.contract.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +14 -9
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +2 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +20 -35
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +2 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +3 -0
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
- package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
- package/dist/modules/workspace/index.d.ts +8 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +8 -0
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/nul-scan.d.ts +19 -0
- package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
- package/dist/modules/workspace/nul-scan.js +55 -0
- package/dist/modules/workspace/nul-scan.js.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
- package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
- package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
- package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
- package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
- package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
- package/dist/modules/workspace/ripgrep-search.js +657 -0
- package/dist/modules/workspace/ripgrep-search.js.map +1 -0
- package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
- package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
- package/dist/modules/workspace/ripgrep-version.js +31 -0
- package/dist/modules/workspace/ripgrep-version.js.map +1 -0
- package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
- package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
- package/dist/modules/workspace/startup/beside-checkout.js +14 -5
- package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
- package/dist/modules/workspace/worker-pool.d.ts +3 -4
- package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
- package/dist/modules/workspace/worker-pool.js.map +1 -1
- package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
- package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
- package/dist/modules/workspace/workspace-tmp.js +435 -0
- package/dist/modules/workspace/workspace-tmp.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +5 -26
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +47 -6
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +86 -6
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +460 -116
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/path-order.d.ts +28 -8
- package/dist/shared/path-order.d.ts.map +1 -1
- package/dist/shared/path-order.js +37 -8
- package/dist/shared/path-order.js.map +1 -1
- package/migrations/0022_session_version.sql +6 -0
- package/migrations/0023_agent_path.sql +3 -0
- package/migrations/meta/0022_snapshot.json +2360 -0
- package/migrations/meta/0023_snapshot.json +2378 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/__tests__/pg-scratch.test.ts +69 -0
- package/src/__tests__/pg-scratch.ts +31 -0
- package/src/__tests__/slow-ripgrep.ts +65 -0
- package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
- package/src/core/__tests__/catalog-revision.test.ts +89 -0
- package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
- package/src/core/__tests__/gated-boot.test.ts +8 -1
- package/src/core/__tests__/lifecycle.test.ts +46 -13
- package/src/core/catalog-revision.ts +24 -2
- package/src/core/core-ports.ts +42 -0
- package/src/core/create-core-server.ts +13 -0
- package/src/core/create-core-services.ts +49 -12
- package/src/core/lifecycle.ts +26 -2
- package/src/index.ts +1 -1
- package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
- package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
- package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
- package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
- package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
- package/src/modules/access/__tests__/access-groups.test.ts +55 -22
- package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
- package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
- package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
- package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
- package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
- package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +2 -0
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
- package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
- package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
- package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
- package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
- package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
- package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
- package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
- package/src/modules/access/access-control.interface.ts +17 -10
- package/src/modules/access/access-control.service.ts +158 -72
- package/src/modules/access/access-mutation.service.ts +48 -53
- package/src/modules/access/access-view.ts +12 -10
- package/src/modules/access/access.routes.ts +43 -33
- package/src/modules/access/groups-admin.routes.ts +4 -2
- package/src/modules/access/groups-admin.service.ts +114 -44
- package/src/modules/access/groups-edit.ts +1 -1
- package/src/modules/access/reference-scan.ts +319 -46
- package/src/modules/access/roles-admin.service.ts +43 -26
- package/src/modules/access/synced-groups-writer.ts +1 -1
- package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
- package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
- package/src/modules/access-model/access-grammar.ts +65 -20
- package/src/modules/access-model/access-splice.ts +31 -23
- package/src/modules/access-model/group-files.ts +5 -0
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +31 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +13 -2
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +1 -0
- package/src/modules/agent-instructions/compose.ts +3 -1
- package/src/modules/agent-instructions/shared-file-rules.ts +17 -1
- package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
- package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
- package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
- package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
- package/src/modules/agent-scope/connection-scope.ts +199 -0
- package/src/modules/agent-scope/index.ts +15 -0
- package/src/modules/audit/agent-audit.service.ts +2 -0
- package/src/modules/audit/audit.contract.ts +6 -0
- package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
- package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
- package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
- package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
- package/src/modules/auth/account-admission.ts +19 -0
- package/src/modules/auth/auth.middleware.ts +45 -4
- package/src/modules/auth/auth.routes.ts +46 -29
- package/src/modules/auth/auth.service.ts +135 -34
- package/src/modules/auth/index.ts +2 -0
- package/src/modules/auth/oidc-auth-provider.ts +2 -9
- package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
- package/src/modules/database/core-schema.ts +33 -0
- package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
- package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
- package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
- package/src/modules/embed/embed.tools.ts +5 -0
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
- package/src/modules/kb-fs/index.ts +2 -0
- package/src/modules/kb-fs/locking-filesystem.ts +7 -1
- package/src/modules/kb-fs/repo-path.ts +33 -7
- package/src/modules/kb-fs/tmp-ref.ts +81 -0
- package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
- package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
- package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
- package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
- package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
- package/src/modules/mcp/mcp.routes.ts +39 -3
- package/src/modules/mcp/mcp.service.ts +107 -17
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
- package/src/modules/plugins/index.ts +1 -1
- package/src/modules/plugins/plugin-links.ts +19 -2
- package/src/modules/plugins/plugins.routes.ts +35 -4
- package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
- package/src/modules/secrets-vault/secrets-vault.routes.ts +23 -3
- package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
- package/src/modules/skills/index.ts +7 -2
- package/src/modules/skills/skills.contract.ts +17 -0
- package/src/modules/skills/skills.service.ts +5 -1
- package/src/modules/skills/skills.tools.ts +44 -13
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
- package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
- package/src/modules/tool-auth/external-api-key.service.ts +5 -2
- package/src/modules/tool-auth/index.ts +1 -1
- package/src/modules/tool-auth/internal-token.service.ts +10 -0
- package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
- package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
- package/src/modules/tool-helpers/tool-context.ts +10 -0
- package/src/modules/tool-helpers/tool-handler.ts +34 -0
- package/src/modules/tool-helpers/tool.contract.ts +10 -0
- package/src/modules/tool-helpers/validate-token.ts +4 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/index.ts +6 -2
- package/src/modules/tool-manuals/tool-manuals.contract.ts +18 -6
- package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
- package/src/modules/tool-manuals/tool-manuals.service.ts +31 -15
- package/src/modules/tool-manuals/tool-manuals.tools.ts +28 -11
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +2 -2
- package/src/modules/tool-registry/manual.routes.ts +26 -5
- package/src/modules/tool-registry/tool.contract.ts +7 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
- package/src/modules/workflow/agent-tools/workflow.tools.ts +14 -9
- package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
- package/src/modules/workflow/git/git.service.ts +29 -48
- package/src/modules/workflow/workflow.service.ts +5 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
- package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
- package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
- package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
- package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +393 -113
- package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
- package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
- package/src/modules/workspace/index.ts +20 -0
- package/src/modules/workspace/nul-scan.ts +50 -0
- package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
- package/src/modules/workspace/ripgrep-search.ts +715 -0
- package/src/modules/workspace/ripgrep-version.ts +35 -0
- package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
- package/src/modules/workspace/startup/beside-checkout.ts +13 -5
- package/src/modules/workspace/worker-pool.ts +3 -4
- package/src/modules/workspace/workspace-tmp.ts +457 -0
- package/src/modules/workspace/workspace.routes.ts +6 -27
- package/src/modules/workspace/workspace.service.ts +92 -15
- package/src/modules/workspace/workspace.tools.ts +500 -155
- package/src/shared/path-order.ts +37 -8
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
- package/starter-packs/general/Plugins/general-starter/access.md +1 -1
- package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
- package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
- package/starter-packs/support/Plugins/support-starter/access.md +1 -1
- package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
- package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
- package/dist/modules/workspace/bounded-matcher.js +0 -200
- package/dist/modules/workspace/bounded-matcher.js.map +0 -1
- package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
- package/src/modules/workspace/bounded-matcher.ts +0 -271
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA reproduction (v0.28.0 hunt, decisions #312/#315): a Business user's
|
|
3
|
+
* agent proposed a group named `Admin` in groups.yaml, an Admin approved the
|
|
4
|
+
* change request, and every member of the group then received the Admin
|
|
5
|
+
* role's grants wherever an access rule said `Admin` — because a bare name
|
|
6
|
+
* meant the GROUP when a group and a role shared it.
|
|
7
|
+
*
|
|
8
|
+
* The rule now: a bare name means the ROLE when one of that name exists; the
|
|
9
|
+
* group is reachable only as `group/<Name>`. This pins that the approved
|
|
10
|
+
* group gives its members nothing the Admin role is granted.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import { execFile } from 'node:child_process';
|
|
17
|
+
import { promisify } from 'node:util';
|
|
18
|
+
|
|
19
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
20
|
+
import type { WorkspaceService } from '../../workspace/index.js';
|
|
21
|
+
import { AccessControlService } from '../access-control.service.js';
|
|
22
|
+
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
24
|
+
const KB_DIR = 'knowledge-base';
|
|
25
|
+
const WS = 'ws-group-named-admin';
|
|
26
|
+
|
|
27
|
+
const ADMIN = 'admin@x.io';
|
|
28
|
+
const BUSINESS = 'business@x.io';
|
|
29
|
+
|
|
30
|
+
const BEFORE: Record<string, string> = {
|
|
31
|
+
'roles.yaml': `roles:\n Admin:\n - ${ADMIN}\n`,
|
|
32
|
+
'groups.yaml': `groups:\n Sales:\n - ${BUSINESS}\n`,
|
|
33
|
+
// The root: Admin writes everything, Sales reads.
|
|
34
|
+
'access.md': '---\nwrite:\n - Admin\n---\nread:\n - group/Sales\nwrite:\n - Admin\n',
|
|
35
|
+
// An admin-only folder, granted by the bare role name and closed to the rest.
|
|
36
|
+
'Knowledge/Board/access.md': '---\nwrite:\n - Admin\n---\nread:\n - Admin\n - deny everyone\nwrite:\n - Admin\nowner:\n - Admin\n',
|
|
37
|
+
'Knowledge/Board/Minutes.md': '# Minutes\n',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The change request's one edit: a group named Admin, holding the proposer. */
|
|
41
|
+
const PROPOSED_GROUPS = `groups:\n Sales:\n - ${BUSINESS}\n Admin:\n - ${BUSINESS}\n`;
|
|
42
|
+
|
|
43
|
+
describe('a group named Admin (QA reproduction)', () => {
|
|
44
|
+
let root: string;
|
|
45
|
+
let repo: string;
|
|
46
|
+
let svc: AccessControlService;
|
|
47
|
+
const git = (...args: string[]) => execFileAsync('git', ['-C', repo, ...args]);
|
|
48
|
+
|
|
49
|
+
beforeEach(async () => {
|
|
50
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-group-admin-'));
|
|
51
|
+
const workspaceDir = path.join(root, WS);
|
|
52
|
+
repo = path.join(workspaceDir, KB_DIR);
|
|
53
|
+
for (const [rel, text] of Object.entries(BEFORE)) {
|
|
54
|
+
await fs.mkdir(path.dirname(path.join(repo, rel)), { recursive: true });
|
|
55
|
+
await fs.writeFile(path.join(repo, rel), text);
|
|
56
|
+
}
|
|
57
|
+
await git('init', '-b', 'main');
|
|
58
|
+
await git('config', 'user.email', 'test@x.io');
|
|
59
|
+
await git('config', 'user.name', 'Test');
|
|
60
|
+
await git('add', '-A');
|
|
61
|
+
await git('commit', '-m', 'seed');
|
|
62
|
+
|
|
63
|
+
// The Business user's proposal, on its own branch…
|
|
64
|
+
await git('checkout', '-b', 'proposal/admin-group');
|
|
65
|
+
await fs.writeFile(path.join(repo, 'groups.yaml'), PROPOSED_GROUPS);
|
|
66
|
+
await git('commit', '-am', 'Add group Admin');
|
|
67
|
+
// …approved by an Admin and merged into main.
|
|
68
|
+
await git('checkout', 'main');
|
|
69
|
+
await git('merge', '--no-ff', '-m', 'Approve: Add group Admin', 'proposal/admin-group');
|
|
70
|
+
|
|
71
|
+
const workspaceService = {
|
|
72
|
+
getWorkspacePath: async () => workspaceDir,
|
|
73
|
+
ensureRemotesFetched: async () => undefined,
|
|
74
|
+
} as unknown as WorkspaceService;
|
|
75
|
+
svc = new AccessControlService(workspaceService, KB_DIR, new NodeFs());
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(async () => {
|
|
79
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('its members receive none of the Admin role grants', async () => {
|
|
83
|
+
// Line endings aside (Git may check the merge out with CRLF on Windows),
|
|
84
|
+
// main now holds the proposed groups file.
|
|
85
|
+
const merged = await fs.readFile(path.join(repo, 'groups.yaml'), 'utf-8');
|
|
86
|
+
expect(merged.replace(/\r\n/g, '\n')).toBe(PROPOSED_GROUPS);
|
|
87
|
+
|
|
88
|
+
// `read: Admin` / `write: Admin` / `owner: Admin` are the ROLE's.
|
|
89
|
+
expect(await svc.canRead(WS, BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(false);
|
|
90
|
+
expect(await svc.canWrite(WS, BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(false);
|
|
91
|
+
expect(await svc.canWrite(WS, BUSINESS, 'Knowledge/Doc.md')).toBe(false);
|
|
92
|
+
// …and the gate paths, which read the committed state, agree.
|
|
93
|
+
expect(await svc.canWriteAtRef(WS, 'main', BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(false);
|
|
94
|
+
// The Admin capability is the role's alone.
|
|
95
|
+
expect(await svc.canWrite(WS, BUSINESS, 'roles.yaml')).toBe(false);
|
|
96
|
+
expect(await svc.adminEmails(WS)).toEqual([ADMIN]);
|
|
97
|
+
|
|
98
|
+
// The real Admin keeps everything, and Sales keeps what Sales had.
|
|
99
|
+
expect(await svc.canRead(WS, ADMIN, 'Knowledge/Board/Minutes.md')).toBe(true);
|
|
100
|
+
expect(await svc.canWrite(WS, ADMIN, 'Knowledge/Board/Minutes.md')).toBe(true);
|
|
101
|
+
expect(await svc.canRead(WS, BUSINESS, 'Knowledge/Doc.md')).toBe(true);
|
|
102
|
+
|
|
103
|
+
// The dialog's lists name the ROLE, never the group.
|
|
104
|
+
const readers = await svc.eligibleReaders(WS, 'Knowledge/Board/Minutes.md');
|
|
105
|
+
expect(readers.principals).toContainEqual({ name: 'Admin', kind: 'role' });
|
|
106
|
+
expect(readers.principals).not.toContainEqual({ name: 'Admin', kind: 'group' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('the group is reachable only by its explicit spelling, group/Admin', async () => {
|
|
110
|
+
await fs.writeFile(
|
|
111
|
+
path.join(repo, 'Knowledge/Board/access.md'),
|
|
112
|
+
'---\nwrite:\n - Admin\n---\nread:\n - Admin\n - group/Admin\n - deny everyone\nwrite:\n - Admin\n',
|
|
113
|
+
);
|
|
114
|
+
svc.invalidate(WS);
|
|
115
|
+
expect(await svc.canRead(WS, BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(true);
|
|
116
|
+
// Reading is all `group/Admin` was granted.
|
|
117
|
+
expect(await svc.canWrite(WS, BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(false);
|
|
118
|
+
const readers = await svc.eligibleReaders(WS, 'Knowledge/Board/Minutes.md');
|
|
119
|
+
expect(readers.principals).toContainEqual({ name: 'Admin', kind: 'group' });
|
|
120
|
+
expect(readers.principals).toContainEqual({ name: 'Admin', kind: 'role' });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -360,6 +360,56 @@ describe('AccessControlService.previewAccessAfterRelocation', () => {
|
|
|
360
360
|
expect(await verbsAt(service(), 'Legal/note.md')).toEqual(after);
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
+
/**
|
|
364
|
+
* A copy whose source is no repository path — a file staged in the
|
|
365
|
+
* workspace's `tmp/` folder — carries rules in by its text alone
|
|
366
|
+
* (`sourceContent`), read exactly as a repository file's would be. Checked,
|
|
367
|
+
* as every other case here, against the gates once the bytes really landed.
|
|
368
|
+
*/
|
|
369
|
+
describe('a source outside the repository, previewed by its content', () => {
|
|
370
|
+
const REF = '__tmp__/staged.md';
|
|
371
|
+
|
|
372
|
+
it('its own frontmatter governs the file it lands as', async () => {
|
|
373
|
+
await write({ ...BASE, 'Work/access.md': rules(`write:\n - Mover <${MOVER}>\n`) });
|
|
374
|
+
const staged = rules(`read:\n - deny everyone\n - Other <other@x.io>\n`) + '# Staged\n';
|
|
375
|
+
|
|
376
|
+
const svc = service();
|
|
377
|
+
expect(await verbsAt(svc, 'Work/staged.md')).toMatchObject({ read: true, write: true });
|
|
378
|
+
const after = await svc.previewAccessAfterRelocation(
|
|
379
|
+
workspaceId, MOVER, REF, 'Work/staged.md', { sourceRemains: true, sourceContent: staged },
|
|
380
|
+
);
|
|
381
|
+
// The staged file shuts the caller out of what it becomes.
|
|
382
|
+
expect(after.read).toBe(false);
|
|
383
|
+
|
|
384
|
+
await write({ 'Work/staged.md': staged });
|
|
385
|
+
expect(await verbsAt(service(), 'Work/staged.md')).toEqual(after);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('landing as an access.md, it gives the destination folder its rules', async () => {
|
|
389
|
+
await write({ ...BASE, 'Legal/note.md': '# Note\n' });
|
|
390
|
+
const staged = rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`);
|
|
391
|
+
|
|
392
|
+
const svc = service();
|
|
393
|
+
expect(await verbsAt(svc, 'Legal/access.md')).toMatchObject({ write: false, owner: false });
|
|
394
|
+
const after = await svc.previewAccessAfterRelocation(
|
|
395
|
+
workspaceId, MOVER, REF, 'Legal/access.md', { sourceRemains: true, sourceContent: staged },
|
|
396
|
+
);
|
|
397
|
+
expect(after).toMatchObject({ write: true, owner: true });
|
|
398
|
+
|
|
399
|
+
await write({ 'Legal/access.md': staged });
|
|
400
|
+
expect(await verbsAt(service(), 'Legal/access.md')).toEqual(after);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('nothing is read at the ref: a repository path spelled like it is not the source', async () => {
|
|
404
|
+
await write({ ...BASE, [REF]: rules(`read:\n - deny everyone\n`) + '# Not the staged file\n' });
|
|
405
|
+
|
|
406
|
+
const after = await service().previewAccessAfterRelocation(
|
|
407
|
+
workspaceId, MOVER, REF, 'Work/staged.md', { sourceRemains: true, sourceContent: '# Staged\n' },
|
|
408
|
+
);
|
|
409
|
+
expect(after).toEqual(await verbsAt(service(), 'Work/staged.md'));
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
363
413
|
it('answers the caller\'s own verbs and nothing else, and writes nothing', async () => {
|
|
364
414
|
await write({
|
|
365
415
|
...BASE,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
AccessDeclarationsError,
|
|
6
6
|
DECLARATION_SCAN_FILE_CAP,
|
|
7
7
|
} from '../access-declarations.js';
|
|
8
|
+
import { registerAccessFrontmatterExtensions } from '../../access-model/access-grammar.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The declaration scan: WHICH files under a folder declare access rules, and
|
|
@@ -111,6 +112,24 @@ describe('listAccessDeclarationsUnder', () => {
|
|
|
111
112
|
]);
|
|
112
113
|
});
|
|
113
114
|
|
|
115
|
+
it("reads verbs out of a distribution's configuration file (an .agent, once its kind is registered), governing the file itself", async () => {
|
|
116
|
+
// `.agent` is not core's: a distribution registers the kind at boot, as
|
|
117
|
+
// `registerAccessFrontmatterExtensions` is for, and the scan then reads it.
|
|
118
|
+
registerAccessFrontmatterExtensions(['.agent']);
|
|
119
|
+
const { overrides } = await scan({
|
|
120
|
+
[`${KB}/Agents/ops.agent`]: fm('name: ops\nplatformTools: [read_file]\nwrite:\n - Developer') + '\nNotes for people.\n',
|
|
121
|
+
}, 'Agents');
|
|
122
|
+
|
|
123
|
+
expect(overrides).toEqual([
|
|
124
|
+
{
|
|
125
|
+
path: 'Agents/ops.agent',
|
|
126
|
+
governs: 'Agents/ops.agent',
|
|
127
|
+
source: 'frontmatter',
|
|
128
|
+
entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'Developer' } }],
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
114
133
|
it('ignores nodes whose frontmatter declares no access verb', async () => {
|
|
115
134
|
const { overrides } = await scan({
|
|
116
135
|
[`${KB}/Plugins/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\ndescription: Runs outreach'),
|
|
@@ -260,43 +260,75 @@ describe('group files as access principals', () => {
|
|
|
260
260
|
expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(false);
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
-
it('a bare name shared by a group and a role resolves to the
|
|
263
|
+
it('a bare name shared by a group and a role resolves to the ROLE (grant side)', async () => {
|
|
264
264
|
const svc = await makeService({
|
|
265
265
|
'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Engineering:\n - lead@x.io\n`,
|
|
266
266
|
// The group "Engineering" collides with the ROLE Engineering.
|
|
267
267
|
'groups.yaml': GROUPS_YAML_TEXT,
|
|
268
268
|
'access.md': '---\nread:\n - Engineering\n---\n',
|
|
269
269
|
});
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
expect(await svc.canRead(workspaceId, '
|
|
273
|
-
expect(await svc.canRead(workspaceId, '
|
|
274
|
-
// The display surface resolves the bare token to the
|
|
275
|
-
// WHAT it is, so the dialog badges the row "
|
|
270
|
+
// Role-first precedence: the role's member reads; the group's members do
|
|
271
|
+
// not resolve through the bare token (group/Engineering is the group).
|
|
272
|
+
expect(await svc.canRead(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(true);
|
|
273
|
+
expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(false);
|
|
274
|
+
// The display surface resolves the bare token to the role too — and says
|
|
275
|
+
// WHAT it is, so the dialog badges the row "Role", not "Group".
|
|
276
276
|
const readers = await svc.eligibleReaders(workspaceId, 'Knowledge/Doc.md');
|
|
277
277
|
expect(readers.roles).toContain('Engineering');
|
|
278
|
+
expect(readers.principals).toContainEqual({ name: 'Engineering', kind: 'role' });
|
|
279
|
+
expect(readers.principals).not.toContainEqual({ name: 'Engineering', kind: 'group' });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('a bare name with no role of that name still resolves to the group (existing files keep working)', async () => {
|
|
283
|
+
const svc = await makeService({
|
|
284
|
+
'roles.yaml': ROLES_YAML,
|
|
285
|
+
'groups.yaml': GROUPS_YAML_TEXT,
|
|
286
|
+
'access.md': '---\nread:\n - Engineering\n---\n',
|
|
287
|
+
});
|
|
288
|
+
expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(true);
|
|
289
|
+
const readers = await svc.eligibleReaders(workspaceId, 'Knowledge/Doc.md');
|
|
278
290
|
expect(readers.principals).toContainEqual({ name: 'Engineering', kind: 'group' });
|
|
279
291
|
});
|
|
280
292
|
|
|
281
|
-
it('
|
|
293
|
+
it('group/<Name> always means the group, role/<Name> always the role', async () => {
|
|
282
294
|
const svc = await makeService({
|
|
283
295
|
'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Engineering:\n - lead@x.io\n`,
|
|
284
296
|
'groups.yaml': GROUPS_YAML_TEXT,
|
|
285
297
|
'access.md':
|
|
286
|
-
'---\nread:\n - everyone\n - deny Engineering\nwrite:\n - role/Engineering\n---\n',
|
|
298
|
+
'---\nread:\n - everyone\n - deny group/Engineering\nwrite:\n - role/Engineering\nowner:\n - group/Sales\n---\n',
|
|
287
299
|
});
|
|
288
|
-
// The deny bites the GROUP's members, not the role's.
|
|
300
|
+
// The explicit group deny bites the GROUP's members, not the role's.
|
|
289
301
|
expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(false);
|
|
290
302
|
expect(await svc.canRead(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(true);
|
|
291
|
-
// The explicit token grants the ROLE write.
|
|
303
|
+
// The explicit role token grants the ROLE write.
|
|
292
304
|
expect(await svc.canWrite(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(true);
|
|
293
305
|
expect(await svc.canWrite(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(false);
|
|
294
|
-
// eligibleWriters resolves the explicit token to the role's display name —
|
|
295
|
-
// and carries kind 'role': the `role/` alias always means the role, even
|
|
296
|
-
// with a same-named group in play.
|
|
297
306
|
const writers = await svc.eligibleWriters(workspaceId, 'Knowledge/Doc.md');
|
|
298
|
-
expect(writers.roles).toContain('Engineering');
|
|
299
307
|
expect(writers.principals).toContainEqual({ name: 'Engineering', kind: 'role' });
|
|
308
|
+
// An unshared group name spelled group/ is the group too.
|
|
309
|
+
const owners = await svc.eligibleOwners(workspaceId, 'Knowledge/Doc.md');
|
|
310
|
+
expect(owners.principals).toContainEqual({ name: 'Sales', kind: 'group' });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('a bare-name DENY resolves to the role when one exists, never the same-named group', async () => {
|
|
314
|
+
const svc = await makeService({
|
|
315
|
+
'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Engineering:\n - lead@x.io\n`,
|
|
316
|
+
'groups.yaml': GROUPS_YAML_TEXT,
|
|
317
|
+
'access.md': '---\nread:\n - everyone\n - deny Engineering\n---\n',
|
|
318
|
+
});
|
|
319
|
+
expect(await svc.canRead(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(false);
|
|
320
|
+
// ada is in the Engineering GROUP only; a bare denial means the role.
|
|
321
|
+
expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('a group/<Name> naming no group grants nobody', async () => {
|
|
325
|
+
const svc = await makeService({
|
|
326
|
+
'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Reviewer:\n - rev@x.io\n`,
|
|
327
|
+
'groups.yaml': GROUPS_YAML_TEXT,
|
|
328
|
+
'access.md': '---\nread:\n - group/Reviewer\n---\n',
|
|
329
|
+
});
|
|
330
|
+
// A role of that name exists, but group/ never reaches a role.
|
|
331
|
+
expect(await svc.canRead(workspaceId, 'rev@x.io', 'Knowledge/Doc.md')).toBe(false);
|
|
300
332
|
});
|
|
301
333
|
|
|
302
334
|
it('role/Admin resolves to the ROLE even when a synced group is named Admin', async () => {
|
|
@@ -308,8 +340,9 @@ describe('group files as access principals', () => {
|
|
|
308
340
|
// The explicit token is the role — the shadowing group gets nothing here.
|
|
309
341
|
expect(await svc.canWrite(workspaceId, 'admin@x.io', 'Knowledge/Doc.md')).toBe(true);
|
|
310
342
|
expect(await svc.canWrite(workspaceId, 'attacker@evil.io', 'Knowledge/Doc.md')).toBe(false);
|
|
311
|
-
// The BARE token is the
|
|
312
|
-
expect(await svc.canRead(workspaceId, 'attacker@evil.io', 'Knowledge/Doc.md')).toBe(
|
|
343
|
+
// The BARE token is the role too (role-first): the group's member gets nothing.
|
|
344
|
+
expect(await svc.canRead(workspaceId, 'attacker@evil.io', 'Knowledge/Doc.md')).toBe(false);
|
|
345
|
+
expect(await svc.canRead(workspaceId, 'admin@x.io', 'Knowledge/Doc.md')).toBe(true);
|
|
313
346
|
// Admin CAPABILITY never leaks to the group: roles.yaml write is a pure
|
|
314
347
|
// is-admin check keyed on the role, and admin-rescue keeps working.
|
|
315
348
|
expect(await svc.canWrite(workspaceId, 'attacker@evil.io', 'roles.yaml')).toBe(false);
|
|
@@ -403,14 +436,14 @@ describe('group files as access principals', () => {
|
|
|
403
436
|
});
|
|
404
437
|
|
|
405
438
|
it('eligible holders keep BOTH kinds when a group and a role share a name', async () => {
|
|
406
|
-
// A group named Engineering
|
|
407
|
-
// grants BOTH
|
|
408
|
-
// role's) — the eligible list must carry two entries, one per kind:
|
|
409
|
-
// collapsing them hides the
|
|
439
|
+
// A group named Engineering shares the role Engineering's name. When a
|
|
440
|
+
// file grants BOTH — group/… (the group's) and the bare or role/… token
|
|
441
|
+
// (the role's) — the eligible list must carry two entries, one per kind:
|
|
442
|
+
// collapsing them hides one of the live grants entirely.
|
|
410
443
|
const svc = await makeService({
|
|
411
444
|
'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Engineering:\n - lead@x.io\n`,
|
|
412
445
|
'groups.yaml': GROUPS_YAML_TEXT,
|
|
413
|
-
'access.md': '---\nread:\n - Engineering\n -
|
|
446
|
+
'access.md': '---\nread:\n - group/Engineering\n - Engineering\n---\n',
|
|
414
447
|
});
|
|
415
448
|
const readers = await svc.eligibleReaders(workspaceId, 'Knowledge/Doc.md');
|
|
416
449
|
expect(readers.principals).toContainEqual({ name: 'Engineering', kind: 'group' });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -361,52 +361,47 @@ describe('AccessMutationService', () => {
|
|
|
361
361
|
expect(await access.canWrite(WS, 'razvan@bevel.software', 'Sales/access.md')).toBe(true);
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
-
it('GROUP deny with a VANISHED group succeeds even when a same-named ROLE keeps
|
|
365
|
-
// The false-negative this pins:
|
|
366
|
-
//
|
|
367
|
-
// post-write effectiveness assert must
|
|
368
|
-
//
|
|
369
|
-
// alias-tolerant
|
|
370
|
-
//
|
|
364
|
+
it('GROUP deny with a VANISHED group succeeds even when a same-named ROLE keeps its grants', async () => {
|
|
365
|
+
// The false-negative this pins: a role named Product Team exists, so the
|
|
366
|
+
// service matches the GROUP's revoke/deny 'exact' — it strips and denies
|
|
367
|
+
// only `group/Product Team`. The post-write effectiveness assert must
|
|
368
|
+
// judge the SAME exact identity: with the group vanished, an
|
|
369
|
+
// alias-tolerant check would read the role's surviving bare and role/
|
|
370
|
+
// grants as the group still having access, roll back a fully effective
|
|
371
371
|
// deny, and answer 409 deny-ineffective.
|
|
372
372
|
await write(
|
|
373
373
|
repo,
|
|
374
374
|
'Sales/access.md',
|
|
375
|
-
// The
|
|
376
|
-
// same-named ROLE's
|
|
377
|
-
'---\nwrite:\n - Product Team\n - role/Product Team\n---\n# Sales folder\n',
|
|
375
|
+
// The group/ grant is the (now vanished) GROUP's; the bare and role/
|
|
376
|
+
// grants are the same-named ROLE's. No groups.yaml — the group is gone.
|
|
377
|
+
'---\nwrite:\n - group/Product Team\n - Product Team\n - role/Product Team\n---\n# Sales folder\n',
|
|
378
378
|
);
|
|
379
379
|
access.invalidate(WS);
|
|
380
380
|
|
|
381
|
-
const r = await mutation.denyHere(
|
|
382
|
-
WS,
|
|
383
|
-
'folder',
|
|
384
|
-
'Sales',
|
|
385
|
-
{ kind: 'role', role: 'Product Team' },
|
|
386
|
-
undefined,
|
|
387
|
-
{ tokenMatch: 'exact' },
|
|
388
|
-
);
|
|
381
|
+
const r = await mutation.denyHere(WS, 'folder', 'Sales', { kind: 'role', role: 'group/Product Team' });
|
|
389
382
|
expect(r.changed).toBe(true);
|
|
390
383
|
access.invalidate(WS);
|
|
391
384
|
|
|
392
385
|
const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
|
|
393
|
-
// The
|
|
394
|
-
expect(text).toContain('deny Product Team');
|
|
395
|
-
// ...and the ROLE's own
|
|
386
|
+
// The group's grant became a deny...
|
|
387
|
+
expect(text).toContain('deny group/Product Team');
|
|
388
|
+
// ...and the ROLE's own grants were never touched.
|
|
389
|
+
expect(text).toMatch(/^ {2}- Product Team$/m);
|
|
396
390
|
expect(text).toContain('role/Product Team');
|
|
391
|
+
expect(text).not.toContain('deny Product Team');
|
|
397
392
|
expect(text).not.toContain('deny role/Product Team');
|
|
398
|
-
// The role still resolves its grant
|
|
393
|
+
// The role still resolves its grant.
|
|
399
394
|
const roleSources = await access.grantSources(WS, 'folder', 'Sales', {
|
|
400
395
|
kind: 'role',
|
|
401
396
|
role: 'role/Product Team',
|
|
402
397
|
});
|
|
403
398
|
expect(roleSources.write).toEqual([{ kind: 'direct' }]);
|
|
404
|
-
// The
|
|
399
|
+
// The group's exact token holds nothing anymore.
|
|
405
400
|
const groupSources = await access.grantSources(
|
|
406
401
|
WS,
|
|
407
402
|
'folder',
|
|
408
403
|
'Sales',
|
|
409
|
-
{ kind: 'role', role: 'Product Team' },
|
|
404
|
+
{ kind: 'role', role: 'group/Product Team' },
|
|
410
405
|
{ tokenMatch: 'exact' },
|
|
411
406
|
);
|
|
412
407
|
expect(groupSources).toEqual({});
|
|
@@ -548,16 +543,17 @@ describe('AccessMutationService', () => {
|
|
|
548
543
|
});
|
|
549
544
|
|
|
550
545
|
|
|
551
|
-
// The
|
|
552
|
-
//
|
|
553
|
-
describe('token-kind family —
|
|
546
|
+
// The grant/revoke x collided/uncollided cases, end-to-end through the REAL
|
|
547
|
+
// resolver (kbPrincipals decides who owns the bare name) — the token-kind family.
|
|
548
|
+
describe('token-kind family — role-first revoke, exact-token grant (real resolver)', () => {
|
|
554
549
|
let root: string;
|
|
555
550
|
let repo: string;
|
|
556
551
|
let access: AccessControlService;
|
|
557
552
|
let mutation: AccessMutationService;
|
|
558
553
|
|
|
559
554
|
const roleP: Principal = { kind: 'role', role: 'role/Product Team' }; // the ROLE, explicit
|
|
560
|
-
const bareP: Principal = { kind: 'role', role: 'Product Team' }; // the bare name (
|
|
555
|
+
const bareP: Principal = { kind: 'role', role: 'Product Team' }; // the bare name (always the role here)
|
|
556
|
+
const groupP: Principal = { kind: 'role', role: 'group/Product Team' }; // the GROUP, explicit
|
|
561
557
|
|
|
562
558
|
beforeEach(async () => {
|
|
563
559
|
root = await mkTmpRoot();
|
|
@@ -575,64 +571,94 @@ describe('token-kind family — shadow-aware revoke, exact-token grant (real res
|
|
|
575
571
|
await fs.rm(root, { recursive: true, force: true });
|
|
576
572
|
});
|
|
577
573
|
|
|
578
|
-
/**
|
|
579
|
-
async function
|
|
574
|
+
/** Collision on = a GROUP named "Product Team" exists beside the role. */
|
|
575
|
+
async function collide(on: boolean): Promise<void> {
|
|
580
576
|
if (on) {
|
|
581
|
-
await write(repo, 'groups.yaml', 'groups:\n Product Team:\n - pat@x.io\n');
|
|
577
|
+
await write(repo, 'groups.yaml', 'groups:\n Product Team:\n - pat@x.io\n Design:\n - dee@x.io\n');
|
|
582
578
|
}
|
|
583
579
|
access.invalidate(WS);
|
|
584
580
|
}
|
|
585
581
|
|
|
586
|
-
it('GRANT
|
|
587
|
-
await
|
|
588
|
-
await write(repo, 'Sales/access.md', '---\nwrite:\n - role/Product Team\n---\n');
|
|
589
|
-
const r = await mutation.grant(WS, 'folder', 'Sales', 'write',
|
|
590
|
-
expect(r.changed).toBe(true); // exact-token idempotency:
|
|
582
|
+
it('GRANT collided: the group grant lands even though the role is already granted', async () => {
|
|
583
|
+
await collide(true);
|
|
584
|
+
await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
|
|
585
|
+
const r = await mutation.grant(WS, 'folder', 'Sales', 'write', groupP);
|
|
586
|
+
expect(r.changed).toBe(true); // exact-token idempotency: group/ is a different principal
|
|
591
587
|
const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
|
|
592
|
-
expect(text).toContain('-
|
|
593
|
-
|
|
594
|
-
// The group member now resolves write through the bare token.
|
|
588
|
+
expect(text.split('\n')).toContain(' - group/Product Team');
|
|
589
|
+
// The group member now resolves write through group/.
|
|
595
590
|
access.invalidate(WS);
|
|
596
591
|
expect(await access.canWrite(WS, 'pat@x.io', 'Sales/Deal.md')).toBe(true);
|
|
597
592
|
});
|
|
598
593
|
|
|
599
|
-
it('GRANT
|
|
600
|
-
await
|
|
594
|
+
it('GRANT uncollided: same-spelling grants stay idempotent', async () => {
|
|
595
|
+
await collide(false);
|
|
601
596
|
await write(repo, 'Sales/access.md', '---\nwrite:\n - role/Product Team\n---\n');
|
|
602
597
|
const r = await mutation.grant(WS, 'folder', 'Sales', 'write', roleP);
|
|
603
598
|
expect(r.changed).toBe(false);
|
|
604
599
|
});
|
|
605
600
|
|
|
606
|
-
it("REVOKE
|
|
607
|
-
await
|
|
601
|
+
it("REVOKE on a model-load failure: a ROLE revoke stays exact and keeps the bare token", async () => {
|
|
602
|
+
await collide(true);
|
|
608
603
|
await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
|
|
604
|
+
// A failed load answers empty principal lists: nothing says who owns the bare name.
|
|
605
|
+
vi.spyOn(access, 'kbPrincipals').mockResolvedValue({ roles: [], groups: [], plugins: [] } as never);
|
|
606
|
+
const r = await mutation.revoke(WS, 'folder', 'Sales', roleP, 'admin@x');
|
|
607
|
+
expect(r.changed).toBe(true);
|
|
608
|
+
const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
|
|
609
|
+
expect(text).not.toContain('role/Product Team');
|
|
610
|
+
expect(text.split('\n')).toContain(' - Product Team');
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("REVOKE collided: revoking the ROLE strips its bare and role/ spellings, never the group's", async () => {
|
|
614
|
+
await collide(true);
|
|
615
|
+
await write(
|
|
616
|
+
repo,
|
|
617
|
+
'Sales/access.md',
|
|
618
|
+
'---\nwrite:\n - Product Team\n - role/Product Team\n - group/Product Team\n---\n',
|
|
619
|
+
);
|
|
609
620
|
const r = await mutation.revoke(WS, 'folder', 'Sales', roleP, 'admin@x');
|
|
610
621
|
expect(r.changed).toBe(true);
|
|
611
622
|
const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
|
|
612
623
|
expect(text).not.toContain('role/Product Team');
|
|
613
|
-
expect(text.split('\n')).toContain(' - Product Team');
|
|
614
|
-
//
|
|
624
|
+
expect(text.split('\n')).not.toContain(' - Product Team');
|
|
625
|
+
expect(text.split('\n')).toContain(' - group/Product Team'); // the GROUP's token survives
|
|
615
626
|
access.invalidate(WS);
|
|
616
627
|
expect(await access.canWrite(WS, 'pat@x.io', 'Sales/Deal.md')).toBe(true);
|
|
628
|
+
expect(await access.canWrite(WS, 'felix@example.com', 'Sales/Deal.md')).toBe(false);
|
|
617
629
|
});
|
|
618
630
|
|
|
619
|
-
it("REVOKE
|
|
620
|
-
await
|
|
621
|
-
await write(
|
|
622
|
-
|
|
631
|
+
it("REVOKE collided: revoking the GROUP strips group/ only — the bare name is the role's", async () => {
|
|
632
|
+
await collide(true);
|
|
633
|
+
await write(
|
|
634
|
+
repo,
|
|
635
|
+
'Sales/access.md',
|
|
636
|
+
'---\nwrite:\n - Product Team\n - role/Product Team\n - group/Product Team\n---\n',
|
|
637
|
+
);
|
|
638
|
+
const r = await mutation.revoke(WS, 'folder', 'Sales', groupP, 'admin@x');
|
|
623
639
|
expect(r.changed).toBe(true);
|
|
624
640
|
const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
|
|
625
|
-
expect(text).toContain('
|
|
626
|
-
expect(text.split('\n')).
|
|
627
|
-
|
|
641
|
+
expect(text).not.toContain('group/Product Team');
|
|
642
|
+
expect(text.split('\n')).toContain(' - Product Team');
|
|
643
|
+
expect(text).toContain('- role/Product Team');
|
|
628
644
|
access.invalidate(WS);
|
|
629
645
|
expect(await access.canWrite(WS, 'felix@example.com', 'Sales/Deal.md')).toBe(true);
|
|
630
|
-
// The group member no longer does.
|
|
631
646
|
expect(await access.canWrite(WS, 'pat@x.io', 'Sales/Deal.md')).toBe(false);
|
|
632
647
|
});
|
|
633
648
|
|
|
634
|
-
it('REVOKE
|
|
635
|
-
await
|
|
649
|
+
it('REVOKE: a group no role shares a name with strips group/ AND its legacy bare spelling', async () => {
|
|
650
|
+
await collide(true);
|
|
651
|
+
await write(repo, 'Sales/access.md', '---\nwrite:\n - Design\n - group/Design\n---\n');
|
|
652
|
+
const r = await mutation.revoke(WS, 'folder', 'Sales', { kind: 'role', role: 'group/Design' }, 'admin@x');
|
|
653
|
+
expect(r.changed).toBe(true);
|
|
654
|
+
const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
|
|
655
|
+
expect(text).not.toContain('Design');
|
|
656
|
+
access.invalidate(WS);
|
|
657
|
+
expect(await access.canWrite(WS, 'dee@x.io', 'Sales/Deal.md')).toBe(false);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('REVOKE uncollided: revoking the role strips BOTH spellings (legacy bare cleanup)', async () => {
|
|
661
|
+
await collide(false);
|
|
636
662
|
await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
|
|
637
663
|
const r = await mutation.revoke(WS, 'folder', 'Sales', roleP, 'admin@x');
|
|
638
664
|
expect(r.changed).toBe(true);
|
|
@@ -642,12 +668,12 @@ describe('token-kind family — shadow-aware revoke, exact-token grant (real res
|
|
|
642
668
|
expect(await access.canWrite(WS, 'felix@example.com', 'Sales/Deal.md')).toBe(false);
|
|
643
669
|
});
|
|
644
670
|
|
|
645
|
-
it('REVOKE
|
|
646
|
-
await
|
|
671
|
+
it('REVOKE uncollided: a bare revoke also strips both spellings', async () => {
|
|
672
|
+
await collide(false);
|
|
647
673
|
await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
|
|
648
674
|
const r = await mutation.revoke(WS, 'folder', 'Sales', bareP, 'admin@x');
|
|
649
675
|
expect(r.changed).toBe(true);
|
|
650
676
|
const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
|
|
651
|
-
expect(text).not.toContain('Product Team'); //
|
|
677
|
+
expect(text).not.toContain('Product Team'); // both spellings are the role
|
|
652
678
|
});
|
|
653
679
|
});
|
|
@@ -123,9 +123,11 @@ describe('restricting below a parent keeps the principal reported', () => {
|
|
|
123
123
|
principal: Principal,
|
|
124
124
|
picked: Partial<Record<Verb, boolean>> | null,
|
|
125
125
|
): Promise<void> {
|
|
126
|
+
// The mutation service derives its matching (no role shares GTM Team's
|
|
127
|
+
// name, so 'exact'); the read side is told the same.
|
|
126
128
|
const opts = principal.kind === 'role' ? ({ tokenMatch: 'exact' } as const) : undefined;
|
|
127
129
|
if (picked === null) {
|
|
128
|
-
await mutation.denyHere(WS, 'folder', FOLDER, principal, undefined
|
|
130
|
+
await mutation.denyHere(WS, 'folder', FOLDER, principal, undefined);
|
|
129
131
|
access.invalidate(WS);
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
@@ -135,11 +137,11 @@ describe('restricting below a parent keeps the principal reported', () => {
|
|
|
135
137
|
const src = await access.grantSources(WS, 'folder', FOLDER, toGrantPrincipal(principal), opts);
|
|
136
138
|
const list = src[verb] ?? [];
|
|
137
139
|
const localOnly = list.length > 0 && list.every((s) => s.kind === 'direct');
|
|
138
|
-
if (localOnly) await mutation.revoke(WS, 'folder', FOLDER, principal, 'me@x', verb
|
|
139
|
-
else await mutation.denyHere(WS, 'folder', FOLDER, principal, verb
|
|
140
|
+
if (localOnly) await mutation.revoke(WS, 'folder', FOLDER, principal, 'me@x', verb);
|
|
141
|
+
else await mutation.denyHere(WS, 'folder', FOLDER, principal, verb);
|
|
140
142
|
access.invalidate(WS);
|
|
141
143
|
if (localOnly && (await effective(principal))[verb]) {
|
|
142
|
-
await mutation.denyHere(WS, 'folder', FOLDER, principal, verb
|
|
144
|
+
await mutation.denyHere(WS, 'folder', FOLDER, principal, verb);
|
|
143
145
|
access.invalidate(WS);
|
|
144
146
|
}
|
|
145
147
|
}
|
|
@@ -153,7 +155,7 @@ describe('restricting below a parent keeps the principal reported', () => {
|
|
|
153
155
|
opts,
|
|
154
156
|
);
|
|
155
157
|
if (!(denials[verb] ?? []).some((s) => s.kind === 'direct')) continue;
|
|
156
|
-
await mutation.revoke(WS, 'folder', FOLDER, principal, 'me@x', verb
|
|
158
|
+
await mutation.revoke(WS, 'folder', FOLDER, principal, 'me@x', verb);
|
|
157
159
|
access.invalidate(WS);
|
|
158
160
|
}
|
|
159
161
|
for (const verb of minimalGrants(picked)) {
|
|
@@ -11,6 +11,7 @@ import type { WorkflowEventBus } from '../../workflow/event-bus.js';
|
|
|
11
11
|
import { createAccessRoutes } from '../access.routes.js';
|
|
12
12
|
import { usersDbDouble } from './users-db-double.js';
|
|
13
13
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
14
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* HTTP contract for `POST /access/batch` — the verb dispatch. Existing callers
|
|
@@ -66,6 +67,7 @@ async function makeHarness(): Promise<Harness> {
|
|
|
66
67
|
{ emit: vi.fn() } as unknown as WorkflowEventBus,
|
|
67
68
|
usersDbDouble(),
|
|
68
69
|
testKbContext({ kbDirName: KB }),
|
|
70
|
+
new NodeFs(),
|
|
69
71
|
),
|
|
70
72
|
);
|
|
71
73
|
|