@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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
2
|
+
import { WorkflowDomainError } from '../../shared/domain-errors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which of `paths` `userEmail` may read at `origin/<base>`, as a membership
|
|
6
|
+
* set — ONE rule for the change-request reads (their file lists, judged at
|
|
7
|
+
* the request's base) and for `branch_status` (the paths a request would
|
|
8
|
+
* carry). A null answer (the ref, or its `roles.yaml`, does not resolve)
|
|
9
|
+
* withholds everything, which is the same reading the app's fork-point
|
|
10
|
+
* route gives it.
|
|
11
|
+
*/
|
|
12
|
+
export async function readablePathsAt(
|
|
13
|
+
accessControl: Pick<IAccessControl, 'canReadBatchAtRef'>,
|
|
14
|
+
workspaceId: string,
|
|
15
|
+
userEmail: string,
|
|
16
|
+
base: string,
|
|
17
|
+
paths: string[],
|
|
18
|
+
): Promise<Set<string>> {
|
|
19
|
+
if (paths.length === 0) return new Set();
|
|
20
|
+
const verdicts = await accessControl.canReadBatchAtRef(workspaceId, `origin/${base}`, userEmail, [...new Set(paths)]);
|
|
21
|
+
if (!verdicts) return new Set();
|
|
22
|
+
return new Set([...verdicts].filter(([, allowed]) => allowed).map(([p]) => p));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether `userEmail` may read `repoRelativePath` at EVERY one of `commits`
|
|
27
|
+
* — ONE rule for content served from another branch, for the app's compare
|
|
28
|
+
* route and the agent's history tools alike: the commits the service read
|
|
29
|
+
* the content at, pinned by the service inside its reservation (a diff's two
|
|
30
|
+
* sides, a file at a branch's published tip, the two commits a merge is of),
|
|
31
|
+
* so the verdict is about the bytes actually served, never about a branch
|
|
32
|
+
* ref a fetch may have moved between the verdict and the read. A commit
|
|
33
|
+
* whose rules cannot be read (null) grants nothing. An access-model FAILURE
|
|
34
|
+
* is not a null: it propagates with its own status (an unreadable access
|
|
35
|
+
* tree is the documented 503 a client retries).
|
|
36
|
+
*/
|
|
37
|
+
export async function readableAtCommits(
|
|
38
|
+
accessControl: Pick<IAccessControl, 'canReadAtRef'>,
|
|
39
|
+
workspaceId: string,
|
|
40
|
+
userEmail: string,
|
|
41
|
+
repoRelativePath: string,
|
|
42
|
+
commits: readonly string[],
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
for (const sha of commits) {
|
|
45
|
+
if ((await accessControl.canReadAtRef(workspaceId, sha, userEmail, repoRelativePath)) !== true) return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The refusal `readableAtCommits` answers false with, as every caller words it: the branch named, never the commit. */
|
|
51
|
+
export class ReadOnBranchDeniedError extends WorkflowDomainError {
|
|
52
|
+
constructor(displayPath: string, branch: string) {
|
|
53
|
+
super(`You don't have permission to read "${displayPath}" on "${branch}".`, 403, {
|
|
54
|
+
kind: 'read-denied',
|
|
55
|
+
path: displayPath,
|
|
56
|
+
branch,
|
|
57
|
+
});
|
|
58
|
+
this.name = 'ReadOnBranchDeniedError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `readableAtCommits` at every commit `branch` resolves to (the one its
|
|
64
|
+
* content was served from, and any other its local head or published tip
|
|
65
|
+
* names), thrown when it answers false.
|
|
66
|
+
*/
|
|
67
|
+
export async function assertReadableAtCommits(
|
|
68
|
+
accessControl: Pick<IAccessControl, 'canReadAtRef'>,
|
|
69
|
+
workspaceId: string,
|
|
70
|
+
userEmail: string,
|
|
71
|
+
repoRelativePath: string,
|
|
72
|
+
commits: readonly string[],
|
|
73
|
+
branch: string,
|
|
74
|
+
displayPath: string = repoRelativePath,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
if (!(await readableAtCommits(accessControl, workspaceId, userEmail, repoRelativePath, commits))) {
|
|
77
|
+
throw new ReadOnBranchDeniedError(displayPath, branch);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -65,7 +65,7 @@ export interface AgentOperationContext {
|
|
|
65
65
|
sessionId?: string;
|
|
66
66
|
/**
|
|
67
67
|
* The workspace-relative path the operation targets. Absent for a
|
|
68
|
-
* write-capable operation with no resolvable target
|
|
68
|
+
* write-capable operation with no resolvable target.
|
|
69
69
|
*/
|
|
70
70
|
wsPath?: string;
|
|
71
71
|
/** The branch (draft) whose workspace the operation acts on. */
|
|
@@ -38,6 +38,7 @@ import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
|
38
38
|
import { branchForWorkspaceId } from '../../shared/workspace-id.js';
|
|
39
39
|
import type { WorkflowEventBus } from './event-bus.js';
|
|
40
40
|
import { WorkflowDomainError } from '../../shared/domain-errors.js';
|
|
41
|
+
import { assertReadableAtCommits } from './read-on-branch.js';
|
|
41
42
|
import { assertValidRelativePath } from '../kb-fs/branch-name.js';
|
|
42
43
|
import { domainErrorBody } from '../../shared/http-errors.js';
|
|
43
44
|
import { canonicalFileIdentity } from '../../shared/canonical-file-identity.js';
|
|
@@ -413,46 +414,27 @@ export function createWorkflowRoutes(
|
|
|
413
414
|
// The other history endpoints serve commits of the URL workspace's own
|
|
414
415
|
// branch, so its working-tree verdict covers what they return. This one
|
|
415
416
|
// serves content of TWO caller-named branches — the verdict must come
|
|
416
|
-
// from the
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
// workspace `canRead`
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
for (const ref of [`origin/${branch}`, branch]) {
|
|
432
|
-
try {
|
|
433
|
-
verdicts.push(await accessControl.canReadAtRef(req.params.id, ref, user.email, repoRelative));
|
|
434
|
-
} catch (err) {
|
|
435
|
-
// An access-model FAILURE is not an unresolvable ref: mapped to
|
|
436
|
-
// null it would let the other candidate's grant serve a ref
|
|
437
|
-
// whose authorization errored. Fail closed — but keep the
|
|
438
|
-
// error's own status: an unreadable access tree is the
|
|
439
|
-
// documented 503 a client retries, not a permanent 403 denial.
|
|
440
|
-
const { status, body } = toHttpError(err);
|
|
441
|
-
res.status(status).json(body);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
const denied = verdicts.some((v) => v === false);
|
|
446
|
-
const unresolvable = verdicts.every((v) => v === null);
|
|
447
|
-
if (denied || unresolvable) {
|
|
448
|
-
res.status(403).json({ error: `You don't have permission to read "${pathParam}" on "${branch}".` });
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
417
|
+
// from the commits actually served (`readableAtCommits`, the one rule
|
|
418
|
+
// this route and the agent's history tools share): the service pins the
|
|
419
|
+
// commit each side is read at and asks this verdict inside its
|
|
420
|
+
// reservation, before anything is said about the path at either, with
|
|
421
|
+
// every commit the branch resolves to — its local head and its published
|
|
422
|
+
// tip, all of which must grant, as this route has always required of
|
|
423
|
+
// both refs — never at a branch ref a fetch may move between a verdict
|
|
424
|
+
// and a read. (The
|
|
425
|
+
// working-tree side is additionally covered by the workspace `canRead`
|
|
426
|
+
// above.) An access-model failure keeps its own status: an unreadable
|
|
427
|
+
// access tree is the documented 503 a client retries, not a permanent
|
|
428
|
+
// 403 denial.
|
|
429
|
+
const user = await requireUser(req, res);
|
|
430
|
+
if (!user) return;
|
|
431
|
+
const repoRelative = toKbRelative(pathParam, kbDirName)!;
|
|
453
432
|
try {
|
|
454
|
-
const
|
|
455
|
-
|
|
433
|
+
const compared = await workflow.compareFile(req.params.id, pathParam, fromBranch, toBranch, async (sides) => {
|
|
434
|
+
await assertReadableAtCommits(accessControl, req.params.id, user.email, repoRelative, sides.from, fromBranch, pathParam);
|
|
435
|
+
await assertReadableAtCommits(accessControl, req.params.id, user.email, repoRelative, sides.to, toBranch, pathParam);
|
|
436
|
+
});
|
|
437
|
+
res.json({ diff: compared.diff });
|
|
456
438
|
} catch (err) {
|
|
457
439
|
const { status, body } = toHttpError(err);
|
|
458
440
|
res.status(status).json(body);
|
|
@@ -58,9 +58,11 @@ import type {
|
|
|
58
58
|
IWorkflowService,
|
|
59
59
|
MergeBranchOutcome,
|
|
60
60
|
MergeChangeRequestOutcome,
|
|
61
|
+
MergeResolution,
|
|
61
62
|
OpenChangeRequestInput,
|
|
62
63
|
PostChangeRequestCommentInput,
|
|
63
64
|
RemoteSyncPullResult,
|
|
65
|
+
UpdateBranchOutcome,
|
|
64
66
|
} from '@bevel-software/platform-shared';
|
|
65
67
|
import {
|
|
66
68
|
isFolderPlaceholder,
|
|
@@ -68,6 +70,7 @@ import {
|
|
|
68
70
|
isPlatformRestoreShape,
|
|
69
71
|
} from '@bevel-software/platform-shared';
|
|
70
72
|
import type { KbContext } from '../../shared/kb-context.js';
|
|
73
|
+
import { readableAtCommits, ReadOnBranchDeniedError } from './read-on-branch.js';
|
|
71
74
|
import { and, desc, eq, inArray, isNotNull, isNull, lt, ne, or, sql } from 'drizzle-orm';
|
|
72
75
|
import type { Database } from '../database/connection.js';
|
|
73
76
|
import { changeRequests } from '../database/schema.js';
|
|
@@ -421,22 +424,29 @@ export class WorkflowService implements IWorkflowService {
|
|
|
421
424
|
* opposite directions (`A -> B` and `B -> A`), and nothing is ever held
|
|
422
425
|
* while waiting for the other key.
|
|
423
426
|
*
|
|
424
|
-
* A PROTECTED
|
|
425
|
-
* delete a protected branch (`git.deleteBranch` throws
|
|
427
|
+
* A PROTECTED branch's key is NOT reserved, at either end. No path in the
|
|
428
|
+
* system can delete a protected branch (`git.deleteBranch` throws
|
|
426
429
|
* `ProtectedBranchError` before any lock, `retireMergedSourceBranch`
|
|
427
430
|
* returns early on one), so the key would exclude nothing. It would only
|
|
428
|
-
* cost: nearly every request targets the protected default branch,
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
431
|
+
* cost: nearly every request targets the protected default branch, and
|
|
432
|
+
* every draft refreshes FROM it (`updateBranch`, the merge tool's sync),
|
|
433
|
+
* so one shared key there would serialize every open on the hottest path for the
|
|
434
|
+
* duration of a fetch + merge + push + diff. A protected SOURCE takes no
|
|
435
|
+
* key for the same reason: `openChangeRequest` refuses protected sources
|
|
436
|
+
* before locking, and a draft refreshing from the default branch is
|
|
437
|
+
* exactly a protected source.
|
|
432
438
|
*/
|
|
433
439
|
private runOnBranchPair<T>(
|
|
434
440
|
source: string,
|
|
435
441
|
target: string,
|
|
436
442
|
fn: () => Promise<T>,
|
|
437
443
|
): Promise<T> {
|
|
438
|
-
|
|
439
|
-
|
|
444
|
+
// A PROTECTED branch's key is not reserved at EITHER end (see above):
|
|
445
|
+
// nothing can delete one, so the key would exclude nothing and only
|
|
446
|
+
// serialize every draft that refreshes from the default branch behind
|
|
447
|
+
// every other. The same branch at both ends (a draft syncing its own
|
|
448
|
+
// shared state) takes its one key.
|
|
449
|
+
const keys = [...new Set([source, target].filter((b) => !this.kb.isProtectedBranch(b)).map((b) => `branch:${b}`))];
|
|
440
450
|
return this.branchLifecycle.runAll(keys, fn);
|
|
441
451
|
}
|
|
442
452
|
|
|
@@ -968,7 +978,17 @@ export class WorkflowService implements IWorkflowService {
|
|
|
968
978
|
workspaceId: string,
|
|
969
979
|
opts: { preserveMerges?: boolean } = {},
|
|
970
980
|
): Promise<boolean> {
|
|
971
|
-
|
|
981
|
+
return this.pullWorkspaceVia(() => this.git.pull(workspaceId, opts), workspaceId);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* `pullWorkspace` over the pull it is given: the workspace's own
|
|
986
|
+
* (`git.pull`, which takes the workspace's reservation) or one a caller
|
|
987
|
+
* already holding that reservation hands in (a merge's publish). The
|
|
988
|
+
* announcement is the same either way.
|
|
989
|
+
*/
|
|
990
|
+
private async pullWorkspaceVia(pull: () => Promise<{ treeChanged: boolean }>, workspaceId: string): Promise<boolean> {
|
|
991
|
+
const { treeChanged } = await pull();
|
|
972
992
|
const defaultBranch = this.kb.defaultBranch;
|
|
973
993
|
if (treeChanged && branchForWorkspaceId(workspaceId) === defaultBranch) {
|
|
974
994
|
this.events?.emit({ kind: 'fs-tree-changed', workspaceId, branch: defaultBranch });
|
|
@@ -1259,8 +1279,9 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1259
1279
|
path: string,
|
|
1260
1280
|
fromBranch: string,
|
|
1261
1281
|
toBranch: string,
|
|
1262
|
-
|
|
1263
|
-
|
|
1282
|
+
authorize: (sides: { from: string[]; to: string[] }) => Promise<void>,
|
|
1283
|
+
): Promise<{ diff: string; from: string; to: string }> {
|
|
1284
|
+
return this.git.diffFileBetweenBranches(workspaceId, path, fromBranch, toBranch, authorize);
|
|
1264
1285
|
}
|
|
1265
1286
|
|
|
1266
1287
|
fileAtForkPoint(
|
|
@@ -1893,8 +1914,33 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1893
1914
|
): Promise<void> {
|
|
1894
1915
|
// No explicit `undefined` third argument when there are no options: the
|
|
1895
1916
|
// call shape stays the bare `push(workspaceId, user)` every caller made.
|
|
1896
|
-
|
|
1897
|
-
|
|
1917
|
+
return this.pushWithRecoveryVia(
|
|
1918
|
+
{
|
|
1919
|
+
push: () => (opts ? this.git.push(workspaceId, user, opts) : this.git.push(workspaceId, user)),
|
|
1920
|
+
pull: () => this.pullWorkspace(workspaceId, { preserveMerges: true }),
|
|
1921
|
+
},
|
|
1922
|
+
workspaceId,
|
|
1923
|
+
branch,
|
|
1924
|
+
targetPath,
|
|
1925
|
+
user,
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
/**
|
|
1930
|
+
* `pushWithRecovery`'s policy over the two moves it is made of — the push,
|
|
1931
|
+
* and the pull-rebase that keeps a stranded merge a merge — so that a
|
|
1932
|
+
* caller already holding the workspace's reservation (a merge publishing
|
|
1933
|
+
* itself, `updateBranch`) runs the SAME policy over its own unlocked moves,
|
|
1934
|
+
* and a push the host refuses answers one way there too.
|
|
1935
|
+
*/
|
|
1936
|
+
private async pushWithRecoveryVia(
|
|
1937
|
+
ops: { push: () => Promise<void>; pull: () => Promise<unknown> },
|
|
1938
|
+
workspaceId: string,
|
|
1939
|
+
branch: string,
|
|
1940
|
+
targetPath: string,
|
|
1941
|
+
user: AuthUser,
|
|
1942
|
+
): Promise<void> {
|
|
1943
|
+
const push = ops.push;
|
|
1898
1944
|
try {
|
|
1899
1945
|
await push();
|
|
1900
1946
|
this.noteGitSyncOk(workspaceId, branch);
|
|
@@ -1915,7 +1961,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1915
1961
|
// with whatever push of the branch comes next — a save, a share —
|
|
1916
1962
|
// and a plain rebase would flatten it into cherry-picks. With no
|
|
1917
1963
|
// merge among the local commits the two spellings replay the same.
|
|
1918
|
-
await
|
|
1964
|
+
await ops.pull();
|
|
1919
1965
|
try {
|
|
1920
1966
|
await push();
|
|
1921
1967
|
} catch (retryErr) {
|
|
@@ -4182,8 +4228,8 @@ export class WorkflowService implements IWorkflowService {
|
|
|
4182
4228
|
// while they are held: a bare read would let a person open the request
|
|
4183
4229
|
// mid-merge and the agent would land its content anyway, which is the one
|
|
4184
4230
|
// thing this tool exists to prevent. `runOnBranchPair` drops a protected
|
|
4185
|
-
//
|
|
4186
|
-
// request from this source takes it too.
|
|
4231
|
+
// branch's key at either end; a draft source's key is held, and every
|
|
4232
|
+
// competing request from this source takes it too.
|
|
4187
4233
|
return this.runOnBranchPair(sourceBranch, targetBranch, async () => {
|
|
4188
4234
|
// A person merges a change request. Only the request's OWN direction is
|
|
4189
4235
|
// blocked: the target merged into the source is how a draft under review
|
|
@@ -4681,4 +4727,145 @@ export class WorkflowService implements IWorkflowService {
|
|
|
4681
4727
|
return formatAffectedOwnersBlock(paths, resolved);
|
|
4682
4728
|
}
|
|
4683
4729
|
|
|
4730
|
+
async updateBranch(
|
|
4731
|
+
workspaceId: string,
|
|
4732
|
+
user: AuthUser,
|
|
4733
|
+
opts: { from?: string } & (
|
|
4734
|
+
| { resolutions?: undefined; validateWrite?: undefined }
|
|
4735
|
+
| { resolutions: MergeResolution[]; validateWrite: (wsPath: string, content: string) => void | Promise<void> }
|
|
4736
|
+
) = {},
|
|
4737
|
+
): Promise<UpdateBranchOutcome> {
|
|
4738
|
+
const branch = branchForWorkspaceId(workspaceId);
|
|
4739
|
+
const from = opts.from ?? branch;
|
|
4740
|
+
// A resolution writes on the draft: it is somebody's to gate, and a
|
|
4741
|
+
// caller that brings none gates nothing (closed) — the type says so for
|
|
4742
|
+
// a compiled caller, this for any other.
|
|
4743
|
+
if ((opts.resolutions?.length ?? 0) > 0 && typeof opts.validateWrite !== 'function') {
|
|
4744
|
+
throw new Error('updateBranch: resolutions need a `validateWrite` to be asked before the merge');
|
|
4745
|
+
}
|
|
4746
|
+
if (this.kb.isProtectedBranch(branch)) {
|
|
4747
|
+
throw new WorkflowDomainError(
|
|
4748
|
+
`"${branch}" is protected: it is changed through change requests, never merged into directly.`,
|
|
4749
|
+
403,
|
|
4750
|
+
{ kind: 'protected-update-target', branch },
|
|
4751
|
+
);
|
|
4752
|
+
}
|
|
4753
|
+
// The one merge a person makes: a change request's. An update that would
|
|
4754
|
+
// land an OPEN request from `from` into this draft is that merge by
|
|
4755
|
+
// another name, and `mergeBranch`'s gate refuses it here too — asked once
|
|
4756
|
+
// before waiting, and again under the locks below, where the answer
|
|
4757
|
+
// counts. (The other direction, the request's target into its source, is
|
|
4758
|
+
// how a draft under review stays current, and is never blocked.)
|
|
4759
|
+
if (from !== branch) {
|
|
4760
|
+
const openBefore = await this.openRequestBlockingMerge(from, branch);
|
|
4761
|
+
if (openBefore !== null) throw new OpenChangeRequestBlocksMergeError(from, branch, openBefore);
|
|
4762
|
+
}
|
|
4763
|
+
// The merge takes what the REMOTE holds of `from`, and the push publishes
|
|
4764
|
+
// the branch: writes still being committed on either would be missed by
|
|
4765
|
+
// the first or swept up by the second. Waited for, as `mergeBranch` waits
|
|
4766
|
+
// — outside the lifecycle locks, which must not be held for seconds of
|
|
4767
|
+
// waiting on a queue.
|
|
4768
|
+
for (const b of from === branch ? [branch] : [from, branch]) {
|
|
4769
|
+
const settled = await this.awaitPendingCommits(b);
|
|
4770
|
+
if (!settled.ok) {
|
|
4771
|
+
const message = settled.needsAttention
|
|
4772
|
+
? `A commit queued on "${b}" failed and needs a person: ${settled.needsAttention} ` +
|
|
4773
|
+
'Nothing was merged. Tell the user; update again once it has been resolved.'
|
|
4774
|
+
: `${settled.pending} write(s) on "${b}" are still being committed. Nothing was merged. Retry shortly.`;
|
|
4775
|
+
return {
|
|
4776
|
+
kind: 'pending-commits',
|
|
4777
|
+
branch: b,
|
|
4778
|
+
pending: settled.pending,
|
|
4779
|
+
...(settled.needsAttention ? { needsAttention: settled.needsAttention } : {}),
|
|
4780
|
+
message,
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
// Under the lifecycle lock of both branches, as `mergeBranch` and
|
|
4785
|
+
// `openChangeRequest` run: the "no open request" answer is only sound
|
|
4786
|
+
// while nobody can open one mid-merge, and a branch being merged into is
|
|
4787
|
+
// not one to delete or recreate meanwhile. A protected `from` takes no
|
|
4788
|
+
// key, and the branch's own shared state (from = branch) takes the one.
|
|
4789
|
+
return this.runOnBranchPair(from, branch, async () => {
|
|
4790
|
+
if (from !== branch) {
|
|
4791
|
+
const open = await this.openRequestBlockingMerge(from, branch);
|
|
4792
|
+
if (open !== null) throw new OpenChangeRequestBlocksMergeError(from, branch, open);
|
|
4793
|
+
}
|
|
4794
|
+
if (!(await this.git.remoteBranchExists(workspaceId, from))) {
|
|
4795
|
+
throw new WorkflowValidationError(`No branch named "${from}" on the shared repository.`);
|
|
4796
|
+
}
|
|
4797
|
+
const resolutions = opts.resolutions ?? [];
|
|
4798
|
+
const subject = from === branch ? `Merge the shared ${branch} into the local one` : `Merge ${from} into ${branch}`;
|
|
4799
|
+
const body =
|
|
4800
|
+
(from === branch
|
|
4801
|
+
? `The branch had moved on the shared repository while this workspace held commits of its own; both are kept.`
|
|
4802
|
+
: `Brings the latest ${from} into ${branch}.`) +
|
|
4803
|
+
(resolutions.length > 0
|
|
4804
|
+
? ` Conflicts resolved by ${user.name} <${user.email}> in: ${resolutions.map((r) => r.path).join(', ')}.`
|
|
4805
|
+
: '');
|
|
4806
|
+
// Published inside the merge's own reservation: nothing lands between
|
|
4807
|
+
// the merge commit and its push, and the push is EVERY workflow push's
|
|
4808
|
+
// `pushWithRecovery`, run over the merge's own unlocked moves — a
|
|
4809
|
+
// rejection replays the merge (kept as a merge) onto the moved branch
|
|
4810
|
+
// and pushes again; a replay that conflicts or a host that refuses
|
|
4811
|
+
// answers as every refused share answers, the banner noted and the
|
|
4812
|
+
// typed hand-off thrown, naming the branch (no single file is at issue)
|
|
4813
|
+
// and never a conflicted path. The merge commit stays local for the
|
|
4814
|
+
// next push of the branch to carry.
|
|
4815
|
+
return this.git.mergeIntoBranch(workspaceId, branch, from, user, {
|
|
4816
|
+
subject,
|
|
4817
|
+
body,
|
|
4818
|
+
resolutions,
|
|
4819
|
+
publish: (ops) =>
|
|
4820
|
+
this.pushWithRecoveryVia(
|
|
4821
|
+
{ push: ops.push, pull: () => this.pullWorkspaceVia(ops.pullRebase, workspaceId) },
|
|
4822
|
+
workspaceId,
|
|
4823
|
+
branch,
|
|
4824
|
+
branch,
|
|
4825
|
+
user,
|
|
4826
|
+
),
|
|
4827
|
+
// A resolution writes a file on the draft from what both branches
|
|
4828
|
+
// hold of it. It is authorized inside the merge's reservation, after
|
|
4829
|
+
// the fetch and before the merge touches the checkout — the draft's
|
|
4830
|
+
// tree clean at its own commit, so the rules read from it are the
|
|
4831
|
+
// draft's, never a rule file mid-merge: the caller must read the
|
|
4832
|
+
// file at the draft's commit and at the source commit the merge is
|
|
4833
|
+
// of, and may change it on the draft by the one rule every draft
|
|
4834
|
+
// write takes — the change gate `acquireLock` asks, not a write
|
|
4835
|
+
// permission a draft never requires.
|
|
4836
|
+
authorizeResolutions: async ({ ours, theirs }) => {
|
|
4837
|
+
for (const r of resolutions) {
|
|
4838
|
+
const rel = r.path.startsWith(`${this.kbDirName}/`) ? r.path.slice(this.kbDirName.length + 1) : r.path;
|
|
4839
|
+
// At the two COMMITS the merge is of — pinned by the merge itself,
|
|
4840
|
+
// never at a ref a concurrent fetch can move between this verdict
|
|
4841
|
+
// and the merge — by the one rule the tool shows a conflict by.
|
|
4842
|
+
if (!(await readableAtCommits(this.accessControl, workspaceId, user.email, rel, [ours, theirs]))) {
|
|
4843
|
+
throw new ReadOnBranchDeniedError(rel, from);
|
|
4844
|
+
}
|
|
4845
|
+
// The gate takes the WORKSPACE path, as `acquireLock` hands it
|
|
4846
|
+
// one: a repository-relative path reads as outside the knowledge
|
|
4847
|
+
// base there, which the gate waves through.
|
|
4848
|
+
await this.changeGate.assertMayChange(workspaceId, user.email, `${this.kbDirName}/${rel}`, 'file');
|
|
4849
|
+
// Content the resolution brings takes the caller's pre-disk write
|
|
4850
|
+
// gate, as its file writes do before a byte lands; a side kept
|
|
4851
|
+
// whole is a commit's own bytes and brings none.
|
|
4852
|
+
if (r.content !== undefined && opts.validateWrite) await opts.validateWrite(`${this.kbDirName}/${rel}`, r.content);
|
|
4853
|
+
}
|
|
4854
|
+
},
|
|
4855
|
+
});
|
|
4856
|
+
});
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
fileAtBranch(
|
|
4860
|
+
workspaceId: string,
|
|
4861
|
+
path: string,
|
|
4862
|
+
branch: string,
|
|
4863
|
+
authorize: (commits: string[]) => Promise<void>,
|
|
4864
|
+
): Promise<{ sha: string; ref: string; content: string | null }> {
|
|
4865
|
+
return this.git.readFileAtBranch(workspaceId, path, branch, authorize);
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
changedPathsBetweenBranches(workspaceId: string, base: string, branch: string): Promise<string[]> {
|
|
4869
|
+
return this.git.changedPathsForPr(workspaceId, base, branch);
|
|
4870
|
+
}
|
|
4684
4871
|
}
|
|
@@ -22,7 +22,6 @@ import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
|
22
22
|
import { ToolDescriptionNotes } from '../agent-access.gate.js';
|
|
23
23
|
import { SpillStore } from '../spill-store.js';
|
|
24
24
|
import { DocExtractService } from '../file-readers/doc-extract.service.js';
|
|
25
|
-
import { WorkspaceService } from '../workspace.service.js';
|
|
26
25
|
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
27
26
|
import { AgentDownloadStore, DOWNLOAD_TOKEN_REFUSAL } from '../agent-download.store.js';
|
|
28
27
|
import { attachmentDisposition, createAgentDownloadRoutes, createDownloadFetcherIdentifier } from '../agent-download.routes.js';
|
|
@@ -31,6 +30,7 @@ import jwt from 'jsonwebtoken';
|
|
|
31
30
|
import { buildDownload, DOWNLOAD_PERMISSION_REQUIRED, NOT_FOUND } from '../agent-download.builder.js';
|
|
32
31
|
import { READ_ONLY_CODE, type IWriteAccess } from '../../write-access/write-access.js';
|
|
33
32
|
import { sharedFileRules } from '../../agent-instructions/shared-file-rules.js';
|
|
33
|
+
import { newWorkspaceService } from '../../../__tests__/workspace-service.js';
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* `request_file_download` end to end, over the REAL access resolver on a
|
|
@@ -136,7 +136,7 @@ async function start(options: StartOptions = {}): Promise<Started> {
|
|
|
136
136
|
await mkdir(dirname(abs), { recursive: true });
|
|
137
137
|
await writeFile(abs, contents);
|
|
138
138
|
}
|
|
139
|
-
const workspaceService =
|
|
139
|
+
const workspaceService = newWorkspaceService(
|
|
140
140
|
root,
|
|
141
141
|
'https://example.invalid/kb.git',
|
|
142
142
|
testKbContext({ kbDirName: KB }),
|