@bevel-software/platform-core-backend 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/core-ports.d.ts +11 -1
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +62 -13
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +158 -23
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +34 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +89 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +0 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +11 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +42 -12
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +67 -10
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +214 -35
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-requests.contract.d.ts +75 -0
- package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
- package/dist/modules/access/access-requests.contract.js +20 -0
- package/dist/modules/access/access-requests.contract.js.map +1 -0
- package/dist/modules/access/access-requests.routes.d.ts +35 -0
- package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
- package/dist/modules/access/access-requests.routes.js +237 -0
- package/dist/modules/access/access-requests.routes.js.map +1 -0
- package/dist/modules/access/access-requests.service.d.ts +123 -0
- package/dist/modules/access/access-requests.service.d.ts.map +1 -0
- package/dist/modules/access/access-requests.service.js +337 -0
- package/dist/modules/access/access-requests.service.js.map +1 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +13 -15
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +12 -0
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +24 -0
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +58 -7
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +44 -1
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +61 -5
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +1 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +18 -7
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +8 -0
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +96 -3
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +187 -9
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +7 -2
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +41 -24
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +35 -27
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/github-app/github-app.client.d.ts +101 -0
- package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.client.js +222 -0
- package/dist/modules/github-app/github-app.client.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +102 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.connection.js +185 -0
- package/dist/modules/github-app/github-app.connection.js.map +1 -0
- package/dist/modules/github-app/github-app.routes.d.ts +59 -0
- package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.routes.js +323 -0
- package/dist/modules/github-app/github-app.routes.js.map +1 -0
- package/dist/modules/github-app/index.d.ts +4 -0
- package/dist/modules/github-app/index.d.ts.map +1 -0
- package/dist/modules/github-app/index.js +4 -0
- package/dist/modules/github-app/index.js.map +1 -0
- package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
- package/dist/modules/kb-fs/branch-name.js +12 -2
- package/dist/modules/kb-fs/branch-name.js.map +1 -1
- package/dist/modules/kb-fs/remote-url.d.ts +22 -0
- package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
- package/dist/modules/kb-fs/remote-url.js +35 -0
- package/dist/modules/kb-fs/remote-url.js.map +1 -0
- package/dist/modules/kb-fs/repo-path.d.ts +11 -16
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +79 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
- package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
- package/dist/modules/kb-sync/sync-auth.js +1 -1
- package/dist/modules/kb-sync/sync-auth.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/plugins/join-proposals.d.ts +17 -5
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
- package/dist/modules/plugins/join-proposals.js +76 -22
- package/dist/modules/plugins/join-proposals.js.map +1 -1
- package/dist/modules/plugins/join-requests.service.d.ts +111 -18
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-requests.service.js +173 -29
- package/dist/modules/plugins/join-requests.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +3 -2
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +101 -7
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/managed-repository.d.ts +43 -0
- package/dist/modules/settings/managed-repository.d.ts.map +1 -0
- package/dist/modules/settings/managed-repository.js +60 -0
- package/dist/modules/settings/managed-repository.js.map +1 -0
- package/dist/modules/settings/repository-source.d.ts +128 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -0
- package/dist/modules/settings/repository-source.js +150 -0
- package/dist/modules/settings/repository-source.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +78 -4
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +379 -17
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.js +24 -63
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +90 -18
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +4 -1
- package/dist/modules/skills/skills.contract.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +98 -9
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +239 -37
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +7 -0
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +71 -7
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +4 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +15 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +25 -4
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +24 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +30 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +170 -6
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +5 -0
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +99 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +55 -0
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
- package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
- package/dist/modules/workflow/workflow-hooks.js +16 -1
- package/dist/modules/workflow/workflow-hooks.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +3 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +94 -13
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +258 -51
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
- package/dist/modules/workspace/agent-access.gate.js +123 -0
- package/dist/modules/workspace/agent-access.gate.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +5 -6
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/session-sink.d.ts +5 -5
- package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
- package/dist/modules/workspace/set-aside-clone.js +92 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +92 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +115 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +272 -26
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +2 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +337 -117
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts +60 -0
- package/dist/modules/write-access/write-access.d.ts.map +1 -0
- package/dist/modules/write-access/write-access.js +129 -0
- package/dist/modules/write-access/write-access.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +64 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +83 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +30 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +26 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts +0 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +0 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/__tests__/kb-layout-config.test.ts +0 -2
- package/src/__tests__/retired-settings.test.ts +96 -0
- package/src/core/__tests__/gated-boot.test.ts +132 -0
- package/src/core/__tests__/lifecycle.test.ts +170 -1
- package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
- package/src/core/core-ports.ts +11 -1
- package/src/core/create-core-server.ts +69 -15
- package/src/core/create-core-services.ts +190 -27
- package/src/core/lifecycle.ts +98 -13
- package/src/core-config.ts +13 -14
- package/src/index.ts +12 -1
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
- package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
- package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
- package/src/modules/access/access-control.interface.ts +43 -12
- package/src/modules/access/access-control.service.ts +234 -36
- package/src/modules/access/access-requests.contract.ts +93 -0
- package/src/modules/access/access-requests.routes.ts +286 -0
- package/src/modules/access/access-requests.service.ts +420 -0
- package/src/modules/access/access.routes.ts +13 -15
- package/src/modules/access-model/access-grammar.ts +22 -0
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
- package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account.routes.ts +63 -7
- package/src/modules/auth/auth.middleware.ts +19 -8
- package/src/modules/auth/auth.routes.ts +8 -0
- package/src/modules/auth/auth.service.ts +207 -7
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
- package/src/modules/github-app/github-app.client.ts +268 -0
- package/src/modules/github-app/github-app.connection.ts +204 -0
- package/src/modules/github-app/github-app.routes.ts +359 -0
- package/src/modules/github-app/index.ts +19 -0
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
- package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
- package/src/modules/kb-fs/branch-name.ts +14 -1
- package/src/modules/kb-fs/remote-url.ts +35 -0
- package/src/modules/kb-fs/repo-path.ts +85 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
- package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
- package/src/modules/kb-sync/sync-auth.ts +2 -2
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
- package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
- package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/plugins/join-proposals.ts +87 -19
- package/src/modules/plugins/join-requests.service.ts +199 -39
- package/src/modules/plugins/plugins.routes.ts +3 -2
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
- package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
- package/src/modules/settings/deployment-settings.service.ts +117 -6
- package/src/modules/settings/managed-repository.ts +68 -0
- package/src/modules/settings/repository-source.ts +197 -0
- package/src/modules/settings/setup.routes.ts +463 -15
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
- package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
- package/src/modules/skills/skill-access-requests.routes.ts +25 -75
- package/src/modules/skills/skills.contract.ts +91 -18
- package/src/modules/skills/skills.service.ts +278 -41
- package/src/modules/skills/skills.tools.ts +80 -8
- package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
- package/src/modules/tool-auth/external-api-key.service.ts +4 -2
- package/src/modules/tool-auth/internal-token.service.ts +3 -3
- package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
- package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
- package/src/modules/tool-helpers/tool-context.ts +15 -0
- package/src/modules/tool-helpers/tool-handler.ts +24 -4
- package/src/modules/tool-helpers/tool.contract.ts +3 -2
- package/src/modules/tool-helpers/validate-token.ts +1 -1
- package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
- package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
- package/src/modules/workflow/file-lock.service.ts +31 -0
- package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
- package/src/modules/workflow/git/git.service.ts +193 -4
- package/src/modules/workflow/git/node-git-runner.ts +6 -0
- package/src/modules/workflow/git/pull-request.service.ts +119 -0
- package/src/modules/workflow/pending-commits.service.ts +59 -1
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.routes.ts +3 -1
- package/src/modules/workflow/workflow.service.ts +290 -55
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
- package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
- package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/routine-write-policy.ts +5 -6
- package/src/modules/workspace/session-sink.ts +5 -5
- package/src/modules/workspace/set-aside-clone.ts +96 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +273 -25
- package/src/modules/workspace/workspace.tools.ts +373 -119
- package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
- package/src/modules/write-access/write-access.ts +153 -0
- package/src/shared/domain-errors.ts +89 -0
- package/src/shared/git.contract.ts +37 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
- package/src/tenancy/static-tenant-source.ts +0 -3
- package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
- package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.policy.js +0 -62
- package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
- package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
- package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.service.js +0 -147
- package/dist/modules/workflow/session-ontology.service.js.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.js +0 -161
- package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
- package/dist/shared/kb-layout.d.ts +0 -39
- package/dist/shared/kb-layout.d.ts.map +0 -1
- package/dist/shared/kb-layout.js +0 -103
- package/dist/shared/kb-layout.js.map +0 -1
- package/dist/shared/kb-layout.test.d.ts +0 -2
- package/dist/shared/kb-layout.test.d.ts.map +0 -1
- package/dist/shared/kb-layout.test.js +0 -75
- package/dist/shared/kb-layout.test.js.map +0 -1
- package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
- package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
- package/src/modules/workflow/session-ontology.policy.ts +0 -70
- package/src/modules/workflow/session-ontology.service.ts +0 -183
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
- package/src/modules/workspace/session-ontology.gate.ts +0 -191
- package/src/shared/kb-layout.test.ts +0 -98
- package/src/shared/kb-layout.ts +0 -102
|
@@ -3,9 +3,10 @@ import nodeFs from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { ToolError } from '../tool-helpers/tool.contract.js';
|
|
5
5
|
import { BRANCH_INPUT, toolDef } from '../tool-helpers/tool-def.js';
|
|
6
|
-
import {
|
|
6
|
+
import { notifyAgentRead, assertAgentWriteAllowed, SESSION_ID_INPUT, } from './agent-access.gate.js';
|
|
7
7
|
import { requireInternalSource, requireExternalSource } from '../tool-auth/tool-auth.middleware.js';
|
|
8
8
|
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
9
|
+
import { assertBranchProvided } from '../../shared/domain-errors.js';
|
|
9
10
|
// Leaf-level shared primitive (same exception `workspace.service.ts` already
|
|
10
11
|
// relies on) — not a workflow service, so this stays inside the module boundary.
|
|
11
12
|
import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
@@ -318,6 +319,25 @@ const WRITE_MODE_NOTE = ' `mode` decides what may happen at a path and DEFAULTS
|
|
|
318
319
|
'already exists (`exists`, with the path — pass `mode: overwrite` to replace it), `overwrite` replaces what is there ' +
|
|
319
320
|
'(creating it if there is nothing), `update` replaces an existing file and refuses a path that does not exist (`missing`). ' +
|
|
320
321
|
'A refused path is left exactly as it was.';
|
|
322
|
+
/**
|
|
323
|
+
* What an agent needs to know about escape sequences in the content it sends,
|
|
324
|
+
* on the three tools that take content as a JSON string.
|
|
325
|
+
*
|
|
326
|
+
* The three write routes — the MCP endpoint, the `/api/agent/tools/<name>`
|
|
327
|
+
* route and `call_tool_chain` — were measured end to end against raw requests
|
|
328
|
+
* and a byte-level read of the stored file (see
|
|
329
|
+
* `__tests__/escape-sequences.routes.test.ts`): each stores content exactly as
|
|
330
|
+
* the JSON string value decodes ONCE. So when an escape arrives already
|
|
331
|
+
* decoded, the decoding happened in the client that built the request, and no
|
|
332
|
+
* tool here can tell that content from content that was meant to be decoded.
|
|
333
|
+
* Hence a warning rather than a fix, and the pointer to the one route whose
|
|
334
|
+
* payload is bytes rather than a JSON string.
|
|
335
|
+
*/
|
|
336
|
+
const ESCAPE_SEQUENCE_NOTE = ' Escape sequences: some clients decode them in arguments before sending, so content meant to CONTAIN an escape rather ' +
|
|
337
|
+
'than what it stands for (the six characters backslash, `u`, `0`, `0`, `4`, `1`, say, rather than the letter `A`) can ' +
|
|
338
|
+
'reach this tool already decoded — what arrives is stored byte for byte, so when that distinction matters, verify what ' +
|
|
339
|
+
'landed (`read_file`, or a hash) and send such content through the upload route (`request_upload_token` + `apply_upload` ' +
|
|
340
|
+
'where offered, otherwise Upload in the app), which lands it unchanged.';
|
|
321
341
|
/** The refusal `create` gives on a path that already holds something. */
|
|
322
342
|
function pathExists(path) {
|
|
323
343
|
return new ToolError(`"${displayPath(path)}" already exists — pass mode: overwrite to replace it, or write to a different path.`, 409, { code: 'exists', path });
|
|
@@ -430,7 +450,7 @@ async function searchRootKind(fs, path) {
|
|
|
430
450
|
}
|
|
431
451
|
}
|
|
432
452
|
/** JS grep over the workspace tree (read methods only) — bounded by match + depth caps. */
|
|
433
|
-
async function grepWalk(fs, dir, re, out, max, depth, gate,
|
|
453
|
+
async function grepWalk(fs, dir, re, out, max, depth, gate, notifyRead, docs) {
|
|
434
454
|
if (out.length >= max || depth > 12)
|
|
435
455
|
return;
|
|
436
456
|
let entries;
|
|
@@ -452,13 +472,13 @@ async function grepWalk(fs, dir, re, out, max, depth, gate, recordOntologyRead,
|
|
|
452
472
|
continue;
|
|
453
473
|
const p = dir ? `${dir}/${e.name}` : e.name;
|
|
454
474
|
if (e.type === 'directory') {
|
|
455
|
-
await grepWalk(fs, p, re, out, max, depth + 1, gate,
|
|
475
|
+
await grepWalk(fs, p, re, out, max, depth + 1, gate, notifyRead, docs);
|
|
456
476
|
}
|
|
457
477
|
else {
|
|
458
|
-
// Opening a file
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
await
|
|
478
|
+
// Opening a file is a read of it, even when the walk started at a root
|
|
479
|
+
// the read hook was already told about — so every file the walk opens
|
|
480
|
+
// reaches the hook by name (closes the read-leak).
|
|
481
|
+
await notifyRead(p);
|
|
462
482
|
// A file the walk cannot read is silently skipped: one unreadable entry
|
|
463
483
|
// must not fail a search over the whole tree.
|
|
464
484
|
try {
|
|
@@ -488,6 +508,17 @@ const BATCH_SAVE_WARNINGS_OUTPUT = {
|
|
|
488
508
|
'The writes still happened.',
|
|
489
509
|
items: { type: 'object' },
|
|
490
510
|
};
|
|
511
|
+
/**
|
|
512
|
+
* The `sessionId` property inside a BUILT tool def's input schema, or
|
|
513
|
+
* `undefined` for a tool that declares none. `toolDef` wraps the flat inputs
|
|
514
|
+
* under `body` and copies the schema it is given, so a note registered after
|
|
515
|
+
* the tools were built has to be written here rather than onto the shared
|
|
516
|
+
* `SESSION_ID_INPUT` constant.
|
|
517
|
+
*/
|
|
518
|
+
function sessionIdInputOf(def) {
|
|
519
|
+
const inputs = def.inputs;
|
|
520
|
+
return inputs?.properties?.body?.properties?.sessionId;
|
|
521
|
+
}
|
|
491
522
|
/**
|
|
492
523
|
* Workspace domain tools: the file primitives (replacing Mastra's auto-injected
|
|
493
524
|
* Workspace tools) + unzip. Most just re-expose the SAME `LocalFilesystem`
|
|
@@ -497,7 +528,7 @@ const BATCH_SAVE_WARNINGS_OUTPUT = {
|
|
|
497
528
|
* method), so they're implemented here. File ops are `both`; `execute_command`
|
|
498
529
|
* is INTERNAL-only (arbitrary shell as the caller is too dangerous to expose).
|
|
499
530
|
*/
|
|
500
|
-
export function registerWorkspaceTools(registry, router, toolAuth, toolHandler, spillStore, docExtract, accessControl, kb,
|
|
531
|
+
export function registerWorkspaceTools(registry, router, toolAuth, toolHandler, spillStore, docExtract, accessControl, kb, agentAccessGate, writePolicy, sessionSink,
|
|
501
532
|
/**
|
|
502
533
|
* Save-time skill check (see `AllowedToolsChecker`): a write to a SKILL.md
|
|
503
534
|
* returns `warnings` for `allowed-tools` entries naming no visible tool.
|
|
@@ -581,7 +612,7 @@ changeGate) {
|
|
|
581
612
|
* Every spelling first, then the resolved form against the branch's
|
|
582
613
|
* workspace, so a link into the folder is refused the same way. The resolved
|
|
583
614
|
* check only runs on a branch that is already cloned: bootstrapping a clone
|
|
584
|
-
* here would happen before the handler's access and
|
|
615
|
+
* here would happen before the handler's access and agent-access gates. A branch
|
|
585
616
|
* not cloned yet (or that does not resolve) is left to the handler; the
|
|
586
617
|
* filesystem refuses again underneath regardless.
|
|
587
618
|
*/
|
|
@@ -604,7 +635,7 @@ changeGate) {
|
|
|
604
635
|
* The branch's workspace root, to judge a spelling against what is on disk
|
|
605
636
|
* — or null when there is nothing to judge it against yet. Only a branch
|
|
606
637
|
* ALREADY cloned is used: bootstrapping one here would clone before the
|
|
607
|
-
* handler's access and
|
|
638
|
+
* handler's access and agent-access gates have had their say.
|
|
608
639
|
*/
|
|
609
640
|
const gitCheckRootFor = async (args, ctx) => {
|
|
610
641
|
if (typeof args.branch !== 'string' || args.branch === '')
|
|
@@ -671,6 +702,112 @@ changeGate) {
|
|
|
671
702
|
]);
|
|
672
703
|
return { read, write, download, owner };
|
|
673
704
|
};
|
|
705
|
+
/** Whether any one of the caller's four verdicts differs between the two sides of a preview. */
|
|
706
|
+
const verbsDiffer = (before, after) => Object.keys(before).some((v) => before[v] !== after[v]);
|
|
707
|
+
/**
|
|
708
|
+
* Why `copy_file` will not take a folder. One sentence, said by the dry
|
|
709
|
+
* run and by the call itself, so the preflight and the execution never
|
|
710
|
+
* disagree — the rule this whole section is built on.
|
|
711
|
+
*/
|
|
712
|
+
const folderCopyRefusal = (src) => `"${src}" is a folder; copy_file copies one file. Copy its files one by one, or move the folder with move_file.`;
|
|
713
|
+
/**
|
|
714
|
+
* The caller's verdicts at `dest` as they will be once `src` has been
|
|
715
|
+
* moved (or, with `sourceRemains`, copied) there — the `after` half of a
|
|
716
|
+
* move's or copy's preview.
|
|
717
|
+
*
|
|
718
|
+
* `accessAt(dest)` is the wrong answer to that question for a folder: the
|
|
719
|
+
* destination on disk has neither the folder nor the `access.md` files it
|
|
720
|
+
* carries, so it describes the destination's PARENT. A rename of a folder
|
|
721
|
+
* that names the caller owner in its own `access.md` therefore warned
|
|
722
|
+
* about losing owner access the move was about to hand straight back, and
|
|
723
|
+
* a warning that is wrong is a warning people learn to click through.
|
|
724
|
+
*
|
|
725
|
+
* Preview only, like everything else in this section: it answers what the
|
|
726
|
+
* caller WILL have, never whether they may do it. The write verdicts that
|
|
727
|
+
* gate the move are `writeBlocked` and the lock gate, both of which read
|
|
728
|
+
* the tree as it is.
|
|
729
|
+
*/
|
|
730
|
+
const accessAfter = async (branch, ctx, src, dest, opts) => {
|
|
731
|
+
const from = toKbRelative(src, kbDirName);
|
|
732
|
+
const to = toKbRelative(dest, kbDirName);
|
|
733
|
+
// Outside the repository there are no rules to carry, and none to land
|
|
734
|
+
// among — the same answer `accessAt` gives for such a path.
|
|
735
|
+
if (from === null || to === null)
|
|
736
|
+
return accessAt(branch, ctx, dest);
|
|
737
|
+
return accessControl.previewAccessAfterRelocation(workspaceIdForBranch(branch), ctx.user.email, from, to, opts);
|
|
738
|
+
};
|
|
739
|
+
/**
|
|
740
|
+
* What `copy_file`'s dry run answers: the same impact shape `move_file`
|
|
741
|
+
* previews, over a copy's own rules.
|
|
742
|
+
*
|
|
743
|
+
* A copy LEAVES the source where it is, so the rules it carries are
|
|
744
|
+
* duplicated rather than relocated (`sourceRemains`) — otherwise the two
|
|
745
|
+
* previews ask the same question. The order of the refusals is `copy_file`'s
|
|
746
|
+
* own and is load-bearing: the write verdict on the destination outranks
|
|
747
|
+
* "that name is taken", because a caller who may not write a folder must
|
|
748
|
+
* not learn what is in it from a refusal.
|
|
749
|
+
*
|
|
750
|
+
* A folder source is reported as the refusal it is. `copy_file` copies one
|
|
751
|
+
* file; the preview says so rather than promising a copy that would fail,
|
|
752
|
+
* and still answers `access.after` for the folder it was asked about.
|
|
753
|
+
*
|
|
754
|
+
* NOTHING is probed on disk until the write verdict on the destination has
|
|
755
|
+
* been taken — not the destination, and not the source either, which is the
|
|
756
|
+
* order the call itself keeps at length: a caller who may not write there
|
|
757
|
+
* gets the same refusal whether the source is a file, a folder, or missing
|
|
758
|
+
* altogether. Probing the source first put a 404 in front of that 403 and
|
|
759
|
+
* handed a denied caller the source's kind and its file count. So a refused
|
|
760
|
+
* preview answers `allowed: false` with the sentence and no `kind` or
|
|
761
|
+
* `descendants`: those are the half of the impact the caller has to have
|
|
762
|
+
* earned. The two `access` sides are the caller's own four verbs and tell
|
|
763
|
+
* them nothing they could not ask `file_stat` for.
|
|
764
|
+
*/
|
|
765
|
+
const copyImpact = async (branch, ctx, src, dest) => {
|
|
766
|
+
const [before, after, blocked] = await Promise.all([
|
|
767
|
+
accessAt(branch, ctx, src),
|
|
768
|
+
accessAfter(branch, ctx, src, dest, { sourceRemains: true }),
|
|
769
|
+
writeBlocked(branch, ctx, [dest]),
|
|
770
|
+
]);
|
|
771
|
+
const access = { before, after };
|
|
772
|
+
const accessChanges = verbsDiffer(before, after);
|
|
773
|
+
if (blocked.length > 0) {
|
|
774
|
+
return {
|
|
775
|
+
src,
|
|
776
|
+
dest,
|
|
777
|
+
access,
|
|
778
|
+
accessChanges,
|
|
779
|
+
allowed: false,
|
|
780
|
+
reason: `You may not write "${dest}", so the copy cannot run.`,
|
|
781
|
+
dryRun: true,
|
|
782
|
+
copied: false,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const fs = await ctx.getFilesystem(branch);
|
|
786
|
+
const kind = await kindOf(fs, src);
|
|
787
|
+
if (kind === null)
|
|
788
|
+
throw notFound(src, 'Nothing to copy');
|
|
789
|
+
const srcFiles = kind === 'folder' ? (await filesUnder(fs, src)).files : [src];
|
|
790
|
+
const occupiedBy = await existingAt(await workspaceRoot(branch, ctx), dest);
|
|
791
|
+
const reason = occupiedBy !== null
|
|
792
|
+
? entryExistsMessage(occupiedBy, dest)
|
|
793
|
+
: kind === 'folder'
|
|
794
|
+
? folderCopyRefusal(src)
|
|
795
|
+
: undefined;
|
|
796
|
+
return {
|
|
797
|
+
src,
|
|
798
|
+
dest,
|
|
799
|
+
kind,
|
|
800
|
+
// The placeholder travels with its folder, but it is never content —
|
|
801
|
+
// counted as `move_file` counts it.
|
|
802
|
+
descendants: srcFiles.filter((f) => !isFolderPlaceholder(f)).length,
|
|
803
|
+
access,
|
|
804
|
+
accessChanges,
|
|
805
|
+
allowed: reason === undefined,
|
|
806
|
+
...(reason !== undefined ? { reason } : {}),
|
|
807
|
+
dryRun: true,
|
|
808
|
+
copied: false,
|
|
809
|
+
};
|
|
810
|
+
};
|
|
674
811
|
/**
|
|
675
812
|
* The paths among `paths` the caller may NOT write, judged exactly as the
|
|
676
813
|
* lock gate judges them (`WorkflowService.acquireLock`): on a protected
|
|
@@ -950,10 +1087,17 @@ changeGate) {
|
|
|
950
1087
|
// tool the one content rule, and every tool a permission can refuse the
|
|
951
1088
|
// proposal route — appended once here so no tool (especially the
|
|
952
1089
|
// read-only ones a session hits first) can miss them.
|
|
1090
|
+
// Whether a call to this tool MUST name a branch, read off the tool's own
|
|
1091
|
+
// declaration rather than assumed of the family. Every tool mounted here
|
|
1092
|
+
// requires `branch` today; keying on the schema means a tool that declares
|
|
1093
|
+
// it optional (and resolves absence itself, as `list_tool_setup` does on its
|
|
1094
|
+
// own route) is not handed a refusal it never asked for.
|
|
1095
|
+
const requiresBranch = (spec.inputs.required ?? []).includes('branch');
|
|
953
1096
|
const describe = () => (typeof spec.description === 'function' ? spec.description() : spec.description) +
|
|
954
1097
|
(spec.proposable ? PROPOSAL_ROUTE_NOTE : '') +
|
|
955
1098
|
(spec.fileTool === false ? '' : CONTENT_RULE) +
|
|
956
|
-
kbConventionsNote(kb.layout)
|
|
1099
|
+
kbConventionsNote(kb.layout) +
|
|
1100
|
+
(spec.gated ? agentAccessGate.notes.gatedToolNote() : '');
|
|
957
1101
|
const def = toolDef({
|
|
958
1102
|
name: spec.name,
|
|
959
1103
|
description: describe(),
|
|
@@ -965,16 +1109,33 @@ changeGate) {
|
|
|
965
1109
|
registry.registerInternalTool(def);
|
|
966
1110
|
if (!spec.internalOnly)
|
|
967
1111
|
registry.registerExternalTool(def);
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1112
|
+
/**
|
|
1113
|
+
* What the agent reads about this tool, rebuilt from whatever is in effect
|
|
1114
|
+
* NOW: the layout's names and the notes the deployment registered.
|
|
1115
|
+
*
|
|
1116
|
+
* The catalog FOLLOWS the layout. The conventions reminder above names the
|
|
1117
|
+
* guide, and several descriptions name it again as a platform file, so the
|
|
1118
|
+
* save that completes first-run setup — which applies the names the admin
|
|
1119
|
+
* just chose, in that same request, without a restart — must be able to
|
|
1120
|
+
* move the text with them. Rewritten in place: the registry holds this
|
|
1121
|
+
* object, both surfaces hold the same one, and re-registering would be a
|
|
1122
|
+
* duplicate name. The `sessionId` input is rewritten on the DEF rather
|
|
1123
|
+
* than on `SESSION_ID_INPUT`, because `toolDef` copies the schema it is
|
|
1124
|
+
* given.
|
|
1125
|
+
*/
|
|
1126
|
+
const redescribe = () => {
|
|
976
1127
|
def.description = describe();
|
|
977
|
-
|
|
1128
|
+
const sessionId = sessionIdInputOf(def);
|
|
1129
|
+
if (sessionId)
|
|
1130
|
+
sessionId.description = agentAccessGate.notes.sessionIdDescription();
|
|
1131
|
+
};
|
|
1132
|
+
// Once for a note registered BEFORE the tools were mounted (the `sessionId`
|
|
1133
|
+
// input is copied by `toolDef`, so it carries the bare default until this
|
|
1134
|
+
// runs), and then on every later change: a note may be registered AFTER
|
|
1135
|
+
// the mount, from the tool-surface hook an overlay registers on.
|
|
1136
|
+
redescribe();
|
|
1137
|
+
kb.onLayoutApplied(redescribe);
|
|
1138
|
+
agentAccessGate.notes.onChange(redescribe);
|
|
978
1139
|
// Internal-only tools (e.g. `execute_command`) keep their route mounted —
|
|
979
1140
|
// our agent calls it over the same loopback — but gate it to internal-source
|
|
980
1141
|
// callers so an external connection key can't invoke it by name.
|
|
@@ -991,6 +1152,17 @@ changeGate) {
|
|
|
991
1152
|
// path, and exempting it there would be a workspace-relative path that
|
|
992
1153
|
// never reached the repository — the whole bug, spelled with a prefix.
|
|
993
1154
|
toolHandler(async (args, ctx) => {
|
|
1155
|
+
// FIRST, before the path work and before any handler: every tool
|
|
1156
|
+
// mounted here declares `branch` as a required, non-empty string, and
|
|
1157
|
+
// nothing enforced that, so a call that named none was carried down
|
|
1158
|
+
// until `workspaceIdForBranch` made a workspace directory out of the
|
|
1159
|
+
// missing value. Most of these tools would meet the same refusal one
|
|
1160
|
+
// layer down at `getFilesystem`, but not all of them do — `unzip`
|
|
1161
|
+
// hands `branch` straight to the workspace service by id — so the
|
|
1162
|
+
// check belongs on the mount every one of them shares rather than on
|
|
1163
|
+
// the resolver only some of them reach.
|
|
1164
|
+
if (requiresBranch && !spec.resolvesBranchItself)
|
|
1165
|
+
assertBranchProvided(args.branch);
|
|
994
1166
|
// BEFORE the normaliser: see `assertToolPathsNotGitInternals`.
|
|
995
1167
|
if (spec.fileTool !== false)
|
|
996
1168
|
await assertToolPathsNotGitInternals(args, ctx);
|
|
@@ -1021,22 +1193,20 @@ changeGate) {
|
|
|
1021
1193
|
}, { write: spec.write }));
|
|
1022
1194
|
};
|
|
1023
1195
|
// ── session bootstrap (external agents) ─────────────────────────────────
|
|
1024
|
-
// Every read/write tool below
|
|
1025
|
-
//
|
|
1026
|
-
// agent
|
|
1027
|
-
//
|
|
1028
|
-
// it onto every later
|
|
1196
|
+
// Every read/write tool below takes a `sessionId`: the conversation the
|
|
1197
|
+
// call belongs to, which is what a deployment's hooks scope their rule to.
|
|
1198
|
+
// The in-process agent carries its thread id, but an external agent has no
|
|
1199
|
+
// ambient run id, so this mints one up front (called ONCE); the MCP proxy
|
|
1200
|
+
// then threads it onto every later call via its sessionId-output continuity
|
|
1029
1201
|
// convention. EXTERNAL-ONLY (not registered internal): the in-process agent
|
|
1030
1202
|
// already supplies its session id and ignores any body value.
|
|
1031
1203
|
//
|
|
1032
1204
|
// WHAT the minted id is backed by is the `ISessionSink` port's business
|
|
1033
1205
|
// (session-sink.ts). In the enterprise app it is a REAL chat-thread id, so
|
|
1034
|
-
// the SAME id works end to end:
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
// deployment (no chat/ask) the default sink mints a bare id, which is all
|
|
1039
|
-
// the ontology gate needs.
|
|
1206
|
+
// the SAME id works end to end: the file tools take it AND `ask` accepts it
|
|
1207
|
+
// (its sessionId IS a chat thread, resolved via getThread), so a run's reads
|
|
1208
|
+
// and its questions are one conversation rather than two. In a core-only
|
|
1209
|
+
// deployment (no chat/ask) the default sink mints a bare id.
|
|
1040
1210
|
//
|
|
1041
1211
|
// The description tells the caller that retrying is safe, and that is a
|
|
1042
1212
|
// property of the sink rather than a promise this route makes on its own:
|
|
@@ -1048,7 +1218,7 @@ changeGate) {
|
|
|
1048
1218
|
// transport hiccup on its first call as an unrecoverable start.
|
|
1049
1219
|
const startSessionDef = toolDef({
|
|
1050
1220
|
name: 'start_session',
|
|
1051
|
-
description: 'Mint the
|
|
1221
|
+
description: 'Mint the id of this conversation, which the KnowledgeBase tools take as `sessionId`. Call this ONCE, at the start of your work and only once per run — minting a new id mid-run starts a second conversation as far as the server is concerned. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: your reads and your questions are then one conversation. Pass the returned id explicitly as `sessionId` on every subsequent KnowledgeBase tool call (direct MCP calls and inside `call_tool_chain` alike). RETRYING IS SAFE: a call that fails created nothing, so retry it — there is no half-made session to clean up. If a retry lands after a success you simply hold two independent ids, which is harmless: keep passing the one id you have already used for the rest of the run and ignore the other. Returns `{ sessionId }`.',
|
|
1052
1222
|
path: '/api/agent/tools/start_session',
|
|
1053
1223
|
inputs: { type: 'object', properties: {}, additionalProperties: false },
|
|
1054
1224
|
outputs: {
|
|
@@ -1060,10 +1230,10 @@ changeGate) {
|
|
|
1060
1230
|
});
|
|
1061
1231
|
registry.registerExternalTool(startSessionDef);
|
|
1062
1232
|
// Mint the session id via the sink and return it (see comment above: one id
|
|
1063
|
-
// spans start_session -> reads -> ask
|
|
1233
|
+
// spans start_session -> reads -> ask).
|
|
1064
1234
|
router.post('/agent/tools/start_session', toolAuth,
|
|
1065
1235
|
// External-only: an internal token already carries its run's sessionId, so
|
|
1066
|
-
// minting a new thread mid-run would
|
|
1236
|
+
// minting a new thread mid-run would split one run in two. Note
|
|
1067
1237
|
// "external" includes the MCP proxy's `externalProxy` loopback tokens
|
|
1068
1238
|
// (OAuth/JWT MCP sessions) — the verifier resolves those to
|
|
1069
1239
|
// `source: 'external'`, and one such session may legitimately mint several
|
|
@@ -1075,8 +1245,8 @@ changeGate) {
|
|
|
1075
1245
|
// ── reads ──────────────────────────────────────────────────────────────
|
|
1076
1246
|
mount({
|
|
1077
1247
|
name: 'read_file',
|
|
1078
|
-
|
|
1079
|
-
|
|
1248
|
+
gated: true,
|
|
1249
|
+
description: 'Read a workspace file as text. Returns `{ path, content }`. Images (.png/.jpg/.jpeg/.gif/.webp) return the IMAGE ITSELF as native MCP image content (plus a one-line text note naming the file), so you can look at the picture — up to 3.5 MB of raw image data; a larger image gets an honest refusal asking for a locally downscaled copy or a smaller export (`.svg` is text and reads as text). Images come back only on a DIRECT call: inside `call_tool_chain` an image read yields an `{ image_omitted, note }` stub instead. Office and OpenDocument files (.docx/.pptx/.xlsx, .odt/.odp/.ods) and PDFs return their EXTRACTED text under an honest `[extracted text of …]` header, with `[slide N]`/`[sheet: Name]`/`[page N]` markers — the extraction is READ-ONLY (layout/images omitted; such files cannot be edited as text, only replaced by uploading a new version). Email files (.eml/.msg) return their EXTRACTED text the same way: a `[from]`/`[to]`/`[subject]`/`[date]` header block, the body (plain-text part preferred; an HTML-only body is stripped to text), and an `[attachments]` name list — attachments are listed, never extracted. Other binary files return a one-line description instead of raw bytes. Optional `offset`/`limit` slice the content (characters for a file, bytes for a `__tool_chain_spill__/…` ref; ignored for an image) — use them to page through large files or a `call_tool_chain` spill rather than reading multi-MB in full. A spill ref is workspace-independent: `branch` is ignored for it.',
|
|
1080
1250
|
inputs: {
|
|
1081
1251
|
type: 'object',
|
|
1082
1252
|
properties: {
|
|
@@ -1102,12 +1272,12 @@ changeGate) {
|
|
|
1102
1272
|
if (spillStore.isSpillRef(p)) {
|
|
1103
1273
|
return { path: p, content: await spillStore.read(p, offset, limit) };
|
|
1104
1274
|
}
|
|
1105
|
-
await
|
|
1275
|
+
await notifyAgentRead(agentAccessGate, ctx, a.branch, p);
|
|
1106
1276
|
await assertCanRead(readGateFor(a.branch, ctx), p);
|
|
1107
1277
|
const fs = await ctx.getFilesystem(a.branch);
|
|
1108
1278
|
// Reading (extraction, image and binary handling included) happens AFTER
|
|
1109
|
-
// the access gate and the
|
|
1110
|
-
//
|
|
1279
|
+
// the access gate and the read hook above — a document read is still a
|
|
1280
|
+
// KB read. ONE registry dispatch picks the reader by
|
|
1111
1281
|
// extension; everything below just maps its ReadResult onto the tool's
|
|
1112
1282
|
// result shape.
|
|
1113
1283
|
const bytes = await orNotFound(p, async () => asBytes(await fs.readFile(p)));
|
|
@@ -1139,8 +1309,8 @@ changeGate) {
|
|
|
1139
1309
|
});
|
|
1140
1310
|
mount({
|
|
1141
1311
|
name: 'list_files',
|
|
1142
|
-
|
|
1143
|
-
|
|
1312
|
+
gated: true,
|
|
1313
|
+
description: `List a directory. Returns \`{ path, entries: [{ name, type, size? }] }\`. Omit \`path\` for the workspace root, which holds the repository as the \`${kbDirName}/\` folder: every content path is under it (e.g. \`${kbDirName}/KnowledgeBase\`), and a path given without that prefix is placed under it.`,
|
|
1144
1314
|
inputs: {
|
|
1145
1315
|
type: 'object',
|
|
1146
1316
|
properties: {
|
|
@@ -1170,7 +1340,7 @@ changeGate) {
|
|
|
1170
1340
|
write: false,
|
|
1171
1341
|
handler: async (a, ctx) => {
|
|
1172
1342
|
const dir = a.path || '';
|
|
1173
|
-
await
|
|
1343
|
+
await notifyAgentRead(agentAccessGate, ctx, a.branch, dir);
|
|
1174
1344
|
const fs = await ctx.getFilesystem(a.branch);
|
|
1175
1345
|
const entries = withoutPlaceholder((await fs.readdir(dir || '.')));
|
|
1176
1346
|
const filtered = await filterReadableEntries(readGateFor(a.branch, ctx), dir, entries);
|
|
@@ -1179,13 +1349,13 @@ changeGate) {
|
|
|
1179
1349
|
});
|
|
1180
1350
|
mount({
|
|
1181
1351
|
name: 'file_stat',
|
|
1352
|
+
gated: true,
|
|
1182
1353
|
description: () => 'Get a file/directory\'s metadata (name, type, size, …) without returning content. A file also reports `contentMode`: `text` (read, write and edit it as text), `document` (read returns an extraction; replace it by upload) or `binary` (bytes: copy, move, delete, or replace by upload), plus `kind` (`text` | `document` | `image` | `binary`), `mime`, `mimeSource` and `textEditable` — decided by the same file readers read_file, grep and the write tools use, so an extensionless text file is `text/plain`.' +
|
|
1183
1354
|
' Every entry also reports what you may DO with it. ' +
|
|
1184
1355
|
`\`managed\` is true for a platform item — a platform file (${platformFileList(kb.layout)}) or a platform folder (the repository root or a reserved root folder such as \`KnowledgeBase/\`); managed items are never movable or deletable through these tools. ` +
|
|
1185
1356
|
'`access: { read, write, download, owner }` is your own verdict under the access rules; pass `explainAccess: true` to learn why, and who else holds each verb. `movable` and `deletable` say whether `move_file` / `delete_file` / `delete_folder` would be allowed for you, judged like their dry runs: not managed, no symbolic link, and on a protected branch you hold write on the item AND on every file under a folder (on a draft branch writes are not gated). `movable` judges the source side only; the destination is judged by a `move_file` dry run. ' +
|
|
1186
1357
|
'For a folder, `descendants` is the number of files under it at any depth; counting stops at 10000 and `descendantsTruncated` says so, and past that point `movable` and `deletable` are false because a folder that large was not judged in full — run the `move_file` or `delete_folder` dry run for the real verdict. ' +
|
|
1187
|
-
'Call this before a move or delete to see what it would touch.'
|
|
1188
|
-
ONTOLOGY_BOUNDARY_NOTE,
|
|
1358
|
+
'Call this before a move or delete to see what it would touch.',
|
|
1189
1359
|
inputs: {
|
|
1190
1360
|
type: 'object',
|
|
1191
1361
|
properties: {
|
|
@@ -1258,7 +1428,7 @@ changeGate) {
|
|
|
1258
1428
|
handler: async (a, ctx) => {
|
|
1259
1429
|
const p = a.path;
|
|
1260
1430
|
const branch = a.branch;
|
|
1261
|
-
await
|
|
1431
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, p);
|
|
1262
1432
|
await assertCanRead(readGateFor(branch, ctx), p);
|
|
1263
1433
|
// Nothing there is a 404, and the placeholder — never content — gets
|
|
1264
1434
|
// exactly that answer: the one every file tool gives (see not-found.ts).
|
|
@@ -1360,8 +1530,8 @@ changeGate) {
|
|
|
1360
1530
|
});
|
|
1361
1531
|
mount({
|
|
1362
1532
|
name: 'grep',
|
|
1363
|
-
|
|
1364
|
-
|
|
1533
|
+
gated: true,
|
|
1534
|
+
description: 'Regex content search across the workspace. Returns `{ matches: [{ path, line, text }] }` (capped). Use to find where something is defined/referenced. `path` may name a DIRECTORY (searches the subtree) or a single FILE (searches just that file); a path with nothing at it is an error, never an empty result. Searches INSIDE Office and OpenDocument files (.docx/.pptx/.xlsx, .odt/.odp/.ods), PDFs and email files (.eml/.msg) via their extracted text — matches there carry the extraction\'s line numbers, and the `[slide N]`/`[sheet: Name]`/`[page N]`/`[from]`/`[subject]` marker lines locate them; a bounded number of not-yet-extracted documents is extracted per call, and the result notes how many were skipped (re-run to cover them).',
|
|
1365
1535
|
inputs: {
|
|
1366
1536
|
type: 'object',
|
|
1367
1537
|
properties: {
|
|
@@ -1412,11 +1582,10 @@ changeGate) {
|
|
|
1412
1582
|
// (an empty path is the handler's to explain), and here it would
|
|
1413
1583
|
// otherwise name the workspace directory by another spelling.
|
|
1414
1584
|
const searchRoot = typeof a.path === 'string' && a.path.length > 0 ? a.path : kbDirName;
|
|
1415
|
-
// The search root itself
|
|
1416
|
-
//
|
|
1417
|
-
//
|
|
1418
|
-
|
|
1419
|
-
await recordOntologyRead(sessionOntologyGate, ctx, searchRoot);
|
|
1585
|
+
// The search root itself goes to the read hook here; each file the walk
|
|
1586
|
+
// actually opens goes to it per-file below, so a hook sees every path a
|
|
1587
|
+
// grep reached rather than only the root it started from.
|
|
1588
|
+
await notifyAgentRead(agentAccessGate, ctx, a.branch, searchRoot);
|
|
1420
1589
|
const fs = await ctx.getFilesystem(a.branch);
|
|
1421
1590
|
const gate = readGateFor(a.branch, ctx);
|
|
1422
1591
|
const out = [];
|
|
@@ -1434,7 +1603,7 @@ changeGate) {
|
|
|
1434
1603
|
/** Why a single-file search found nothing, when "no matches" would be a lie. */
|
|
1435
1604
|
let fileNote;
|
|
1436
1605
|
if (kind === 'directory') {
|
|
1437
|
-
await grepWalk(fs, searchRoot, re, out, max, 0, gate, (p) =>
|
|
1606
|
+
await grepWalk(fs, searchRoot, re, out, max, 0, gate, (p) => notifyAgentRead(agentAccessGate, ctx, a.branch, p), docs);
|
|
1438
1607
|
}
|
|
1439
1608
|
else {
|
|
1440
1609
|
// Not a directory: the permission verdict comes BEFORE every other
|
|
@@ -1473,11 +1642,12 @@ changeGate) {
|
|
|
1473
1642
|
// ── writes (through the lock/commit pipeline) ───────────────────────────
|
|
1474
1643
|
mount({
|
|
1475
1644
|
name: 'write_file',
|
|
1645
|
+
gated: true,
|
|
1476
1646
|
description: 'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
|
|
1477
1647
|
'`created`, `replaced` or `updated`.' +
|
|
1478
1648
|
WRITE_MODE_NOTE +
|
|
1479
1649
|
IMAGE_CONVENTION_NOTE +
|
|
1480
|
-
|
|
1650
|
+
ESCAPE_SEQUENCE_NOTE,
|
|
1481
1651
|
inputs: {
|
|
1482
1652
|
type: 'object',
|
|
1483
1653
|
properties: {
|
|
@@ -1513,7 +1683,7 @@ changeGate) {
|
|
|
1513
1683
|
// (today only `watchlist_check`, to `.html`). Unrestricted sessions pass straight
|
|
1514
1684
|
// through (see `assertPathWritable`), so it does not limit other agents.
|
|
1515
1685
|
writePolicy.assertPathWritable(ctx.sessionId, a.path);
|
|
1516
|
-
await
|
|
1686
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, a.branch, a.path);
|
|
1517
1687
|
const mode = modeOf(a);
|
|
1518
1688
|
const fs = await ctx.getFilesystem(a.branch);
|
|
1519
1689
|
await assertNotBinaryOverwrite(readers, a.path, fs);
|
|
@@ -1546,17 +1716,18 @@ changeGate) {
|
|
|
1546
1716
|
});
|
|
1547
1717
|
mount({
|
|
1548
1718
|
name: 'write_files',
|
|
1719
|
+
gated: true,
|
|
1549
1720
|
description: 'Batch-write many files in ONE commit — far faster than calling write_file once per file when ' +
|
|
1550
1721
|
'creating many files at once (e.g. seeding a knowledge base). Each entry is `{ path, content }`, and the files it ' +
|
|
1551
1722
|
'writes are committed + pushed together as you. Prefer this over many write_file ' +
|
|
1552
|
-
'calls.
|
|
1723
|
+
'calls. Text files only. ' +
|
|
1553
1724
|
'Returns `{ count, files }`: one entry per REQUESTED path, in the order you gave them, each `{ path, outcome }` — ' +
|
|
1554
1725
|
'`created` / `replaced` / `updated` for a path it wrote, or `refused` with `error` (the code) and `message` (why) for a ' +
|
|
1555
1726
|
'path it could not. `count` is how many were written. A path it refuses — the mode said no, or the file is not text — ' +
|
|
1556
1727
|
'does not stop the others; read `files` to see what landed.' +
|
|
1557
1728
|
WRITE_MODE_NOTE +
|
|
1558
1729
|
IMAGE_CONVENTION_NOTE +
|
|
1559
|
-
|
|
1730
|
+
ESCAPE_SEQUENCE_NOTE,
|
|
1560
1731
|
inputs: {
|
|
1561
1732
|
type: 'object',
|
|
1562
1733
|
properties: {
|
|
@@ -1610,15 +1781,14 @@ changeGate) {
|
|
|
1610
1781
|
if (files.length === 0)
|
|
1611
1782
|
return { count: 0, files: [] };
|
|
1612
1783
|
const mode = modeOf(a);
|
|
1613
|
-
// The POLICY
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
//
|
|
1617
|
-
// path already holds (the mode) is
|
|
1784
|
+
// The POLICY gate still judges the whole batch: a restricted run is a
|
|
1785
|
+
// call that should not have been made at all, not a per-path outcome.
|
|
1786
|
+
// The write hook is asked PER PATH, below, so a path it refuses is that
|
|
1787
|
+
// path's outcome and the rest of the batch still lands. What a single
|
|
1788
|
+
// FILE is (not text) or what its path already holds (the mode) is
|
|
1789
|
+
// decided per path too.
|
|
1618
1790
|
for (const f of files)
|
|
1619
1791
|
writePolicy.assertPathWritable(ctx.sessionId, f.path);
|
|
1620
|
-
for (const f of files)
|
|
1621
|
-
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, f.path);
|
|
1622
1792
|
const fs = await ctx.getFilesystem(a.branch);
|
|
1623
1793
|
// `write: true` guarantees a LockingFilesystem here; `writeFiles` lands the
|
|
1624
1794
|
// batch as one commit. Structural cast avoids a workflow-internal import.
|
|
@@ -1639,6 +1809,19 @@ changeGate) {
|
|
|
1639
1809
|
for (const f of files) {
|
|
1640
1810
|
const entry = { path: f.path };
|
|
1641
1811
|
outcomes.push(entry);
|
|
1812
|
+
try {
|
|
1813
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, a.branch, f.path);
|
|
1814
|
+
}
|
|
1815
|
+
catch (err) {
|
|
1816
|
+
// A DELIBERATE refusal by the deployment's write hook is this path's
|
|
1817
|
+
// outcome and no more: its message is what the caller is meant to
|
|
1818
|
+
// read, and one refused path must not take the others down. Anything
|
|
1819
|
+
// else the hook throws is not a verdict — it is the gate itself
|
|
1820
|
+
// failing — so `refuse` rethrows it and the whole batch fails loudly,
|
|
1821
|
+
// exactly as it does in `write_file`.
|
|
1822
|
+
refuse(entry, err);
|
|
1823
|
+
continue;
|
|
1824
|
+
}
|
|
1642
1825
|
try {
|
|
1643
1826
|
assertNotDocumentEdit(readers, f.path);
|
|
1644
1827
|
await assertNotBinaryOverwrite(readers, f.path, fs);
|
|
@@ -1706,8 +1889,9 @@ changeGate) {
|
|
|
1706
1889
|
});
|
|
1707
1890
|
mount({
|
|
1708
1891
|
name: 'edit_file',
|
|
1892
|
+
gated: true,
|
|
1709
1893
|
description: 'Replace an exact string in a workspace TEXT file. `old_string` must appear exactly once unless `replace_all`. Committed + pushed as you.' +
|
|
1710
|
-
|
|
1894
|
+
ESCAPE_SEQUENCE_NOTE,
|
|
1711
1895
|
inputs: {
|
|
1712
1896
|
type: 'object',
|
|
1713
1897
|
properties: {
|
|
@@ -1731,7 +1915,7 @@ changeGate) {
|
|
|
1731
1915
|
handler: async (a, ctx) => {
|
|
1732
1916
|
assertNotDocumentEdit(readers, a.path);
|
|
1733
1917
|
writePolicy.assertPathWritable(ctx.sessionId, a.path);
|
|
1734
|
-
await
|
|
1918
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, a.branch, a.path);
|
|
1735
1919
|
const fs = await ctx.getFilesystem(a.branch);
|
|
1736
1920
|
const path = a.path;
|
|
1737
1921
|
const oldStr = a.old_string;
|
|
@@ -1755,9 +1939,9 @@ changeGate) {
|
|
|
1755
1939
|
});
|
|
1756
1940
|
mount({
|
|
1757
1941
|
name: 'delete_file',
|
|
1942
|
+
gated: true,
|
|
1758
1943
|
description: () => 'Delete ONE workspace file (a symbolic link is refused: links are never followed or removed). Committed + pushed as you. Its folder stays, even when this was its last file. Files only: a folder is refused with a pointer to `delete_folder`. ' +
|
|
1759
|
-
`A platform file (\`access.md\` or \`.bevelignore\` in any folder, \`roles.yaml\` or \`${kb.layout.agentsFile}\` at the repository root) and git metadata are refused
|
|
1760
|
-
ONTOLOGY_BOUNDARY_NOTE,
|
|
1944
|
+
`A platform file (\`access.md\` or \`.bevelignore\` in any folder, \`roles.yaml\` or \`${kb.layout.agentsFile}\` at the repository root) and git metadata are refused.`,
|
|
1761
1945
|
inputs: {
|
|
1762
1946
|
type: 'object',
|
|
1763
1947
|
properties: {
|
|
@@ -1776,14 +1960,14 @@ changeGate) {
|
|
|
1776
1960
|
write: true,
|
|
1777
1961
|
proposable: true,
|
|
1778
1962
|
handler: async (a, ctx) => {
|
|
1779
|
-
// A delete
|
|
1780
|
-
//
|
|
1781
|
-
//
|
|
1782
|
-
//
|
|
1963
|
+
// A delete carries no bytes from anywhere else — it removes a node — so
|
|
1964
|
+
// it goes to the READ hook, like a read, not the write hook. The
|
|
1965
|
+
// extension policy DOES apply though: a dashboard-only run must not
|
|
1966
|
+
// delete graph `.md` nodes.
|
|
1783
1967
|
const path = a.path;
|
|
1784
1968
|
const branch = a.branch;
|
|
1785
1969
|
writePolicy.assertPathWritable(ctx.sessionId, path);
|
|
1786
|
-
await
|
|
1970
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, path);
|
|
1787
1971
|
const fs = await ctx.getFilesystem(branch);
|
|
1788
1972
|
assertPlainPath(path);
|
|
1789
1973
|
const root = await workspaceRoot(branch, ctx);
|
|
@@ -1809,12 +1993,12 @@ changeGate) {
|
|
|
1809
1993
|
});
|
|
1810
1994
|
mount({
|
|
1811
1995
|
name: 'delete_folder',
|
|
1996
|
+
gated: true,
|
|
1812
1997
|
description: 'Delete a workspace FOLDER and every file under it, at any depth; the whole folder lands as ONE committed + pushed change as you — all of it or none of it — then the empty folder is removed. This is the one way a folder goes away: the folder that held it stays, even if this was all it had, and a folder holding nothing but its empty-folder placeholder counts as empty. ' +
|
|
1813
1998
|
'Preflight first: `dryRun: true` changes nothing and answers `{ path, kind: "folder", descendants, files, filesTruncated, allowed, reason? }` — `descendants` is the file count, `files` names up to 100 of them. ' +
|
|
1814
1999
|
'A non-empty folder is deleted only with `confirm: true`; without it the call deletes nothing and returns the same impact with `confirmationRequired: true`. Do NOT set `confirm: true` on your first call — dry-run, check the impact, then confirm. ' +
|
|
1815
2000
|
'Refused (in a dry run as `allowed: false` with the `reason`): a platform folder (the repository root or a reserved root folder such as `KnowledgeBase/`), git metadata, a folder holding a symbolic link (links are never removed), and a folder holding any file you may not write. A path that is a file is refused with a pointer to `delete_file`, and a path through a symbolic link is refused (links are never followed). ' +
|
|
1816
|
-
'The folder\'s own platform files (`access.md`, `.bevelignore`) go with it in that same one change, so its files are never left ungoverned part-way; you must be able to write those platform files too.'
|
|
1817
|
-
ONTOLOGY_BOUNDARY_NOTE,
|
|
2001
|
+
'The folder\'s own platform files (`access.md`, `.bevelignore`) go with it in that same one change, so its files are never left ungoverned part-way; you must be able to write those platform files too.',
|
|
1818
2002
|
inputs: {
|
|
1819
2003
|
type: 'object',
|
|
1820
2004
|
properties: {
|
|
@@ -1852,7 +2036,7 @@ changeGate) {
|
|
|
1852
2036
|
// The normaliser has already placed the path inside the repository; this
|
|
1853
2037
|
// is the check that it really is in there before a folder is walked.
|
|
1854
2038
|
assertInsideRepo(path, kbDirName);
|
|
1855
|
-
await
|
|
2039
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, path);
|
|
1856
2040
|
const fs = await ctx.getFilesystem(branch);
|
|
1857
2041
|
const kind = await kindOf(fs, path);
|
|
1858
2042
|
if (kind === null)
|
|
@@ -1946,7 +2130,8 @@ changeGate) {
|
|
|
1946
2130
|
});
|
|
1947
2131
|
mount({
|
|
1948
2132
|
name: 'mkdir',
|
|
1949
|
-
|
|
2133
|
+
gated: true,
|
|
2134
|
+
description: 'Create a directory (recursive). It lists as an empty folder and persists in git until it is deleted explicitly.',
|
|
1950
2135
|
inputs: {
|
|
1951
2136
|
type: 'object',
|
|
1952
2137
|
properties: {
|
|
@@ -1966,18 +2151,18 @@ changeGate) {
|
|
|
1966
2151
|
proposable: true,
|
|
1967
2152
|
handler: async (a, ctx) => {
|
|
1968
2153
|
writePolicy.assertPathWritable(ctx.sessionId, a.path);
|
|
1969
|
-
await
|
|
2154
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, a.branch, a.path);
|
|
1970
2155
|
await (await ctx.getFilesystem(a.branch)).mkdir(a.path, { recursive: true });
|
|
1971
2156
|
return { path: a.path, created: true };
|
|
1972
2157
|
},
|
|
1973
2158
|
});
|
|
1974
2159
|
mount({
|
|
1975
2160
|
name: 'move_file',
|
|
2161
|
+
gated: true,
|
|
1976
2162
|
description: () => 'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into. Lands as a delete + create, committed + pushed as you. ' +
|
|
1977
2163
|
`Rules: the destination must not exist — a move never overwrites a file or merges into a folder; a platform file (\`access.md\` or \`.bevelignore\` in any folder, \`roles.yaml\` or \`${kb.layout.agentsFile}\` at the repository root) is refused with "<name> is a platform file and stays in its folder." — a folder that moves takes its own platform files along, still in their folder; a platform folder (the repository root or a reserved root folder such as \`KnowledgeBase/\`) and git metadata are refused; a move cannot create a platform file or folder at \`dest\` either (renaming a note to \`access.md\` is refused); a path through a symbolic link is refused, since links are never followed; on a protected branch you must be able to write both ends — for a folder, every file under it at its old and its new path. ` +
|
|
1978
|
-
'Access follows the destination folder. Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination. ' +
|
|
1979
|
-
'A move whose `accessChanges` is true runs only with `confirm: true`; without it the call moves nothing and returns the same impact with `confirmationRequired: true`. Do NOT set `confirm: true` on your first call — dry-run, check the impact, then confirm.'
|
|
1980
|
-
ONTOLOGY_BOUNDARY_NOTE,
|
|
2164
|
+
'Access follows the destination folder. Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the move has landed, with every `access.md` inside a moved folder counted at its new place. ' +
|
|
2165
|
+
'A move whose `accessChanges` is true runs only with `confirm: true`; without it the call moves nothing and returns the same impact with `confirmationRequired: true`. Do NOT set `confirm: true` on your first call — dry-run, check the impact, then confirm.',
|
|
1981
2166
|
inputs: {
|
|
1982
2167
|
type: 'object',
|
|
1983
2168
|
properties: {
|
|
@@ -1998,8 +2183,8 @@ changeGate) {
|
|
|
1998
2183
|
dest: str('Destination path (echoes the input).'),
|
|
1999
2184
|
kind: str('`file` or `folder`.'),
|
|
2000
2185
|
descendants: int('Files that move: 1 for a file, the file count under a folder.'),
|
|
2001
|
-
access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and destination (`after`).' },
|
|
2002
|
-
accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between
|
|
2186
|
+
access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`).' },
|
|
2187
|
+
accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between `before` and `after`.' },
|
|
2003
2188
|
allowed: { type: 'boolean', description: 'Whether the move may run.' },
|
|
2004
2189
|
reason: str('Why it may not, when `allowed` is false.'),
|
|
2005
2190
|
dryRun: { type: 'boolean', description: 'True on a dry run.' },
|
|
@@ -2017,12 +2202,12 @@ changeGate) {
|
|
|
2017
2202
|
const src = a.src.replace(/\/+$/, '');
|
|
2018
2203
|
const dest = a.dest.replace(/\/+$/, '');
|
|
2019
2204
|
const branch = a.branch;
|
|
2020
|
-
// A move CARRIES the source content into the destination
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2023
|
-
//
|
|
2024
|
-
await
|
|
2025
|
-
await
|
|
2205
|
+
// A move CARRIES the source content into the destination, so BOTH ends
|
|
2206
|
+
// go to the write hook (unlike a plain delete, which moves no content).
|
|
2207
|
+
// Ask about both BEFORE touching disk, so a refused end can't leave the
|
|
2208
|
+
// source already deleted.
|
|
2209
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, src);
|
|
2210
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, dest);
|
|
2026
2211
|
const fs = await ctx.getFilesystem(branch);
|
|
2027
2212
|
const root = await workspaceRoot(branch, ctx);
|
|
2028
2213
|
// Before the kind check, which stats THROUGH a link: a dangling link at
|
|
@@ -2039,8 +2224,13 @@ changeGate) {
|
|
|
2039
2224
|
writePolicy.assertPathWritable(ctx.sessionId, f);
|
|
2040
2225
|
writePolicy.assertPathWritable(ctx.sessionId, dest + f.slice(src.length));
|
|
2041
2226
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2227
|
+
// `after` is the destination as it WILL be — with the `access.md` files
|
|
2228
|
+
// under `src` counted where they land. See `accessAfter`.
|
|
2229
|
+
const [before, after] = await Promise.all([
|
|
2230
|
+
accessAt(branch, ctx, src),
|
|
2231
|
+
accessAfter(branch, ctx, src, dest),
|
|
2232
|
+
]);
|
|
2233
|
+
const accessChanges = verbsDiffer(before, after);
|
|
2044
2234
|
// The placeholder moves with its folder, but it is never content.
|
|
2045
2235
|
const descendants = srcFiles.filter((f) => !isFolderPlaceholder(f)).length;
|
|
2046
2236
|
// Neither end may be the platform's own: a move neither takes a platform
|
|
@@ -2135,14 +2325,17 @@ changeGate) {
|
|
|
2135
2325
|
});
|
|
2136
2326
|
mount({
|
|
2137
2327
|
name: 'copy_file',
|
|
2138
|
-
|
|
2139
|
-
|
|
2328
|
+
gated: true,
|
|
2329
|
+
description: 'Copy a workspace FILE to a new path. The destination must not exist — like a move, a copy never overwrites a file or a folder; to change what is in a file that already exists, write it. Committed + pushed as you. ' +
|
|
2330
|
+
'Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the copy has landed, with every `access.md` inside a copied folder counted at its new place. ' +
|
|
2331
|
+
'One exception to that shape: when the destination is one you may not write, the answer is the refusal alone — `allowed: false` with `reason`, and no `kind` and no `descendants`, because nothing about the source is read before that verdict.',
|
|
2140
2332
|
inputs: {
|
|
2141
2333
|
type: 'object',
|
|
2142
2334
|
properties: {
|
|
2143
2335
|
branch: BRANCH_INPUT,
|
|
2144
2336
|
src: wsPath(kbDirName, 'Source path'),
|
|
2145
2337
|
dest: wsPath(kbDirName, 'Destination path — must not exist yet'),
|
|
2338
|
+
dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
|
|
2146
2339
|
sessionId: SESSION_ID_INPUT,
|
|
2147
2340
|
},
|
|
2148
2341
|
required: ['branch', 'src', 'dest'],
|
|
@@ -2150,20 +2343,30 @@ changeGate) {
|
|
|
2150
2343
|
},
|
|
2151
2344
|
outputs: {
|
|
2152
2345
|
type: 'object',
|
|
2153
|
-
properties: {
|
|
2346
|
+
properties: {
|
|
2347
|
+
src: str('Source path (echoes the input).'),
|
|
2348
|
+
dest: str('Destination path (echoes the input).'),
|
|
2349
|
+
kind: str('`file` or `folder` (dry run only; absent when `allowed` is false because you may not write the destination).'),
|
|
2350
|
+
descendants: int('Files the copy would carry: 1 for a file, the file count under a folder (dry run only; absent when `allowed` is false because you may not write the destination).'),
|
|
2351
|
+
access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the copy has landed (`after`) — dry run only.' },
|
|
2352
|
+
accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between `before` and `after` (dry run only).' },
|
|
2353
|
+
allowed: { type: 'boolean', description: 'Whether the copy may run (dry run only).' },
|
|
2354
|
+
reason: str('Why it may not, when `allowed` is false.'),
|
|
2355
|
+
dryRun: { type: 'boolean', description: 'True on a dry run.' },
|
|
2356
|
+
copied: { type: 'boolean', description: 'True once the copy landed; false on a dry run.' },
|
|
2357
|
+
},
|
|
2154
2358
|
required: ['src', 'dest', 'copied'],
|
|
2155
2359
|
},
|
|
2156
2360
|
write: true,
|
|
2157
2361
|
proposable: true,
|
|
2158
2362
|
handler: async (a, ctx) => {
|
|
2159
|
-
// A copy CARRIES the source content into the destination
|
|
2160
|
-
//
|
|
2161
|
-
// Check both before touching disk.
|
|
2363
|
+
// A copy CARRIES the source content into the destination, so BOTH ends
|
|
2364
|
+
// go to the write hook. Ask about both before touching disk.
|
|
2162
2365
|
writePolicy.assertPathWritable(ctx.sessionId, a.src);
|
|
2163
2366
|
writePolicy.assertPathWritable(ctx.sessionId, a.dest);
|
|
2164
|
-
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, a.src);
|
|
2165
|
-
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, a.dest);
|
|
2166
2367
|
const branch = a.branch;
|
|
2368
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.src);
|
|
2369
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.dest);
|
|
2167
2370
|
const src = a.src;
|
|
2168
2371
|
const dest = a.dest;
|
|
2169
2372
|
// A copy lands bytes at a name of its own, so it is refused by the same
|
|
@@ -2175,6 +2378,8 @@ changeGate) {
|
|
|
2175
2378
|
// own containment check: a path with a `..` segment must not reach
|
|
2176
2379
|
// `lstat` outside the workspace, even to be told a name is taken.
|
|
2177
2380
|
assertPlainPath(dest);
|
|
2381
|
+
if (a.dryRun === true)
|
|
2382
|
+
return copyImpact(branch, ctx, src, dest);
|
|
2178
2383
|
// The write verdict comes FIRST, for the reason `move_file` gives at
|
|
2179
2384
|
// length: "already exists" is a fact about the destination folder, and a
|
|
2180
2385
|
// caller who may not write there must not be told it. The lock gate
|
|
@@ -2214,6 +2419,12 @@ changeGate) {
|
|
|
2214
2419
|
await asEntryExists(() => fs.copyFile(src, dest));
|
|
2215
2420
|
}
|
|
2216
2421
|
catch (err) {
|
|
2422
|
+
// The filesystem's own "that is a directory" becomes the sentence the
|
|
2423
|
+
// dry run predicts, instead of escaping as a 500 carrying the
|
|
2424
|
+
// server's absolute path.
|
|
2425
|
+
if (err?.name === 'IsDirectoryError') {
|
|
2426
|
+
throw new ToolError(folderCopyRefusal(src), 400);
|
|
2427
|
+
}
|
|
2217
2428
|
const missing = isAbsence(err) || err.name === 'FileNotFoundError';
|
|
2218
2429
|
if (missing) {
|
|
2219
2430
|
throw (await kindOf(fs, src)) === null
|
|
@@ -2227,8 +2438,8 @@ changeGate) {
|
|
|
2227
2438
|
});
|
|
2228
2439
|
mount({
|
|
2229
2440
|
name: 'unzip',
|
|
2230
|
-
|
|
2231
|
-
|
|
2441
|
+
gated: true,
|
|
2442
|
+
description: 'Extract a .zip already in the workspace (defaults to the zip\'s parent). Returns extracted files + skipped entries. Existing files are overwritten.',
|
|
2232
2443
|
inputs: {
|
|
2233
2444
|
type: 'object',
|
|
2234
2445
|
properties: {
|
|
@@ -2260,19 +2471,19 @@ changeGate) {
|
|
|
2260
2471
|
write: true,
|
|
2261
2472
|
handler: async (a, ctx) => {
|
|
2262
2473
|
const zipPath = a.path;
|
|
2263
|
-
//
|
|
2264
|
-
//
|
|
2265
|
-
await
|
|
2474
|
+
// Opening the archive is a read of the archive, so the read hook hears
|
|
2475
|
+
// about it before a single entry is extracted out of it.
|
|
2476
|
+
await notifyAgentRead(agentAccessGate, ctx, a.branch, zipPath);
|
|
2266
2477
|
// A .zip that is not there is a missing PATH, not an unreadable archive:
|
|
2267
2478
|
// the service now says so (PathNotFoundError) and the helper turns it
|
|
2268
2479
|
// into the same 404 every other file tool answers. Only that declared
|
|
2269
2480
|
// answer maps — a failure part-way through an extraction is not the
|
|
2270
2481
|
// archive going missing.
|
|
2271
2482
|
return orDeclaredNotFound(() => ctx.workspaceService.unzipFile(workspaceIdForBranch(a.branch), zipPath, typeof a.destination === 'string' ? a.destination : undefined,
|
|
2272
|
-
// Each extracted file is a write:
|
|
2273
|
-
// is skipped (not extracted), so an archive can't
|
|
2274
|
-
// extension policy applies per entry too, so
|
|
2275
|
-
// `.md` into the graph.
|
|
2483
|
+
// Each extracted file is a write of its own: an entry the write
|
|
2484
|
+
// hook refuses is skipped (not extracted), so an archive can't be
|
|
2485
|
+
// a way around it — the extension policy applies per entry too, so
|
|
2486
|
+
// a restricted run can't unzip a `.md` into the graph.
|
|
2276
2487
|
(wsRelPath) => {
|
|
2277
2488
|
// An entry that would land beside the repository is skipped with the
|
|
2278
2489
|
// corrected-path reason, like any other refused entry.
|
|
@@ -2283,17 +2494,20 @@ changeGate) {
|
|
|
2283
2494
|
throw new ToolError('roles.yaml is never extracted from an archive — change it with edit_file or write_file, where the change is checked.', 422);
|
|
2284
2495
|
}
|
|
2285
2496
|
writePolicy.assertPathWritable(ctx.sessionId, wsRelPath);
|
|
2286
|
-
return
|
|
2497
|
+
return assertAgentWriteAllowed(agentAccessGate, ctx, a.branch, wsRelPath);
|
|
2287
2498
|
}), 'Nothing to extract');
|
|
2288
2499
|
},
|
|
2289
2500
|
});
|
|
2290
2501
|
// ── shell (internal-only) ───────────────────────────────────────────────
|
|
2291
2502
|
mount({
|
|
2292
2503
|
name: 'execute_command',
|
|
2293
|
-
|
|
2294
|
-
|
|
2504
|
+
gated: true,
|
|
2505
|
+
description: 'Run a shell command in the workspace directory. Returns `{ stdout, stderr, exitCode }` (output capped). Use for git status/log, grep/rg, build/test commands.',
|
|
2295
2506
|
internalOnly: true,
|
|
2296
2507
|
fileTool: false,
|
|
2508
|
+
// The one tool the mount's branch check skips: the handler below resolves an
|
|
2509
|
+
// omitted `branch` to the internal caller's focused branch before refusing.
|
|
2510
|
+
resolvesBranchItself: true,
|
|
2297
2511
|
inputs: {
|
|
2298
2512
|
type: 'object',
|
|
2299
2513
|
properties: {
|
|
@@ -2347,7 +2561,12 @@ changeGate) {
|
|
|
2347
2561
|
const raw = a.branch;
|
|
2348
2562
|
const branch = raw === undefined ? ctx.focusedBranch : raw;
|
|
2349
2563
|
if (typeof branch !== 'string' || branch.length === 0) {
|
|
2350
|
-
throw new ToolError('execute_command requires a `branch`: pass the branch (draft) whose workspace to run the command in — the one you are currently working on.', 400
|
|
2564
|
+
throw new ToolError('execute_command requires a `branch`: pass the branch (draft) whose workspace to run the command in — the one you are currently working on.', 400,
|
|
2565
|
+
// The same discriminator every other KB tool answers a branch-less
|
|
2566
|
+
// call with, so a client switches on one kind across the surface.
|
|
2567
|
+
// The MESSAGE stays this tool's own: it can name the focused-branch
|
|
2568
|
+
// fallback that only applies here.
|
|
2569
|
+
{ kind: 'branch-required' });
|
|
2351
2570
|
}
|
|
2352
2571
|
// A stringified absent value. Both are syntactically valid git branch names,
|
|
2353
2572
|
// so `assertValidBranchName` below happily accepts them — and accepting one
|
|
@@ -2356,7 +2575,7 @@ changeGate) {
|
|
|
2356
2575
|
// and 500s. Reject them by name, ahead of the shape check.
|
|
2357
2576
|
if (branch === 'undefined' || branch === 'null') {
|
|
2358
2577
|
throw new ToolError(`execute_command got the literal string "${branch}" as \`branch\` — that is a stringified absent value, not a branch. ` +
|
|
2359
|
-
'Pass the real branch (draft) whose workspace to run the command in.', 400);
|
|
2578
|
+
'Pass the real branch (draft) whose workspace to run the command in.', 400, { kind: 'branch-required' });
|
|
2360
2579
|
}
|
|
2361
2580
|
// Then the SHAPE, via the one canonical validator every other branch path
|
|
2362
2581
|
// uses — no hand-maintained list of suspicious literals, which would both
|
|
@@ -2371,11 +2590,12 @@ changeGate) {
|
|
|
2371
2590
|
throw new ToolError(`execute_command got an invalid \`branch\`: ${err.message} — ` +
|
|
2372
2591
|
'pass the exact branch (draft) whose workspace to run the command in.', 400);
|
|
2373
2592
|
}
|
|
2374
|
-
// Shell is a write path with no single target path to check, so
|
|
2375
|
-
//
|
|
2376
|
-
//
|
|
2593
|
+
// Shell is a write path with no single target path to check, so the
|
|
2594
|
+
// write hook is asked once for the call itself, with no path — and the
|
|
2595
|
+
// run must not be restricted to a file type either, since shell could
|
|
2596
|
+
// write anything.
|
|
2377
2597
|
writePolicy.assertUnrestricted(ctx.sessionId);
|
|
2378
|
-
await
|
|
2598
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, branch);
|
|
2379
2599
|
// Canonical per-branch bootstrap entry point — it owns the workspace-id
|
|
2380
2600
|
// encoding and the single-flight clone, so the shell never derives a
|
|
2381
2601
|
// workspace path by hand.
|