@bevel-software/platform-core-backend 0.14.0 → 0.19.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/THIRD-PARTY-NOTICES.md +8 -8
- package/dist/core/catalog-cache-invalidation.d.ts +45 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
- package/dist/core/catalog-cache-invalidation.js +76 -0
- package/dist/core/catalog-cache-invalidation.js.map +1 -0
- package/dist/core/catalog-revision.d.ts +39 -0
- package/dist/core/catalog-revision.d.ts.map +1 -0
- package/dist/core/catalog-revision.js +94 -0
- package/dist/core/catalog-revision.js.map +1 -0
- package/dist/core/create-core-server.d.ts +7 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +279 -64
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +66 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +290 -111
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +138 -0
- package/dist/core/lifecycle.d.ts.map +1 -0
- package/dist/core/lifecycle.js +218 -0
- package/dist/core/lifecycle.js.map +1 -0
- package/dist/core/public-config.d.ts +82 -0
- package/dist/core/public-config.d.ts.map +1 -0
- package/dist/core/public-config.js +78 -0
- package/dist/core/public-config.js.map +1 -0
- package/dist/core/readiness.d.ts +86 -0
- package/dist/core/readiness.d.ts.map +1 -0
- package/dist/core/readiness.js +56 -0
- package/dist/core/readiness.js.map +1 -0
- package/dist/core-config.d.ts +30 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +64 -2
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +231 -15
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +83 -12
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +815 -212
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +71 -5
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +161 -13
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-view.d.ts +147 -0
- package/dist/modules/access/access-view.d.ts.map +1 -0
- package/dist/modules/access/access-view.js +211 -0
- package/dist/modules/access/access-view.js.map +1 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +312 -84
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/admin-locked-commit.d.ts +2 -3
- package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
- package/dist/modules/access/admin-locked-commit.js +9 -8
- package/dist/modules/access/admin-locked-commit.js.map +1 -1
- package/dist/modules/access/admin-route-helpers.d.ts.map +1 -1
- package/dist/modules/access/admin-route-helpers.js +4 -4
- package/dist/modules/access/admin-route-helpers.js.map +1 -1
- package/dist/modules/access/change-read-gate.d.ts +53 -0
- package/dist/modules/access/change-read-gate.d.ts.map +1 -0
- package/dist/modules/access/change-read-gate.js +134 -0
- package/dist/modules/access/change-read-gate.js.map +1 -0
- package/dist/modules/access/creator-access.d.ts +33 -39
- package/dist/modules/access/creator-access.d.ts.map +1 -1
- package/dist/modules/access/creator-access.js +92 -119
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +3 -1
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/access/groups-admin.service.d.ts +3 -3
- package/dist/modules/access/groups-admin.service.js +2 -2
- package/dist/modules/access/reference-scan.d.ts +1 -1
- package/dist/modules/access/reference-scan.d.ts.map +1 -1
- package/dist/modules/access/reference-scan.js +5 -4
- package/dist/modules/access/reference-scan.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +72 -13
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +71 -13
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
- package/dist/modules/access/synced-groups-committer.js +8 -6
- package/dist/modules/access/synced-groups-committer.js.map +1 -1
- package/dist/modules/access/synced-groups-writer.d.ts.map +1 -1
- package/dist/modules/access/synced-groups-writer.js +3 -8
- package/dist/modules/access/synced-groups-writer.js.map +1 -1
- package/dist/modules/access/user-access-removal.service.d.ts +209 -0
- package/dist/modules/access/user-access-removal.service.d.ts.map +1 -0
- package/dist/modules/access/user-access-removal.service.js +662 -0
- package/dist/modules/access/user-access-removal.service.js.map +1 -0
- package/dist/modules/access-model/access-errors.d.ts +32 -1
- package/dist/modules/access-model/access-errors.d.ts.map +1 -1
- package/dist/modules/access-model/access-errors.js +38 -11
- package/dist/modules/access-model/access-errors.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +100 -25
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +162 -48
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/access-splice.d.ts.map +1 -1
- package/dist/modules/access-model/access-splice.js +18 -18
- package/dist/modules/access-model/access-splice.js.map +1 -1
- package/dist/modules/access-model/access-template.d.ts +22 -0
- package/dist/modules/access-model/access-template.d.ts.map +1 -0
- package/dist/modules/access-model/access-template.js +42 -0
- package/dist/modules/access-model/access-template.js.map +1 -0
- package/dist/modules/access-model/change-gate.d.ts +65 -0
- package/dist/modules/access-model/change-gate.d.ts.map +1 -0
- package/dist/modules/access-model/change-gate.js +25 -0
- package/dist/modules/access-model/change-gate.js.map +1 -0
- package/dist/modules/access-model/creator.d.ts +13 -16
- package/dist/modules/access-model/creator.d.ts.map +1 -1
- package/dist/modules/access-model/creator.js.map +1 -1
- package/dist/modules/access-model/frontmatter-lines.d.ts +27 -0
- package/dist/modules/access-model/frontmatter-lines.d.ts.map +1 -0
- package/dist/modules/access-model/frontmatter-lines.js +74 -0
- package/dist/modules/access-model/frontmatter-lines.js.map +1 -0
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +4 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/access-model/plugin-principals.d.ts +51 -0
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
- package/dist/modules/access-model/plugin-principals.js +119 -0
- package/dist/modules/access-model/plugin-principals.js.map +1 -0
- package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
- package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
- package/dist/modules/access-model/roles-yaml-guard.js +153 -3
- package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
- package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
- package/dist/modules/admin/admin-access.service.js +5 -2
- package/dist/modules/admin/admin-access.service.js.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
- package/dist/modules/agent-instructions/compose.d.ts +71 -0
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
- package/dist/modules/agent-instructions/compose.js +234 -0
- package/dist/modules/agent-instructions/compose.js.map +1 -0
- package/dist/modules/agent-instructions/index.d.ts +4 -0
- package/dist/modules/agent-instructions/index.d.ts.map +1 -0
- package/dist/modules/agent-instructions/index.js +4 -0
- package/dist/modules/agent-instructions/index.js.map +1 -0
- package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
- package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
- package/dist/modules/agent-instructions/read-preamble.js +81 -0
- package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
- package/dist/modules/auth/account-erasure.service.d.ts +30 -4
- package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
- package/dist/modules/auth/account-erasure.service.js +96 -10
- package/dist/modules/auth/account-erasure.service.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +6 -3
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +109 -10
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts +10 -3
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +9 -4
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +89 -5
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +160 -25
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +99 -21
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +14 -2
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.js +56 -7
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
- package/dist/modules/database/advisory-lock.d.ts +103 -0
- package/dist/modules/database/advisory-lock.d.ts.map +1 -0
- package/dist/modules/database/advisory-lock.js +320 -0
- package/dist/modules/database/advisory-lock.js.map +1 -0
- package/dist/modules/database/connection.d.ts.map +1 -1
- package/dist/modules/database/connection.js +9 -1
- package/dist/modules/database/connection.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +672 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +171 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/database/migrate.d.ts.map +1 -1
- package/dist/modules/database/migrate.js +26 -6
- package/dist/modules/database/migrate.js.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
- package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
- package/dist/modules/diff/diff.routes.d.ts.map +1 -1
- package/dist/modules/diff/diff.routes.js +2 -4
- package/dist/modules/diff/diff.routes.js.map +1 -1
- package/dist/modules/diff/diff.service.d.ts +4 -1
- package/dist/modules/diff/diff.service.d.ts.map +1 -1
- package/dist/modules/diff/diff.service.js +79 -81
- package/dist/modules/diff/diff.service.js.map +1 -1
- package/dist/modules/kb-fs/bevel-ignore.d.ts +35 -0
- package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
- package/dist/modules/kb-fs/bevel-ignore.js +71 -0
- package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
- package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
- package/dist/modules/kb-fs/file-change-notifier.js +3 -1
- package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
- package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +37 -0
- package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
- package/dist/modules/kb-fs/git-guarded-filesystem.js +104 -0
- package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +353 -76
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/node-fs.d.ts +23 -0
- package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
- package/dist/modules/kb-fs/node-fs.js +158 -0
- package/dist/modules/kb-fs/node-fs.js.map +1 -0
- package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
- package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
- package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +100 -3
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +195 -3
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/index.d.ts +6 -0
- package/dist/modules/kb-sync/index.d.ts.map +1 -0
- package/dist/modules/kb-sync/index.js +5 -0
- package/dist/modules/kb-sync/index.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
- package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.js +137 -0
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.js +185 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
- package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-auth.js +62 -0
- package/dist/modules/kb-sync/sync-auth.js.map +1 -0
- package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
- package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-payload.js +105 -0
- package/dist/modules/kb-sync/sync-payload.js.map +1 -0
- package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/git-http.routes.js +192 -0
- package/dist/modules/marketplace/git-http.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +33 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +88 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +126 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +192 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +252 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +121 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js +213 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/index.js +6 -0
- package/dist/modules/marketplace/github-facade/index.js.map +1 -0
- package/dist/modules/marketplace/index.d.ts +4 -0
- package/dist/modules/marketplace/index.d.ts.map +1 -0
- package/dist/modules/marketplace/index.js +4 -0
- package/dist/modules/marketplace/index.js.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +135 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.js +318 -0
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
- package/dist/modules/mcp/downstream-pool.d.ts +107 -0
- package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
- package/dist/modules/mcp/downstream-pool.js +221 -0
- package/dist/modules/mcp/downstream-pool.js.map +1 -0
- package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
- package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
- package/dist/modules/mcp/downstream-token-refresh.js +166 -0
- package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
- package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
- package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
- package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
- package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
- package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
- package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
- package/dist/modules/mcp/first-call-probe.cli.js +62 -0
- package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
- package/dist/modules/mcp/first-call-probe.d.ts +165 -0
- package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
- package/dist/modules/mcp/first-call-probe.js +298 -0
- package/dist/modules/mcp/first-call-probe.js.map +1 -0
- package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
- package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
- package/dist/modules/mcp/manual-failure-memo.js +79 -23
- package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +9 -5
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +24 -16
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +4 -4
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +83 -179
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +218 -61
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +713 -159
- package/dist/modules/mcp/mcp.service.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 +8 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +16 -2
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
- package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.js +3 -1
- package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
- package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
- package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
- package/dist/modules/mcp/surface-log-throttle.js +94 -0
- package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +104 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.js +323 -0
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +73 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +135 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +158 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +65 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +204 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +39 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js +110 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
- package/dist/modules/plugins/discovery/native.source.d.ts +16 -0
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/native.source.js +120 -0
- package/dist/modules/plugins/discovery/native.source.js.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts +110 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.js +2 -0
- package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +15 -3
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +13 -2
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
- package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-request-jobs.service.js +602 -0
- package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
- package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
- package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
- package/dist/modules/plugins/join-request-records.store.js +179 -0
- package/dist/modules/plugins/join-request-records.store.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-requests.service.js +4 -2
- package/dist/modules/plugins/join-requests.service.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +69 -0
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.js +163 -0
- package/dist/modules/plugins/plugin-links.js.map +1 -0
- package/dist/modules/plugins/plugin-links.service.d.ts +111 -0
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.service.js +295 -0
- package/dist/modules/plugins/plugin-links.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +129 -21
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +320 -75
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +71 -0
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-rename.service.js +254 -0
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +78 -4
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +44 -5
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +297 -115
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +82 -3
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +178 -48
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/plugins/plugins.tools.d.ts +25 -0
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.tools.js +82 -0
- package/dist/modules/plugins/plugins.tools.js.map +1 -0
- package/dist/modules/plugins/teams.routes.d.ts +42 -0
- package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
- package/dist/modules/plugins/teams.routes.js +79 -0
- package/dist/modules/plugins/teams.routes.js.map +1 -0
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/index.d.ts +1 -1
- package/dist/modules/secrets-vault/index.d.ts.map +1 -1
- package/dist/modules/secrets-vault/index.js.map +1 -1
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
- package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
- package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/connection-check.d.ts +118 -0
- package/dist/modules/settings/connection-check.d.ts.map +1 -0
- package/dist/modules/settings/connection-check.js +287 -0
- package/dist/modules/settings/connection-check.js.map +1 -0
- package/dist/modules/settings/deployment-settings.service.d.ts +157 -2
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +412 -35
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/git-root-folders.d.ts +54 -0
- package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
- package/dist/modules/settings/git-root-folders.js +130 -0
- package/dist/modules/settings/git-root-folders.js.map +1 -0
- package/dist/modules/settings/oidc-check.d.ts +85 -0
- package/dist/modules/settings/oidc-check.d.ts.map +1 -0
- package/dist/modules/settings/oidc-check.js +172 -0
- package/dist/modules/settings/oidc-check.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +57 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +408 -108
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
- package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
- package/dist/modules/skills/allowed-tools-check.js +260 -0
- package/dist/modules/skills/allowed-tools-check.js.map +1 -0
- package/dist/modules/skills/index.d.ts +4 -1
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +2 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts +6 -11
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +24 -71
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
- package/dist/modules/skills/skill-access-requests.routes.js +171 -0
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +67 -2
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +35 -3
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +57 -4
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +31 -3
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +236 -65
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +4 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +41 -6
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
- package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
- package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
- package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
- package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
- package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
- package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
- package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +66 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js +2 -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 +23 -4
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +79 -19
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +5 -2
- package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.js +5 -2
- package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
- package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
- package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
- package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
- package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +7 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +8 -5
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +28 -6
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +22 -5
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js +7 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-manuals/index.d.ts +2 -1
- package/dist/modules/tool-manuals/index.d.ts.map +1 -1
- package/dist/modules/tool-manuals/index.js +1 -0
- package/dist/modules/tool-manuals/index.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +136 -96
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
- package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
- package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
- package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
- package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
- package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +121 -2
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +415 -92
- 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 +78 -6
- package/dist/modules/tool-manuals/tool-manuals.tools.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 +88 -54
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/event-bus.d.ts +10 -4
- package/dist/modules/workflow/event-bus.d.ts.map +1 -1
- package/dist/modules/workflow/event-bus.js +14 -11
- package/dist/modules/workflow/event-bus.js.map +1 -1
- package/dist/modules/workflow/events.routes.d.ts +17 -2
- package/dist/modules/workflow/events.routes.d.ts.map +1 -1
- package/dist/modules/workflow/events.routes.js +31 -12
- package/dist/modules/workflow/events.routes.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +22 -4
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +27 -4
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
- package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
- package/dist/modules/workflow/git/change-request-link.js +34 -0
- package/dist/modules/workflow/git/change-request-link.js.map +1 -0
- package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
- package/dist/modules/workflow/git/git-version.js +3 -1
- package/dist/modules/workflow/git/git-version.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +303 -6
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +753 -170
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
- package/dist/modules/workflow/git/node-git-runner.js +273 -0
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts +57 -2
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +165 -25
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +24 -4
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +107 -58
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +3 -1
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
- package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
- package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
- package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
- package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
- package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/sanitize-error.d.ts +5 -2
- package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
- package/dist/modules/workflow/sanitize-error.js +8 -4
- package/dist/modules/workflow/sanitize-error.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +2 -0
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +259 -29
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +217 -14
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +1142 -109
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/empty-dirs.d.ts +15 -0
- package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
- package/dist/modules/workspace/empty-dirs.js +90 -0
- package/dist/modules/workspace/empty-dirs.js.map +1 -0
- package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
- package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
- package/dist/modules/workspace/file-readers/content-mode.js +55 -0
- package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
- package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
- package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
- package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
- package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/document-reader.js +5 -0
- package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
- package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
- package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
- package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
- package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
- package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/file-reader.js +11 -6
- package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
- package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
- package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
- package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
- package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/image-reader.js +9 -5
- package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
- package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
- package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
- package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
- package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.js +57 -3
- package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
- package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
- package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
- package/dist/modules/workspace/git-internals.middleware.js +93 -0
- package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
- package/dist/modules/workspace/not-found.d.ts +80 -0
- package/dist/modules/workspace/not-found.d.ts.map +1 -0
- package/dist/modules/workspace/not-found.js +112 -0
- package/dist/modules/workspace/not-found.js.map +1 -0
- package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
- package/dist/modules/workspace/session-ontology.gate.js +1 -1
- package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
- package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
- package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
- package/dist/modules/workspace/startup/beside-checkout.js +110 -0
- package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
- package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
- package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-git.js +34 -35
- package/dist/modules/workspace/startup/kb-git.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
- package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
- package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
- package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
- package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
- package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
- package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +41 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +85 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +174 -54
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +521 -126
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/template-source.js +160 -0
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts +14 -2
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +657 -246
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +323 -12
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +1006 -257
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +42 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +1590 -146
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/workspace/write-denial.d.ts +76 -0
- package/dist/modules/workspace/write-denial.d.ts.map +1 -0
- package/dist/modules/workspace/write-denial.js +138 -0
- package/dist/modules/workspace/write-denial.js.map +1 -0
- package/dist/shared/canonical-file-identity.d.ts +64 -0
- package/dist/shared/canonical-file-identity.d.ts.map +1 -0
- package/dist/shared/canonical-file-identity.js +92 -0
- package/dist/shared/canonical-file-identity.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +128 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +166 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/email-identity.d.ts +32 -0
- package/dist/shared/email-identity.d.ts.map +1 -0
- package/dist/shared/email-identity.js +37 -0
- package/dist/shared/email-identity.js.map +1 -0
- package/dist/shared/fs-errors.d.ts +8 -0
- package/dist/shared/fs-errors.d.ts.map +1 -0
- package/dist/shared/fs-errors.js +11 -0
- package/dist/shared/fs-errors.js.map +1 -0
- package/dist/shared/fs-walk.d.ts +20 -6
- package/dist/shared/fs-walk.d.ts.map +1 -1
- package/dist/shared/fs-walk.js +35 -27
- package/dist/shared/fs-walk.js.map +1 -1
- package/dist/shared/fs.contract.d.ts +206 -0
- package/dist/shared/fs.contract.d.ts.map +1 -0
- package/dist/shared/fs.contract.js +79 -0
- package/dist/shared/fs.contract.js.map +1 -0
- package/dist/shared/git-failure.d.ts +80 -0
- package/dist/shared/git-failure.d.ts.map +1 -0
- package/dist/shared/git-failure.js +139 -0
- package/dist/shared/git-failure.js.map +1 -0
- package/dist/shared/git-internals.d.ts +17 -0
- package/dist/shared/git-internals.d.ts.map +1 -0
- package/dist/shared/git-internals.js +123 -0
- package/dist/shared/git-internals.js.map +1 -0
- package/dist/shared/git.contract.d.ts +157 -0
- package/dist/shared/git.contract.d.ts.map +1 -0
- package/dist/shared/git.contract.js +103 -0
- package/dist/shared/git.contract.js.map +1 -0
- package/dist/shared/http-errors.d.ts +33 -0
- package/dist/shared/http-errors.d.ts.map +1 -0
- package/dist/shared/http-errors.js +35 -0
- package/dist/shared/http-errors.js.map +1 -0
- package/dist/shared/kb-walk.d.ts +52 -0
- package/dist/shared/kb-walk.d.ts.map +1 -0
- package/dist/shared/kb-walk.js +76 -0
- package/dist/shared/kb-walk.js.map +1 -0
- package/dist/shared/logger.contract.d.ts +63 -0
- package/dist/shared/logger.contract.d.ts.map +1 -0
- package/dist/shared/logger.contract.js +126 -0
- package/dist/shared/logger.contract.js.map +1 -0
- package/dist/shared/logging.d.ts +31 -0
- package/dist/shared/logging.d.ts.map +1 -0
- package/dist/shared/logging.js +92 -0
- package/dist/shared/logging.js.map +1 -0
- package/dist/shared/path-containment.d.ts +21 -0
- package/dist/shared/path-containment.d.ts.map +1 -0
- package/dist/shared/path-containment.js +34 -0
- package/dist/shared/path-containment.js.map +1 -0
- package/dist/shared/path-order.d.ts +18 -0
- package/dist/shared/path-order.d.ts.map +1 -0
- package/dist/shared/path-order.js +35 -0
- package/dist/shared/path-order.js.map +1 -0
- package/dist/shared/pending-proposals.d.ts +71 -0
- package/dist/shared/pending-proposals.d.ts.map +1 -0
- package/dist/shared/pending-proposals.js +98 -0
- package/dist/shared/pending-proposals.js.map +1 -0
- package/dist/shared/printable.d.ts +22 -0
- package/dist/shared/printable.d.ts.map +1 -0
- package/dist/shared/printable.js +65 -0
- package/dist/shared/printable.js.map +1 -0
- package/dist/shared/redact-secret.d.ts +30 -0
- package/dist/shared/redact-secret.d.ts.map +1 -0
- package/dist/shared/redact-secret.js +114 -0
- package/dist/shared/redact-secret.js.map +1 -0
- package/dist/shared/rename-no-replace.d.ts +119 -0
- package/dist/shared/rename-no-replace.d.ts.map +1 -0
- package/dist/shared/rename-no-replace.js +323 -0
- package/dist/shared/rename-no-replace.js.map +1 -0
- package/dist/shared/ssrf.d.ts.map +1 -1
- package/dist/shared/ssrf.js +49 -18
- package/dist/shared/ssrf.js.map +1 -1
- package/dist/shared/ttl-cache.d.ts +28 -1
- package/dist/shared/ttl-cache.d.ts.map +1 -1
- package/dist/shared/ttl-cache.js +33 -1
- package/dist/shared/ttl-cache.js.map +1 -1
- package/dist/version.d.ts +10 -6
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +10 -6
- package/dist/version.js.map +1 -1
- package/kb-template/.bevelignore +4 -3
- package/kb-template/AGENTS.md +364 -54
- package/kb-template/KnowledgeBase/How to get started.md +1 -1
- package/kb-template/Skills/.gitkeep +0 -0
- package/kb-template/access.md +5 -3
- package/kb-template/mcp-description.md +27 -0
- package/migrations/0005_github_facade.sql +25 -0
- package/migrations/0006_api_tokens_revoked_by.sql +1 -0
- package/migrations/0007_github_facade_registered.sql +1 -0
- package/migrations/0008_change_request_apply_failure.sql +4 -0
- package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
- package/migrations/0010_plugin_join_requests.sql +15 -0
- package/migrations/0011_plugin_join_request_claim.sql +11 -0
- package/migrations/meta/0005_snapshot.json +1717 -0
- package/migrations/meta/0006_snapshot.json +1723 -0
- package/migrations/meta/0007_snapshot.json +1729 -0
- package/migrations/meta/0008_snapshot.json +1753 -0
- package/migrations/meta/0009_snapshot.json +1759 -0
- package/migrations/meta/0010_snapshot.json +1872 -0
- package/migrations/meta/0011_snapshot.json +1884 -0
- package/migrations/meta/_journal.json +49 -0
- package/package.json +8 -7
- package/src/__tests__/core-config.domain.test.ts +67 -2
- package/src/__tests__/kb-layout-config.test.ts +295 -0
- package/src/__tests__/open-change-gate.ts +15 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +226 -0
- package/src/core/__tests__/catalog-revision.test.ts +190 -0
- package/src/core/__tests__/lifecycle.test.ts +369 -0
- package/src/core/__tests__/public-config.test.ts +30 -0
- package/src/core/__tests__/readiness.test.ts +109 -0
- package/src/core/catalog-cache-invalidation.ts +111 -0
- package/src/core/catalog-revision.ts +110 -0
- package/src/core/create-core-server.ts +358 -63
- package/src/core/create-core-services.ts +449 -107
- package/src/core/lifecycle.ts +312 -0
- package/src/core/public-config.ts +86 -0
- package/src/core/readiness.ts +123 -0
- package/src/core-config.ts +70 -4
- package/src/index.ts +15 -0
- package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
- package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
- package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
- package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
- package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +88 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
- package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
- package/src/modules/access/__tests__/access-control.service.test.ts +142 -81
- package/src/modules/access/__tests__/access-groups.test.ts +47 -14
- package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
- package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
- package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +385 -0
- package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
- package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
- package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
- package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
- package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
- package/src/modules/access/__tests__/creator-access.test.ts +81 -83
- package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
- package/src/modules/access/__tests__/groups-admin.service.test.ts +14 -5
- package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
- package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
- package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
- package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
- package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
- package/src/modules/access/__tests__/users-db-double.ts +63 -0
- package/src/modules/access/access-control.interface.ts +269 -18
- package/src/modules/access/access-control.service.ts +943 -226
- package/src/modules/access/access-mutation.service.ts +182 -13
- package/src/modules/access/access-view.ts +316 -0
- package/src/modules/access/access.routes.ts +385 -105
- package/src/modules/access/admin-locked-commit.ts +12 -14
- package/src/modules/access/admin-route-helpers.ts +4 -4
- package/src/modules/access/change-read-gate.ts +148 -0
- package/src/modules/access/creator-access.ts +89 -124
- package/src/modules/access/directory-sync-bot.ts +4 -1
- package/src/modules/access/groups-admin.service.ts +3 -3
- package/src/modules/access/reference-scan.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +94 -15
- package/src/modules/access/synced-groups-committer.ts +10 -15
- package/src/modules/access/synced-groups-writer.ts +3 -12
- package/src/modules/access/user-access-removal.service.ts +711 -0
- package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
- package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
- package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
- package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
- package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
- package/src/modules/access-model/access-errors.ts +56 -15
- package/src/modules/access-model/access-grammar.ts +198 -49
- package/src/modules/access-model/access-splice.ts +18 -18
- package/src/modules/access-model/access-template.ts +44 -0
- package/src/modules/access-model/change-gate.ts +80 -0
- package/src/modules/access-model/creator.ts +18 -21
- package/src/modules/access-model/frontmatter-lines.ts +98 -0
- package/src/modules/access-model/group-files.ts +4 -0
- package/src/modules/access-model/plugin-principals.ts +130 -0
- package/src/modules/access-model/roles-yaml-guard.ts +182 -4
- package/src/modules/admin/admin-access.service.ts +6 -2
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
- package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
- package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
- package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
- package/src/modules/agent-instructions/compose.ts +271 -0
- package/src/modules/agent-instructions/index.ts +14 -0
- package/src/modules/agent-instructions/read-preamble.ts +88 -0
- package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
- package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
- package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
- package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
- package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
- package/src/modules/auth/account-erasure.service.ts +117 -10
- package/src/modules/auth/account.routes.ts +115 -14
- package/src/modules/auth/auth.routes.ts +20 -7
- package/src/modules/auth/auth.service.ts +187 -28
- package/src/modules/auth/oidc-auth-provider.ts +120 -22
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
- package/src/modules/code-mode/code-mode.tool.ts +13 -2
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
- package/src/modules/connection-probe/connection-probe.service.ts +59 -7
- package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
- package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
- package/src/modules/database/advisory-lock.ts +346 -0
- package/src/modules/database/connection.ts +9 -1
- package/src/modules/database/core-schema.ts +177 -0
- package/src/modules/database/migrate.ts +27 -6
- package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
- package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
- package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
- package/src/modules/diff/diff.routes.ts +2 -4
- package/src/modules/diff/diff.service.ts +75 -79
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
- package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
- package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
- package/src/modules/kb-fs/file-change-notifier.ts +4 -1
- package/src/modules/kb-fs/git-guarded-filesystem.ts +123 -0
- package/src/modules/kb-fs/locking-filesystem.ts +399 -109
- package/src/modules/kb-fs/node-fs.ts +165 -0
- package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
- package/src/modules/kb-fs/repo-path.ts +213 -3
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
- package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
- package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
- package/src/modules/kb-sync/index.ts +19 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
- package/src/modules/kb-sync/kb-sync.routes.ts +200 -0
- package/src/modules/kb-sync/kb-sync.service.ts +204 -0
- package/src/modules/kb-sync/sync-auth.ts +118 -0
- package/src/modules/kb-sync/sync-payload.ts +143 -0
- package/src/modules/marketplace/__tests__/github-facade.test.ts +575 -0
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +180 -0
- package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
- package/src/modules/marketplace/git-http.routes.ts +196 -0
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +110 -0
- package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +263 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +294 -0
- package/src/modules/marketplace/github-facade/github-facade.service.ts +282 -0
- package/src/modules/marketplace/github-facade/index.ts +26 -0
- package/src/modules/marketplace/index.ts +3 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +352 -0
- package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
- package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
- package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
- package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
- package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
- package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +8 -5
- package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
- package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +274 -6
- package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
- package/src/modules/mcp/downstream-pool.ts +256 -0
- package/src/modules/mcp/downstream-token-refresh.ts +170 -0
- package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
- package/src/modules/mcp/first-call-probe.cli.ts +60 -0
- package/src/modules/mcp/first-call-probe.ts +418 -0
- package/src/modules/mcp/manual-failure-memo.ts +81 -23
- package/src/modules/mcp/mcp-auth.middleware.ts +25 -16
- package/src/modules/mcp/mcp.routes.ts +376 -482
- package/src/modules/mcp/mcp.service.ts +846 -176
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +8 -2
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +28 -6
- package/src/modules/mcp/oauth/oauth-state.ts +15 -3
- package/src/modules/mcp/surface-log-throttle.ts +120 -0
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +166 -0
- package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +423 -0
- package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
- package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
- package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +201 -16
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +284 -0
- package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +330 -20
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +456 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +441 -43
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
- package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
- package/src/modules/plugins/compile/compile-marketplace.ts +500 -0
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +151 -0
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +365 -0
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +161 -0
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +250 -0
- package/src/modules/plugins/discovery/kb-plugin-source.ts +117 -0
- package/src/modules/plugins/discovery/native.source.ts +140 -0
- package/src/modules/plugins/discovery/plugin-source.ts +113 -0
- package/src/modules/plugins/index.ts +25 -2
- package/src/modules/plugins/join-request-jobs.service.ts +675 -0
- package/src/modules/plugins/join-request-records.store.ts +353 -0
- package/src/modules/plugins/join-requests.service.ts +7 -4
- package/src/modules/plugins/plugin-links.service.ts +350 -0
- package/src/modules/plugins/plugin-links.ts +187 -0
- package/src/modules/plugins/plugin-provision.service.ts +356 -78
- package/src/modules/plugins/plugin-rename.service.ts +310 -0
- package/src/modules/plugins/plugins.contract.ts +79 -4
- package/src/modules/plugins/plugins.routes.ts +320 -123
- package/src/modules/plugins/plugins.service.ts +184 -57
- package/src/modules/plugins/plugins.tools.ts +88 -0
- package/src/modules/plugins/teams.routes.ts +129 -0
- package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
- package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
- package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
- package/src/modules/secrets-vault/index.ts +1 -0
- package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
- package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
- package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
- package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
- package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +240 -1
- package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
- package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
- package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
- package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
- package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
- package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
- package/src/modules/settings/connection-check.ts +351 -0
- package/src/modules/settings/deployment-settings.service.ts +478 -39
- package/src/modules/settings/git-root-folders.ts +171 -0
- package/src/modules/settings/oidc-check.ts +229 -0
- package/src/modules/settings/setup.routes.ts +480 -110
- package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
- package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +308 -2
- package/src/modules/skills/allowed-tools-check.ts +352 -0
- package/src/modules/skills/index.ts +5 -0
- package/src/modules/skills/pending-skills.service.ts +26 -85
- package/src/modules/skills/skill-access-requests.routes.ts +203 -0
- package/src/modules/skills/skills.contract.ts +67 -3
- package/src/modules/skills/skills.routes.ts +75 -4
- package/src/modules/skills/skills.service.ts +258 -66
- package/src/modules/skills/skills.tools.ts +43 -5
- package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
- package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +124 -0
- package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
- package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
- package/src/modules/tool-auth/external-api-key.interface.ts +73 -1
- package/src/modules/tool-auth/external-api-key.service.ts +95 -29
- package/src/modules/tool-auth/internal-token.service.ts +5 -2
- package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
- package/src/modules/tool-auth/tool-auth.middleware.ts +16 -7
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
- package/src/modules/tool-helpers/tool-context.ts +46 -7
- package/src/modules/tool-helpers/tool-handler.ts +23 -5
- package/src/modules/tool-helpers/tool.contract.ts +5 -0
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
- package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
- package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
- package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +11 -4
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
- package/src/modules/tool-manuals/index.ts +3 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +164 -95
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
- package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
- package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
- package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
- package/src/modules/tool-manuals/tool-manuals.service.ts +440 -104
- package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
- package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
- package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
- package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
- package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
- package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
- package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +3 -1
- package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
- package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
- package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
- package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +3 -0
- package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +296 -27
- package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
- package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
- package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +133 -2
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +442 -0
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
- package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
- package/src/modules/workflow/event-bus.ts +26 -28
- package/src/modules/workflow/events.routes.ts +56 -21
- package/src/modules/workflow/file-lock.service.ts +27 -4
- package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
- package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
- package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
- package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
- package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
- package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
- package/src/modules/workflow/git/change-request-link.ts +35 -0
- package/src/modules/workflow/git/git-version.ts +5 -2
- package/src/modules/workflow/git/git.service.ts +922 -199
- package/src/modules/workflow/git/node-git-runner.ts +315 -0
- package/src/modules/workflow/git/pull-request.service.ts +188 -27
- package/src/modules/workflow/pending-commits.service.ts +26 -7
- package/src/modules/workflow/pending-commits.worker.ts +114 -69
- package/src/modules/workflow/recovery-bot.ts +4 -1
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
- package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
- package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
- package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
- package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
- package/src/modules/workflow/sanitize-error.ts +9 -4
- package/src/modules/workflow/workflow.routes.ts +273 -29
- package/src/modules/workflow/workflow.service.ts +1341 -174
- package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +792 -0
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
- package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
- package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
- package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
- package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
- package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +31 -13
- package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +94 -0
- package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
- package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +98 -13
- package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
- package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
- package/src/modules/workspace/empty-dirs.ts +79 -0
- package/src/modules/workspace/file-readers/README.md +32 -0
- package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
- package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
- package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
- package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
- package/src/modules/workspace/file-readers/content-mode.ts +87 -0
- package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
- package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
- package/src/modules/workspace/file-readers/document-reader.ts +6 -0
- package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
- package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
- package/src/modules/workspace/file-readers/file-reader.ts +27 -6
- package/src/modules/workspace/file-readers/image-reader.ts +10 -5
- package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
- package/src/modules/workspace/file-readers/text-reader.ts +61 -4
- package/src/modules/workspace/git-internals.middleware.ts +86 -0
- package/src/modules/workspace/not-found.ts +119 -0
- package/src/modules/workspace/session-ontology.gate.ts +1 -1
- package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
- package/src/modules/workspace/startup/beside-checkout.ts +112 -0
- package/src/modules/workspace/startup/kb-git.ts +43 -36
- package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1405 -48
- package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
- package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
- package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
- package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +90 -0
- package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
- package/src/modules/workspace/startup/steps/seed-tree.ts +177 -54
- package/src/modules/workspace/startup/steps/template-files.step.ts +581 -126
- package/src/modules/workspace/startup/steps/template-source.ts +168 -0
- package/src/modules/workspace/workspace.routes.ts +691 -269
- package/src/modules/workspace/workspace.service.ts +1071 -292
- package/src/modules/workspace/workspace.tools.ts +1787 -180
- package/src/modules/workspace/write-denial.ts +189 -0
- package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
- package/src/shared/__tests__/email-identity.test.ts +35 -0
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
- package/src/shared/__tests__/git-failure.test.ts +198 -0
- package/src/shared/__tests__/git-internals.test.ts +115 -0
- package/src/shared/__tests__/http-errors.test.ts +42 -0
- package/src/shared/__tests__/logging.test.ts +185 -0
- package/src/shared/__tests__/path-containment.test.ts +60 -0
- package/src/shared/__tests__/path-order.test.ts +24 -0
- package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
- package/src/shared/__tests__/printable.test.ts +64 -0
- package/src/shared/__tests__/redact-secret.test.ts +122 -0
- package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
- package/src/shared/__tests__/ssrf.test.ts +20 -0
- package/src/shared/__tests__/ttl-cache.test.ts +55 -0
- package/src/shared/canonical-file-identity.ts +92 -0
- package/src/shared/domain-errors.ts +178 -0
- package/src/shared/email-identity.ts +38 -0
- package/src/shared/fs.contract.ts +225 -0
- package/src/shared/git-failure.ts +196 -0
- package/src/shared/git-internals.ts +124 -0
- package/src/shared/git.contract.ts +170 -0
- package/src/shared/http-errors.ts +36 -0
- package/src/shared/logger.contract.ts +141 -0
- package/src/shared/logging.ts +96 -0
- package/src/shared/path-containment.ts +34 -0
- package/src/shared/path-order.ts +33 -0
- package/src/shared/pending-proposals.ts +161 -0
- package/src/shared/printable.ts +68 -0
- package/src/shared/redact-secret.ts +118 -0
- package/src/shared/rename-no-replace.ts +386 -0
- package/src/shared/ssrf.ts +44 -16
- package/src/shared/ttl-cache.ts +33 -1
- package/src/version.ts +10 -6
- package/src/modules/diff/diff-paths.ts +0 -9
- package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
- package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
- package/src/modules/mcp/mcp-session-store.ts +0 -162
- package/src/shared/__tests__/fs-walk.test.ts +0 -42
- package/src/shared/fs-walk.ts +0 -30
- package/src/shared/hash-email.ts +0 -14
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import { createHash } from 'node:crypto';
|
|
3
|
+
import { logger } from '../../shared/logging.js';
|
|
6
4
|
|
|
5
|
+
const log = logger('access');
|
|
6
|
+
|
|
7
|
+
import { isAbsence, type ITreeWalker, type WalkListener } from '../../shared/fs.contract.js';
|
|
8
|
+
import { isGitTimeout, type IGitRunner } from '../../shared/git.contract.js';
|
|
9
|
+
import { hashEmail } from '../../shared/email-identity.js';
|
|
10
|
+
import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
|
|
7
11
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
8
12
|
import type {
|
|
9
13
|
IAccessControl,
|
|
14
|
+
AccessDecision,
|
|
15
|
+
AccessDecisionSource,
|
|
10
16
|
AccessTargetKind,
|
|
17
|
+
DenialSources,
|
|
11
18
|
GrantPrincipal,
|
|
12
19
|
GrantSource,
|
|
13
20
|
GrantSources,
|
|
21
|
+
PathHolders,
|
|
22
|
+
ProspectiveHolders,
|
|
14
23
|
ResolvedPrincipal,
|
|
15
24
|
} from './access-control.interface.js';
|
|
25
|
+
import { PLUGINS_DIR, PLUGIN_MANIFEST_FILE, isPersonalPluginDir,
|
|
26
|
+
pluginIdentityOf,
|
|
27
|
+
platformRestoreDestination,
|
|
28
|
+
} from '@bevel-software/platform-shared';
|
|
16
29
|
import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
|
|
30
|
+
import { WorkflowDomainError } from '../../shared/domain-errors.js';
|
|
31
|
+
import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
|
|
17
32
|
import {
|
|
18
33
|
GROUPS_YAML,
|
|
19
34
|
SYNCED_GROUPS_YAML,
|
|
@@ -34,6 +49,7 @@ import {
|
|
|
34
49
|
parseAccessFile,
|
|
35
50
|
parseOwnAccessEntries,
|
|
36
51
|
parseRolesYaml,
|
|
52
|
+
requiredVerbsFor,
|
|
37
53
|
sourceVerbsFor,
|
|
38
54
|
type AccessFile,
|
|
39
55
|
type OwnEntries,
|
|
@@ -46,8 +62,6 @@ import {
|
|
|
46
62
|
DIRECTORY_SYNC_BOT_NAME,
|
|
47
63
|
} from './directory-sync-bot.js';
|
|
48
64
|
|
|
49
|
-
const execFileAsync = promisify(execFile);
|
|
50
|
-
|
|
51
65
|
/**
|
|
52
66
|
* Read many objects from a git repo in ONE `git cat-file --batch` process.
|
|
53
67
|
* `specs` are `<ref>:<path>` lines; the result array is index-aligned with
|
|
@@ -56,60 +70,39 @@ const execFileAsync = promisify(execFile);
|
|
|
56
70
|
* request: `<oid> <type> <size>\n<size bytes>\n`, or `<spec> missing\n`
|
|
57
71
|
* (the spec may itself contain spaces, hence the endsWith checks).
|
|
58
72
|
*/
|
|
59
|
-
function catFileBatch(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// fires with the exit code, which is the error we want to surface.
|
|
70
|
-
child.stdin.on('error', () => undefined);
|
|
71
|
-
child.on('error', reject);
|
|
72
|
-
child.on('close', (code) => {
|
|
73
|
-
if (code !== 0) {
|
|
74
|
-
reject(new Error(`git cat-file --batch exited ${code}: ${Buffer.concat(errChunks).toString('utf-8').trim()}`));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const out = Buffer.concat(chunks);
|
|
79
|
-
const results: (string | null)[] = [];
|
|
80
|
-
let off = 0;
|
|
81
|
-
for (let i = 0; i < specs.length; i++) {
|
|
82
|
-
const nl = out.indexOf(0x0a, off);
|
|
83
|
-
if (nl < 0) throw new Error('unexpected end of git cat-file output');
|
|
84
|
-
const header = out.subarray(off, nl).toString('utf-8');
|
|
85
|
-
off = nl + 1;
|
|
86
|
-
if (header.endsWith(' missing') || header.endsWith(' ambiguous')) {
|
|
87
|
-
results.push(null);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const parts = header.split(' ');
|
|
91
|
-
const size = Number(parts[2]);
|
|
92
|
-
if (parts.length !== 3 || !Number.isInteger(size) || size < 0) {
|
|
93
|
-
throw new Error(`unexpected git cat-file header: ${header}`);
|
|
94
|
-
}
|
|
95
|
-
// Non-blob (a directory path resolves to a tree) → null, but the
|
|
96
|
-
// payload still has to be skipped to stay aligned.
|
|
97
|
-
results.push(parts[1] === 'blob' ? out.subarray(off, off + size).toString('utf-8') : null);
|
|
98
|
-
off += size + 1; // payload + trailing LF
|
|
99
|
-
}
|
|
100
|
-
resolve(results);
|
|
101
|
-
} catch (err) {
|
|
102
|
-
reject(err);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
child.stdin.write(`${specs.join('\n')}\n`);
|
|
106
|
-
child.stdin.end();
|
|
73
|
+
async function catFileBatch(
|
|
74
|
+
runner: IGitRunner,
|
|
75
|
+
repoDir: string,
|
|
76
|
+
specs: string[],
|
|
77
|
+
): Promise<(string | null)[]> {
|
|
78
|
+
// Bytes, not text: every `<size>` in the header counts bytes, and the walk
|
|
79
|
+
// below has to step by that count through content that is not all ASCII.
|
|
80
|
+
const { stdout: out } = await runner.run(repoDir, ['cat-file', '--batch'], {
|
|
81
|
+
input: `${specs.join('\n')}\n`,
|
|
82
|
+
encoding: 'buffer',
|
|
107
83
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
84
|
+
const results: (string | null)[] = [];
|
|
85
|
+
let off = 0;
|
|
86
|
+
for (let i = 0; i < specs.length; i++) {
|
|
87
|
+
const nl = out.indexOf(0x0a, off);
|
|
88
|
+
if (nl < 0) throw new Error('unexpected end of git cat-file output');
|
|
89
|
+
const header = out.subarray(off, nl).toString('utf-8');
|
|
90
|
+
off = nl + 1;
|
|
91
|
+
if (header.endsWith(' missing') || header.endsWith(' ambiguous')) {
|
|
92
|
+
results.push(null);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const parts = header.split(' ');
|
|
96
|
+
const size = Number(parts[2]);
|
|
97
|
+
if (parts.length !== 3 || !Number.isInteger(size) || size < 0) {
|
|
98
|
+
throw new Error(`unexpected git cat-file header: ${header}`);
|
|
99
|
+
}
|
|
100
|
+
// Non-blob (a directory path resolves to a tree) → null, but the
|
|
101
|
+
// payload still has to be skipped to stay aligned.
|
|
102
|
+
results.push(parts[1] === 'blob' ? out.subarray(off, off + size).toString('utf-8') : null);
|
|
103
|
+
off += size + 1; // payload + trailing LF
|
|
104
|
+
}
|
|
105
|
+
return results;
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
|
|
@@ -127,10 +120,11 @@ interface AccessModel {
|
|
|
127
120
|
* Canonical emails that count as Admin whatever `roles.yaml` says — the
|
|
128
121
|
* deployment owner (`ADMIN_EMAIL`). Empty when none is configured.
|
|
129
122
|
*
|
|
130
|
-
* This exists for the two hardcoded `write` rescues below
|
|
131
|
-
* It is NOT a general grant: it never enters scope
|
|
132
|
-
* no read, no download, and no write
|
|
133
|
-
* `access.md
|
|
123
|
+
* This exists for the two hardcoded `write` rescues below and the root write
|
|
124
|
+
* floor, and only those. It is NOT a general grant: it never enters scope
|
|
125
|
+
* resolution, so it gives no read, no download, and no write beyond
|
|
126
|
+
* `roles.yaml`, `access.md`, and what the root floor reaches (see
|
|
127
|
+
* `hasPermissionResolved`). The deployment owner is the person who can already change
|
|
134
128
|
* `ADMIN_EMAIL` itself, so admitting them to the rescue path concedes
|
|
135
129
|
* nothing they could not already take — while withholding it turns a
|
|
136
130
|
* `roles.yaml` that has lost its last Admin into a knowledge base nobody
|
|
@@ -154,16 +148,10 @@ function roleKnown(roles: RolesIndex, canonicalRole: string): boolean {
|
|
|
154
148
|
* nothing, bare grant tokens fall through to roles, and group-backed denies
|
|
155
149
|
* drop (the owner's explicit degrade-loudly decision, NOT fail-closed). The
|
|
156
150
|
* marker is carried on the loaded model and surfaced by the groups admin
|
|
157
|
-
* endpoints so the Groups page can banner it.
|
|
151
|
+
* endpoints so the Groups & Members page can banner it.
|
|
158
152
|
*/
|
|
159
153
|
export type GroupsHealth = { ok: true } | { ok: false; file: string; reason: string };
|
|
160
154
|
|
|
161
|
-
/** True for the errno codes that mean "the file genuinely is not there". */
|
|
162
|
-
function isAbsenceError(err: unknown): boolean {
|
|
163
|
-
const code = (err as NodeJS.ErrnoException | null)?.code;
|
|
164
|
-
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
165
|
-
}
|
|
166
|
-
|
|
167
155
|
/**
|
|
168
156
|
* Load the ACTIVE group source through `read` (working tree or at-ref — the
|
|
169
157
|
* caller supplies the reader, so both model loaders share one mode rule):
|
|
@@ -171,13 +159,13 @@ function isAbsenceError(err: unknown): boolean {
|
|
|
171
159
|
* even when the synced file is empty or malformed — falling back would
|
|
172
160
|
* resurrect retired manual groups); otherwise `groups.yaml` → manual mode.
|
|
173
161
|
*
|
|
174
|
-
* `read` returns null for a genuinely-absent file
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
162
|
+
* `read` returns null for a genuinely-absent file — ENOENT/ENOTDIR, the
|
|
163
|
+
* reader's judgement through the fs probe — and THROWS for any other failure.
|
|
164
|
+
* Every thrown error — notably one on `synced-groups.yaml` — is treated as a
|
|
165
|
+
* BROKEN source: no groups, no fallback to the manual file (falling back
|
|
166
|
+
* would resurrect retired groups), and an `ok: false` health marker.
|
|
167
|
+
* Structural parse failures degrade the same way; this function never throws
|
|
168
|
+
* (except to let an `AccessUnreadableError` through — see below).
|
|
181
169
|
*/
|
|
182
170
|
export async function loadActiveGroups(
|
|
183
171
|
read: (filename: string) => Promise<string | null>,
|
|
@@ -201,14 +189,10 @@ export async function loadActiveGroups(
|
|
|
201
189
|
// subprocess. roles.yaml and access.md already fail the build closed on
|
|
202
190
|
// the same error; groups cannot be the one input that does not.
|
|
203
191
|
if (err instanceof AccessUnreadableError) throw err;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// groups.yaml — the synced file may exist and its manual predecessor is
|
|
209
|
-
// retired. Broken-groups instead.
|
|
210
|
-
return broken(SYNCED_GROUPS_YAML, err instanceof Error ? err.message : String(err));
|
|
211
|
-
}
|
|
192
|
+
// A read error on the SYNCED source must NOT fall back to groups.yaml —
|
|
193
|
+
// the synced file may exist and its manual predecessor is retired.
|
|
194
|
+
// Broken-groups instead.
|
|
195
|
+
return broken(SYNCED_GROUPS_YAML, err instanceof Error ? err.message : String(err));
|
|
212
196
|
}
|
|
213
197
|
|
|
214
198
|
const sourceFile = syncedText !== null ? SYNCED_GROUPS_YAML : GROUPS_YAML;
|
|
@@ -220,8 +204,7 @@ export async function loadActiveGroups(
|
|
|
220
204
|
text = await read(GROUPS_YAML);
|
|
221
205
|
} catch (err) {
|
|
222
206
|
if (err instanceof AccessUnreadableError) throw err; // see above
|
|
223
|
-
|
|
224
|
-
else return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
|
|
207
|
+
return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
|
|
225
208
|
}
|
|
226
209
|
}
|
|
227
210
|
if (text === null) return { groups: new Map(), sourceFile, warnings: [], health: { ok: true } };
|
|
@@ -253,13 +236,32 @@ export type ScopeSource = { kind: 'own' } | { kind: 'access-md'; path: string };
|
|
|
253
236
|
* already folded in (grant-only — see `buildScope`), so `byRole`/`byEmail`
|
|
254
237
|
* hold the *effective* verdict each named principal gets at this one scope.
|
|
255
238
|
*
|
|
239
|
+
* `byRole` and `byEmail` hold FILE-BACKED entries only — every key is a
|
|
240
|
+
* token some line in the file spells, which is what makes them the source
|
|
241
|
+
* of "what can be removed here" (`grantSources`). `everyone` is the one
|
|
242
|
+
* DERIVED verdict: what an unnamed signed-in caller gets at this scope. It
|
|
243
|
+
* is the literal `everyone` entry's state, promoted to a grant when the
|
|
244
|
+
* scope grants a PUBLIC plugin principal (one whose plugin admits everyone,
|
|
245
|
+
* so everyone holds it). Kept apart from `byRole` on purpose: a synthetic
|
|
246
|
+
* `everyone` key there would read as a line to revoke that no file has.
|
|
247
|
+
*
|
|
256
248
|
* `source` identifies the file the scope's rules come from (see `ScopeSource`),
|
|
257
249
|
* so a caller can map a per-scope verdict back to the editable file.
|
|
258
250
|
*/
|
|
259
251
|
interface AccessScope {
|
|
260
252
|
byRole: Map<string, GrantState>;
|
|
261
253
|
byEmail: Map<string, GrantState>;
|
|
254
|
+
everyone: GrantState | undefined;
|
|
262
255
|
source: ScopeSource;
|
|
256
|
+
/**
|
|
257
|
+
* The keys whose `'denied'` in `byRole` / `byEmail` is IMPLIED — a denial
|
|
258
|
+
* of a verb the target presupposes (`deny write` when resolving `owner`),
|
|
259
|
+
* with no `deny <target>` line of their own here. The verdict is as real as
|
|
260
|
+
* any (the effective walk, the eligible lists and the collapsed view treat
|
|
261
|
+
* it as denied), but it is not a LINE in this file: `denialSources`, which
|
|
262
|
+
* reports the entries a person can lift, leaves it out.
|
|
263
|
+
*/
|
|
264
|
+
implied: { byRole: Set<string>; byEmail: Set<string> };
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
/**
|
|
@@ -273,6 +275,65 @@ interface AccessScope {
|
|
|
273
275
|
* `hasPermissionResolved`). `collapseScopes` flattens this into the
|
|
274
276
|
* closest-wins-per-principal view the display helpers use.
|
|
275
277
|
*/
|
|
278
|
+
/**
|
|
279
|
+
* The rules listener on the one walk of the checkout: every `access.md` at
|
|
280
|
+
* any depth (the access tree is structure-agnostic — the root's included),
|
|
281
|
+
* plus every plugin manifest under the plugins root, for principal
|
|
282
|
+
* synthesis. A reader: a folder the walk could not list is simply not in
|
|
283
|
+
* the model (a writer that needs completeness asks the walk for its holes).
|
|
284
|
+
*
|
|
285
|
+
* Per-file failures (malformed YAML, bad role refs, unknown verbs) are
|
|
286
|
+
* logged and the offending file is dropped from the model — they do NOT
|
|
287
|
+
* throw. A typo in one nested access.md must not 500 the entire editor;
|
|
288
|
+
* admins can still write `access.md` / `roles.yaml` because
|
|
289
|
+
* `hasPermissionResolved` admin-rescues those paths, so the bad config
|
|
290
|
+
* remains fixable from inside the app.
|
|
291
|
+
*/
|
|
292
|
+
function accessFileListener(
|
|
293
|
+
repoDir: string,
|
|
294
|
+
out: Map<string, AccessFile>,
|
|
295
|
+
pluginDirs: Map<string, string>,
|
|
296
|
+
): WalkListener {
|
|
297
|
+
return {
|
|
298
|
+
async onFile(dir, name) {
|
|
299
|
+
const rel = dir ? `${dir}/${name}` : name;
|
|
300
|
+
if (name === PLUGIN_MANIFEST_FILE && dir.startsWith(`${PLUGINS_DIR}/`)) {
|
|
301
|
+
const text = await fs.readFile(path.join(repoDir, rel), 'utf-8').catch(() => null);
|
|
302
|
+
pluginDirs.set(dir, pluginIdentityOf(parseManifestText(text), path.posix.basename(dir)));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (name !== 'access.md') return;
|
|
306
|
+
const text = await fs.readFile(path.join(repoDir, rel), 'utf-8');
|
|
307
|
+
const parsed = parseAccessFile(text, rel);
|
|
308
|
+
if (!parsed.ok) {
|
|
309
|
+
// Treat as if the file didn't exist for resolution purposes.
|
|
310
|
+
// Admin-rescue on access.md paths still lets an admin fix it.
|
|
311
|
+
for (const e of parsed.errors) log.warn(`${e} — file ignored`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
for (const w of parsed.warnings) log.warn(w);
|
|
315
|
+
if (out.has(parsed.file.dir)) {
|
|
316
|
+
log.warn(`${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
out.set(parsed.file.dir, parsed.file);
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** A manifest's parsed object, or null for absent, unparsable or not-an-object text. */
|
|
325
|
+
function parseManifestText(text: string | null): Record<string, unknown> | null {
|
|
326
|
+
if (text === null) return null;
|
|
327
|
+
try {
|
|
328
|
+
const parsed: unknown = JSON.parse(text);
|
|
329
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
330
|
+
? (parsed as Record<string, unknown>)
|
|
331
|
+
: null;
|
|
332
|
+
} catch {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
276
337
|
function resolveScopes(
|
|
277
338
|
model: AccessModel,
|
|
278
339
|
verb: Verb,
|
|
@@ -280,13 +341,21 @@ function resolveScopes(
|
|
|
280
341
|
fileOwn?: OwnEntries | null,
|
|
281
342
|
): AccessScope[] {
|
|
282
343
|
const target = verb;
|
|
344
|
+
// Both lists come from the grammar's one dependency graph (`VERB_REQUIRES`):
|
|
345
|
+
// the verbs whose grant confers the target, and the verbs the target
|
|
346
|
+
// presupposes, whose denial strips it.
|
|
283
347
|
const supersets = sourceVerbsFor(verb).filter((v) => v !== verb);
|
|
348
|
+
const prerequisites = requiredVerbsFor(verb).filter((v) => v !== verb);
|
|
284
349
|
|
|
285
350
|
// Build one scope's effective verdicts. The target verb contributes both
|
|
286
|
-
// grants and denials
|
|
287
|
-
// never strips `read`)
|
|
288
|
-
//
|
|
289
|
-
//
|
|
351
|
+
// grants and denials. Superset verbs contribute grants only (a `deny write`
|
|
352
|
+
// never strips `read`); prerequisite verbs contribute DENIALS only (a `deny
|
|
353
|
+
// write` strips `owner`, a `deny read` strips all three — nobody owns what
|
|
354
|
+
// they may not edit, or edits what they may not open), and a `read` grant
|
|
355
|
+
// confers no `write`. Within the scope a grant always wins over a deny of
|
|
356
|
+
// the same principal, so a same-file grant of the target or of a superset
|
|
357
|
+
// overrides a deny of the target or of a prerequisite (`owner:` beats `deny
|
|
358
|
+
// write`, `write:` beats `deny read`).
|
|
290
359
|
const buildScope = (
|
|
291
360
|
entries: Record<Verb, ParsedEntry[]>,
|
|
292
361
|
filterRoles: boolean,
|
|
@@ -294,24 +363,62 @@ function resolveScopes(
|
|
|
294
363
|
): AccessScope => {
|
|
295
364
|
const byRole = new Map<string, GrantState>();
|
|
296
365
|
const byEmail = new Map<string, GrantState>();
|
|
366
|
+
const implied = { byRole: new Set<string>(), byEmail: new Set<string>() };
|
|
297
367
|
const set = (entry: ParsedEntry, state: GrantState) => {
|
|
298
368
|
const map = entry.kind === 'role' ? byRole : byEmail;
|
|
299
369
|
const key = entry.kind === 'role' ? entry.role : entry.email;
|
|
300
370
|
if (map.get(key) === 'grant') return; // a grant in this scope sticks
|
|
301
371
|
map.set(key, state);
|
|
302
372
|
};
|
|
373
|
+
const skip = (entry: ParsedEntry) =>
|
|
374
|
+
filterRoles && entry.kind === 'role' && !roleKnown(model.roles, entry.role);
|
|
303
375
|
for (const entry of entries[target]) {
|
|
304
|
-
if (
|
|
376
|
+
if (skip(entry)) continue;
|
|
305
377
|
set(entry, entry.deny ? 'denied' : 'grant');
|
|
306
378
|
}
|
|
307
379
|
for (const src of supersets) {
|
|
308
380
|
for (const entry of entries[src]) {
|
|
309
381
|
if (entry.deny) continue; // grant-only fold
|
|
310
|
-
if (
|
|
382
|
+
if (skip(entry)) continue;
|
|
311
383
|
set(entry, 'grant');
|
|
312
384
|
}
|
|
313
385
|
}
|
|
314
|
-
|
|
386
|
+
// Last, so a target or superset GRANT already in the map stands, and a
|
|
387
|
+
// target's own `deny` line is never re-labelled as implied.
|
|
388
|
+
for (const src of prerequisites) {
|
|
389
|
+
for (const entry of entries[src]) {
|
|
390
|
+
if (!entry.deny) continue; // deny-only fold
|
|
391
|
+
if (skip(entry)) continue;
|
|
392
|
+
const map = entry.kind === 'role' ? byRole : byEmail;
|
|
393
|
+
const key = entry.kind === 'role' ? entry.role : entry.email;
|
|
394
|
+
if (map.has(key)) continue; // an own verdict (grant or deny) is already the answer
|
|
395
|
+
map.set(key, 'denied');
|
|
396
|
+
(entry.kind === 'role' ? implied.byRole : implied.byEmail).add(key);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// The anonymous caller's verdict at this scope — what someone who holds
|
|
400
|
+
// exactly the PUBLIC keys (a plugin principal whose plugin admits
|
|
401
|
+
// everyone, so everyone holds it) resolves to, by the same tiers a named
|
|
402
|
+
// caller gets: a grant via any public key wins, else a denial via one
|
|
403
|
+
// denies, else the built-in `everyone` entry decides. Derived HERE, where
|
|
404
|
+
// a scope's verdicts are built, so every reader of it — the permission
|
|
405
|
+
// check, the eligible lists, the `restricted` flag, the compiler's
|
|
406
|
+
// everyone audience — sees one truth; and derived into its own field,
|
|
407
|
+
// never into `byRole`, so the entries a person can remove stay exactly
|
|
408
|
+
// the lines the files hold.
|
|
409
|
+
let publicGrant = false;
|
|
410
|
+
let publicDeny = false;
|
|
411
|
+
for (const [key, state] of byRole) {
|
|
412
|
+
if (!model.roles.publicKeys?.has(key)) continue;
|
|
413
|
+
if (state === 'grant') publicGrant = true;
|
|
414
|
+
else if (state === 'denied') publicDeny = true;
|
|
415
|
+
}
|
|
416
|
+
const everyone: GrantState | undefined = publicGrant
|
|
417
|
+
? 'grant'
|
|
418
|
+
: publicDeny
|
|
419
|
+
? 'denied'
|
|
420
|
+
: byRole.get(EVERYONE_CANONICAL);
|
|
421
|
+
return { byRole, byEmail, everyone, source, implied };
|
|
315
422
|
};
|
|
316
423
|
|
|
317
424
|
const scopes: AccessScope[] = [];
|
|
@@ -328,7 +435,7 @@ function resolveScopes(
|
|
|
328
435
|
|
|
329
436
|
/** The collapsed closest-wins view: per-principal verdicts with no single
|
|
330
437
|
* source (it's a flattening across scopes). */
|
|
331
|
-
type CollapsedScope = Pick<AccessScope, 'byRole' | 'byEmail'>;
|
|
438
|
+
type CollapsedScope = Pick<AccessScope, 'byRole' | 'byEmail' | 'everyone'>;
|
|
332
439
|
|
|
333
440
|
/**
|
|
334
441
|
* Flatten ordered scopes (closest→farthest) into a single closest-wins
|
|
@@ -339,11 +446,13 @@ type CollapsedScope = Pick<AccessScope, 'byRole' | 'byEmail'>;
|
|
|
339
446
|
function collapseScopes(scopes: AccessScope[]): CollapsedScope {
|
|
340
447
|
const byRole = new Map<string, GrantState>();
|
|
341
448
|
const byEmail = new Map<string, GrantState>();
|
|
449
|
+
let everyone: GrantState | undefined;
|
|
342
450
|
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
343
451
|
for (const [k, v] of scopes[i].byRole) byRole.set(k, v);
|
|
344
452
|
for (const [k, v] of scopes[i].byEmail) byEmail.set(k, v);
|
|
453
|
+
if (scopes[i].everyone !== undefined) everyone = scopes[i].everyone;
|
|
345
454
|
}
|
|
346
|
-
return { byRole, byEmail };
|
|
455
|
+
return { byRole, byEmail, everyone };
|
|
347
456
|
}
|
|
348
457
|
|
|
349
458
|
function resolveAtPath(
|
|
@@ -355,13 +464,118 @@ function resolveAtPath(
|
|
|
355
464
|
return collapseScopes(resolveScopes(model, verb, relativePath, fileOwn));
|
|
356
465
|
}
|
|
357
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Every principal key a caller holds: the tokens their email is a member of,
|
|
469
|
+
* plus the PUBLIC keys every caller holds regardless (a plugin principal
|
|
470
|
+
* whose plugin grants `everyone`). Undefined when there is nothing at all,
|
|
471
|
+
* matching the plain `byEmail.get` the tier-2 check used to read.
|
|
472
|
+
*/
|
|
473
|
+
function principalKeysOf(model: AccessModel, email: string): Set<string> | undefined {
|
|
474
|
+
const own = model.roles.byEmail.get(email);
|
|
475
|
+
const pub = model.roles.publicKeys;
|
|
476
|
+
if (!pub?.size) return own;
|
|
477
|
+
return new Set([...(own ?? []), ...pub]);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The explicit key of the Admin ROLE — never the bare token, which a same-named group may own. */
|
|
481
|
+
const ADMIN_ROLE_KEY = `${ROLE_TOKEN_PREFIX}${ADMIN_CANONICAL}`;
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Add to `keys` what holding them confers on top: every plugin principal
|
|
485
|
+
* expanded from one of the keys, and the public keys every caller holds.
|
|
486
|
+
*/
|
|
487
|
+
function addDerivedKeys(model: AccessModel, keys: Set<string>): Set<string> {
|
|
488
|
+
for (const [key, principal] of model.roles.byCanonical) {
|
|
489
|
+
if (principal.kind !== 'plugin' || !principal.sourceKeys) continue;
|
|
490
|
+
for (const source of principal.sourceKeys) {
|
|
491
|
+
if (keys.has(source)) {
|
|
492
|
+
keys.add(key);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const key of model.roles.publicKeys ?? []) keys.add(key);
|
|
498
|
+
return keys;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Every principal key that being in `group` confers — the group's OWN key,
|
|
503
|
+
* the roles that list the group (`group:<Name>` in roles.yaml), the plugin
|
|
504
|
+
* principals whose roster admits the group directly or through one of those
|
|
505
|
+
* roles, and the public keys every caller holds. This is the group-shaped
|
|
506
|
+
* twin of `principalKeysOf`: a member's key set minus everything they hold
|
|
507
|
+
* for reasons of their own. `null` when no group of that name exists.
|
|
508
|
+
*
|
|
509
|
+
* Groups are keyed by their bare canonical name (`mergeGroupsIntoRoles`);
|
|
510
|
+
* roles by `role/<name>`. A bare key whose record is a ROLE is not a group,
|
|
511
|
+
* however it was spelled.
|
|
512
|
+
*/
|
|
513
|
+
function principalKeysOfGroup(model: AccessModel, group: string): Set<string> | null {
|
|
514
|
+
const canonical = canonicalRoleName(group);
|
|
515
|
+
const record = model.roles.byCanonical.get(canonical);
|
|
516
|
+
if (!record || record.kind !== 'group') return null;
|
|
517
|
+
const keys = new Set<string>([canonical]);
|
|
518
|
+
for (const [key, principal] of model.roles.byCanonical) {
|
|
519
|
+
if (principal.kind === 'role' && principal.groupRefs?.has(canonical)) keys.add(key);
|
|
520
|
+
}
|
|
521
|
+
// Plugin principals were expanded from role/group entries; a plugin that
|
|
522
|
+
// admits the group, or a role the group is folded into, admits the group.
|
|
523
|
+
return addDerivedKeys(model, keys);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** The key set of a caller who holds nothing: `hasPermissionForKeys` then answers as `everyone`. */
|
|
527
|
+
const NO_KEYS: ReadonlySet<string> = new Set();
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The tier-2 half of `hasPermissionResolved` on its own: a verdict for a set
|
|
531
|
+
* of principal KEYS with no person behind them — no direct-email tier, no
|
|
532
|
+
* admin rescue, no machine-owned rule — only the Admin write floor, when the
|
|
533
|
+
* keys include the Admin role. Closest scope first; within a scope a
|
|
534
|
+
* grant to any key wins over a denial to another, exactly as for a person.
|
|
535
|
+
*/
|
|
536
|
+
function hasPermissionForKeys(
|
|
537
|
+
model: AccessModel,
|
|
538
|
+
verb: Verb,
|
|
539
|
+
keys: ReadonlySet<string>,
|
|
540
|
+
relativePath: string,
|
|
541
|
+
fileOwn?: OwnEntries | null,
|
|
542
|
+
): boolean {
|
|
543
|
+
const adminFloor = verb === 'write' && keys.has(ADMIN_ROLE_KEY);
|
|
544
|
+
for (const scope of resolveScopes(model, verb, relativePath, fileOwn)) {
|
|
545
|
+
if (adminFloor && isAdminFloorScope(scope, relativePath)) return true;
|
|
546
|
+
let grant = false;
|
|
547
|
+
let deny = false;
|
|
548
|
+
for (const key of keys) {
|
|
549
|
+
const state = scope.byRole.get(key);
|
|
550
|
+
if (state === 'denied') deny = true;
|
|
551
|
+
else if (state === 'grant') grant = true;
|
|
552
|
+
}
|
|
553
|
+
if (grant) return true;
|
|
554
|
+
if (deny) return false;
|
|
555
|
+
if (scope.everyone) return scope.everyone === 'grant';
|
|
556
|
+
}
|
|
557
|
+
return adminFloor;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Whether a scope sits at the Admin write floor: the repository root's own
|
|
562
|
+
* `access.md`, or the own frontmatter of a file directly in the root (those
|
|
563
|
+
* files are part of the root, so their own rules cannot take Admin's write
|
|
564
|
+
* away either). A folder directly in the root is not covered — its own
|
|
565
|
+
* `access.md` is an ordinary subfolder scope that may exclude Admin.
|
|
566
|
+
*/
|
|
567
|
+
function isAdminFloorScope(scope: AccessScope, relativePath: string): boolean {
|
|
568
|
+
if (scope.source.kind === 'own') return !relativePath.includes('/');
|
|
569
|
+
return scope.source.path === 'access.md';
|
|
570
|
+
}
|
|
571
|
+
|
|
358
572
|
function isAdminEmail(model: AccessModel, email: string): boolean {
|
|
359
573
|
if (model.deploymentOwners.has(email)) return true;
|
|
360
574
|
const roles = model.roles.byEmail.get(email);
|
|
361
575
|
// Check the explicit `role/admin` alias, NOT the bare token: bare-name
|
|
362
576
|
// precedence is group-first, so a group that happens to be named "Admin"
|
|
363
577
|
// owns the bare key — and its members must never inherit the capability.
|
|
364
|
-
return !!roles && roles.has(
|
|
578
|
+
return !!roles && roles.has(ADMIN_ROLE_KEY);
|
|
365
579
|
}
|
|
366
580
|
|
|
367
581
|
/**
|
|
@@ -395,6 +609,16 @@ function isAdminEmail(model: AccessModel, email: string): boolean {
|
|
|
395
609
|
* owner is whoever can already set `ADMIN_EMAIL`, so this concedes no
|
|
396
610
|
* authority they did not have; it only gives it a door.
|
|
397
611
|
*
|
|
612
|
+
* One floor for `write`, applied during the walk: an admin (same definition)
|
|
613
|
+
* always holds `write` at the repository root. When the walk reaches the root
|
|
614
|
+
* `access.md` (or the own frontmatter of a file directly in the root) — or runs
|
|
615
|
+
* out of scopes without a verdict — an admin is granted,
|
|
616
|
+
* whatever the root file says, so no root rule can lock the deployment out of
|
|
617
|
+
* its own tree. A NEARER scope still decides first: a subfolder that denies
|
|
618
|
+
* Admin (or denies `everyone` without naming Admin) excludes admins there like
|
|
619
|
+
* anyone else. The root file's own entries are not rewritten — the floor is a
|
|
620
|
+
* verdict, not a grant line — so `grantSources` stays file-backed.
|
|
621
|
+
*
|
|
398
622
|
* No special-cases for `read`/`download` — they fall through to scope resolution.
|
|
399
623
|
*/
|
|
400
624
|
function hasPermissionResolved(
|
|
@@ -404,44 +628,88 @@ function hasPermissionResolved(
|
|
|
404
628
|
relativePath: string,
|
|
405
629
|
fileOwn?: OwnEntries | null,
|
|
406
630
|
): boolean {
|
|
631
|
+
return decidePermissionResolved(model, verb, userEmail, relativePath, fileOwn).allowed;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* WHAT decided one caller's verdict: `hasPermissionResolved` with its reasoning
|
|
636
|
+
* kept. `scope` is the scope whose entry settled it (null for the hardcoded
|
|
637
|
+
* admin rescues and for default-deny; the root scope, or null when there is
|
|
638
|
+
* none, for the Admin write floor); `via` is the tier that matched and `key`
|
|
639
|
+
* the principal key it matched on (the caller's email, a role/group/plugin
|
|
640
|
+
* key, or `everyone`). `hasPermissionResolved` IS this function's `allowed`,
|
|
641
|
+
* so an explanation can never disagree with the gate.
|
|
642
|
+
*/
|
|
643
|
+
interface PermissionDecision {
|
|
644
|
+
allowed: boolean;
|
|
645
|
+
scope: AccessScope | null;
|
|
646
|
+
via: 'email' | 'principal' | 'everyone' | 'admin-rescue' | 'admin-floor' | 'default-deny';
|
|
647
|
+
key?: string;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function decidePermissionResolved(
|
|
651
|
+
model: AccessModel,
|
|
652
|
+
verb: Verb,
|
|
653
|
+
userEmail: string,
|
|
654
|
+
relativePath: string,
|
|
655
|
+
fileOwn?: OwnEntries | null,
|
|
656
|
+
): PermissionDecision {
|
|
407
657
|
const email = canonicalEmail(userEmail);
|
|
408
658
|
|
|
409
659
|
if (verb === 'write') {
|
|
410
|
-
if (relativePath === 'roles.yaml')
|
|
411
|
-
|
|
660
|
+
if (relativePath === 'roles.yaml') {
|
|
661
|
+
return { allowed: isAdminEmail(model, email), scope: null, via: 'admin-rescue' };
|
|
662
|
+
}
|
|
663
|
+
if (isAccessMdPath(relativePath) && isAdminEmail(model, email)) {
|
|
664
|
+
return { allowed: true, scope: null, via: 'admin-rescue' };
|
|
665
|
+
}
|
|
412
666
|
}
|
|
413
667
|
|
|
414
|
-
const
|
|
668
|
+
const adminFloor = verb === 'write' && isAdminEmail(model, email);
|
|
669
|
+
// For write, the deployment owner IS Admin — the floor admits them, so the
|
|
670
|
+
// Admin rules along the way bind them too: a subfolder denying Admin excludes
|
|
671
|
+
// the owner exactly as it excludes the role's members.
|
|
672
|
+
let userRoles = principalKeysOf(model, email);
|
|
673
|
+
if (adminFloor && !userRoles?.has(ADMIN_ROLE_KEY)) {
|
|
674
|
+
userRoles = new Set([...(userRoles ?? []), ...principalKeysOfToken(model, ADMIN_ROLE_KEY)]);
|
|
675
|
+
}
|
|
415
676
|
const scopes = resolveScopes(model, verb, relativePath, fileOwn);
|
|
416
677
|
|
|
417
678
|
for (const scope of scopes) {
|
|
679
|
+
if (adminFloor && isAdminFloorScope(scope, relativePath)) return { allowed: true, scope, via: 'admin-floor' };
|
|
680
|
+
|
|
418
681
|
// Tier 1 — direct email entry is the most specific verdict at this scope.
|
|
419
682
|
const direct = scope.byEmail.get(email);
|
|
420
|
-
if (direct) return direct === 'grant';
|
|
683
|
+
if (direct) return { allowed: direct === 'grant', scope, via: 'email', key: email };
|
|
421
684
|
|
|
422
685
|
// Tier 2 — the caller's roles. A grant via any one of them wins over a deny
|
|
423
686
|
// via another at this same scope (a `deny Engineer` doesn't undo an
|
|
424
687
|
// unrelated `Admin` grant).
|
|
425
688
|
if (userRoles && userRoles.size) {
|
|
426
|
-
let
|
|
427
|
-
let
|
|
689
|
+
let granted: string | undefined;
|
|
690
|
+
let denied: string | undefined;
|
|
428
691
|
for (const r of userRoles) {
|
|
429
692
|
const s = scope.byRole.get(r);
|
|
430
|
-
if (s === 'denied')
|
|
431
|
-
else if (s === 'grant')
|
|
693
|
+
if (s === 'denied') denied ??= r;
|
|
694
|
+
else if (s === 'grant') granted ??= r;
|
|
432
695
|
}
|
|
433
|
-
if (
|
|
434
|
-
if (
|
|
696
|
+
if (granted !== undefined) return { allowed: true, scope, via: 'principal', key: granted };
|
|
697
|
+
if (denied !== undefined) return { allowed: false, scope, via: 'principal', key: denied };
|
|
435
698
|
}
|
|
436
699
|
|
|
437
|
-
// Tier 3 — the built-in `everyone` role
|
|
438
|
-
|
|
439
|
-
if (everyone)
|
|
700
|
+
// Tier 3 — the built-in `everyone` role (derived: a public plugin
|
|
701
|
+
// principal granted here counts, see `AccessScope.everyone`).
|
|
702
|
+
if (scope.everyone) {
|
|
703
|
+
return { allowed: scope.everyone === 'grant', scope, via: 'everyone', key: EVERYONE_CANONICAL };
|
|
704
|
+
}
|
|
440
705
|
|
|
441
706
|
// No verdict at this scope — fall through to the next (farther) one.
|
|
442
707
|
}
|
|
443
708
|
|
|
444
|
-
|
|
709
|
+
// No verdict anywhere (and no root access.md to stop at): the floor still holds.
|
|
710
|
+
return adminFloor
|
|
711
|
+
? { allowed: true, scope: null, via: 'admin-floor' }
|
|
712
|
+
: { allowed: false, scope: null, via: 'default-deny' };
|
|
445
713
|
}
|
|
446
714
|
|
|
447
715
|
/**
|
|
@@ -473,6 +741,49 @@ function scopeToGrantSource(
|
|
|
473
741
|
return { kind: 'ancestor', path };
|
|
474
742
|
}
|
|
475
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Where a scope's rules live, as the place a person edits: a folder (the
|
|
746
|
+
* directory of its `access.md`) or the target's own frontmatter.
|
|
747
|
+
*/
|
|
748
|
+
function decisionSourceOf(
|
|
749
|
+
scope: AccessScope,
|
|
750
|
+
kind: AccessTargetKind,
|
|
751
|
+
relativePath: string,
|
|
752
|
+
): AccessDecisionSource {
|
|
753
|
+
if (scope.source.kind === 'own') return { kind: 'frontmatter', path: relativePath, inherited: false };
|
|
754
|
+
const folder = path.posix.dirname(scope.source.path);
|
|
755
|
+
const dir = folder === '.' ? '' : folder;
|
|
756
|
+
const grant = scopeToGrantSource(scope, kind, relativePath);
|
|
757
|
+
return { kind: 'folder', path: dir, inherited: grant.kind === 'ancestor' };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** A resolver decision in the terms `explainAccess` reports. */
|
|
761
|
+
function explainDecision(
|
|
762
|
+
model: AccessModel,
|
|
763
|
+
decision: PermissionDecision,
|
|
764
|
+
kind: AccessTargetKind,
|
|
765
|
+
relativePath: string,
|
|
766
|
+
): AccessDecision {
|
|
767
|
+
const source = decision.scope ? decisionSourceOf(decision.scope, kind, relativePath) : null;
|
|
768
|
+
const base = { allowed: decision.allowed, source };
|
|
769
|
+
switch (decision.via) {
|
|
770
|
+
case 'admin-rescue':
|
|
771
|
+
case 'default-deny':
|
|
772
|
+
return { ...base, via: decision.via, principal: null };
|
|
773
|
+
case 'admin-floor':
|
|
774
|
+
return { ...base, via: decision.via, principal: 'Admin' };
|
|
775
|
+
case 'email':
|
|
776
|
+
return { ...base, via: 'person', principal: decision.key ?? null };
|
|
777
|
+
case 'everyone':
|
|
778
|
+
return { ...base, via: 'everyone', principal: EVERYONE_CANONICAL };
|
|
779
|
+
case 'principal': {
|
|
780
|
+
const key = decision.key ?? '';
|
|
781
|
+
const record = model.roles.byCanonical.get(key);
|
|
782
|
+
return { ...base, via: record?.kind ?? 'role', principal: record?.displayName ?? key };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
476
787
|
/**
|
|
477
788
|
* Resolve EVERY file scope that names a principal for `verb`, ordered
|
|
478
789
|
* closest-first — the source-returning twin of `hasPermissionResolved`, but
|
|
@@ -531,14 +842,7 @@ function resolveGrantSourcesForVerb(
|
|
|
531
842
|
// a GROUP whose group has VANISHED reads "unshadowed" here, and the
|
|
532
843
|
// alias-tolerant pair would then attribute a same-named role's
|
|
533
844
|
// surviving `role/<name>` grant to the group.
|
|
534
|
-
const
|
|
535
|
-
const explicit = canonical.startsWith(ROLE_TOKEN_PREFIX);
|
|
536
|
-
const bare = explicit ? canonical.slice(ROLE_TOKEN_PREFIX.length) : canonical;
|
|
537
|
-
const shadowed = model.roles.byCanonical.get(bare)?.kind === 'group';
|
|
538
|
-
const tokens =
|
|
539
|
-
tokenMatch === 'exact' || shadowed
|
|
540
|
-
? [explicit ? `${ROLE_TOKEN_PREFIX}${bare}` : bare]
|
|
541
|
-
: [bare, `${ROLE_TOKEN_PREFIX}${bare}`];
|
|
845
|
+
const tokens = ownTokensOf(model, principal.role, tokenMatch);
|
|
542
846
|
for (const scope of scopes) {
|
|
543
847
|
const states = tokens.map((t) => scope.byRole.get(t));
|
|
544
848
|
if (states.includes('grant')) {
|
|
@@ -564,6 +868,84 @@ function resolveGrantSourcesForVerb(
|
|
|
564
868
|
return out;
|
|
565
869
|
}
|
|
566
870
|
|
|
871
|
+
/**
|
|
872
|
+
* WHICH token spellings in a file are THIS role-shaped principal's own entries
|
|
873
|
+
* — the shadowing rule, in one place because the grant walk, the denial walk
|
|
874
|
+
* and the revoke splice must all agree on it (a disagreement attributes a
|
|
875
|
+
* shadowed bare token to the wrong principal, which is how a "restrict" silently
|
|
876
|
+
* strips a same-named role's grant). See `resolveGrantSourcesForVerb` for the
|
|
877
|
+
* full reasoning behind each branch.
|
|
878
|
+
*/
|
|
879
|
+
function ownTokensOf(
|
|
880
|
+
model: AccessModel,
|
|
881
|
+
role: string,
|
|
882
|
+
tokenMatch?: 'exact' | 'name',
|
|
883
|
+
): string[] {
|
|
884
|
+
const canonical = canonicalRoleName(role);
|
|
885
|
+
const explicit = canonical.startsWith(ROLE_TOKEN_PREFIX);
|
|
886
|
+
const bare = explicit ? canonical.slice(ROLE_TOKEN_PREFIX.length) : canonical;
|
|
887
|
+
const shadowed = model.roles.byCanonical.get(bare)?.kind === 'group';
|
|
888
|
+
return tokenMatch === 'exact' || shadowed
|
|
889
|
+
? [explicit ? `${ROLE_TOKEN_PREFIX}${bare}` : bare]
|
|
890
|
+
: [bare, `${ROLE_TOKEN_PREFIX}${bare}`];
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* The polarity twin of {@link resolveGrantSourcesForVerb}: every file scope
|
|
895
|
+
* whose own entry for this principal DENIES `verb`, closest-first.
|
|
896
|
+
*
|
|
897
|
+
* Same walk, mirrored stopping rule. A grant in the principal's own entry at a
|
|
898
|
+
* scope ENDS the list — under closest-wins a farther deny is dead, and reporting
|
|
899
|
+
* it would have the dialog say "restricted" about a verb the principal holds.
|
|
900
|
+
* A grant or deny reaching the principal through a group/role/`everyone` is not
|
|
901
|
+
* their own entry: it is neither a denial to report nor a reason to stop, so the
|
|
902
|
+
* walk carries on past it, exactly as the grant twin does.
|
|
903
|
+
*
|
|
904
|
+
* Returns `[]` when the principal is not denied `verb` by any file entry.
|
|
905
|
+
*/
|
|
906
|
+
function resolveDenialSourcesForVerb(
|
|
907
|
+
model: AccessModel,
|
|
908
|
+
verb: Verb,
|
|
909
|
+
kind: AccessTargetKind,
|
|
910
|
+
relativePath: string,
|
|
911
|
+
principal: GrantPrincipal,
|
|
912
|
+
fileOwn?: OwnEntries | null,
|
|
913
|
+
tokenMatch?: 'exact' | 'name',
|
|
914
|
+
): GrantSource[] {
|
|
915
|
+
const scopes = resolveScopes(model, verb, relativePath, fileOwn);
|
|
916
|
+
const out: GrantSource[] = [];
|
|
917
|
+
|
|
918
|
+
if (principal.kind === 'role') {
|
|
919
|
+
const tokens = ownTokensOf(model, principal.role, tokenMatch);
|
|
920
|
+
for (const scope of scopes) {
|
|
921
|
+
const states = tokens.map((t) => scope.byRole.get(t));
|
|
922
|
+
// A grant under EITHER spelling wins within the scope (`buildScope` lets a
|
|
923
|
+
// grant stick over a deny of the same principal), so it ends the walk
|
|
924
|
+
// whether or not the other spelling denies here.
|
|
925
|
+
if (states.includes('grant')) break;
|
|
926
|
+
// Only a `deny <verb>` LINE is a source; a denial the scope implies from
|
|
927
|
+
// a prerequisite verb's line (`deny write` when asking about owner) is
|
|
928
|
+
// real but is not this verb's entry — the prerequisite's own report is
|
|
929
|
+
// where it can be lifted.
|
|
930
|
+
const written = tokens.some(
|
|
931
|
+
(t) => scope.byRole.get(t) === 'denied' && !scope.implied.byRole.has(t),
|
|
932
|
+
);
|
|
933
|
+
if (written) out.push(scopeToGrantSource(scope, kind, relativePath));
|
|
934
|
+
}
|
|
935
|
+
return out;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const email = canonicalEmail(principal.email);
|
|
939
|
+
for (const scope of scopes) {
|
|
940
|
+
const own = scope.byEmail.get(email);
|
|
941
|
+
if (own === 'grant') break;
|
|
942
|
+
if (own === 'denied' && !scope.implied.byEmail.has(email)) {
|
|
943
|
+
out.push(scopeToGrantSource(scope, kind, relativePath));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
|
|
567
949
|
/**
|
|
568
950
|
* Build the repo-root → path chain of directory scopes that govern
|
|
569
951
|
* `relativePath`, e.g. `Knowledge/Sales/Foo.md` →
|
|
@@ -600,11 +982,11 @@ function canEveryoneReadResolved(
|
|
|
600
982
|
relativePath: string,
|
|
601
983
|
fileOwn?: OwnEntries | null,
|
|
602
984
|
): boolean {
|
|
603
|
-
const {
|
|
985
|
+
const { byEmail, everyone } = resolveAtPath(model, 'read', relativePath, fileOwn);
|
|
604
986
|
// Baseline: an unnamed signed-in user (no email/role entries) reads only via
|
|
605
987
|
// `everyone`. As a single principal, its collapsed verdict is its closest —
|
|
606
988
|
// exactly what that user resolves to.
|
|
607
|
-
if (
|
|
989
|
+
if (everyone !== 'grant') return false;
|
|
608
990
|
// Every *named* principal must also still read. A collapsed deny may have
|
|
609
991
|
// been shadowed by a closer-scope `everyone` grant, so re-resolve each
|
|
610
992
|
// candidate through the closeness-first gate rather than trusting the
|
|
@@ -639,13 +1021,37 @@ function canReadResolved(
|
|
|
639
1021
|
return hasPermissionResolved(model, 'read', userEmail, relativePath, fileOwn);
|
|
640
1022
|
}
|
|
641
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* The key set someone holding exactly the principal behind `token` carries —
|
|
1026
|
+
* the question "would a member of this principal be allowed?" put to
|
|
1027
|
+
* `hasPermissionForKeys`. A group gets its full member key set
|
|
1028
|
+
* (`principalKeysOfGroup`); a role or plugin principal gets every key its
|
|
1029
|
+
* record is registered under (the bare name and the `role/` alias share one
|
|
1030
|
+
* record) plus the plugin principals expanded from those keys; a token with no
|
|
1031
|
+
* record (`everyone`, a vanished principal) is just itself. Public keys are
|
|
1032
|
+
* always included — every caller holds them.
|
|
1033
|
+
*/
|
|
1034
|
+
function principalKeysOfToken(model: AccessModel, token: string): Set<string> {
|
|
1035
|
+
const record = model.roles.byCanonical.get(token);
|
|
1036
|
+
if (record?.kind === 'group') return principalKeysOfGroup(model, token) ?? new Set([token]);
|
|
1037
|
+
const keys = new Set<string>([token]);
|
|
1038
|
+
if (!record) {
|
|
1039
|
+
for (const key of model.roles.publicKeys ?? []) keys.add(key);
|
|
1040
|
+
return keys;
|
|
1041
|
+
}
|
|
1042
|
+
for (const [key, principal] of model.roles.byCanonical) {
|
|
1043
|
+
if (principal === record) keys.add(key);
|
|
1044
|
+
}
|
|
1045
|
+
return addDerivedKeys(model, keys);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
642
1048
|
function eligibleHoldersResolved(
|
|
643
1049
|
model: AccessModel,
|
|
644
1050
|
verb: Verb,
|
|
645
1051
|
relativePath: string,
|
|
646
1052
|
fileOwn?: OwnEntries | null,
|
|
647
1053
|
): { principals: ResolvedPrincipal[]; roles: string[]; users: { name: string; email: string }[] } {
|
|
648
|
-
const { byRole, byEmail } = resolveAtPath(model, verb, relativePath, fileOwn);
|
|
1054
|
+
const { byRole, byEmail, everyone } = resolveAtPath(model, verb, relativePath, fileOwn);
|
|
649
1055
|
|
|
650
1056
|
// (kind, display name) → one entry. The `byRole` keys are the merged
|
|
651
1057
|
// index's canonical tokens exactly as granted (bare, or the
|
|
@@ -659,15 +1065,35 @@ function eligibleHoldersResolved(
|
|
|
659
1065
|
// principals, and collapsing them to one would hide the role's live
|
|
660
1066
|
// `role/<name>` grant from every consumer of the eligible list.
|
|
661
1067
|
const byIdentity = new Map<string, ResolvedPrincipal>();
|
|
662
|
-
const addPrincipal = (name: string, kind: '
|
|
1068
|
+
const addPrincipal = (name: string, kind: ResolvedPrincipal['kind']) => {
|
|
663
1069
|
const key = `${kind}\0${name.toLowerCase()}`;
|
|
664
1070
|
if (!byIdentity.has(key)) byIdentity.set(key, { name, kind });
|
|
665
1071
|
};
|
|
1072
|
+
// Every row is re-checked against the resolver. The collapsed view keeps a
|
|
1073
|
+
// principal's closest OWN verdict, but a nearer scope can still decide for
|
|
1074
|
+
// them without naming them — a `deny everyone` in a subfolder cuts off a
|
|
1075
|
+
// root `write: Admin` — and a list that named them anyway would tell the
|
|
1076
|
+
// dialog and the denial message that someone is eligible whom the gate then
|
|
1077
|
+
// refuses.
|
|
1078
|
+
const allowedFor = (keys: ReadonlySet<string>) =>
|
|
1079
|
+
hasPermissionForKeys(model, verb, keys, relativePath, fileOwn);
|
|
666
1080
|
for (const [canonical, state] of byRole) {
|
|
667
1081
|
if (state !== 'grant') continue;
|
|
1082
|
+
if (!allowedFor(principalKeysOfToken(model, canonical))) continue;
|
|
668
1083
|
const record = model.roles.byCanonical.get(canonical);
|
|
669
1084
|
addPrincipal(record ? record.displayName : canonical, record?.kind ?? 'role');
|
|
670
1085
|
}
|
|
1086
|
+
// The Admin write floor (see `hasPermissionResolved`): Admin holds write here
|
|
1087
|
+
// whenever no nearer scope excludes it, whether or not any file names it.
|
|
1088
|
+
if (verb === 'write' && model.roles.byCanonical.has(ADMIN_ROLE_KEY)) {
|
|
1089
|
+
const adminRole = model.roles.byCanonical.get(ADMIN_ROLE_KEY)!;
|
|
1090
|
+
if (allowedFor(principalKeysOfToken(model, ADMIN_ROLE_KEY))) addPrincipal(adminRole.displayName, 'role');
|
|
1091
|
+
}
|
|
1092
|
+
// The derived everyone verdict: a public plugin principal granted here
|
|
1093
|
+
// means anyone signed in holds the verb, and a list that counts holders (an
|
|
1094
|
+
// approval gate, a "restricted to" banner) must say so — as `everyone`, the
|
|
1095
|
+
// same row a literal grant produces.
|
|
1096
|
+
if (everyone === 'grant' && allowedFor(NO_KEYS)) addPrincipal(EVERYONE_CANONICAL, 'role');
|
|
671
1097
|
|
|
672
1098
|
// Mirror the admin overrides applied in `hasPermissionResolved`: write on
|
|
673
1099
|
// `roles.yaml` and on any `access.md` is granted to Admin even if the
|
|
@@ -681,7 +1107,7 @@ function eligibleHoldersResolved(
|
|
|
681
1107
|
// Look the Admin ROLE up via its explicit alias — the bare key may be
|
|
682
1108
|
// owned by a same-named group under group-first precedence. The override
|
|
683
1109
|
// is the ROLE's capability, so the row's kind is 'role' regardless.
|
|
684
|
-
const adminRole = model.roles.byCanonical.get(
|
|
1110
|
+
const adminRole = model.roles.byCanonical.get(ADMIN_ROLE_KEY);
|
|
685
1111
|
addPrincipal(adminRole ? adminRole.displayName : ADMIN_CANONICAL, 'role');
|
|
686
1112
|
}
|
|
687
1113
|
|
|
@@ -696,6 +1122,7 @@ function eligibleHoldersResolved(
|
|
|
696
1122
|
const users: { name: string; email: string }[] = [];
|
|
697
1123
|
for (const [email, state] of byEmail) {
|
|
698
1124
|
if (state !== 'grant') continue;
|
|
1125
|
+
if (!hasPermissionResolved(model, verb, email, relativePath, fileOwn)) continue;
|
|
699
1126
|
users.push({ name: '', email });
|
|
700
1127
|
}
|
|
701
1128
|
users.sort((a, b) => a.email.localeCompare(b.email));
|
|
@@ -703,6 +1130,18 @@ function eligibleHoldersResolved(
|
|
|
703
1130
|
return { principals, roles, users };
|
|
704
1131
|
}
|
|
705
1132
|
|
|
1133
|
+
/**
|
|
1134
|
+
* Every individual the model names: `roles.yaml` members, emails named inline
|
|
1135
|
+
* at this path's scope, and the deployment owner (Admin for the write floor
|
|
1136
|
+
* even when `roles.yaml` does not list them).
|
|
1137
|
+
*/
|
|
1138
|
+
function namedCandidateEmails(model: AccessModel, byEmail: CollapsedScope['byEmail']): Set<string> {
|
|
1139
|
+
const candidates = new Set<string>(model.deploymentOwners);
|
|
1140
|
+
for (const email of model.roles.byEmail.keys()) candidates.add(email);
|
|
1141
|
+
for (const email of byEmail.keys()) candidates.add(email);
|
|
1142
|
+
return candidates;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
706
1145
|
/**
|
|
707
1146
|
* Expand the eligible-writer set to the underlying emails.
|
|
708
1147
|
*
|
|
@@ -728,10 +1167,10 @@ function eligibleHolderEmailsResolved(
|
|
|
728
1167
|
// in an access.md entry at this path's scope (whether granted or denied —
|
|
729
1168
|
// `hasPermissionResolved` filters denials out below). The built-in
|
|
730
1169
|
// `everyone` role can grant arbitrary signed-in users, but this finite
|
|
731
|
-
// expansion can only return emails the access tree explicitly names.
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1170
|
+
// expansion can only return emails the access tree explicitly names. The
|
|
1171
|
+
// deployment owner is a candidate too: the Admin write floor admits them
|
|
1172
|
+
// whether or not `roles.yaml` lists them.
|
|
1173
|
+
const candidates = namedCandidateEmails(model, byEmail);
|
|
735
1174
|
|
|
736
1175
|
for (const email of candidates) {
|
|
737
1176
|
if (hasPermissionResolved(model, verb, email, relativePath, fileOwn)) {
|
|
@@ -758,9 +1197,7 @@ function ineligibleNamedEmailsResolved(
|
|
|
758
1197
|
fileOwn?: OwnEntries | null,
|
|
759
1198
|
): Set<string> {
|
|
760
1199
|
const { byEmail } = resolveAtPath(model, verb, relativePath, fileOwn);
|
|
761
|
-
const candidates =
|
|
762
|
-
for (const email of model.roles.byEmail.keys()) candidates.add(email);
|
|
763
|
-
for (const email of byEmail.keys()) candidates.add(email);
|
|
1200
|
+
const candidates = namedCandidateEmails(model, byEmail);
|
|
764
1201
|
|
|
765
1202
|
const out = new Set<string>();
|
|
766
1203
|
for (const email of candidates) {
|
|
@@ -849,6 +1286,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
849
1286
|
constructor(
|
|
850
1287
|
private readonly workspaceService: WorkspaceService,
|
|
851
1288
|
private readonly kbDirName: string,
|
|
1289
|
+
private readonly disk: ITreeWalker,
|
|
852
1290
|
/**
|
|
853
1291
|
* Emails that count as Admin for the two hardcoded `write` rescues,
|
|
854
1292
|
* whatever `roles.yaml` says — in practice `ADMIN_EMAIL`. Optional so the
|
|
@@ -856,6 +1294,13 @@ export class AccessControlService implements IAccessControl {
|
|
|
856
1294
|
* current behaviour (no owner, roles.yaml is the only authority).
|
|
857
1295
|
*/
|
|
858
1296
|
deploymentOwners: readonly string[] = [],
|
|
1297
|
+
/**
|
|
1298
|
+
* How git is run — see `shared/git.contract.ts`. The composition root
|
|
1299
|
+
* passes the one runner; the default is the same runner on its default
|
|
1300
|
+
* deadline, kept for the same reason as `deploymentOwners` above: a great
|
|
1301
|
+
* many fixtures construct this service directly.
|
|
1302
|
+
*/
|
|
1303
|
+
private readonly gitRunner: IGitRunner = new NodeGitRunner(),
|
|
859
1304
|
) {
|
|
860
1305
|
this.deploymentOwners = new Set(
|
|
861
1306
|
deploymentOwners.filter(Boolean).map((e) => canonicalEmail(e)),
|
|
@@ -871,8 +1316,31 @@ export class AccessControlService implements IAccessControl {
|
|
|
871
1316
|
invalidate(workspaceId: string): void {
|
|
872
1317
|
this.cache.delete(workspaceId);
|
|
873
1318
|
this.ownEntriesCache.delete(workspaceId);
|
|
1319
|
+
this.generation++;
|
|
874
1320
|
}
|
|
875
1321
|
|
|
1322
|
+
/**
|
|
1323
|
+
* Bumped by every `invalidate()`, for any workspace. A model load reads the
|
|
1324
|
+
* tree across many awaits, and an invalidation fires exactly when that tree
|
|
1325
|
+
* changes underneath it: the load that started on the old tree would
|
|
1326
|
+
* otherwise store its model AFTER the drop, and every read for the next TTL
|
|
1327
|
+
* would resolve against a tree that is already gone. `loadModel` takes the
|
|
1328
|
+
* generation before its first read and refuses to store a model fetched
|
|
1329
|
+
* under an older one — the caller that started it still gets its answer,
|
|
1330
|
+
* as old as the moment it started, and the next caller reloads.
|
|
1331
|
+
*
|
|
1332
|
+
* ONE counter, not one per workspace: a per-workspace map would keep an
|
|
1333
|
+
* entry for every branch workspace the process ever loaded, with nothing to
|
|
1334
|
+
* tell it when a workspace is gone. The price of sharing is that a drop on
|
|
1335
|
+
* some other workspace, landing mid-load, costs this one a single reload —
|
|
1336
|
+
* a load is tens of milliseconds and a drop is a commit, so that is rare
|
|
1337
|
+
* and cheap, and it never stores a wrong model. (The own-entries memo needs
|
|
1338
|
+
* no token: it is keyed by the map object a reader took BEFORE its disk
|
|
1339
|
+
* read, and `invalidate()` replaces the map, so a late store lands in an
|
|
1340
|
+
* orphan nobody consults.)
|
|
1341
|
+
*/
|
|
1342
|
+
private generation = 0;
|
|
1343
|
+
|
|
876
1344
|
/** Memoized `readOwnEntries` for the batch paths; see `ownEntriesCache`. */
|
|
877
1345
|
private async cachedOwnEntries(
|
|
878
1346
|
workspaceId: string,
|
|
@@ -944,6 +1412,40 @@ export class AccessControlService implements IAccessControl {
|
|
|
944
1412
|
return result;
|
|
945
1413
|
}
|
|
946
1414
|
|
|
1415
|
+
async canReadAsGroupBatch(
|
|
1416
|
+
workspaceId: string,
|
|
1417
|
+
group: string,
|
|
1418
|
+
relativePaths: string[],
|
|
1419
|
+
): Promise<Map<string, boolean> | null> {
|
|
1420
|
+
const model = await this.loadModel(workspaceId);
|
|
1421
|
+
const keys = principalKeysOfGroup(model, group);
|
|
1422
|
+
if (keys === null) return null;
|
|
1423
|
+
const repoDir = await this.repoDir(workspaceId);
|
|
1424
|
+
// The same per-path own-entries `canReadBatch` folds in: a file's own
|
|
1425
|
+
// frontmatter rules are part of the walk for a group as for a person.
|
|
1426
|
+
const owns = await Promise.all(relativePaths.map((p) => this.cachedOwnEntries(workspaceId, repoDir, p)));
|
|
1427
|
+
const result = new Map<string, boolean>();
|
|
1428
|
+
relativePaths.forEach((p, i) => {
|
|
1429
|
+
result.set(p, hasPermissionForKeys(model, 'read', keys, p, owns[i]));
|
|
1430
|
+
});
|
|
1431
|
+
return result;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async canReadAsEveryoneBatch(workspaceId: string, relativePaths: string[]): Promise<Map<string, boolean>> {
|
|
1435
|
+
const model = await this.loadModel(workspaceId);
|
|
1436
|
+
const repoDir = await this.repoDir(workspaceId);
|
|
1437
|
+
const owns = await Promise.all(relativePaths.map((p) => this.cachedOwnEntries(workspaceId, repoDir, p)));
|
|
1438
|
+
const result = new Map<string, boolean>();
|
|
1439
|
+
relativePaths.forEach((p, i) => {
|
|
1440
|
+
// No keys at all: the walk then falls through to each scope's derived
|
|
1441
|
+
// `everyone` verdict — the built-in entry, or a public plugin's grant
|
|
1442
|
+
// or denial, whichever the scope builder settled on — which is exactly
|
|
1443
|
+
// what a caller holding nothing of their own resolves to.
|
|
1444
|
+
result.set(p, hasPermissionForKeys(model, 'read', NO_KEYS, p, owns[i]));
|
|
1445
|
+
});
|
|
1446
|
+
return result;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
947
1449
|
async canWriteBatch(
|
|
948
1450
|
workspaceId: string,
|
|
949
1451
|
userEmail: string,
|
|
@@ -1031,17 +1533,30 @@ export class AccessControlService implements IAccessControl {
|
|
|
1031
1533
|
principals: ResolvedPrincipal[];
|
|
1032
1534
|
roles: string[];
|
|
1033
1535
|
users: { name: string; email: string }[];
|
|
1536
|
+
publicVia: string[];
|
|
1034
1537
|
}> {
|
|
1035
1538
|
const model = await this.loadModel(workspaceId);
|
|
1036
1539
|
const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
|
|
1037
|
-
//
|
|
1038
|
-
// and the
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1540
|
+
// `restricted` is the verdict — whether `read: everyone` applies cleanly —
|
|
1541
|
+
// and the lists are the GRANTS that exist regardless: on a public node
|
|
1542
|
+
// they name what makes it public (`everyone`, and a public plugin
|
|
1543
|
+
// principal whose grant is the one to remove), so the share dialog can
|
|
1544
|
+
// show that grant as a row. Consumers that only want to know whether the
|
|
1545
|
+
// node is public read the flag; the lists may be empty for a
|
|
1546
|
+
// default-denied path with no grants.
|
|
1043
1547
|
const { principals, roles, users } = eligibleHoldersResolved(model, 'read', relativePath, own);
|
|
1044
|
-
|
|
1548
|
+
// WHY it is public, when it is: the public plugin principals granted read
|
|
1549
|
+
// here — the one thing the dialog cannot tell from the lists alone (a
|
|
1550
|
+
// plugin principal in them may or may not be public), so the resolver,
|
|
1551
|
+
// which knows, says it. A literal `everyone` line shows up as a source
|
|
1552
|
+
// of the `everyone` row instead; together the two answer "what remains
|
|
1553
|
+
// public after this grant is removed".
|
|
1554
|
+
const collapsed = resolveAtPath(model, 'read', relativePath, own);
|
|
1555
|
+
const publicVia = [...collapsed.byRole]
|
|
1556
|
+
.filter(([key, state]) => state === 'grant' && model.roles.publicKeys?.has(key))
|
|
1557
|
+
.map(([key]) => key)
|
|
1558
|
+
.sort();
|
|
1559
|
+
return { restricted: !canEveryoneReadResolved(model, relativePath, own), principals, roles, users, publicVia };
|
|
1045
1560
|
}
|
|
1046
1561
|
|
|
1047
1562
|
async eligibleDownloaders(
|
|
@@ -1057,6 +1572,52 @@ export class AccessControlService implements IAccessControl {
|
|
|
1057
1572
|
return eligibleHoldersResolved(model, 'download', relativePath, own);
|
|
1058
1573
|
}
|
|
1059
1574
|
|
|
1575
|
+
/**
|
|
1576
|
+
* The read and write holders of one file at its current path and at a path
|
|
1577
|
+
* it has not moved to yet (see the interface).
|
|
1578
|
+
*
|
|
1579
|
+
* A move changes nothing about the file's own frontmatter — that travels
|
|
1580
|
+
* with the bytes — and everything about the folder chain above it. So the
|
|
1581
|
+
* hypothetical side is the ordinary resolution with the destination path
|
|
1582
|
+
* substituted: the same model, the same own-entries (read from the SOURCE,
|
|
1583
|
+
* since nothing sits at the destination to read), resolved against the
|
|
1584
|
+
* destination's ancestors. One model load and one file read serve all four
|
|
1585
|
+
* lookups.
|
|
1586
|
+
*/
|
|
1587
|
+
async prospectiveHolders(
|
|
1588
|
+
workspaceId: string,
|
|
1589
|
+
fromPath: string,
|
|
1590
|
+
toPath: string,
|
|
1591
|
+
): Promise<ProspectiveHolders> {
|
|
1592
|
+
const model = await this.loadModel(workspaceId);
|
|
1593
|
+
const repoDir = await this.repoDir(workspaceId);
|
|
1594
|
+
// A FILE question only. A folder carries its own `access.md` — which moves
|
|
1595
|
+
// with it and governs everything under it — so resolving it as a file
|
|
1596
|
+
// would read frontmatter it does not have and omit the rules it does,
|
|
1597
|
+
// naming principals that are not the ones a folder move changes. Refuse
|
|
1598
|
+
// rather than answer the wrong question convincingly.
|
|
1599
|
+
//
|
|
1600
|
+
// Only absence is an answer here (see `isAbsence`): a probe that failed on
|
|
1601
|
+
// permissions or I/O does not say "this is a file", and folding it into
|
|
1602
|
+
// one would let the very case above through on an unreadable source.
|
|
1603
|
+
const fromStat = await fs.stat(path.join(repoDir, fromPath)).catch((err: unknown) => {
|
|
1604
|
+
if (isAbsence(err)) return null;
|
|
1605
|
+
throw err;
|
|
1606
|
+
});
|
|
1607
|
+
if (fromStat?.isDirectory()) {
|
|
1608
|
+
throw new WorkflowDomainError(
|
|
1609
|
+
'prospective access answers for a file, not a folder',
|
|
1610
|
+
400,
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
const own = await this.readOwnEntries(repoDir, fromPath);
|
|
1614
|
+
const holdersAt = (relativePath: string): PathHolders => ({
|
|
1615
|
+
read: eligibleHoldersResolved(model, 'read', relativePath, own),
|
|
1616
|
+
write: eligibleHoldersResolved(model, 'write', relativePath, own),
|
|
1617
|
+
});
|
|
1618
|
+
return { before: holdersAt(fromPath), after: holdersAt(toPath) };
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1060
1621
|
async eligibleWriterEmails(
|
|
1061
1622
|
workspaceId: string,
|
|
1062
1623
|
relativePath: string,
|
|
@@ -1106,14 +1667,122 @@ export class AccessControlService implements IAccessControl {
|
|
|
1106
1667
|
return out;
|
|
1107
1668
|
}
|
|
1108
1669
|
|
|
1109
|
-
async
|
|
1670
|
+
async denialSources(
|
|
1671
|
+
workspaceId: string,
|
|
1672
|
+
kind: AccessTargetKind,
|
|
1673
|
+
relativePath: string,
|
|
1674
|
+
principal: GrantPrincipal,
|
|
1675
|
+
opts?: { tokenMatch?: 'exact' | 'name' },
|
|
1676
|
+
): Promise<DenialSources> {
|
|
1677
|
+
const model = await this.loadModel(workspaceId);
|
|
1678
|
+
// Same own-scope rule as `grantSources`: a file target consults its own
|
|
1679
|
+
// frontmatter, a folder target's own access.md is already in the dir chain.
|
|
1680
|
+
const own =
|
|
1681
|
+
kind === 'file'
|
|
1682
|
+
? await this.readOwnEntries(await this.repoDir(workspaceId), relativePath)
|
|
1683
|
+
: null;
|
|
1684
|
+
const out: DenialSources = {};
|
|
1685
|
+
for (const verb of KNOWN_VERBS) {
|
|
1686
|
+
const sources = resolveDenialSourcesForVerb(
|
|
1687
|
+
model,
|
|
1688
|
+
verb,
|
|
1689
|
+
kind,
|
|
1690
|
+
relativePath,
|
|
1691
|
+
principal,
|
|
1692
|
+
own,
|
|
1693
|
+
opts?.tokenMatch,
|
|
1694
|
+
);
|
|
1695
|
+
if (sources.length > 0) out[verb] = sources;
|
|
1696
|
+
}
|
|
1697
|
+
return out;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
async locallyDeniedPrincipals(
|
|
1110
1701
|
workspaceId: string,
|
|
1111
|
-
|
|
1702
|
+
kind: AccessTargetKind,
|
|
1703
|
+
relativePath: string,
|
|
1704
|
+
): Promise<{ principals: ResolvedPrincipal[]; users: { name: string; email: string }[] }> {
|
|
1705
|
+
const model = await this.loadModel(workspaceId);
|
|
1706
|
+
// The target's OWN entries, and only those — the one scope whose rules a
|
|
1707
|
+
// restriction made here is written into. A folder's are its `access.md` in
|
|
1708
|
+
// the dir model; a file node's are its own frontmatter.
|
|
1709
|
+
const own: OwnEntries | null =
|
|
1710
|
+
kind === 'folder'
|
|
1711
|
+
? (model.accessFilesByDir.get(relativePath)?.entries ?? null)
|
|
1712
|
+
: await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
|
|
1713
|
+
if (!own) return { principals: [], users: [] };
|
|
1714
|
+
|
|
1715
|
+
const byIdentity = new Map<string, ResolvedPrincipal>();
|
|
1716
|
+
const byEmail = new Map<string, { name: string; email: string }>();
|
|
1717
|
+
for (const verb of KNOWN_VERBS) {
|
|
1718
|
+
for (const entry of own[verb] ?? []) {
|
|
1719
|
+
// Grants are already reported by the eligible lists; only the denials
|
|
1720
|
+
// are invisible there, and only they are this method's business.
|
|
1721
|
+
if (!entry.deny) continue;
|
|
1722
|
+
if (entry.kind === 'user') {
|
|
1723
|
+
if (!byEmail.has(entry.email)) {
|
|
1724
|
+
byEmail.set(entry.email, { name: entry.displayName, email: entry.email });
|
|
1725
|
+
}
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
// A token naming a role nobody knows is not a rule: `loadModel` drops
|
|
1729
|
+
// such entries from every `access.md`, and `resolveScopes` filters them
|
|
1730
|
+
// out of a file's own frontmatter (the one scope it does not pre-filter)
|
|
1731
|
+
// — so the resolver denies nothing, `denialSources` reports nothing, and
|
|
1732
|
+
// a row here would claim a restriction the server does not enforce. Skip
|
|
1733
|
+
// it on the same rule, so this list and the denial sources agree.
|
|
1734
|
+
if (!roleKnown(model.roles, entry.role)) continue;
|
|
1735
|
+
// Resolve the token's kind and display name through the SAME merged
|
|
1736
|
+
// index the eligible lists use, so a denied group keys as `g:` and a
|
|
1737
|
+
// denied role as `r:` — matching the row keys the view is built on. A
|
|
1738
|
+
// KNOWN token with no record is a built-in (`everyone`, `admin` before
|
|
1739
|
+
// roles.yaml names it), which displays as the role it is.
|
|
1740
|
+
const record = model.roles.byCanonical.get(entry.role);
|
|
1741
|
+
const name = record ? record.displayName : (entry.displayRole || entry.role);
|
|
1742
|
+
const kindOf = record?.kind ?? 'role';
|
|
1743
|
+
const key = `${kindOf}\0${name.toLowerCase()}`;
|
|
1744
|
+
if (!byIdentity.has(key)) byIdentity.set(key, { name, kind: kindOf });
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
const principals = [...byIdentity.values()].sort((a, b) =>
|
|
1748
|
+
a.name < b.name ? -1 : a.name > b.name ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0,
|
|
1749
|
+
);
|
|
1750
|
+
const users = [...byEmail.values()].sort((a, b) => a.email.localeCompare(b.email));
|
|
1751
|
+
return { principals, users };
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
async explainAccess(
|
|
1755
|
+
workspaceId: string,
|
|
1756
|
+
userEmail: string,
|
|
1757
|
+
kind: AccessTargetKind,
|
|
1758
|
+
relativePath: string,
|
|
1759
|
+
): Promise<Record<Verb, AccessDecision>> {
|
|
1760
|
+
const model = await this.loadModel(workspaceId);
|
|
1761
|
+
// The same own-entries read `canRead`/`canWrite`/… make, whatever the
|
|
1762
|
+
// kind: a directory simply has none.
|
|
1763
|
+
const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
|
|
1764
|
+
const out = {} as Record<Verb, AccessDecision>;
|
|
1765
|
+
for (const verb of KNOWN_VERBS) {
|
|
1766
|
+
const machineOwned = verb === 'write' ? this.machineOwnedWriteRule(userEmail, relativePath) : null;
|
|
1767
|
+
out[verb] =
|
|
1768
|
+
machineOwned !== null
|
|
1769
|
+
? { allowed: machineOwned, source: null, via: 'machine-owned', principal: null }
|
|
1770
|
+
: explainDecision(model, decidePermissionResolved(model, verb, userEmail, relativePath, own), kind, relativePath);
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
async kbPrincipals(workspaceId: string): Promise<{
|
|
1776
|
+
roles: string[];
|
|
1777
|
+
groups: string[];
|
|
1778
|
+
plugins: { name: string; folder: string }[];
|
|
1779
|
+
people: { name: string; email: string }[];
|
|
1780
|
+
}> {
|
|
1112
1781
|
let model: AccessModel;
|
|
1113
1782
|
try {
|
|
1114
1783
|
model = await this.loadModel(workspaceId);
|
|
1115
1784
|
} catch {
|
|
1116
|
-
return { roles: [], groups: [], people: [] };
|
|
1785
|
+
return { roles: [], groups: [], plugins: [], people: [] };
|
|
1117
1786
|
}
|
|
1118
1787
|
// Roles = the built-in `everyone` role plus every declared role's display
|
|
1119
1788
|
// name — ROLE principals only, never groups. Each role is enumerated via
|
|
@@ -1125,9 +1794,22 @@ export class AccessControlService implements IAccessControl {
|
|
|
1125
1794
|
// direct-access.md edit).
|
|
1126
1795
|
const roles = [EVERYONE_DISPLAY];
|
|
1127
1796
|
const groups: string[] = [];
|
|
1797
|
+
// Plugins = the identities behind the synthesised `plugin/<name>/<verb>`
|
|
1798
|
+
// principals, once each (three keys share a folder). Personal shelves are
|
|
1799
|
+
// plugins to the resolver but not to a person picking a grantee — told
|
|
1800
|
+
// apart by their FOLDER (the one structural rule), never by their name.
|
|
1801
|
+
const plugins = new Map<string, string>();
|
|
1128
1802
|
for (const [key, principal] of model.roles.byCanonical) {
|
|
1129
1803
|
if (key.startsWith(ROLE_TOKEN_PREFIX)) roles.push(principal.displayName);
|
|
1130
1804
|
else if (principal.kind === 'group') groups.push(principal.displayName);
|
|
1805
|
+
else if (
|
|
1806
|
+
principal.kind === 'plugin' &&
|
|
1807
|
+
principal.pluginName &&
|
|
1808
|
+
principal.pluginDir &&
|
|
1809
|
+
!isPersonalPluginDir(principal.pluginDir)
|
|
1810
|
+
) {
|
|
1811
|
+
plugins.set(principal.pluginName, principal.pluginDir);
|
|
1812
|
+
}
|
|
1131
1813
|
}
|
|
1132
1814
|
// People = roles.yaml member emails (name-less) ∪ access.md `Name <email>`
|
|
1133
1815
|
// grants (named). The login-only users table is unioned in by the caller.
|
|
@@ -1148,7 +1830,14 @@ export class AccessControlService implements IAccessControl {
|
|
|
1148
1830
|
name: name || email.split('@')[0],
|
|
1149
1831
|
email,
|
|
1150
1832
|
}));
|
|
1151
|
-
return {
|
|
1833
|
+
return {
|
|
1834
|
+
roles,
|
|
1835
|
+
groups,
|
|
1836
|
+
plugins: [...plugins.entries()]
|
|
1837
|
+
.map(([name, folder]) => ({ name, folder }))
|
|
1838
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1839
|
+
people,
|
|
1840
|
+
};
|
|
1152
1841
|
}
|
|
1153
1842
|
|
|
1154
1843
|
async findEmailByHash(
|
|
@@ -1183,7 +1872,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
1183
1872
|
...namesByEmail.keys(),
|
|
1184
1873
|
]);
|
|
1185
1874
|
for (const email of candidates) {
|
|
1186
|
-
if (
|
|
1875
|
+
if (hashEmail(email) === hash) {
|
|
1187
1876
|
const displayName = namesByEmail.get(email) ?? email.split('@')[0];
|
|
1188
1877
|
return { email, displayName };
|
|
1189
1878
|
}
|
|
@@ -1202,7 +1891,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
1202
1891
|
*/
|
|
1203
1892
|
private machineOwnedWriteRule(userEmail: string, relativePath: string): boolean | null {
|
|
1204
1893
|
if (relativePath !== SYNCED_GROUPS_YAML) return null;
|
|
1205
|
-
return userEmail
|
|
1894
|
+
return canonicalEmail(userEmail) === DIRECTORY_SYNC_BOT_EMAIL;
|
|
1206
1895
|
}
|
|
1207
1896
|
|
|
1208
1897
|
async canWriteAtRef(
|
|
@@ -1283,6 +1972,54 @@ export class AccessControlService implements IAccessControl {
|
|
|
1283
1972
|
return eligibleHoldersResolved(loaded.model, 'write', relativePath, own);
|
|
1284
1973
|
}
|
|
1285
1974
|
|
|
1975
|
+
async holdsAdminRootWrite(workspaceId: string, userEmail: string): Promise<boolean> {
|
|
1976
|
+
const model = await this.loadModel(workspaceId);
|
|
1977
|
+
return isAdminEmail(model, canonicalEmail(userEmail));
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
async canRestorePlatformFile(
|
|
1981
|
+
workspaceId: string,
|
|
1982
|
+
userEmail: string,
|
|
1983
|
+
destinationRelativePath: string,
|
|
1984
|
+
): Promise<boolean> {
|
|
1985
|
+
const target = platformRestoreDestination(destinationRelativePath);
|
|
1986
|
+
if (target === null) return false;
|
|
1987
|
+
const email = canonicalEmail(userEmail);
|
|
1988
|
+
|
|
1989
|
+
let admin: boolean;
|
|
1990
|
+
try {
|
|
1991
|
+
admin = isAdminEmail(await this.loadModel(workspaceId), email);
|
|
1992
|
+
} catch {
|
|
1993
|
+
// The repository this rescue exists for is exactly the one whose
|
|
1994
|
+
// `roles.yaml` may be the file that went missing, and `loadModel`
|
|
1995
|
+
// throws when it cannot be read. With no model to ask, the deployment
|
|
1996
|
+
// owner is the only admin left — and they are the person who sets
|
|
1997
|
+
// `ADMIN_EMAIL`, so admitting them concedes nothing they did not have.
|
|
1998
|
+
admin = this.deploymentOwners.has(email);
|
|
1999
|
+
}
|
|
2000
|
+
if (!admin) return false;
|
|
2001
|
+
|
|
2002
|
+
// A restore puts back what is MISSING, so the place it lands must be
|
|
2003
|
+
// empty. An `access.md` goes back only into a folder that has none —
|
|
2004
|
+
// into a folder that already has one it would not be a restore but a rule
|
|
2005
|
+
// change wearing a move's clothes, and the destination's own rules would
|
|
2006
|
+
// be the thing it bypassed the write gate to overwrite. The same holds at
|
|
2007
|
+
// the root: a move is a rename on disk, so landing `.bevelignore` on a
|
|
2008
|
+
// root that already has one would silently replace it, and the repository
|
|
2009
|
+
// was never missing that file to begin with.
|
|
2010
|
+
const destination =
|
|
2011
|
+
target.kind === 'root' ? target.name : path.join(target.dir, 'access.md');
|
|
2012
|
+
const repoDir = await this.repoDir(workspaceId);
|
|
2013
|
+
try {
|
|
2014
|
+
await fs.stat(path.join(repoDir, destination));
|
|
2015
|
+
return false;
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
if (isAbsence(err)) return true;
|
|
2018
|
+
// Anything other than genuine absence is not an answer: fail closed.
|
|
2019
|
+
return false;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
1286
2023
|
async eligibleWritersForPathsAtRef(
|
|
1287
2024
|
workspaceId: string,
|
|
1288
2025
|
ref: string,
|
|
@@ -1347,6 +2084,8 @@ export class AccessControlService implements IAccessControl {
|
|
|
1347
2084
|
if (cached && Date.now() - cached.loadedAt < AccessControlService.CACHE_TTL_MS) {
|
|
1348
2085
|
return cached.model;
|
|
1349
2086
|
}
|
|
2087
|
+
// Taken before the first read; see `generation`.
|
|
2088
|
+
const generation = this.generation;
|
|
1350
2089
|
|
|
1351
2090
|
const repoDir = await this.repoDir(workspaceId);
|
|
1352
2091
|
|
|
@@ -1371,23 +2110,22 @@ export class AccessControlService implements IAccessControl {
|
|
|
1371
2110
|
try {
|
|
1372
2111
|
return await fs.readFile(path.join(repoDir, filename), 'utf-8');
|
|
1373
2112
|
} catch (err) {
|
|
1374
|
-
|
|
1375
|
-
if (code === 'ENOENT' || code === 'ENOTDIR') return null;
|
|
2113
|
+
if (isAbsence(err)) return null;
|
|
1376
2114
|
throw err;
|
|
1377
2115
|
}
|
|
1378
2116
|
});
|
|
1379
2117
|
if (!activeGroups.health.ok) {
|
|
1380
|
-
|
|
1381
|
-
`
|
|
2118
|
+
log.error(
|
|
2119
|
+
`groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`,
|
|
1382
2120
|
);
|
|
1383
2121
|
}
|
|
1384
|
-
for (const w of activeGroups.warnings)
|
|
2122
|
+
for (const w of activeGroups.warnings) log.warn(w);
|
|
1385
2123
|
const mergeWarnings = mergeGroupsIntoRoles(
|
|
1386
2124
|
rolesParsed.index,
|
|
1387
2125
|
activeGroups.groups,
|
|
1388
2126
|
activeGroups.sourceFile,
|
|
1389
2127
|
);
|
|
1390
|
-
for (const w of mergeWarnings)
|
|
2128
|
+
for (const w of mergeWarnings) log.warn(w);
|
|
1391
2129
|
|
|
1392
2130
|
const accessFiles = new Map<string, AccessFile>();
|
|
1393
2131
|
|
|
@@ -1404,7 +2142,15 @@ export class AccessControlService implements IAccessControl {
|
|
|
1404
2142
|
// must not 500 the entire editor; admins can still write `access.md`
|
|
1405
2143
|
// / `roles.yaml` because `hasPermissionResolved` admin-rescues those
|
|
1406
2144
|
// paths, so the bad config remains fixable from inside the app.
|
|
1407
|
-
|
|
2145
|
+
const pluginDirs = new Map<string, string>();
|
|
2146
|
+
await this.disk.walkKb(repoDir, [accessFileListener(repoDir, accessFiles, pluginDirs)]);
|
|
2147
|
+
|
|
2148
|
+
// Plugin principals (`plugin/<Name>/<verb>`), derived from each plugin
|
|
2149
|
+
// folder's own access.md — a plugin being a folder with a manifest, at
|
|
2150
|
+
// any depth — after groups merged (so their rosters expand through the
|
|
2151
|
+
// finished index) and before the unknown-role sweep below (so a grant
|
|
2152
|
+
// naming one counts as known).
|
|
2153
|
+
synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginDirs);
|
|
1408
2154
|
|
|
1409
2155
|
// Validate role refs against roles.yaml. `everyone` is a built-in role and
|
|
1410
2156
|
// is valid without a roles.yaml entry. Unknown refs are dropped from the
|
|
@@ -1414,9 +2160,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
1414
2160
|
for (const verb of KNOWN_VERBS) {
|
|
1415
2161
|
file.entries[verb] = file.entries[verb].filter((entry) => {
|
|
1416
2162
|
if (entry.kind === 'role' && !roleKnown(rolesParsed.index, entry.role)) {
|
|
1417
|
-
|
|
1418
|
-
`[access] ${file.path}: '${verb}' references unknown role '${entry.displayRole}' — entry ignored`,
|
|
1419
|
-
);
|
|
2163
|
+
log.warn(`${file.path}: '${verb}' references unknown role '${entry.displayRole}' — entry ignored`);
|
|
1420
2164
|
return false;
|
|
1421
2165
|
}
|
|
1422
2166
|
return true;
|
|
@@ -1430,50 +2174,12 @@ export class AccessControlService implements IAccessControl {
|
|
|
1430
2174
|
groupsHealth: activeGroups.health,
|
|
1431
2175
|
deploymentOwners: this.deploymentOwners,
|
|
1432
2176
|
};
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
private async collectAccessFiles(
|
|
1438
|
-
absDir: string,
|
|
1439
|
-
relDir: string,
|
|
1440
|
-
out: Map<string, AccessFile>,
|
|
1441
|
-
): Promise<void> {
|
|
1442
|
-
let entries;
|
|
1443
|
-
try {
|
|
1444
|
-
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
1445
|
-
} catch {
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
for (const entry of entries) {
|
|
1449
|
-
// Skip VCS metadata + vendored deps. Hidden dirs (`.git`, `.vscode`,
|
|
1450
|
-
// etc.) and `node_modules` can't host KB rules and are often huge —
|
|
1451
|
-
// walking them would slow every cache miss without benefit.
|
|
1452
|
-
if (entry.name.startsWith('.')) continue;
|
|
1453
|
-
if (entry.name === 'node_modules') continue;
|
|
1454
|
-
const abs = path.join(absDir, entry.name);
|
|
1455
|
-
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1456
|
-
if (entry.isDirectory()) {
|
|
1457
|
-
await this.collectAccessFiles(abs, rel, out);
|
|
1458
|
-
} else if (entry.isFile() && entry.name === 'access.md') {
|
|
1459
|
-
const text = await fs.readFile(abs, 'utf-8');
|
|
1460
|
-
const parsed = parseAccessFile(text, rel);
|
|
1461
|
-
if (!parsed.ok) {
|
|
1462
|
-
// Treat as if the file didn't exist for resolution purposes.
|
|
1463
|
-
// Admin-rescue on access.md paths still lets an admin fix it.
|
|
1464
|
-
for (const e of parsed.errors) console.warn(`[access] ${e} — file ignored`);
|
|
1465
|
-
continue;
|
|
1466
|
-
}
|
|
1467
|
-
for (const w of parsed.warnings) console.warn(`[access] ${w}`);
|
|
1468
|
-
if (out.has(parsed.file.dir)) {
|
|
1469
|
-
console.warn(
|
|
1470
|
-
`[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`,
|
|
1471
|
-
);
|
|
1472
|
-
continue;
|
|
1473
|
-
}
|
|
1474
|
-
out.set(parsed.file.dir, parsed.file);
|
|
1475
|
-
}
|
|
2177
|
+
// An invalidation landed mid-load: this model describes a tree that is
|
|
2178
|
+
// already gone, so it is returned to this caller but never stored.
|
|
2179
|
+
if (this.generation === generation) {
|
|
2180
|
+
this.cache.set(workspaceId, { model, loadedAt: Date.now() });
|
|
1476
2181
|
}
|
|
2182
|
+
return model;
|
|
1477
2183
|
}
|
|
1478
2184
|
|
|
1479
2185
|
private async repoDir(workspaceId: string): Promise<string> {
|
|
@@ -1542,7 +2248,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
1542
2248
|
else if (!wanted.includes(p)) wanted.push(p);
|
|
1543
2249
|
}
|
|
1544
2250
|
if (wanted.length === 0) return result;
|
|
1545
|
-
const texts = await catFileBatch(repoDir, wanted.map((p) => `${ref}:${p}`));
|
|
2251
|
+
const texts = await catFileBatch(this.gitRunner, repoDir, wanted.map((p) => `${ref}:${p}`));
|
|
1546
2252
|
wanted.forEach((p, i) => {
|
|
1547
2253
|
const text = texts[i];
|
|
1548
2254
|
if (text === null) result.set(p, null);
|
|
@@ -1586,15 +2292,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
1586
2292
|
relativePath: string,
|
|
1587
2293
|
): Promise<{ kind: 'text'; text: string } | { kind: 'absent' } | { kind: 'error' }> {
|
|
1588
2294
|
try {
|
|
1589
|
-
const { stdout } = await
|
|
1590
|
-
'git',
|
|
1591
|
-
['-C', repoDir, 'show', `${ref}:${relativePath}`],
|
|
1592
|
-
{
|
|
1593
|
-
encoding: 'utf-8',
|
|
1594
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
1595
|
-
env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
|
|
1596
|
-
},
|
|
1597
|
-
);
|
|
2295
|
+
const { stdout } = await this.gitRunner.run(repoDir, ['show', `${ref}:${relativePath}`]);
|
|
1598
2296
|
return { kind: 'text', text: stdout };
|
|
1599
2297
|
} catch (err) {
|
|
1600
2298
|
const stderr =
|
|
@@ -1614,20 +2312,23 @@ export class AccessControlService implements IAccessControl {
|
|
|
1614
2312
|
* `'error'` when git failed: a model built without its access files is
|
|
1615
2313
|
* not the model, and the caller must not cache it as one.
|
|
1616
2314
|
*/
|
|
1617
|
-
private async listAccessFilesAtRef(
|
|
2315
|
+
private async listAccessFilesAtRef(
|
|
2316
|
+
repoDir: string,
|
|
2317
|
+
ref: string,
|
|
2318
|
+
): Promise<{ accessFiles: string[]; pluginDirs: string[] } | 'error'> {
|
|
1618
2319
|
try {
|
|
1619
|
-
const { stdout } = await
|
|
1620
|
-
'git',
|
|
1621
|
-
['-C', repoDir, 'ls-tree', '-r', '--name-only', ref],
|
|
1622
|
-
{ encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 },
|
|
1623
|
-
);
|
|
2320
|
+
const { stdout } = await this.gitRunner.run(repoDir, ['ls-tree', '-r', '--name-only', ref]);
|
|
1624
2321
|
const out: string[] = [];
|
|
2322
|
+
const pluginDirs: string[] = [];
|
|
1625
2323
|
for (const line of stdout.split('\n')) {
|
|
1626
2324
|
const p = line.trim();
|
|
1627
2325
|
if (!p) continue;
|
|
1628
2326
|
if (p === 'access.md' || p.endsWith('/access.md')) out.push(p);
|
|
2327
|
+
else if (p.startsWith(`${PLUGINS_DIR}/`) && p.endsWith(`/${PLUGIN_MANIFEST_FILE}`)) {
|
|
2328
|
+
pluginDirs.push(p.slice(0, -(PLUGIN_MANIFEST_FILE.length + 1)));
|
|
2329
|
+
}
|
|
1629
2330
|
}
|
|
1630
|
-
return out;
|
|
2331
|
+
return { accessFiles: out, pluginDirs };
|
|
1631
2332
|
} catch {
|
|
1632
2333
|
return 'error';
|
|
1633
2334
|
}
|
|
@@ -1699,14 +2400,19 @@ export class AccessControlService implements IAccessControl {
|
|
|
1699
2400
|
private async revParseCommit(repoDir: string, ref: string): Promise<string | null> {
|
|
1700
2401
|
if (!ref || ref.startsWith('-')) return null;
|
|
1701
2402
|
try {
|
|
1702
|
-
const { stdout } = await
|
|
1703
|
-
'
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2403
|
+
const { stdout } = await this.gitRunner.run(repoDir, [
|
|
2404
|
+
'rev-parse',
|
|
2405
|
+
'--verify',
|
|
2406
|
+
'--quiet',
|
|
2407
|
+
`${ref}^{commit}`,
|
|
2408
|
+
]);
|
|
1707
2409
|
const sha = stdout.trim();
|
|
1708
2410
|
return /^[0-9a-f]{40,64}$/.test(sha) ? sha : null;
|
|
1709
|
-
} catch {
|
|
2411
|
+
} catch (err) {
|
|
2412
|
+
// Null means "does not resolve". A deadline says nothing about the ref,
|
|
2413
|
+
// and as a null it would turn a stalled git into a gate that quietly
|
|
2414
|
+
// reads no roles at that ref; it surfaces instead, as a read failure.
|
|
2415
|
+
if (isGitTimeout(err)) throw err;
|
|
1710
2416
|
return null;
|
|
1711
2417
|
}
|
|
1712
2418
|
}
|
|
@@ -1743,7 +2449,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
1743
2449
|
if (outcome.kind === 'error') {
|
|
1744
2450
|
// The operational signal: one line per failed read, naming the commit
|
|
1745
2451
|
// and the path, so a run of these is visible in the logs.
|
|
1746
|
-
|
|
2452
|
+
logger(`access@${tag}`).warn(`git read of ${relativePath} failed; refusing to decide from a partial tree`);
|
|
1747
2453
|
throw new AccessUnreadableError(label, relativePath);
|
|
1748
2454
|
}
|
|
1749
2455
|
return outcome.kind === 'text' ? outcome.text : null;
|
|
@@ -1763,25 +2469,25 @@ export class AccessControlService implements IAccessControl {
|
|
|
1763
2469
|
// fires only on a file that was read and would not parse.
|
|
1764
2470
|
const activeGroups = await loadActiveGroups(read);
|
|
1765
2471
|
if (!activeGroups.health.ok) {
|
|
1766
|
-
|
|
1767
|
-
`
|
|
2472
|
+
logger(`access@${tag}`).error(
|
|
2473
|
+
`groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`,
|
|
1768
2474
|
);
|
|
1769
2475
|
}
|
|
1770
|
-
for (const w of activeGroups.warnings)
|
|
2476
|
+
for (const w of activeGroups.warnings) logger(`access@${tag}`).warn(w);
|
|
1771
2477
|
const mergeWarnings = mergeGroupsIntoRoles(
|
|
1772
2478
|
rolesParsed.index,
|
|
1773
2479
|
activeGroups.groups,
|
|
1774
2480
|
activeGroups.sourceFile,
|
|
1775
2481
|
);
|
|
1776
|
-
for (const w of mergeWarnings)
|
|
2482
|
+
for (const w of mergeWarnings) logger(`access@${tag}`).warn(w);
|
|
1777
2483
|
|
|
1778
2484
|
const accessFiles = new Map<string, AccessFile>();
|
|
1779
2485
|
const listed = await this.listAccessFilesAtRef(repoDir, commit);
|
|
1780
2486
|
if (listed === 'error') {
|
|
1781
|
-
|
|
2487
|
+
logger(`access@${tag}`).warn('listing access.md files failed; refusing to decide from a partial tree');
|
|
1782
2488
|
throw new AccessUnreadableError(label, 'access.md (ls-tree)');
|
|
1783
2489
|
}
|
|
1784
|
-
for (const p of listed) {
|
|
2490
|
+
for (const p of listed.accessFiles) {
|
|
1785
2491
|
const text = await read(p);
|
|
1786
2492
|
if (text === null) continue;
|
|
1787
2493
|
const parsed = parseAccessFile(text, p);
|
|
@@ -1792,6 +2498,17 @@ export class AccessControlService implements IAccessControl {
|
|
|
1792
2498
|
accessFiles.set(parsed.file.dir, parsed.file);
|
|
1793
2499
|
}
|
|
1794
2500
|
|
|
2501
|
+
// Mirror `loadModel`: plugin principals from the plugins the ref carries,
|
|
2502
|
+
// each under the identity its manifest AT THAT REF declares, so a PR-time
|
|
2503
|
+
// verdict on a skill granted to `plugin/<name>/write` counts the plugin's
|
|
2504
|
+
// writers exactly as the working tree would.
|
|
2505
|
+
const pluginIdentities = new Map<string, string>();
|
|
2506
|
+
for (const dir of listed.pluginDirs) {
|
|
2507
|
+
const manifest = parseManifestText(await read(`${dir}/${PLUGIN_MANIFEST_FILE}`));
|
|
2508
|
+
pluginIdentities.set(dir, pluginIdentityOf(manifest, path.posix.basename(dir)));
|
|
2509
|
+
}
|
|
2510
|
+
synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginIdentities);
|
|
2511
|
+
|
|
1795
2512
|
// Mirror `loadModel`: drop entries whose role ref is unknown to roles.yaml,
|
|
1796
2513
|
// while preserving the built-in `everyone` role.
|
|
1797
2514
|
for (const [, file] of accessFiles) {
|