@bevel-software/platform-core-backend 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-guide/access-control.md +49 -4
- package/agent-guide/html-views.md +76 -2
- package/dist/__tests__/test-repositories.d.ts +3 -0
- package/dist/__tests__/test-repositories.d.ts.map +1 -0
- package/dist/__tests__/test-repositories.js +18 -0
- package/dist/__tests__/test-repositories.js.map +1 -0
- package/dist/core/catalog-revision.js +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +3 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +3 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +35 -5
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +8 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +4 -0
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +20 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +32 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +135 -17
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +20 -6
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +66 -6
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/audit.contract.d.ts +52 -8
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js +37 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/audit/audit.routes.d.ts +1 -1
- package/dist/modules/audit/audit.routes.d.ts.map +1 -1
- package/dist/modules/audit/audit.routes.js +15 -13
- package/dist/modules/audit/audit.routes.js.map +1 -1
- package/dist/modules/audit/request-audit.d.ts +21 -10
- package/dist/modules/audit/request-audit.d.ts.map +1 -1
- package/dist/modules/audit/request-audit.js +26 -10
- package/dist/modules/audit/request-audit.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +22 -3
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +15 -6
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/diff/diff.service.d.ts.map +1 -1
- package/dist/modules/diff/diff.service.js +7 -1
- package/dist/modules/diff/diff.service.js.map +1 -1
- package/dist/modules/kb-fs/git-dir.d.ts +21 -0
- package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
- package/dist/modules/kb-fs/git-dir.js +58 -0
- package/dist/modules/kb-fs/git-dir.js.map +1 -0
- package/dist/modules/kb-fs/index.d.ts +3 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -1
- package/dist/modules/kb-fs/index.js +3 -1
- package/dist/modules/kb-fs/index.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +3 -12
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/mutex.d.ts +11 -0
- package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
- package/dist/modules/kb-fs/mutex.js +14 -0
- package/dist/modules/kb-fs/mutex.js.map +1 -1
- package/dist/modules/kb-fs/ref-files.d.ts +63 -0
- package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
- package/dist/modules/kb-fs/ref-files.js +249 -0
- package/dist/modules/kb-fs/ref-files.js.map +1 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +14 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +4 -0
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +5 -0
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +67 -0
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +107 -27
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +257 -50
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/request-context.d.ts +25 -0
- package/dist/modules/mcp/request-context.d.ts.map +1 -0
- package/dist/modules/mcp/request-context.js +11 -0
- package/dist/modules/mcp/request-context.js.map +1 -0
- package/dist/modules/mcp/shared-definitions.d.ts +56 -0
- package/dist/modules/mcp/shared-definitions.d.ts.map +1 -0
- package/dist/modules/mcp/shared-definitions.js +117 -0
- package/dist/modules/mcp/shared-definitions.js.map +1 -0
- package/dist/modules/mcp/surface-cache.d.ts +70 -0
- package/dist/modules/mcp/surface-cache.d.ts.map +1 -0
- package/dist/modules/mcp/surface-cache.js +104 -0
- package/dist/modules/mcp/surface-cache.js.map +1 -0
- package/dist/modules/plugins/teams.routes.d.ts +4 -0
- package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
- package/dist/modules/plugins/teams.routes.js +9 -1
- package/dist/modules/plugins/teams.routes.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +33 -12
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts +10 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +3 -14
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +18 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +453 -2
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +142 -4
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +645 -128
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/read-on-branch.d.ts +35 -0
- package/dist/modules/workflow/read-on-branch.d.ts.map +1 -0
- package/dist/modules/workflow/read-on-branch.js +58 -0
- package/dist/modules/workflow/read-on-branch.js.map +1 -0
- package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +22 -41
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +48 -8
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +163 -16
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
- package/dist/modules/workspace/agent-access.gate.js +2 -2
- package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
- package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
- package/dist/modules/workspace/bounded-matcher.js +261 -0
- package/dist/modules/workspace/bounded-matcher.js.map +1 -0
- package/dist/modules/workspace/fetch-instant.d.ts +9 -0
- package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
- package/dist/modules/workspace/fetch-instant.js +13 -0
- package/dist/modules/workspace/fetch-instant.js.map +1 -0
- package/dist/modules/workspace/index.d.ts +9 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +10 -0
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
- package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.contract.js +19 -0
- package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
- package/dist/modules/workspace/kb-repository.d.ts +236 -0
- package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.js +811 -0
- package/dist/modules/workspace/kb-repository.js.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
- package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +0 -5
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
- package/dist/modules/workspace/set-aside-clone.js +139 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +68 -32
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +240 -161
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +13 -4
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +162 -336
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/git.contract.d.ts +7 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +9 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/no-link-below.d.ts +10 -0
- package/dist/shared/no-link-below.d.ts.map +1 -0
- package/dist/shared/no-link-below.js +30 -0
- package/dist/shared/no-link-below.js.map +1 -0
- package/dist/test-setup.js +4 -0
- package/dist/test-setup.js.map +1 -1
- package/migrations/0018_agent_events_page.sql +11 -0
- package/migrations/meta/0018_snapshot.json +2429 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +3 -3
- package/src/__tests__/test-repositories.ts +22 -0
- package/src/__tests__/workspace-service.ts +69 -0
- package/src/core/__tests__/lifecycle.test.ts +16 -0
- package/src/core/catalog-revision.ts +3 -3
- package/src/core/create-core-server.ts +3 -2
- package/src/core/create-core-services.ts +39 -4
- package/src/core/lifecycle.ts +10 -0
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +144 -4
- package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
- package/src/modules/access/access-control.interface.ts +22 -0
- package/src/modules/access/access-control.service.ts +138 -15
- package/src/modules/access/access.routes.ts +22 -6
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +22 -1
- package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +78 -1
- package/src/modules/audit/__tests__/audit.routes.test.ts +43 -5
- package/src/modules/audit/agent-audit.service.ts +66 -6
- package/src/modules/audit/audit.contract.ts +62 -10
- package/src/modules/audit/audit.routes.ts +14 -12
- package/src/modules/audit/request-audit.ts +28 -13
- package/src/modules/database/core-schema.ts +15 -6
- package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
- package/src/modules/diff/diff.service.ts +6 -4
- package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
- package/src/modules/kb-fs/__tests__/mutex.canonical.test.ts +57 -0
- package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
- package/src/modules/kb-fs/git-dir.ts +53 -0
- package/src/modules/kb-fs/index.ts +3 -0
- package/src/modules/kb-fs/locking-filesystem.ts +3 -12
- package/src/modules/kb-fs/mutex.ts +16 -0
- package/src/modules/kb-fs/ref-files.ts +279 -0
- package/src/modules/mcp/__tests__/mcp-routes-harness.ts +7 -1
- package/src/modules/mcp/__tests__/mcp.routes.page.test.ts +115 -0
- package/src/modules/mcp/__tests__/mcp.surface-cache.test.ts +521 -0
- package/src/modules/mcp/__tests__/shared-definitions.test.ts +28 -0
- package/src/modules/mcp/__tests__/surface-cache.test.ts +127 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +21 -0
- package/src/modules/mcp/mcp.routes.ts +68 -0
- package/src/modules/mcp/mcp.service.ts +315 -51
- package/src/modules/mcp/request-context.ts +34 -0
- package/src/modules/mcp/shared-definitions.ts +121 -0
- package/src/modules/mcp/surface-cache.ts +154 -0
- package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
- package/src/modules/plugins/teams.routes.ts +14 -1
- package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
- package/src/modules/tool-auth/internal-token.service.ts +1 -1
- package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
- package/src/modules/tool-helpers/tool-context.ts +37 -18
- package/src/modules/tool-helpers/tool-handler.ts +2 -2
- package/src/modules/tool-helpers/tool.contract.ts +10 -2
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +1 -1
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +7 -2
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +23 -12
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +250 -4
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +464 -4
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +4 -22
- package/src/modules/workflow/agent-tools/workflow.tools.ts +462 -1
- package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
- package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +76 -11
- package/src/modules/workflow/git/__tests__/git.service.mergeIntoBranch.test.ts +721 -0
- package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
- package/src/modules/workflow/git/git.service.ts +719 -126
- package/src/modules/workflow/read-on-branch.ts +79 -0
- package/src/modules/workflow/workflow-hooks.ts +1 -1
- package/src/modules/workflow/workflow.routes.ts +21 -39
- package/src/modules/workflow/workflow.service.ts +203 -16
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
- package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
- package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
- package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
- package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +153 -298
- package/src/modules/workspace/agent-access.gate.ts +2 -2
- package/src/modules/workspace/bounded-matcher.ts +328 -0
- package/src/modules/workspace/fetch-instant.ts +13 -0
- package/src/modules/workspace/index.ts +23 -0
- package/src/modules/workspace/kb-repository.contract.ts +124 -0
- package/src/modules/workspace/kb-repository.ts +844 -0
- package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
- package/src/modules/workspace/routine-write-policy.ts +0 -15
- package/src/modules/workspace/set-aside-clone.ts +140 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
- package/src/modules/workspace/workspace.service.ts +247 -169
- package/src/modules/workspace/workspace.tools.ts +179 -365
- package/src/shared/__tests__/no-link-below.test.ts +45 -0
- package/src/shared/git.contract.ts +10 -0
- package/src/shared/no-link-below.ts +29 -0
- package/src/test-setup.ts +5 -0
|
@@ -127,6 +127,13 @@
|
|
|
127
127
|
"when": 1791359019161,
|
|
128
128
|
"tag": "0017_atlassian_account_links",
|
|
129
129
|
"breakpoints": true
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"idx": 18,
|
|
133
|
+
"version": "7",
|
|
134
|
+
"when": 1792106401545,
|
|
135
|
+
"tag": "0018_agent_events_page",
|
|
136
|
+
"breakpoints": true
|
|
130
137
|
}
|
|
131
138
|
]
|
|
132
|
-
}
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bevel-software/platform-core-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, workflow (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
56
56
|
"yaml": "^2.9.0",
|
|
57
57
|
"zod": "^3.24.0",
|
|
58
|
-
"@bevel-software/platform-
|
|
59
|
-
"@bevel-software/platform-
|
|
58
|
+
"@bevel-software/platform-shared": "0.31.0",
|
|
59
|
+
"@bevel-software/platform-mcp-core": "0.31.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/adm-zip": "^0.5.8",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The repository roots `testKbRepository` placed beside a suite's workspaces
|
|
5
|
+
* root (`<workspacesRoot>.repository`). A suite removes its fixture root, not
|
|
6
|
+
* what sits next to it, so `test-setup.ts` removes these once the file's
|
|
7
|
+
* suites are done. Kept free of imports so the setup file can load it without
|
|
8
|
+
* pulling in a module a suite means to mock.
|
|
9
|
+
*/
|
|
10
|
+
const roots = new Set<string>();
|
|
11
|
+
|
|
12
|
+
export function recordTestRepositoryRoot(root: string): void {
|
|
13
|
+
roots.add(root);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function removeTestRepositoryRoots(): Promise<void> {
|
|
17
|
+
const pending = [...roots];
|
|
18
|
+
roots.clear();
|
|
19
|
+
await Promise.all(
|
|
20
|
+
pending.map((root) => fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { NodeGitRunner } from '../modules/workflow/git/node-git-runner.js';
|
|
3
|
+
import type { IGitRunner } from '../shared/git.contract.js';
|
|
4
|
+
import type { ITreeWalker } from '../shared/fs.contract.js';
|
|
5
|
+
import type { KbContext } from '../shared/kb-context.js';
|
|
6
|
+
import { KbRepository, repositoryDirFor } from '../modules/workspace/kb-repository.js';
|
|
7
|
+
import type { IKbRepository } from '../modules/workspace/kb-repository.contract.js';
|
|
8
|
+
import { WorkspaceService } from '../modules/workspace/workspace.service.js';
|
|
9
|
+
import { KbStartupRunner, type KbStartupRunnerOptions } from '../modules/workspace/startup/kb-startup-runner.js';
|
|
10
|
+
import { recordTestRepositoryRoot } from './test-repositories.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The repository a suite's services share when it builds them by hand: one
|
|
14
|
+
* next to the workspaces root (see `repositoryDirFor`), as the composition
|
|
15
|
+
* root would place it there for a deployment with no backups root. Removed
|
|
16
|
+
* once the file's suites are done (see `test-repositories.ts`): a suite's
|
|
17
|
+
* teardown removes its fixture root, not what sits beside it.
|
|
18
|
+
*/
|
|
19
|
+
export function testKbRepository(
|
|
20
|
+
workspacesRoot: string,
|
|
21
|
+
kbDirName: string,
|
|
22
|
+
kbRepoUrl: string | (() => string),
|
|
23
|
+
gitRunner?: IGitRunner,
|
|
24
|
+
): IKbRepository {
|
|
25
|
+
const dir = repositoryDirFor(workspacesRoot, kbDirName);
|
|
26
|
+
recordTestRepositoryRoot(path.dirname(dir));
|
|
27
|
+
return new KbRepository({
|
|
28
|
+
dir,
|
|
29
|
+
kbRepoUrl: typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl,
|
|
30
|
+
gitRunner: gitRunner ?? new NodeGitRunner(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A WorkspaceService as a suite builds it: the constructor's arguments, with
|
|
36
|
+
* the repository — which only the composition root builds in the product —
|
|
37
|
+
* made here when the suite passes none.
|
|
38
|
+
*/
|
|
39
|
+
export function newWorkspaceService(
|
|
40
|
+
workspacesRoot: string,
|
|
41
|
+
kbRepoUrl: string | (() => string),
|
|
42
|
+
kb: Pick<KbContext, 'kbDirName' | 'defaultBranch'>,
|
|
43
|
+
disk: ITreeWalker,
|
|
44
|
+
gitRunner: IGitRunner = new NodeGitRunner(),
|
|
45
|
+
setAsideRoot?: string,
|
|
46
|
+
aroundSetAside?: (workspaceId: string, move: () => Promise<void>) => Promise<void>,
|
|
47
|
+
repository?: IKbRepository,
|
|
48
|
+
): WorkspaceService {
|
|
49
|
+
return new WorkspaceService(
|
|
50
|
+
workspacesRoot,
|
|
51
|
+
kbRepoUrl,
|
|
52
|
+
kb,
|
|
53
|
+
disk,
|
|
54
|
+
gitRunner,
|
|
55
|
+
setAsideRoot,
|
|
56
|
+
aroundSetAside,
|
|
57
|
+
repository ?? testKbRepository(workspacesRoot, kb.kbDirName, kbRepoUrl, gitRunner),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A KbStartupRunner as a suite builds it — the repository made here when the suite passes none. */
|
|
62
|
+
export function newKbStartupRunner(
|
|
63
|
+
opts: Omit<KbStartupRunnerOptions, 'repository'> & { repository?: IKbRepository },
|
|
64
|
+
): KbStartupRunner {
|
|
65
|
+
return new KbStartupRunner({
|
|
66
|
+
...opts,
|
|
67
|
+
repository: opts.repository ?? testKbRepository(opts.workspacesRoot, opts.kbDirName, opts.kbRepoUrl, opts.gitRunner),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -5,6 +5,7 @@ import { createShutdown, stopCore, withStartupTask, type LeasedWorker } from '..
|
|
|
5
5
|
import { holdCommitWorkerLease, holdable } from '../lifecycle.js';
|
|
6
6
|
import { BevelSecretsVariableLoader, registerBevelSecretsVariableLoader } from '../../modules/secrets-vault/secrets-variable-loader.js';
|
|
7
7
|
import type { AdvisoryLease } from '../../modules/database/advisory-lock.js';
|
|
8
|
+
import { WorkerPatternMatcher } from '../../modules/workspace/bounded-matcher.js';
|
|
8
9
|
|
|
9
10
|
/** A lease the suite scripts: what each `tryAcquire` answers, and a way to lose it. */
|
|
10
11
|
function fakeLease(answers: boolean[]) {
|
|
@@ -537,6 +538,21 @@ describe('stopCore', () => {
|
|
|
537
538
|
]);
|
|
538
539
|
});
|
|
539
540
|
|
|
541
|
+
it('ends the grep matcher\'s workers, so an evicted graph leaves no search running behind it', async () => {
|
|
542
|
+
const g = graph();
|
|
543
|
+
const patternMatcher = new WorkerPatternMatcher();
|
|
544
|
+
// A search holding a worker for its whole 5 s budget when the graph is stopped.
|
|
545
|
+
const running = patternMatcher.search('^(a+)+$', '').matchingLines([`${'a'.repeat(39)}!`], 1);
|
|
546
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
547
|
+
const started = Date.now();
|
|
548
|
+
await stopCore({ ...g.core, patternMatcher } as never);
|
|
549
|
+
await expect(running).rejects.toMatchObject({ status: 503, details: { code: 'search_busy' } });
|
|
550
|
+
expect(Date.now() - started).toBeLessThan(1_000);
|
|
551
|
+
// Closed for good: nothing after the stop starts a worker.
|
|
552
|
+
await expect(patternMatcher.search('b', '').matchingLines(['b'], 1)).rejects.toMatchObject({ status: 503 });
|
|
553
|
+
expect(g.order.at(-1)).toBe('db.end');
|
|
554
|
+
});
|
|
555
|
+
|
|
540
556
|
it('does not wait past the budget for a worker that never stops, and still ends the pool', async () => {
|
|
541
557
|
const g = graph();
|
|
542
558
|
const core = { ...g.core, commitWorker: { stop: () => new Promise<void>(() => undefined) } };
|
|
@@ -12,9 +12,9 @@ const log = logger('catalog-revision');
|
|
|
12
12
|
* skill the default branch serves them, right now.
|
|
13
13
|
*
|
|
14
14
|
* It exists for a client that holds a long-lived connection and cannot be told
|
|
15
|
-
* anything: the local `hexis-mcp` server. The hosted endpoint
|
|
16
|
-
* every request
|
|
17
|
-
* (`mcp.
|
|
15
|
+
* anything: the local `hexis-mcp` server. The hosted endpoint reads it too, on
|
|
16
|
+
* every request, to pick the caller's cached tool client: a new revision is a
|
|
17
|
+
* new build (`mcp.surface-cache.test.ts` pins that). A process on someone's laptop
|
|
18
18
|
* registered the deployment's manual ONCE, at startup, and would otherwise
|
|
19
19
|
* serve that startup's toolset until it is restarted. Polling this tells it —
|
|
20
20
|
* cheaply, and without a second way for the catalogs to be read — exactly when
|
|
@@ -470,12 +470,13 @@ export async function createCoreServer(
|
|
|
470
470
|
// `get_skill`. Warnings only; it never refuses a save.
|
|
471
471
|
const allowedToolsChecker = new AllowedToolsChecker(core.toolRegistry, core.toolManualService, core.kb);
|
|
472
472
|
// `delete_branch` sees change requests as `get_change_request` does.
|
|
473
|
-
|
|
473
|
+
// The history tools take the per-file read the file itself takes (`accessControl`).
|
|
474
|
+
registerWorkflowTools(core.toolRegistry, toolsRouter, ta, th, core.kb, changeRequestScope(core.accessControl, core.kb).maySee, core.accessControl, agentAccessGate);
|
|
474
475
|
// The five read tools over change requests. Separate from the workflow tools
|
|
475
476
|
// because they are the only ones that gate their whole payload on the
|
|
476
477
|
// caller's read access, so they take the access service and nothing else.
|
|
477
478
|
registerChangeRequestReadTools(core.toolRegistry, toolsRouter, ta, th, core.accessControl, core.kb);
|
|
478
|
-
const workspaceTools = registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate, core.agentUploadStore, core.agentGuide, core.agentDownloadStore, core.starterPackService, core.disk);
|
|
479
|
+
const workspaceTools = registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate, core.agentUploadStore, core.agentGuide, core.agentDownloadStore, core.starterPackService, core.disk, core.patternMatcher);
|
|
479
480
|
// `open_page` — the knowledge-base page shown inside a chat. Registered
|
|
480
481
|
// here, on the same router as the file tools, because it answers with
|
|
481
482
|
// `read_file`'s own read: the read hook, the access gate and the
|
|
@@ -17,6 +17,8 @@ import { getDb, type Database } from '../modules/database/connection.js';
|
|
|
17
17
|
import { runCoreMigrations } from '../modules/database/migrate.js';
|
|
18
18
|
import { coreMigrationsDir } from '../assets.js';
|
|
19
19
|
import { WorkspaceService } from '../modules/workspace/workspace.service.js';
|
|
20
|
+
import { KbRepository, repositoryDirFor } from '../modules/workspace/kb-repository.js';
|
|
21
|
+
import type { IKbRepository } from '../modules/workspace/kb-repository.contract.js';
|
|
20
22
|
import { AccountLinkService } from '../modules/embed/account-link.service.js';
|
|
21
23
|
import type { EmbedService } from '../modules/embed/embed.service.js';
|
|
22
24
|
import { composeEmbedService } from './compose-embed.js';
|
|
@@ -62,6 +64,8 @@ import { SpillStore } from '../modules/workspace/spill-store.js';
|
|
|
62
64
|
import { AgentUploadStore, assertUploadsRootOutsideWorkspaces } from '../modules/workspace/agent-upload.store.js';
|
|
63
65
|
import { AgentDownloadStore, type IAgentDownloadStore } from '../modules/workspace/agent-download.store.js';
|
|
64
66
|
import { createDownloadFetcherIdentifier } from '../modules/workspace/agent-download.routes.js';
|
|
67
|
+
import { WorkerPatternMatcher } from '../modules/workspace/bounded-matcher.js';
|
|
68
|
+
import type { IPatternMatcher } from '../modules/workspace/pattern-matcher.contract.js';
|
|
65
69
|
import type { Request } from 'express';
|
|
66
70
|
import { DocExtractService } from '../modules/workspace/file-readers/doc-extract.service.js';
|
|
67
71
|
import { UuidSessionSink, type ISessionSink } from '../modules/workspace/session-sink.js';
|
|
@@ -113,7 +117,7 @@ import { ConnectionProbeService } from '../modules/connection-probe/index.js';
|
|
|
113
117
|
import { GitService } from '../modules/workflow/git/git.service.js';
|
|
114
118
|
import { NodeGitRunner } from '../modules/workflow/git/node-git-runner.js';
|
|
115
119
|
import { PullRequestService } from '../modules/workflow/git/pull-request.service.js';
|
|
116
|
-
import { WorkspaceMutex } from '../modules/kb-fs/mutex.js';
|
|
120
|
+
import { WorkspaceIdMutex, WorkspaceMutex } from '../modules/kb-fs/mutex.js';
|
|
117
121
|
import { assertGitVersion } from '../modules/workflow/git/git-version.js';
|
|
118
122
|
import { DiffService } from '../modules/diff/diff.service.js';
|
|
119
123
|
import { ReviewWorkflowService } from '../modules/workflow/review-workflow/review-workflow.service.js';
|
|
@@ -243,6 +247,8 @@ export interface CoreServices {
|
|
|
243
247
|
agentUploadStore: AgentUploadStore;
|
|
244
248
|
/** The bytes `request_file_download` captured, held until their one-time link is fetched or expires. */
|
|
245
249
|
agentDownloadStore: IAgentDownloadStore;
|
|
250
|
+
/** Runs a caller's `grep` pattern off the request thread under a budget; its workers end when the graph stops. */
|
|
251
|
+
patternMatcher: IPatternMatcher;
|
|
246
252
|
/**
|
|
247
253
|
* Every user a download fetch identifies itself as, by its bearer and its
|
|
248
254
|
* session cookie — none when it carries none that verifies — so the
|
|
@@ -570,6 +576,32 @@ export async function createCoreServices(
|
|
|
570
576
|
* Under the backups root: a persistent volume of its own, and one nothing
|
|
571
577
|
* sweeps (see the runner's `reconcileClonesWithConfiguredRepository`).
|
|
572
578
|
*/
|
|
579
|
+
// Shared mutex so git and diff operations on the same workspace serialize
|
|
580
|
+
// against each other — a backup-reseed races a concurrent commit otherwise.
|
|
581
|
+
// Keyed by the CANONICAL workspace id: a route's Express-decoded `:id` and
|
|
582
|
+
// a tool's encoded one reserve the same clone (see `WorkspaceIdMutex`).
|
|
583
|
+
const workspaceMutex = new WorkspaceIdMutex();
|
|
584
|
+
/**
|
|
585
|
+
* The knowledge base's ONE repository: every branch workspace is a worktree
|
|
586
|
+
* of it (see `kb-repository.ts`). One instance, handed to the workspace
|
|
587
|
+
* service, the startup phase and (through the workspace service) the git
|
|
588
|
+
* layer, so one fetch serves every workspace and the writes to its one
|
|
589
|
+
* config file are serialized in one place. Git operations in the worktrees
|
|
590
|
+
* are not serialized against each other beyond the workspace mutex: git's
|
|
591
|
+
* own locks cover a repository shared by worktrees.
|
|
592
|
+
*
|
|
593
|
+
* Under the backups root, like the set-aside copies: a persistent volume in
|
|
594
|
+
* every shipped compose file, so no new mount is needed, and outside the
|
|
595
|
+
* workspaces root, where the orphan sweep never sees it. The folder name
|
|
596
|
+
* starts with a dot, which no branch — and so no backup ledger's workspace
|
|
597
|
+
* id — can. On hexis-cloud every tenant has its own backups root: one
|
|
598
|
+
* repository per tenant.
|
|
599
|
+
*/
|
|
600
|
+
const kbRepository: IKbRepository = new KbRepository({
|
|
601
|
+
dir: repositoryDirFor(config.workspacesRoot, kbDirName, path.join(config.backupsRoot, '.kb-repository')),
|
|
602
|
+
kbRepoUrl: () => repositorySource.url(),
|
|
603
|
+
gitRunner,
|
|
604
|
+
});
|
|
573
605
|
const replacedWorkingCopiesRoot = path.join(config.backupsRoot, 'replaced-working-copies');
|
|
574
606
|
/**
|
|
575
607
|
* What must leave with a working copy that is set aside, whoever sets it
|
|
@@ -635,6 +667,7 @@ export async function createCoreServices(
|
|
|
635
667
|
);
|
|
636
668
|
});
|
|
637
669
|
}),
|
|
670
|
+
kbRepository,
|
|
638
671
|
);
|
|
639
672
|
// The KB startup phase: every seeding, scaffolding and migration concern,
|
|
640
673
|
// run through one runner at the deployment's quiet moments (boot + setup
|
|
@@ -672,6 +705,9 @@ export async function createCoreServices(
|
|
|
672
705
|
// The same folder the workspace service sets aside into — see
|
|
673
706
|
// `replacedWorkingCopiesRoot` above for why the two must agree.
|
|
674
707
|
setAsideRoot: replacedWorkingCopiesRoot,
|
|
708
|
+
// The one repository every branch workspace is a worktree of — the same
|
|
709
|
+
// instance the workspace service holds (see `kbRepository` above).
|
|
710
|
+
repository: kbRepository,
|
|
675
711
|
kbDirName,
|
|
676
712
|
templateDir: config.kbTemplateDir,
|
|
677
713
|
defaultBranch: () => kb.defaultBranch,
|
|
@@ -741,6 +777,7 @@ export async function createCoreServices(
|
|
|
741
777
|
'captured download bytes wait there for their one-time links, so a root inside a workspace would let the ' +
|
|
742
778
|
'file tools read them. Point AGENT_UPLOADS_ROOT at a directory whose parent is outside WORKSPACES_ROOT.',
|
|
743
779
|
});
|
|
780
|
+
const patternMatcher = new WorkerPatternMatcher();
|
|
744
781
|
const agentDownloadStore = new AgentDownloadStore({
|
|
745
782
|
root: agentDownloadsRoot,
|
|
746
783
|
publicBaseUrl: config.publicBackendUrl,
|
|
@@ -827,9 +864,6 @@ export async function createCoreServices(
|
|
|
827
864
|
// registers its listener here, after this returns (see `AccountHooks`).
|
|
828
865
|
const accountHooks = new AccountHooks();
|
|
829
866
|
|
|
830
|
-
// Shared mutex so git and diff operations on the same workspace serialize
|
|
831
|
-
// against each other — a backup-reseed races a concurrent commit otherwise.
|
|
832
|
-
const workspaceMutex = new WorkspaceMutex();
|
|
833
867
|
// Workflow lifecycle hooks — the ONE registry this composition shares
|
|
834
868
|
// between GitService (advisory commit validation), the agent-access gate
|
|
835
869
|
// (the blocking agentRead / preWrite hooks), and the composition root,
|
|
@@ -1598,6 +1632,7 @@ export async function createCoreServices(
|
|
|
1598
1632
|
spillStore,
|
|
1599
1633
|
agentUploadStore,
|
|
1600
1634
|
agentDownloadStore,
|
|
1635
|
+
patternMatcher,
|
|
1601
1636
|
agentDownloadFetcher,
|
|
1602
1637
|
docExtractService,
|
|
1603
1638
|
accessControl,
|
package/src/core/lifecycle.ts
CHANGED
|
@@ -448,6 +448,12 @@ export interface CoreStopDeps {
|
|
|
448
448
|
agentUploadStore?: { stopSweeping(): void; drainSweep(): Promise<void> };
|
|
449
449
|
/** The agent download store, whose sweep deletes captured bytes nobody fetched — stopped for the same reason. */
|
|
450
450
|
agentDownloadStore?: { stopSweeping(): void; drainSweep(): Promise<void> };
|
|
451
|
+
/**
|
|
452
|
+
* The grep pattern matcher, whose worker threads are this graph's own: ended
|
|
453
|
+
* here so a graph a tenant host evicts leaves no worker behind. Optional: a
|
|
454
|
+
* caller that has not built one passes nothing.
|
|
455
|
+
*/
|
|
456
|
+
patternMatcher?: { close(): Promise<void> };
|
|
451
457
|
/**
|
|
452
458
|
* The workflow service, for the tidy-up it runs after each sweep (closing
|
|
453
459
|
* empty requests, removing leftover branches): detached from whatever
|
|
@@ -581,6 +587,10 @@ async function releaseCore(deps: CoreStopDeps, remaining: () => number, log: (m:
|
|
|
581
587
|
if (deps.workflowService) {
|
|
582
588
|
await bounded(deps.workflowService.drainTidy(), remaining(), 'finishing the branch tidy-up', log);
|
|
583
589
|
}
|
|
590
|
+
// Its workers end with the graph; a search still running is answered busy.
|
|
591
|
+
if (deps.patternMatcher) {
|
|
592
|
+
await bounded(deps.patternMatcher.close(), remaining(), 'ending the search workers', log);
|
|
593
|
+
}
|
|
584
594
|
await bounded(deps.commitWorker.stop(), remaining(), 'stopping the commit worker', log);
|
|
585
595
|
if (deps.secretsScope !== undefined) unregisterBevelSecretsVariableLoader(deps.secretsScope);
|
|
586
596
|
await bounded(closeDb(deps.db), remaining(), 'ending the database pool', log);
|
|
@@ -129,6 +129,11 @@ function lsTreeBuilds(): number {
|
|
|
129
129
|
return spawnLog.filter((argv) => argv.includes('ls-tree')).length;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/** Ref resolutions through git: none, since the ref files answer. */
|
|
133
|
+
function revParses(): number {
|
|
134
|
+
return spawnLog.filter((argv) => argv.includes('rev-parse')).length;
|
|
135
|
+
}
|
|
136
|
+
|
|
132
137
|
/** Batched reads of a commit's access tree: one per build. */
|
|
133
138
|
function catFileBatches(): number {
|
|
134
139
|
return spawnLog.filter((argv) => argv.includes('cat-file')).length;
|
|
@@ -190,6 +195,7 @@ describe('AccessControlService — at-ref model cache', () => {
|
|
|
190
195
|
let repo: string;
|
|
191
196
|
let other: string;
|
|
192
197
|
let svc: AccessControlService;
|
|
198
|
+
let stub: WorkspaceService;
|
|
193
199
|
const workspaceId = 'ws-atref-cache';
|
|
194
200
|
const admin = 'razvan@bevel.software';
|
|
195
201
|
const DOC = 'Team/Doc.md';
|
|
@@ -242,7 +248,7 @@ describe('AccessControlService — at-ref model cache', () => {
|
|
|
242
248
|
await git(other, 'config', 'user.email', 'other@example.com');
|
|
243
249
|
await git(other, 'config', 'user.name', 'Other');
|
|
244
250
|
|
|
245
|
-
|
|
251
|
+
stub = {
|
|
246
252
|
getWorkspacePath: async () => workspaceDir,
|
|
247
253
|
// The real service TTLs this at 30s; the stub always fetches so a push
|
|
248
254
|
// from `other` is visible on the next gate call.
|
|
@@ -386,9 +392,11 @@ describe('AccessControlService — at-ref model cache', () => {
|
|
|
386
392
|
// The seed holds roles.yaml and two access.md files; a read per file was
|
|
387
393
|
// what the build used to cost, and what a server forking a large heap
|
|
388
394
|
// cannot afford.
|
|
395
|
+
// One listing and one batch build the model; a second batch reads the
|
|
396
|
+
// asked path's own entries, once per commit and path (see the memo test).
|
|
389
397
|
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
390
398
|
expect(lsTreeBuilds()).toBe(1);
|
|
391
|
-
expect(catFileBatches()).toBe(
|
|
399
|
+
expect(catFileBatches()).toBe(2);
|
|
392
400
|
expect(fileShows()).toBe(0);
|
|
393
401
|
// Both access files were read through the batch: Team's denial is seen.
|
|
394
402
|
await pushFromOther('deny Admin in Team', () =>
|
|
@@ -396,7 +404,7 @@ describe('AccessControlService — at-ref model cache', () => {
|
|
|
396
404
|
);
|
|
397
405
|
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(false);
|
|
398
406
|
expect(lsTreeBuilds()).toBe(2);
|
|
399
|
-
expect(catFileBatches()).toBe(
|
|
407
|
+
expect(catFileBatches()).toBe(4);
|
|
400
408
|
expect(fileShows()).toBe(0);
|
|
401
409
|
});
|
|
402
410
|
|
|
@@ -534,7 +542,139 @@ describe('AccessControlService — at-ref model cache', () => {
|
|
|
534
542
|
// ...and nothing was pinned: the next call, with git answering in full,
|
|
535
543
|
// builds again and answers.
|
|
536
544
|
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
537
|
-
|
|
545
|
+
// Three batches: the refused build, then the build that answered and the
|
|
546
|
+
// asked path's own entries.
|
|
547
|
+
expect(catFileBatches()).toBe(batchesBefore + 3);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('resolves refs from the ref files and remembers own entries at a commit: a repeated gate call starts no git', async () => {
|
|
551
|
+
// First call at this commit: the model is built (two git calls) and the
|
|
552
|
+
// asked path's own entries are read (one batch). The refs themselves —
|
|
553
|
+
// `origin/main`, and the candidates a short name tries — come from the
|
|
554
|
+
// ref files, so no `rev-parse` runs at all.
|
|
555
|
+
expect(await svc.canWriteBatchAtRef(workspaceId, 'origin/main', admin, [DOC])).toEqual(new Map([[DOC, true]]));
|
|
556
|
+
expect(revParses()).toBe(0);
|
|
557
|
+
expect(lsTreeBuilds()).toBe(1);
|
|
558
|
+
expect(catFileBatches()).toBe(2);
|
|
559
|
+
|
|
560
|
+
// The same gate again, and the single-path gate for the same path: the
|
|
561
|
+
// model and the own entries are both remembered, so apart from the
|
|
562
|
+
// fixture's own fetch nothing is started.
|
|
563
|
+
spawnLog.length = 0;
|
|
564
|
+
expect(await svc.canWriteBatchAtRef(workspaceId, 'origin/main', admin, [DOC])).toEqual(new Map([[DOC, true]]));
|
|
565
|
+
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
566
|
+
expect(await svc.canWriteAtRef(workspaceId, 'main', admin, DOC)).toBe(true);
|
|
567
|
+
expect(spawnLog.filter((argv) => !argv.includes('fetch'))).toEqual([]);
|
|
568
|
+
|
|
569
|
+
// A path not asked before at this commit is read, and only that path.
|
|
570
|
+
spawnLog.length = 0;
|
|
571
|
+
expect(await svc.canWriteBatchAtRef(workspaceId, 'origin/main', admin, [DOC, QUOTED_DOC])).toEqual(
|
|
572
|
+
new Map([
|
|
573
|
+
[DOC, true],
|
|
574
|
+
[QUOTED_DOC, false],
|
|
575
|
+
]),
|
|
576
|
+
);
|
|
577
|
+
expect(catFileBatches()).toBe(1);
|
|
578
|
+
expect(revParses()).toBe(0);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('asks git for a ref the files cannot decide, instead of reading it as missing', async () => {
|
|
582
|
+
// An annotated tag names a tag object, not a commit; the reader steps
|
|
583
|
+
// aside and the gate resolves it through `rev-parse <tag>^{commit}` —
|
|
584
|
+
// one process, the answer git gives, never "no such ref".
|
|
585
|
+
await git(repo, 'tag', '-a', 'v1', '-m', 'v1', 'origin/main');
|
|
586
|
+
expect(await svc.canWriteAtRef(workspaceId, 'v1', admin, DOC)).toBe(true);
|
|
587
|
+
expect(revParses()).toBe(1);
|
|
588
|
+
expect(lsTreeBuilds()).toBe(1);
|
|
589
|
+
// The same commit under its branch name: resolved from the files, the
|
|
590
|
+
// model already held.
|
|
591
|
+
spawnLog.length = 0;
|
|
592
|
+
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
593
|
+
expect(revParses()).toBe(0);
|
|
594
|
+
expect(lsTreeBuilds()).toBe(0);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('knows from the tree that a path is absent at a commit, and reads nothing for it', async () => {
|
|
598
|
+
// A change request that adds a file asks about it at the base, where it
|
|
599
|
+
// does not exist yet. The model's listing already named every path the
|
|
600
|
+
// commit carries, so the absence is a fact about the commit: remembered,
|
|
601
|
+
// and not read — on the first call or any later one.
|
|
602
|
+
const NEW = 'Team/New.md';
|
|
603
|
+
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
604
|
+
spawnLog.length = 0;
|
|
605
|
+
expect(await svc.canWriteBatchAtRef(workspaceId, 'origin/main', admin, [NEW])).toEqual(new Map([[NEW, true]]));
|
|
606
|
+
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, NEW)).toBe(true);
|
|
607
|
+
expect(catFileBatches()).toBe(0);
|
|
608
|
+
expect(spawnLog.filter((argv) => !argv.includes('fetch'))).toEqual([]);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("keeps a commit's tree up to the cap and not past it, as an absent path's cost shows", async () => {
|
|
612
|
+
// The seed carries seven paths. A service that may hold seven keeps the
|
|
613
|
+
// tree, so an absent path is answered without a read; one that may hold
|
|
614
|
+
// six does not keep it, so the same path is read on every call.
|
|
615
|
+
const NEW = 'Team/New.md';
|
|
616
|
+
const limited = (treeIndexMaxPaths: number) =>
|
|
617
|
+
new AccessControlService(stub, PROCESS_MAP_DIR, new NodeFs(), [], undefined, undefined, { treeIndexMaxPaths });
|
|
618
|
+
const keeps = limited(7);
|
|
619
|
+
expect(await keeps.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
620
|
+
spawnLog.length = 0;
|
|
621
|
+
expect(await keeps.canWriteAtRef(workspaceId, 'origin/main', admin, NEW)).toBe(true);
|
|
622
|
+
expect(await keeps.canWriteAtRef(workspaceId, 'origin/main', admin, NEW)).toBe(true);
|
|
623
|
+
expect(catFileBatches()).toBe(0);
|
|
624
|
+
const drops = limited(6);
|
|
625
|
+
expect(await drops.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
626
|
+
spawnLog.length = 0;
|
|
627
|
+
expect(await drops.canWriteAtRef(workspaceId, 'origin/main', admin, NEW)).toBe(true);
|
|
628
|
+
expect(await drops.canWriteAtRef(workspaceId, 'origin/main', admin, NEW)).toBe(true);
|
|
629
|
+
expect(catFileBatches()).toBe(2);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('lets the oldest models give their tree up past the budget, and they still answer', async () => {
|
|
633
|
+
// Seven paths at the first commit, eight at the second, a budget of ten:
|
|
634
|
+
// remembering the second puts the first past the budget. The first
|
|
635
|
+
// model keeps answering gates without a rebuild; only an absent path at
|
|
636
|
+
// it is read again, while at the second it costs nothing.
|
|
637
|
+
const NEW = 'Team/New.md';
|
|
638
|
+
const budgeted = new AccessControlService(stub, PROCESS_MAP_DIR, new NodeFs(), [], undefined, undefined, {
|
|
639
|
+
treeIndexBudget: 10,
|
|
640
|
+
});
|
|
641
|
+
const first = await git(repo, 'rev-parse', 'origin/main');
|
|
642
|
+
expect(await budgeted.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
643
|
+
await pushFromOther('a later page', () => fs.writeFile(path.join(other, 'Team/Later.md'), '# later\n'));
|
|
644
|
+
expect(await budgeted.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
645
|
+
const second = await git(repo, 'rev-parse', 'origin/main');
|
|
646
|
+
expect(second).not.toBe(first);
|
|
647
|
+
spawnLog.length = 0;
|
|
648
|
+
expect(await budgeted.canWriteAtRef(workspaceId, second, admin, NEW)).toBe(true);
|
|
649
|
+
expect(lsTreeBuilds()).toBe(0);
|
|
650
|
+
expect(catFileBatches()).toBe(0);
|
|
651
|
+
spawnLog.length = 0;
|
|
652
|
+
expect(await budgeted.canWriteAtRef(workspaceId, first, admin, NEW)).toBe(true);
|
|
653
|
+
expect(lsTreeBuilds()).toBe(0);
|
|
654
|
+
expect(catFileBatches()).toBe(1);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('remembers what a blob declares for itself, never a blob the batch could not read', async () => {
|
|
658
|
+
const SECRET = 'Team/Secret.md';
|
|
659
|
+
await pushFromOther('a page that denies the admin for itself', () =>
|
|
660
|
+
fs.writeFile(path.join(other, SECRET), '---\nwrite:\n - deny Admin\n---\n# secret\n'),
|
|
661
|
+
);
|
|
662
|
+
// Build the model first, so the next batch is the own-entries read.
|
|
663
|
+
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, DOC)).toBe(true);
|
|
664
|
+
// The own-entries batch loses the blob (a lost object reads as missing,
|
|
665
|
+
// exactly like an absent path): answered as no own entries, as before —
|
|
666
|
+
// the inherited write stands — but NOT remembered for the commit.
|
|
667
|
+
injected.rewriteNext = (argv) => (argv.includes('cat-file') ? rewriteBatchRecord(/:Team\/Secret\.md$/, 'missing') : null);
|
|
668
|
+
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, SECRET)).toBe(true);
|
|
669
|
+
expect(injected.rewriteNext).toBeNull();
|
|
670
|
+
// The next call reads the blob again, and its own denial now holds...
|
|
671
|
+
spawnLog.length = 0;
|
|
672
|
+
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, SECRET)).toBe(false);
|
|
673
|
+
expect(catFileBatches()).toBe(1);
|
|
674
|
+
// ...and that answer, a fact about the commit, is remembered.
|
|
675
|
+
spawnLog.length = 0;
|
|
676
|
+
expect(await svc.canWriteAtRef(workspaceId, 'origin/main', admin, SECRET)).toBe(false);
|
|
677
|
+
expect(catFileBatches()).toBe(0);
|
|
538
678
|
});
|
|
539
679
|
|
|
540
680
|
it('holds the models of far more commits than one listing resolves', async () => {
|
|
@@ -161,3 +161,76 @@ describe('canReadAsGroupBatch', () => {
|
|
|
161
161
|
expect(verdict?.get('Plugins/GTM')).toBe(false);
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
|
+
|
|
165
|
+
describe('groupsOfUser', () => {
|
|
166
|
+
let root: string;
|
|
167
|
+
const workspaceId = 'ws-groups-of-user-1';
|
|
168
|
+
|
|
169
|
+
beforeEach(async () => {
|
|
170
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-groups-of-user-'));
|
|
171
|
+
});
|
|
172
|
+
afterEach(async () => {
|
|
173
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
async function makeService() {
|
|
177
|
+
const workspaceDir = path.join(root, workspaceId);
|
|
178
|
+
const repo = path.join(workspaceDir, KB_DIR);
|
|
179
|
+
await fs.mkdir(repo, { recursive: true });
|
|
180
|
+
// `lee@x.io` holds Leads, the role that lists `group:Sales Team`, without
|
|
181
|
+
// being in the group: the role reaches the group, not the other way round.
|
|
182
|
+
await fs.writeFile(path.join(repo, 'roles.yaml'), ROLES_YAML.replace(' - group:Sales Team\n', ' - group:Sales Team\n - lee@x.io\n'));
|
|
183
|
+
await fs.writeFile(
|
|
184
|
+
path.join(repo, 'groups.yaml'),
|
|
185
|
+
`groups:\n Sales Team:\n - sam@x.io\n - sue@x.io\n Design:\n - dee@x.io\n - sue@x.io\n`,
|
|
186
|
+
);
|
|
187
|
+
await fs.writeFile(path.join(repo, 'access.md'), rules('write:\n - Admin\n'));
|
|
188
|
+
return new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), KB_DIR, new NodeFs());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
it('names the groups a person is in, as groups.yaml spells them', async () => {
|
|
192
|
+
const svc = await makeService();
|
|
193
|
+
expect(await svc.groupsOfUser(workspaceId, 'sam@x.io')).toEqual(['Sales Team']);
|
|
194
|
+
expect((await svc.groupsOfUser(workspaceId, 'sue@x.io')).sort()).toEqual(['Design', 'Sales Team']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('matches the email in any case', async () => {
|
|
198
|
+
const svc = await makeService();
|
|
199
|
+
expect(await svc.groupsOfUser(workspaceId, 'Sam@X.io')).toEqual(['Sales Team']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('is empty for someone in no group, however many roles they hold', async () => {
|
|
203
|
+
const svc = await makeService();
|
|
204
|
+
expect(await svc.groupsOfUser(workspaceId, 'eng@x.io')).toEqual([]);
|
|
205
|
+
expect(await svc.groupsOfUser(workspaceId, 'admin@x.io')).toEqual([]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('does not count holding a role that names a group as being in it', async () => {
|
|
209
|
+
const svc = await makeService();
|
|
210
|
+
expect(await svc.groupsOfUser(workspaceId, 'lee@x.io')).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('visibleGroups: the one answer the Groups tab, the share dialog and a grant use', () => {
|
|
214
|
+
// The active source's groups, in the order a caller happens to hold them.
|
|
215
|
+
const listed = ['Sales Team', 'Design'];
|
|
216
|
+
|
|
217
|
+
it('is every group, as given, for an admin', async () => {
|
|
218
|
+
const svc = await makeService();
|
|
219
|
+
expect(await svc.visibleGroups(workspaceId, 'admin@x.io', listed)).toEqual(listed);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('is only the groups a person is in, in the order given, for anyone else', async () => {
|
|
223
|
+
const svc = await makeService();
|
|
224
|
+
expect(await svc.visibleGroups(workspaceId, 'sue@x.io', listed)).toEqual(['Sales Team', 'Design']);
|
|
225
|
+
expect(await svc.visibleGroups(workspaceId, 'sam@x.io', listed)).toEqual(['Sales Team']);
|
|
226
|
+
expect(await svc.visibleGroups(workspaceId, 'dee@x.io', listed)).toEqual(['Design']);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('is empty for someone in no group, a role that names a group included', async () => {
|
|
230
|
+
const svc = await makeService();
|
|
231
|
+
expect(await svc.visibleGroups(workspaceId, 'eng@x.io', listed)).toEqual([]);
|
|
232
|
+
expect(await svc.visibleGroups(workspaceId, 'lee@x.io', listed)).toEqual([]);
|
|
233
|
+
expect(await svc.visibleGroups(workspaceId, 'admin@x.io', [])).toEqual([]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|