@bevel-software/platform-core-backend 0.21.0 → 0.23.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/dist/core/core-ports.d.ts +11 -1
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +62 -13
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +158 -23
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +34 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +89 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +0 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +11 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +42 -12
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +67 -10
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +214 -35
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-requests.contract.d.ts +75 -0
- package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
- package/dist/modules/access/access-requests.contract.js +20 -0
- package/dist/modules/access/access-requests.contract.js.map +1 -0
- package/dist/modules/access/access-requests.routes.d.ts +35 -0
- package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
- package/dist/modules/access/access-requests.routes.js +237 -0
- package/dist/modules/access/access-requests.routes.js.map +1 -0
- package/dist/modules/access/access-requests.service.d.ts +123 -0
- package/dist/modules/access/access-requests.service.d.ts.map +1 -0
- package/dist/modules/access/access-requests.service.js +337 -0
- package/dist/modules/access/access-requests.service.js.map +1 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +13 -15
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +12 -0
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +24 -0
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +58 -7
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +44 -1
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +61 -5
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +1 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +18 -7
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +8 -0
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +96 -3
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +187 -9
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +7 -2
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +41 -24
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +35 -27
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/github-app/github-app.client.d.ts +101 -0
- package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.client.js +222 -0
- package/dist/modules/github-app/github-app.client.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +102 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.connection.js +185 -0
- package/dist/modules/github-app/github-app.connection.js.map +1 -0
- package/dist/modules/github-app/github-app.routes.d.ts +59 -0
- package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.routes.js +323 -0
- package/dist/modules/github-app/github-app.routes.js.map +1 -0
- package/dist/modules/github-app/index.d.ts +4 -0
- package/dist/modules/github-app/index.d.ts.map +1 -0
- package/dist/modules/github-app/index.js +4 -0
- package/dist/modules/github-app/index.js.map +1 -0
- package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
- package/dist/modules/kb-fs/branch-name.js +12 -2
- package/dist/modules/kb-fs/branch-name.js.map +1 -1
- package/dist/modules/kb-fs/remote-url.d.ts +22 -0
- package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
- package/dist/modules/kb-fs/remote-url.js +35 -0
- package/dist/modules/kb-fs/remote-url.js.map +1 -0
- package/dist/modules/kb-fs/repo-path.d.ts +11 -16
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +79 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
- package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
- package/dist/modules/kb-sync/sync-auth.js +1 -1
- package/dist/modules/kb-sync/sync-auth.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/plugins/join-proposals.d.ts +17 -5
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
- package/dist/modules/plugins/join-proposals.js +76 -22
- package/dist/modules/plugins/join-proposals.js.map +1 -1
- package/dist/modules/plugins/join-requests.service.d.ts +111 -18
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-requests.service.js +173 -29
- package/dist/modules/plugins/join-requests.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +3 -2
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +101 -7
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/managed-repository.d.ts +43 -0
- package/dist/modules/settings/managed-repository.d.ts.map +1 -0
- package/dist/modules/settings/managed-repository.js +60 -0
- package/dist/modules/settings/managed-repository.js.map +1 -0
- package/dist/modules/settings/repository-source.d.ts +128 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -0
- package/dist/modules/settings/repository-source.js +150 -0
- package/dist/modules/settings/repository-source.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +78 -4
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +379 -17
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.js +24 -63
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +90 -18
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +4 -1
- package/dist/modules/skills/skills.contract.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +98 -9
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +239 -37
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +7 -0
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +71 -7
- package/dist/modules/skills/skills.tools.js.map +1 -1
- 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 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +15 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +25 -4
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
- 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.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +24 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +30 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +170 -6
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +5 -0
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +99 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +55 -0
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
- package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
- package/dist/modules/workflow/workflow-hooks.js +16 -1
- package/dist/modules/workflow/workflow-hooks.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +3 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +94 -13
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +258 -51
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
- package/dist/modules/workspace/agent-access.gate.js +123 -0
- package/dist/modules/workspace/agent-access.gate.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +5 -6
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/session-sink.d.ts +5 -5
- package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
- package/dist/modules/workspace/set-aside-clone.js +92 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +92 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +115 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +272 -26
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +2 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +337 -117
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts +60 -0
- package/dist/modules/write-access/write-access.d.ts.map +1 -0
- package/dist/modules/write-access/write-access.js +129 -0
- package/dist/modules/write-access/write-access.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +64 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +83 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +30 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +26 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts +0 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +0 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/__tests__/kb-layout-config.test.ts +0 -2
- package/src/__tests__/retired-settings.test.ts +96 -0
- package/src/core/__tests__/gated-boot.test.ts +132 -0
- package/src/core/__tests__/lifecycle.test.ts +170 -1
- package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
- package/src/core/core-ports.ts +11 -1
- package/src/core/create-core-server.ts +69 -15
- package/src/core/create-core-services.ts +190 -27
- package/src/core/lifecycle.ts +98 -13
- package/src/core-config.ts +13 -14
- package/src/index.ts +12 -1
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
- package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
- package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
- package/src/modules/access/access-control.interface.ts +43 -12
- package/src/modules/access/access-control.service.ts +234 -36
- package/src/modules/access/access-requests.contract.ts +93 -0
- package/src/modules/access/access-requests.routes.ts +286 -0
- package/src/modules/access/access-requests.service.ts +420 -0
- package/src/modules/access/access.routes.ts +13 -15
- package/src/modules/access-model/access-grammar.ts +22 -0
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
- package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account.routes.ts +63 -7
- package/src/modules/auth/auth.middleware.ts +19 -8
- package/src/modules/auth/auth.routes.ts +8 -0
- package/src/modules/auth/auth.service.ts +207 -7
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
- package/src/modules/github-app/github-app.client.ts +268 -0
- package/src/modules/github-app/github-app.connection.ts +204 -0
- package/src/modules/github-app/github-app.routes.ts +359 -0
- package/src/modules/github-app/index.ts +19 -0
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
- package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
- package/src/modules/kb-fs/branch-name.ts +14 -1
- package/src/modules/kb-fs/remote-url.ts +35 -0
- package/src/modules/kb-fs/repo-path.ts +85 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
- package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
- package/src/modules/kb-sync/sync-auth.ts +2 -2
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
- package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
- package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/plugins/join-proposals.ts +87 -19
- package/src/modules/plugins/join-requests.service.ts +199 -39
- package/src/modules/plugins/plugins.routes.ts +3 -2
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
- package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
- package/src/modules/settings/deployment-settings.service.ts +117 -6
- package/src/modules/settings/managed-repository.ts +68 -0
- package/src/modules/settings/repository-source.ts +197 -0
- package/src/modules/settings/setup.routes.ts +463 -15
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
- package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
- package/src/modules/skills/skill-access-requests.routes.ts +25 -75
- package/src/modules/skills/skills.contract.ts +91 -18
- package/src/modules/skills/skills.service.ts +278 -41
- package/src/modules/skills/skills.tools.ts +80 -8
- package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
- package/src/modules/tool-auth/external-api-key.service.ts +4 -2
- package/src/modules/tool-auth/internal-token.service.ts +3 -3
- package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
- package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
- package/src/modules/tool-helpers/tool-context.ts +15 -0
- package/src/modules/tool-helpers/tool-handler.ts +24 -4
- package/src/modules/tool-helpers/tool.contract.ts +3 -2
- package/src/modules/tool-helpers/validate-token.ts +1 -1
- package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
- package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
- package/src/modules/workflow/file-lock.service.ts +31 -0
- package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
- package/src/modules/workflow/git/git.service.ts +193 -4
- package/src/modules/workflow/git/node-git-runner.ts +6 -0
- package/src/modules/workflow/git/pull-request.service.ts +119 -0
- package/src/modules/workflow/pending-commits.service.ts +59 -1
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.routes.ts +3 -1
- package/src/modules/workflow/workflow.service.ts +290 -55
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
- package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
- package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/routine-write-policy.ts +5 -6
- package/src/modules/workspace/session-sink.ts +5 -5
- package/src/modules/workspace/set-aside-clone.ts +96 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +273 -25
- package/src/modules/workspace/workspace.tools.ts +373 -119
- package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
- package/src/modules/write-access/write-access.ts +153 -0
- package/src/shared/domain-errors.ts +89 -0
- package/src/shared/git.contract.ts +37 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
- package/src/tenancy/static-tenant-source.ts +0 -3
- package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
- package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.policy.js +0 -62
- package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
- package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
- package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.service.js +0 -147
- package/dist/modules/workflow/session-ontology.service.js.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.js +0 -161
- package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
- package/dist/shared/kb-layout.d.ts +0 -39
- package/dist/shared/kb-layout.d.ts.map +0 -1
- package/dist/shared/kb-layout.js +0 -103
- package/dist/shared/kb-layout.js.map +0 -1
- package/dist/shared/kb-layout.test.d.ts +0 -2
- package/dist/shared/kb-layout.test.d.ts.map +0 -1
- package/dist/shared/kb-layout.test.js +0 -75
- package/dist/shared/kb-layout.test.js.map +0 -1
- package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
- package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
- package/src/modules/workflow/session-ontology.policy.ts +0 -70
- package/src/modules/workflow/session-ontology.service.ts +0 -183
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
- package/src/modules/workspace/session-ontology.gate.ts +0 -191
- package/src/shared/kb-layout.test.ts +0 -98
- package/src/shared/kb-layout.ts +0 -102
package/src/core/lifecycle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Server } from 'node:http';
|
|
2
2
|
import { closeDb, type Database } from '../modules/database/connection.js';
|
|
3
3
|
import type { AdvisoryLease } from '../modules/database/advisory-lock.js';
|
|
4
|
-
import {
|
|
4
|
+
import { bootMaySurvive, type KbStartupRunner } from '../modules/workspace/startup/kb-startup-runner.js';
|
|
5
5
|
import { noteBesideCheckout } from '../modules/workspace/startup/beside-checkout.js';
|
|
6
6
|
import { unregisterBevelSecretsVariableLoader } from '../modules/secrets-vault/secrets-variable-loader.js';
|
|
7
7
|
import type { WorkflowService } from '../modules/workflow/workflow.service.js';
|
|
@@ -164,7 +164,13 @@ export function holdCommitWorkerLease(
|
|
|
164
164
|
running = false;
|
|
165
165
|
wake?.();
|
|
166
166
|
await loop;
|
|
167
|
+
// The worker is asked to stop whether or not this process is running
|
|
168
|
+
// it: a worker that can be held still (`holdable`) may be in the
|
|
169
|
+
// middle of a hold that uses the database, and its stop is what
|
|
170
|
+
// waits for that. Stopping a worker that is not running is nothing.
|
|
171
|
+
const idle = !workerRunning && !stopping;
|
|
167
172
|
await stopWorker();
|
|
173
|
+
if (idle) await worker.stop().catch((err: unknown) => log(`commit worker stop failed: ${String(err)}`));
|
|
168
174
|
await lease.release();
|
|
169
175
|
})();
|
|
170
176
|
return stopped;
|
|
@@ -202,6 +208,80 @@ export function withStartupTask(
|
|
|
202
208
|
};
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
/** A worker the lease loop drives, which something else may hold still for a while. */
|
|
212
|
+
export interface HoldableWorker extends LeasedWorker {
|
|
213
|
+
/**
|
|
214
|
+
* Run `work` with the worker stopped, and start it again afterwards if the
|
|
215
|
+
* lease loop still wants it running. A hold waits for the commit in flight
|
|
216
|
+
* to finish, as a stop does. Holds may overlap; the worker starts again
|
|
217
|
+
* when the last one ends, whether `work` resolved or threw.
|
|
218
|
+
*/
|
|
219
|
+
whileHeld<T>(work: () => Promise<T>): Promise<T>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A worker that can be HELD STILL without the lease loop knowing.
|
|
224
|
+
*
|
|
225
|
+
* The lease loop decides whether this process is the one draining the commit
|
|
226
|
+
* queue; it starts the worker when the lease is taken and stops it when the
|
|
227
|
+
* lease is lost. Moving the deployment to another repository needs a second,
|
|
228
|
+
* independent reason for the worker not to run: while working copies are
|
|
229
|
+
* being set aside and cloned again, a queued commit must not be written into
|
|
230
|
+
* a directory that is being renamed away, nor into the fresh clone of a
|
|
231
|
+
* repository it was never meant for. Stopping the loop itself would release
|
|
232
|
+
* the lease, and another process would take the queue over mid-move.
|
|
233
|
+
*
|
|
234
|
+
* So the two reasons are kept apart. `wanted` is the lease loop's word,
|
|
235
|
+
* `holds` counts the moves in flight, and the worker runs exactly when it is
|
|
236
|
+
* wanted and nothing holds it.
|
|
237
|
+
*
|
|
238
|
+
* A stop waits for the holds in flight as well as for the worker. Shutdown
|
|
239
|
+
* awaits this stop (bounded by its own budget) before it ends the database
|
|
240
|
+
* pool, and a move still setting copies aside and cloning needs that pool:
|
|
241
|
+
* it is either finished, or known to have been cut.
|
|
242
|
+
*/
|
|
243
|
+
export function holdable(worker: LeasedWorker): HoldableWorker {
|
|
244
|
+
let wanted = false;
|
|
245
|
+
const inFlight = new Set<Promise<unknown>>();
|
|
246
|
+
/**
|
|
247
|
+
* ONE stop at a time, shared by everyone who asks. A worker's own stop is
|
|
248
|
+
* idempotent by answering a second caller at once, while the first is
|
|
249
|
+
* still waiting for the commit in flight; a second hold given that answer
|
|
250
|
+
* would set a working copy aside under that commit.
|
|
251
|
+
*/
|
|
252
|
+
let stopping: Promise<void> | null = null;
|
|
253
|
+
const stopWorker = (): Promise<void> => {
|
|
254
|
+
stopping ??= worker.stop().finally(() => {
|
|
255
|
+
stopping = null;
|
|
256
|
+
});
|
|
257
|
+
return stopping;
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
start() {
|
|
261
|
+
wanted = true;
|
|
262
|
+
if (inFlight.size === 0) worker.start();
|
|
263
|
+
},
|
|
264
|
+
async stop() {
|
|
265
|
+
wanted = false;
|
|
266
|
+
await stopWorker();
|
|
267
|
+
await Promise.allSettled([...inFlight]);
|
|
268
|
+
},
|
|
269
|
+
whileHeld<T>(work: () => Promise<T>): Promise<T> {
|
|
270
|
+
const held = (async () => {
|
|
271
|
+
await stopWorker();
|
|
272
|
+
return work();
|
|
273
|
+
})();
|
|
274
|
+
inFlight.add(held);
|
|
275
|
+
const ended = (): void => {
|
|
276
|
+
inFlight.delete(held);
|
|
277
|
+
if (inFlight.size === 0 && wanted) worker.start();
|
|
278
|
+
};
|
|
279
|
+
held.then(ended, ended);
|
|
280
|
+
return held;
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
205
285
|
/**
|
|
206
286
|
* What one knowledge base's graph needs of itself to be STARTED: the boot
|
|
207
287
|
* side effects that used to run inside `createCoreServer`, named so a host
|
|
@@ -228,7 +308,8 @@ export interface BootableCore {
|
|
|
228
308
|
* jobs. `createCoreServer` runs this for a single-tenant deployment; a host
|
|
229
309
|
* serving several knowledge bases runs it when a tenant is activated.
|
|
230
310
|
*
|
|
231
|
-
* Throws to stop the boot on every failure but
|
|
311
|
+
* Throws to stop the boot on every failure but the ones the host answered
|
|
312
|
+
* about the repository or the credentials (see `bootMaySurvive`) — the
|
|
232
313
|
* container's restart policy is the retry — see `kb-startup-runner.ts`.
|
|
233
314
|
*/
|
|
234
315
|
export async function startCore<C extends BootableCore>(
|
|
@@ -267,18 +348,22 @@ export async function startCore<C extends BootableCore>(
|
|
|
267
348
|
try {
|
|
268
349
|
await core.kbStartupRunner.runAll();
|
|
269
350
|
} catch (err) {
|
|
270
|
-
// The
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
351
|
+
// The failures a boot survives: the host ANSWERED about the repository or
|
|
352
|
+
// the credentials, or could not be reached at all. None of them says
|
|
353
|
+
// anything about the knowledge base — what we would write is not known to
|
|
354
|
+
// be wrong — and every one of them is fixed somewhere else: the host comes
|
|
355
|
+
// back, the repository is created, the token is granted access, a rotated
|
|
356
|
+
// token is typed into the setup screen. Refusing to boot took away the
|
|
357
|
+
// login and setup screens the fix is entered on, and on 2026-09-28 a
|
|
358
|
+
// repository that had been replaced crash-looped a deployment for exactly
|
|
359
|
+
// that reason. The deployment comes up GATED instead: the setup routes
|
|
275
360
|
// read the runner's standing failure and keep the app shut, the setup
|
|
276
|
-
// screen shows why, saving it retries, and the runner keeps trying on
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
if (!(err
|
|
280
|
-
startupLog.error('booting UNMAINTAINED and gated — the knowledge
|
|
281
|
-
detail: err.message,
|
|
361
|
+
// screen shows why, saving it retries, and the runner keeps trying on its
|
|
362
|
+
// own. Every other failure still stops the boot, because it means the
|
|
363
|
+
// template or a step would write something wrong. See `bootMaySurvive`.
|
|
364
|
+
if (!bootMaySurvive(err)) throw err;
|
|
365
|
+
startupLog.error('booting UNMAINTAINED and gated — the knowledge base could not be initialized:', {
|
|
366
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
282
367
|
});
|
|
283
368
|
// Kept on the graph so that stopping the graph stops the asking: a
|
|
284
369
|
// retry that outlived its graph would clone into a workspaces folder
|
package/src/core-config.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { assertKeyDecodesTo32Bytes } from './shared/token-crypto.js';
|
|
|
6
6
|
import { DEFAULT_GIT_TIMEOUT_MS } from './modules/workflow/git/node-git-runner.js';
|
|
7
7
|
import { DEFAULT_DB_SCHEMA, assertSchemaName } from './modules/database/connection.js';
|
|
8
8
|
import { logger } from './shared/logging.js';
|
|
9
|
+
import { hasControlCharacter } from './modules/kb-fs/repo-path.js';
|
|
10
|
+
import { sanitizedPath } from './shared/printable.js';
|
|
9
11
|
|
|
10
12
|
const log = logger('config');
|
|
11
13
|
|
|
@@ -100,7 +102,6 @@ export interface TenantConfig {
|
|
|
100
102
|
readonly gitUsername: string;
|
|
101
103
|
readonly gitToken: string;
|
|
102
104
|
readonly kbTemplateDir: string;
|
|
103
|
-
readonly ontologySessionBlock: boolean;
|
|
104
105
|
readonly updateCheckEnabled: boolean;
|
|
105
106
|
readonly loginPasswordEnabled: boolean;
|
|
106
107
|
readonly allowedEmailDomains: string[];
|
|
@@ -259,17 +260,6 @@ export class CoreConfig implements TenantConfig, ProcessConfig {
|
|
|
259
260
|
* existing one. Override with `KB_TEMPLATE_DIR`.
|
|
260
261
|
*/
|
|
261
262
|
readonly kbTemplateDir: string;
|
|
262
|
-
/**
|
|
263
|
-
* Ontology-session boundary kill-switch. When true (default), an agent run
|
|
264
|
-
* that has read across more than one ontology can no longer write. A no-op on
|
|
265
|
-
* a single-ontology KB. Set `ONTOLOGY_SESSION_BLOCK=false` to disable.
|
|
266
|
-
*
|
|
267
|
-
* Stays on CORE config (not `AppConfig`) even though the write BLOCK itself
|
|
268
|
-
* is an enterprise-registered hook: the flag also switches off the core-owned
|
|
269
|
-
* touch TRACKING (the gate skips entirely when false — see
|
|
270
|
-
* `session-ontology.gate.ts`), so it must exist wherever the tracking runs.
|
|
271
|
-
*/
|
|
272
|
-
readonly ontologySessionBlock: boolean;
|
|
273
263
|
/**
|
|
274
264
|
* In-app update check. When true (default), `GET /api/update-check` lazily
|
|
275
265
|
* asks api.github.com for the newest Hexis release — only when an admin's
|
|
@@ -469,8 +459,6 @@ export class CoreConfig implements TenantConfig, ProcessConfig {
|
|
|
469
459
|
// Default: the `kb-template/` folder shipped inside this package (works
|
|
470
460
|
// both from src/ and compiled dist/ — see assets.ts).
|
|
471
461
|
this.kbTemplateDir = process.env.KB_TEMPLATE_DIR || defaultKbTemplateDir();
|
|
472
|
-
this.ontologySessionBlock =
|
|
473
|
-
(process.env.ONTOLOGY_SESSION_BLOCK ?? 'true').trim().toLowerCase() !== 'false';
|
|
474
462
|
this.updateCheckEnabled =
|
|
475
463
|
(process.env.UPDATE_CHECK ?? 'true').trim().toLowerCase() !== 'false';
|
|
476
464
|
this.allowedEmailDomains = (process.env.ALLOWED_EMAIL_DOMAINS || '')
|
|
@@ -621,5 +609,16 @@ export class CoreConfig implements TenantConfig, ProcessConfig {
|
|
|
621
609
|
) {
|
|
622
610
|
throw new Error(`KB_DIR_NAME must be a single path segment: ${this.kbDirName}`);
|
|
623
611
|
}
|
|
612
|
+
// A control character, a line break above all: this name is PREFIXED onto
|
|
613
|
+
// every workspace-relative path, and a line break in a path is a separator
|
|
614
|
+
// in git's line-oriented stdin protocols (`cat-file --batch` reads one
|
|
615
|
+
// `<ref>:<path>` per line). A name carrying one would make the normaliser
|
|
616
|
+
// emit the very path it refuses from a caller — so the deployment fails to
|
|
617
|
+
// boot rather than resolving paths nothing downstream can safely read.
|
|
618
|
+
if (hasControlCharacter(this.kbDirName)) {
|
|
619
|
+
throw new Error(
|
|
620
|
+
`KB_DIR_NAME must not contain a control character: "${sanitizedPath(this.kbDirName)}"`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
624
623
|
}
|
|
625
624
|
}
|
package/src/index.ts
CHANGED
|
@@ -101,8 +101,18 @@ export type {
|
|
|
101
101
|
export type { ILlmUsageMeter } from './modules/tool-auth/llm-usage-meter.js';
|
|
102
102
|
export type { AuthProviderPlugin } from './modules/auth/auth.routes.js';
|
|
103
103
|
export type { IErasureParticipant } from './modules/auth/account-erasure.service.js';
|
|
104
|
+
export {
|
|
105
|
+
alwaysWritable,
|
|
106
|
+
READ_ONLY_CODE,
|
|
107
|
+
type IWriteAccess,
|
|
108
|
+
type WriteAccessVerdict,
|
|
109
|
+
} from './modules/write-access/write-access.js';
|
|
104
110
|
export {
|
|
105
111
|
AccountAdmissionRefusedError,
|
|
112
|
+
AccountDeactivatedError,
|
|
113
|
+
AccountChangeRefusedError,
|
|
114
|
+
AuthBackendError,
|
|
115
|
+
ACCOUNT_DEACTIVATED_MESSAGE,
|
|
106
116
|
admitEveryone,
|
|
107
117
|
type AccountAdmissionVerdict,
|
|
108
118
|
type AccountProvisionReason,
|
|
@@ -112,6 +122,7 @@ export {
|
|
|
112
122
|
WorkflowHooks,
|
|
113
123
|
type CommitValidationHook,
|
|
114
124
|
type CommitValidationContext,
|
|
125
|
+
type AgentOperationContext,
|
|
126
|
+
type AgentReadHook,
|
|
115
127
|
type PreWriteHook,
|
|
116
|
-
type PreWriteContext,
|
|
117
128
|
} from './modules/workflow/workflow-hooks.js';
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
|
|
6
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
7
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
8
|
+
import { AccessControlService } from '../access-control.service.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `previewAccessAfterRelocation`: the caller's OWN verbs at a destination as
|
|
12
|
+
* they will be once a move or a copy has landed there.
|
|
13
|
+
*
|
|
14
|
+
* The question the four gates cannot answer, because the `access.md` files a
|
|
15
|
+
* folder carries are not at the destination yet — which is how a rename came
|
|
16
|
+
* to warn about losing owner access the folder's own rules were about to hand
|
|
17
|
+
* straight back. Driven over a real on-disk tree, and every answer checked
|
|
18
|
+
* against the ordinary gates once the tree really says what the preview said
|
|
19
|
+
* it would.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const KB_DIR = 'knowledge-base';
|
|
23
|
+
const MOVER = 'mover@x.io';
|
|
24
|
+
|
|
25
|
+
function stubWorkspaceService(workspaceId: string, workspaceDir: string): WorkspaceService {
|
|
26
|
+
return {
|
|
27
|
+
getWorkspacePath: async (id: string) => {
|
|
28
|
+
if (id !== workspaceId) throw new Error(`unexpected workspace ${id}`);
|
|
29
|
+
return workspaceDir;
|
|
30
|
+
},
|
|
31
|
+
ensureRemotesFetched: async () => undefined,
|
|
32
|
+
} as unknown as WorkspaceService;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const rules = (body: string) => `---\n${body}---\n`;
|
|
36
|
+
|
|
37
|
+
describe('AccessControlService.previewAccessAfterRelocation', () => {
|
|
38
|
+
let root: string;
|
|
39
|
+
let repo: string;
|
|
40
|
+
const workspaceId = 'ws-preview-relocation';
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-preview-reloc-'));
|
|
44
|
+
repo = path.join(root, workspaceId, KB_DIR);
|
|
45
|
+
});
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
if (root) await fs.rm(root, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
async function write(files: Record<string, string>): Promise<void> {
|
|
51
|
+
for (const [rel, contents] of Object.entries(files)) {
|
|
52
|
+
const abs = path.join(repo, rel);
|
|
53
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
54
|
+
await fs.writeFile(abs, contents);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A service reading the tree as it is NOW. Built fresh after every change
|
|
60
|
+
* on purpose: in production the file-change notifier invalidates the model,
|
|
61
|
+
* and a test that leaned on the five-second cache would be asserting the
|
|
62
|
+
* cache rather than the resolver.
|
|
63
|
+
*/
|
|
64
|
+
const service = () =>
|
|
65
|
+
new AccessControlService(
|
|
66
|
+
stubWorkspaceService(workspaceId, path.join(root, workspaceId)),
|
|
67
|
+
KB_DIR,
|
|
68
|
+
new NodeFs(),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/** The caller's four verbs at a path, through the ordinary gates. */
|
|
72
|
+
async function verbsAt(svc: AccessControlService, rel: string) {
|
|
73
|
+
const [read, write, download, owner] = await Promise.all([
|
|
74
|
+
svc.canRead(workspaceId, MOVER, rel),
|
|
75
|
+
svc.canWrite(workspaceId, MOVER, rel),
|
|
76
|
+
svc.canDownload(workspaceId, MOVER, rel),
|
|
77
|
+
svc.canOwner(workspaceId, MOVER, rel),
|
|
78
|
+
]);
|
|
79
|
+
return { read, write, download, owner };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Move `from` to `to` on disk, as the move itself does. */
|
|
83
|
+
async function reallyMove(from: string, to: string): Promise<void> {
|
|
84
|
+
const dest = path.join(repo, to);
|
|
85
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
86
|
+
await fs.rename(path.join(repo, from), dest);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Copy `from` to `to` on disk, leaving the source where it is. */
|
|
90
|
+
async function reallyCopy(from: string, to: string): Promise<void> {
|
|
91
|
+
const dest = path.join(repo, to);
|
|
92
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
93
|
+
await fs.cp(path.join(repo, from), dest, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const BASE = {
|
|
97
|
+
'roles.yaml': 'roles:\n Admin:\n - admin@x.io\n',
|
|
98
|
+
'access.md': rules('read:\n - everyone\n'),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
it('a folder keeps the access its own access.md gives it when it is renamed', async () => {
|
|
102
|
+
await write({
|
|
103
|
+
...BASE,
|
|
104
|
+
'Sales/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
|
|
105
|
+
'Sales/deal.md': '# Deal\n',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const svc = service();
|
|
109
|
+
const before = await verbsAt(svc, 'Sales');
|
|
110
|
+
const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Sales', 'Revenue');
|
|
111
|
+
|
|
112
|
+
expect(before.owner).toBe(true);
|
|
113
|
+
expect(after).toEqual(before);
|
|
114
|
+
|
|
115
|
+
// The root grants read and nothing else, so the destination AS IT STANDS
|
|
116
|
+
// says the opposite — which is the warning people were shown.
|
|
117
|
+
expect(await verbsAt(svc, 'Revenue')).toMatchObject({ write: false, owner: false });
|
|
118
|
+
|
|
119
|
+
await reallyMove('Sales', 'Revenue');
|
|
120
|
+
expect(await verbsAt(service(), 'Revenue')).toEqual(after);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('a folder with no access.md of its own takes the new parent\'s rules, as it always did', async () => {
|
|
124
|
+
await write({
|
|
125
|
+
...BASE,
|
|
126
|
+
'Work/access.md': rules(`write:\n - Mover <${MOVER}>\n`),
|
|
127
|
+
'Work/Notes/note.md': '# Note\n',
|
|
128
|
+
'Legal/access.md': rules('read:\n - everyone\n'),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const svc = service();
|
|
132
|
+
const before = await verbsAt(svc, 'Work/Notes');
|
|
133
|
+
const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Notes', 'Legal/Notes');
|
|
134
|
+
|
|
135
|
+
expect(before.write).toBe(true);
|
|
136
|
+
expect(after.write).toBe(false);
|
|
137
|
+
// Nothing travels, so the preview is exactly what the gates say today.
|
|
138
|
+
expect(after).toEqual(await verbsAt(svc, 'Legal/Notes'));
|
|
139
|
+
|
|
140
|
+
await reallyMove('Work/Notes', 'Legal/Notes');
|
|
141
|
+
expect(await verbsAt(service(), 'Legal/Notes')).toEqual(after);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('what the folder inherited is left behind; what it carries comes along', async () => {
|
|
145
|
+
await write({
|
|
146
|
+
...BASE,
|
|
147
|
+
'Work/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
|
|
148
|
+
'Work/Team/access.md': rules(`write:\n - Mover <${MOVER}>\n`),
|
|
149
|
+
'Work/Team/plan.md': '# Plan\n',
|
|
150
|
+
'Legal/access.md': rules('read:\n - everyone\n'),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const svc = service();
|
|
154
|
+
expect(await verbsAt(svc, 'Work/Team')).toMatchObject({ write: true, owner: true });
|
|
155
|
+
|
|
156
|
+
const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Team', 'Legal/Team');
|
|
157
|
+
|
|
158
|
+
// Write is the folder's own and travels; owner came from `Work/` and
|
|
159
|
+
// does not.
|
|
160
|
+
expect(after).toMatchObject({ write: true, owner: false });
|
|
161
|
+
|
|
162
|
+
await reallyMove('Work/Team', 'Legal/Team');
|
|
163
|
+
expect(await verbsAt(service(), 'Legal/Team')).toEqual(after);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('a nested access.md does not change the answer at the folder itself', async () => {
|
|
167
|
+
const tree = {
|
|
168
|
+
...BASE,
|
|
169
|
+
'Work/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
|
|
170
|
+
'Work/Team/access.md': rules(`write:\n - Mover <${MOVER}>\n`),
|
|
171
|
+
'Work/Team/plan.md': '# Plan\n',
|
|
172
|
+
'Legal/access.md': rules('read:\n - everyone\n'),
|
|
173
|
+
};
|
|
174
|
+
await write(tree);
|
|
175
|
+
const withoutNested = await service().previewAccessAfterRelocation(
|
|
176
|
+
workspaceId, MOVER, 'Work/Team', 'Legal/Team',
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// `Sub/` shuts the caller out entirely, and it moves too — but it governs
|
|
180
|
+
// `Legal/Team/Sub`, not `Legal/Team`.
|
|
181
|
+
await write({ 'Work/Team/Sub/access.md': rules(`read:\n - deny Mover <${MOVER}>\n`) });
|
|
182
|
+
const svc = service();
|
|
183
|
+
expect(await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Team', 'Legal/Team'))
|
|
184
|
+
.toEqual(withoutNested);
|
|
185
|
+
// It is counted, though — at the path it lands on.
|
|
186
|
+
expect(
|
|
187
|
+
await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Team', 'Legal/Team'),
|
|
188
|
+
).not.toEqual(
|
|
189
|
+
await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Team/Sub', 'Legal/Team/Sub'),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
await reallyMove('Work/Team', 'Legal/Team');
|
|
193
|
+
const after = service();
|
|
194
|
+
expect(await verbsAt(after, 'Legal/Team')).toEqual(withoutNested);
|
|
195
|
+
expect(await verbsAt(after, 'Legal/Team/Sub')).toMatchObject({ read: false });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('a copy leaves the source rules where they are and lands a second set', async () => {
|
|
199
|
+
await write({
|
|
200
|
+
...BASE,
|
|
201
|
+
'Sales/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
|
|
202
|
+
'Sales/deal.md': '# Deal\n',
|
|
203
|
+
// A destination governed DIFFERENTLY from the source, so the answer can
|
|
204
|
+
// only come from the rules the copy carries: Legal grants read and
|
|
205
|
+
// nothing else, as the root does.
|
|
206
|
+
'Legal/access.md': rules('read:\n - everyone\n'),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const svc = service();
|
|
210
|
+
const after = await svc.previewAccessAfterRelocation(
|
|
211
|
+
workspaceId, MOVER, 'Sales', 'Legal/Sales-Copy', { sourceRemains: true },
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// The copied `access.md` governs the copy, which is the opposite of what
|
|
215
|
+
// the destination says today.
|
|
216
|
+
expect(after).toMatchObject({ write: true, owner: true });
|
|
217
|
+
expect(await verbsAt(svc, 'Legal')).toMatchObject({ write: false, owner: false });
|
|
218
|
+
|
|
219
|
+
await reallyCopy('Sales', 'Legal/Sales-Copy');
|
|
220
|
+
const done = service();
|
|
221
|
+
expect(await verbsAt(done, 'Legal/Sales-Copy')).toEqual(after);
|
|
222
|
+
// `sourceRemains`: both sets of rules are real afterwards, and the source
|
|
223
|
+
// keeps the access it always had.
|
|
224
|
+
expect(await verbsAt(done, 'Sales')).toMatchObject({ owner: true });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A lone `access.md` is a single FILE whose rules are a whole folder's.
|
|
229
|
+
* Keyed by the directory it sits in, it is not a path "under" the source,
|
|
230
|
+
* so the folder walk cannot see it — and `copy_file` will copy one, which
|
|
231
|
+
* is how a preview came to answer the destination's old rules about a
|
|
232
|
+
* directory that was about to be governed by the file landing in it.
|
|
233
|
+
*/
|
|
234
|
+
it('a lone access.md governs the folder it is copied into', async () => {
|
|
235
|
+
await write({
|
|
236
|
+
...BASE,
|
|
237
|
+
'Sales/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
|
|
238
|
+
'Legal/note.md': '# Note\n',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const svc = service();
|
|
242
|
+
// `Legal/` has no rules of its own; the root grants read and nothing else.
|
|
243
|
+
expect(await verbsAt(svc, 'Legal')).toMatchObject({ write: false, owner: false });
|
|
244
|
+
|
|
245
|
+
const after = await svc.previewAccessAfterRelocation(
|
|
246
|
+
workspaceId, MOVER, 'Sales/access.md', 'Legal/access.md', { sourceRemains: true },
|
|
247
|
+
);
|
|
248
|
+
expect(after).toMatchObject({ write: true, owner: true });
|
|
249
|
+
|
|
250
|
+
await fs.copyFile(path.join(repo, 'Sales/access.md'), path.join(repo, 'Legal/access.md'));
|
|
251
|
+
const done = service();
|
|
252
|
+
expect(await verbsAt(done, 'Legal/access.md')).toEqual(after);
|
|
253
|
+
// What landed governs the folder, not only itself.
|
|
254
|
+
expect(await verbsAt(done, 'Legal')).toMatchObject({ write: true, owner: true });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The name the bytes land under decides what they GOVERN, and it decides
|
|
259
|
+
* how they are PARSED: the same frontmatter is a folder's rules in an
|
|
260
|
+
* `access.md` and the file's own rules anywhere else. So these bytes
|
|
261
|
+
* govern no folder at `Legal/rules-copy.md` — and still grant the caller
|
|
262
|
+
* write and owner, on that one file. Reading them as the source's name said
|
|
263
|
+
* `write: false, owner: false`, and the copy then handed over both: a
|
|
264
|
+
* preview contradicting the operation it was previewing, which is the
|
|
265
|
+
* entire fault this ticket exists to kill.
|
|
266
|
+
*/
|
|
267
|
+
it('a file copied under any other name governs no folder, but keeps its own grants', async () => {
|
|
268
|
+
await write({
|
|
269
|
+
...BASE,
|
|
270
|
+
'Sales/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
|
|
271
|
+
'Legal/note.md': '# Note\n',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const svc = service();
|
|
275
|
+
const after = await svc.previewAccessAfterRelocation(
|
|
276
|
+
workspaceId, MOVER, 'Sales/access.md', 'Legal/rules-copy.md', { sourceRemains: true },
|
|
277
|
+
);
|
|
278
|
+
// Pinned, not merely equal to the outcome: both sides reading `false`
|
|
279
|
+
// would satisfy the comparison below and be wrong together.
|
|
280
|
+
expect(after).toMatchObject({ write: true, owner: true });
|
|
281
|
+
|
|
282
|
+
await reallyCopy('Sales/access.md', 'Legal/rules-copy.md');
|
|
283
|
+
const done = service();
|
|
284
|
+
expect(await verbsAt(done, 'Legal/rules-copy.md')).toEqual(after);
|
|
285
|
+
// The folder it landed in is untouched: an ordinary note governs nothing
|
|
286
|
+
// but itself, whatever its frontmatter says.
|
|
287
|
+
expect(await verbsAt(done, 'Legal')).toMatchObject({ write: false, owner: false });
|
|
288
|
+
expect(await verbsAt(done, 'Legal/note.md')).toMatchObject({ write: false, owner: false });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* And the mirror, which `copy_file` also allows: ordinary bytes landing as
|
|
293
|
+
* a directory's `access.md` become that directory's rules the moment they
|
|
294
|
+
* land. The preview reads them as the destination will.
|
|
295
|
+
*/
|
|
296
|
+
it('a file copied TO an access.md gives the destination folder its rules', async () => {
|
|
297
|
+
await write({
|
|
298
|
+
...BASE,
|
|
299
|
+
'Draft/proposed-rules.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
|
|
300
|
+
'Legal/note.md': '# Note\n',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const svc = service();
|
|
304
|
+
expect(await verbsAt(svc, 'Legal')).toMatchObject({ write: false, owner: false });
|
|
305
|
+
|
|
306
|
+
const after = await svc.previewAccessAfterRelocation(
|
|
307
|
+
workspaceId, MOVER, 'Draft/proposed-rules.md', 'Legal/access.md', { sourceRemains: true },
|
|
308
|
+
);
|
|
309
|
+
expect(after).toMatchObject({ write: true, owner: true });
|
|
310
|
+
|
|
311
|
+
await reallyCopy('Draft/proposed-rules.md', 'Legal/access.md');
|
|
312
|
+
const done = service();
|
|
313
|
+
expect(await verbsAt(done, 'Legal/access.md')).toEqual(after);
|
|
314
|
+
// It governs the folder now, which is the whole difference the name makes.
|
|
315
|
+
expect(await verbsAt(done, 'Legal')).toMatchObject({ write: true, owner: true });
|
|
316
|
+
expect(await verbsAt(done, 'Legal/note.md')).toMatchObject({ write: true, owner: true });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The same rule for the other thing that travels with a file: its own
|
|
321
|
+
* frontmatter. Asking the DESTINATION for it always answered null — nothing
|
|
322
|
+
* is there yet — so a rename of a self-governing file previewed a loss the
|
|
323
|
+
* move hands straight back, and the preview contradicted the access the
|
|
324
|
+
* caller really has afterwards (Specification requirement 6).
|
|
325
|
+
*/
|
|
326
|
+
it("a file's own frontmatter travels with its bytes, so a rename costs nothing", async () => {
|
|
327
|
+
await write({
|
|
328
|
+
...BASE,
|
|
329
|
+
'Work/access.md': rules('read:\n - everyone\n'),
|
|
330
|
+
'Work/plan.md': rules(`owner:\n - Mover <${MOVER}>\n`) + '# Plan\n',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const svc = service();
|
|
334
|
+
const before = await verbsAt(svc, 'Work/plan.md');
|
|
335
|
+
expect(before.owner).toBe(true);
|
|
336
|
+
|
|
337
|
+
const after = await svc.previewAccessAfterRelocation(
|
|
338
|
+
workspaceId, MOVER, 'Work/plan.md', 'Work/roadmap.md',
|
|
339
|
+
);
|
|
340
|
+
expect(after).toEqual(before);
|
|
341
|
+
|
|
342
|
+
await reallyMove('Work/plan.md', 'Work/roadmap.md');
|
|
343
|
+
expect(await verbsAt(service(), 'Work/roadmap.md')).toEqual(after);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('a single file answers exactly as the gates at the destination do', async () => {
|
|
347
|
+
await write({
|
|
348
|
+
...BASE,
|
|
349
|
+
'Work/access.md': rules(`write:\n - Mover <${MOVER}>\n`),
|
|
350
|
+
'Work/note.md': '# Note\n',
|
|
351
|
+
'Legal/access.md': rules('read:\n - everyone\n'),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const svc = service();
|
|
355
|
+
const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/note.md', 'Legal/note.md');
|
|
356
|
+
|
|
357
|
+
expect(after).toEqual(await verbsAt(svc, 'Legal/note.md'));
|
|
358
|
+
|
|
359
|
+
await reallyMove('Work/note.md', 'Legal/note.md');
|
|
360
|
+
expect(await verbsAt(service(), 'Legal/note.md')).toEqual(after);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('answers the caller\'s own verbs and nothing else, and writes nothing', async () => {
|
|
364
|
+
await write({
|
|
365
|
+
...BASE,
|
|
366
|
+
'Sales/access.md': rules(`read:\n - deny everyone\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
|
|
367
|
+
'Sales/deal.md': '# Deal\n',
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const svc = service();
|
|
371
|
+
const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Sales', 'Revenue');
|
|
372
|
+
|
|
373
|
+
// Four booleans. Nothing about who else holds what, and nothing out of
|
|
374
|
+
// an `access.md` the caller may not read.
|
|
375
|
+
expect(Object.keys(after).sort()).toEqual(['download', 'owner', 'read', 'write']);
|
|
376
|
+
for (const v of Object.values(after)) expect(typeof v).toBe('boolean');
|
|
377
|
+
|
|
378
|
+
// A stranger gets their own answer, not the caller's.
|
|
379
|
+
expect(await svc.previewAccessAfterRelocation(workspaceId, 'stranger@x.io', 'Sales', 'Revenue'))
|
|
380
|
+
.toMatchObject({ read: false, owner: false });
|
|
381
|
+
|
|
382
|
+
expect(await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8')).toContain('owner:');
|
|
383
|
+
await expect(fs.access(path.join(repo, 'Revenue'))).rejects.toThrow();
|
|
384
|
+
});
|
|
385
|
+
});
|