@bevel-software/platform-core-backend 0.22.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 +30 -11
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +9 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +112 -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.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/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 +70 -2
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +176 -7
- 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/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/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/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +8 -3
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +52 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +207 -19
- package/dist/modules/settings/setup.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-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 +75 -35
- 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 +69 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +153 -2
- package/dist/modules/workflow/git/git.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.service.d.ts +60 -6
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +111 -4
- 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 +59 -9
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
- 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 +255 -25
- 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 +309 -115
- 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 +25 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +28 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +20 -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 +32 -14
- package/src/core/create-core-services.ts +129 -23
- 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.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.routes.ts +13 -15
- 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 +1 -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 +179 -6
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -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/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__/plugins.tools.test.ts +2 -1
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
- 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 +8 -3
- package/src/modules/settings/setup.routes.ts +263 -19
- 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__/skills.service.test.ts +229 -5
- 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-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__/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.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
- 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/git.service.ts +168 -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.service.ts +121 -4
- 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.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +685 -14
- 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 +86 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +256 -24
- package/src/modules/workspace/workspace.tools.ts +338 -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 +31 -0
- package/src/shared/git.contract.ts +27 -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
|
@@ -31,6 +31,7 @@ import type { KbContext } from '../../shared/kb-context.js';
|
|
|
31
31
|
import { failureOf, type GitFailure } from '../../shared/git-failure.js';
|
|
32
32
|
import { redactSecret, urlQuerySecrets } from '../../shared/redact-secret.js';
|
|
33
33
|
import { listRootFolders, pickListingBranch } from './git-root-folders.js';
|
|
34
|
+
import { sameRepository } from '../kb-fs/remote-url.js';
|
|
34
35
|
import { MANAGED_DEFAULT_BRANCH } from './managed-repository.js';
|
|
35
36
|
import { GIT_MODES, type GitHubAppRepository, type RepositorySource } from './repository-source.js';
|
|
36
37
|
import '../auth/auth.middleware.js'; // Express Request augmentation
|
|
@@ -90,6 +91,49 @@ export interface LastSyncStatus {
|
|
|
90
91
|
results: Array<{ branch: string; outcome: string; error?: string }>;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The open change requests, as the repository-change confirmation needs them:
|
|
96
|
+
* how many there are to decide about, and the close the admin may choose.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately narrow — the setup routes never read a request, never write
|
|
99
|
+
* one, and never learn what is in it. Nothing here deletes anything: closing
|
|
100
|
+
* leaves every row where it is.
|
|
101
|
+
*/
|
|
102
|
+
export interface RepositoryChangeRequests {
|
|
103
|
+
/** How many change requests are open right now. */
|
|
104
|
+
countOpen(): Promise<number>;
|
|
105
|
+
/** Close every open request as "repository replaced", releasing the file locks on their branches. Answers how many closed. */
|
|
106
|
+
closeAsRepositoryReplaced(): Promise<number>;
|
|
107
|
+
/**
|
|
108
|
+
* Run the move with no queued commit being written. The startup phase sets
|
|
109
|
+
* working copies aside and clones them again while the deployment is
|
|
110
|
+
* serving, and a commit worker left running would write into a directory
|
|
111
|
+
* that is being renamed away, or into the fresh clone of a repository the
|
|
112
|
+
* commit was never meant for. Absent (a minimal mount has no worker), the
|
|
113
|
+
* move runs as it is.
|
|
114
|
+
*/
|
|
115
|
+
whileCommitsHeld?<T>(work: () => Promise<T>): Promise<T>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* What the admin is asked to confirm before a save MOVES the deployment to
|
|
120
|
+
* another repository, and what they answered about its open change requests.
|
|
121
|
+
*
|
|
122
|
+
* `keep` — the repository only moved: its branches came along, so the open
|
|
123
|
+
* change requests still mean something and stay open.
|
|
124
|
+
* `close` — it is a different repository: none of those branches is in it,
|
|
125
|
+
* so the requests are closed as "repository replaced" (nothing deleted).
|
|
126
|
+
*
|
|
127
|
+
* What happens to the working copies is not a choice, and is decided by the
|
|
128
|
+
* histories, not by this answer (see the startup phase's
|
|
129
|
+
* `reconcileClonesWithConfiguredRepository`): a copy whose history the new
|
|
130
|
+
* repository holds is pointed at it and kept, unpushed commits included; any
|
|
131
|
+
* other is set aside whole and cloned fresh. Nothing is deleted. The
|
|
132
|
+
* confirmation exists so that work which leaves the app this way does so
|
|
133
|
+
* KNOWINGLY.
|
|
134
|
+
*/
|
|
135
|
+
export type RepositoryChangeChoice = 'keep' | 'close';
|
|
136
|
+
|
|
93
137
|
/**
|
|
94
138
|
* First-run setup — the deployment's own configuration, for the things that
|
|
95
139
|
* used to be environment-only.
|
|
@@ -161,6 +205,16 @@ export function createSetupRoutes(
|
|
|
161
205
|
* one there always was: an address and a token, read from the settings.
|
|
162
206
|
*/
|
|
163
207
|
repository?: RepositorySetup,
|
|
208
|
+
/**
|
|
209
|
+
* What a change of the knowledge-base repository has to decide about, and
|
|
210
|
+
* what to do when the admin decides to close.
|
|
211
|
+
*
|
|
212
|
+
* Injected because the setup routes must stay free of the workflow module —
|
|
213
|
+
* and a minimal mount (tests, a distribution without change requests) simply
|
|
214
|
+
* has none: the default answers "no open requests", so the confirmation
|
|
215
|
+
* offers no choice it cannot honour.
|
|
216
|
+
*/
|
|
217
|
+
changeRequests: RepositoryChangeRequests = { async countOpen() { return 0; }, async closeAsRepositoryReplaced() { return 0; } },
|
|
164
218
|
): express.Router {
|
|
165
219
|
const router = express.Router();
|
|
166
220
|
const source = repository?.source;
|
|
@@ -299,7 +353,15 @@ export function createSetupRoutes(
|
|
|
299
353
|
});
|
|
300
354
|
|
|
301
355
|
async function handleSave(req: express.Request, res: express.Response): Promise<void> {
|
|
302
|
-
const body = (req.body ?? {}) as {
|
|
356
|
+
const body = (req.body ?? {}) as {
|
|
357
|
+
settings?: Record<string, unknown>;
|
|
358
|
+
/** The admin's answer to the repository-change confirmation, when they have given one. */
|
|
359
|
+
confirmRepositoryChange?: unknown;
|
|
360
|
+
/** The count of open change requests that answer was given about. */
|
|
361
|
+
seenOpenChangeRequests?: unknown;
|
|
362
|
+
};
|
|
363
|
+
/** Lets the commit worker go again, once a move has held it. */
|
|
364
|
+
let releaseCommits: (() => void) | undefined;
|
|
303
365
|
const entries: Record<string, string> = {};
|
|
304
366
|
for (const [key, value] of Object.entries(body.settings ?? {})) {
|
|
305
367
|
if (typeof value !== 'string') {
|
|
@@ -321,6 +383,32 @@ export function createSetupRoutes(
|
|
|
321
383
|
const wasServing = kbReady();
|
|
322
384
|
nameBranchesOfManagedRepository(entries);
|
|
323
385
|
if (!(await connectionHoldsFor(entries, wasComplete, res))) return;
|
|
386
|
+
// AFTER the connection check, BEFORE anything is stored. After, because
|
|
387
|
+
// an address the host will not answer for is refused on its own terms
|
|
388
|
+
// and there is then nothing to confirm — nobody should have to agree to
|
|
389
|
+
// losing their working copies only to be told the token was missing.
|
|
390
|
+
// Before, because a refused save must destroy nothing: the check only
|
|
391
|
+
// reads the new address and dry-runs a push to it.
|
|
392
|
+
// Asked only of a deployment that has SERVED on the repository it would
|
|
393
|
+
// leave. A first run whose initialisation failed has answered
|
|
394
|
+
// everything and served nobody: correcting its address is finishing
|
|
395
|
+
// setup, and a question about leaving a repository nobody ever worked
|
|
396
|
+
// on would be a question with nothing behind it.
|
|
397
|
+
const hasServed = wasComplete && kbInit === null;
|
|
398
|
+
const repositoryChange = await repositoryChangeFor(
|
|
399
|
+
entries,
|
|
400
|
+
body.confirmRepositoryChange,
|
|
401
|
+
body.seenOpenChangeRequests,
|
|
402
|
+
hasServed,
|
|
403
|
+
res,
|
|
404
|
+
);
|
|
405
|
+
if (!repositoryChange) return;
|
|
406
|
+
// HELD FROM HERE, before anything is stored. The way chosen takes
|
|
407
|
+
// effect a few lines down, and with it the credential git is handed; a
|
|
408
|
+
// commit worker still running in between would push a queued commit
|
|
409
|
+
// from a working copy of the repository being left, with the
|
|
410
|
+
// credential of the one moved to. Let go in the `finally` below.
|
|
411
|
+
if (repositoryChange.changing) releaseCommits = await holdCommits();
|
|
324
412
|
const oidc = await signInHoldsFor(entries, res);
|
|
325
413
|
if (!oidc) return;
|
|
326
414
|
// Recorded BEFORE the save: the record is keyed by the values it is
|
|
@@ -331,23 +419,63 @@ export function createSetupRoutes(
|
|
|
331
419
|
if (oidc.record) {
|
|
332
420
|
await settings.recordOidcVerification(oidc.record.state, oidc.record.credentials);
|
|
333
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* How many open change requests this save closed as "repository
|
|
424
|
+
* replaced". Nothing is ever deleted; `keep` leaves them exactly as they
|
|
425
|
+
* are, and answers 0.
|
|
426
|
+
*
|
|
427
|
+
* Closed BEFORE the address is stored, as a PRECONDITION of the save. A
|
|
428
|
+
* close that fails after the address is stored has nowhere to go: the
|
|
429
|
+
* address now matches, so the next save sees no change, asks nothing and
|
|
430
|
+
* never retries — leaving requests open on branches the new repository
|
|
431
|
+
* does not have, and their file locks refusing paths to everybody else,
|
|
432
|
+
* with no way for the admin to put it right. Refusing the save instead
|
|
433
|
+
* destroys nothing: the old address still stands, the working copies are
|
|
434
|
+
* untouched (the phase below never ran), and pressing Save again asks the
|
|
435
|
+
* same question and tries again.
|
|
436
|
+
*
|
|
437
|
+
* The other order of failure — closed, then the save itself fails — is
|
|
438
|
+
* recoverable by the admin, who wanted these requests closed and whose
|
|
439
|
+
* next Save completes the replacement. Closing deletes nothing either
|
|
440
|
+
* way.
|
|
441
|
+
*/
|
|
442
|
+
let closedChangeRequests = 0;
|
|
443
|
+
if (repositoryChange.choice === 'close') {
|
|
444
|
+
try {
|
|
445
|
+
closedChangeRequests = await changeRequests.closeAsRepositoryReplaced();
|
|
446
|
+
} catch (err) {
|
|
447
|
+
log.error('the open change requests could not be closed, so the repository was not replaced:', { err });
|
|
448
|
+
res.status(500).json({
|
|
449
|
+
error:
|
|
450
|
+
'The open change requests could not be closed, so the repository was not changed. ' +
|
|
451
|
+
'Nothing was saved and no working copy was touched. Try again.',
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
334
456
|
const saved = await settings.save(entries, req.userId ?? null);
|
|
335
457
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* what a restart would have run. One that WAS serving stays on the
|
|
339
|
-
* mode it is on, working copies and credential alike, until it is
|
|
340
|
-
* started again.
|
|
458
|
+
* THE WAY CHOSEN TAKES EFFECT ON THIS SAVE, in the two cases where the
|
|
459
|
+
* startup phase below runs for it.
|
|
341
460
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
461
|
+
* A deployment that was not serving has nothing on the mode it had: a
|
|
462
|
+
* first run, one whose initialisation failed, a boot that found its
|
|
463
|
+
* repository unreachable. Pinned there, the way out of a repository
|
|
464
|
+
* that does not work would be closed.
|
|
465
|
+
*
|
|
466
|
+
* And a deployment that WAS serving, when its admin has just confirmed
|
|
467
|
+
* the move. The mode in effect was once held until the next restart, so
|
|
468
|
+
* that the running process never presented one way's credential to
|
|
469
|
+
* another way's repository. That reason is answered differently now:
|
|
470
|
+
* the phase below sets the working copies of the repository that was
|
|
471
|
+
* left aside, under a held commit worker, before anything uses the new
|
|
472
|
+
* credential. And an admin of a hosted workspace has no restart to
|
|
473
|
+
* give, so a move that waited for one never happened.
|
|
474
|
+
*
|
|
475
|
+
* What is left pinned is a save that changes the mode WITHOUT moving
|
|
476
|
+
* the repository, which no path produces today; it keeps the restart.
|
|
349
477
|
*/
|
|
350
|
-
if (source && !wasServing) source.takeEffect();
|
|
478
|
+
if (source && (!wasServing || repositoryChange.changing)) source.takeEffect();
|
|
351
479
|
/**
|
|
352
480
|
* The restart the mode owes is read off the two modes themselves, not
|
|
353
481
|
* off what this save wrote: it is owed for as long as the mode chosen
|
|
@@ -398,11 +526,27 @@ export function createSetupRoutes(
|
|
|
398
526
|
* Runs on the false→true completion transition — including when the
|
|
399
527
|
* branch model was configured by an EARLIER save and the repository
|
|
400
528
|
* URL arrives on a later one — and again on any save while a previous
|
|
401
|
-
* setup-time run stands failed (`kbInit`), so a save is the retry.
|
|
402
|
-
*
|
|
403
|
-
*
|
|
529
|
+
* setup-time run stands failed (`kbInit`), so a save is the retry.
|
|
530
|
+
*
|
|
531
|
+
* AND on a save that MOVES THE REPOSITORY, complete and healthy or
|
|
532
|
+
* not. That one is not a quiet moment — the gate is open and sessions
|
|
533
|
+
* may be live — but it is the moment the working copies stop belonging
|
|
534
|
+
* to the configured repository, and leaving them until the next restart
|
|
535
|
+
* is what made Save and Retry fail against a repository that was gone.
|
|
536
|
+
* The admin has just confirmed the move, so the phase runs, with the
|
|
537
|
+
* commit worker held for as long as it does, and the app opens on the
|
|
538
|
+
* new repository's content.
|
|
539
|
+
*
|
|
540
|
+
* Never on an ordinary re-save of a complete, healthy setup: with the
|
|
541
|
+
* gate open, sessions may be live and nothing needs doing.
|
|
404
542
|
*/
|
|
405
|
-
if (
|
|
543
|
+
if (
|
|
544
|
+
(!wasComplete ||
|
|
545
|
+
kbInit !== null ||
|
|
546
|
+
bootFailure() !== null ||
|
|
547
|
+
repositoryChange.changing) &&
|
|
548
|
+
isComplete(settings, kb, source)
|
|
549
|
+
) {
|
|
406
550
|
/**
|
|
407
551
|
* The folder names, applied BEFORE the phase for the same reason as
|
|
408
552
|
* the branch model above: they are otherwise applied once at boot, so
|
|
@@ -425,6 +569,9 @@ export function createSetupRoutes(
|
|
|
425
569
|
// One run at a time. The save chain already serializes handlers
|
|
426
570
|
// whole, so no second run can start while one executes; the `??=`
|
|
427
571
|
// is defense in depth should another invoker ever appear.
|
|
572
|
+
// A move runs with the commit worker held (see `holdCommits`
|
|
573
|
+
// above): nothing queued is written while working copies are set
|
|
574
|
+
// aside and cloned again.
|
|
428
575
|
kbInitInFlight ??= kbStartupRunner.runAll();
|
|
429
576
|
await kbInitInFlight;
|
|
430
577
|
// Under KB_SAFE_BOOT a run that abandoned the phase still resolves
|
|
@@ -473,6 +620,11 @@ export function createSetupRoutes(
|
|
|
473
620
|
settings: settings.describe(),
|
|
474
621
|
...repositoryStatus(),
|
|
475
622
|
oidcVerification: await settings.oidcVerification(),
|
|
623
|
+
// Only on the save that changed the repository, so an ordinary save
|
|
624
|
+
// carries no word about change requests at all.
|
|
625
|
+
...(repositoryChange.changing
|
|
626
|
+
? { repositoryChange: { choice: repositoryChange.choice, closedChangeRequests } }
|
|
627
|
+
: {}),
|
|
476
628
|
});
|
|
477
629
|
} catch (err) {
|
|
478
630
|
if (err instanceof SettingsValidationError) {
|
|
@@ -483,9 +635,98 @@ export function createSetupRoutes(
|
|
|
483
635
|
// the schema or the connection string.
|
|
484
636
|
log.error('save failed:', { err });
|
|
485
637
|
res.status(500).json({ error: 'Could not save these settings.' });
|
|
638
|
+
} finally {
|
|
639
|
+
releaseCommits?.();
|
|
486
640
|
}
|
|
487
641
|
}
|
|
488
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Stop the commit worker until the function this resolves to is called.
|
|
645
|
+
* The hold the composition root offers runs a piece of work with the
|
|
646
|
+
* worker stopped; a move spans the store, the way taking effect and the
|
|
647
|
+
* startup phase, with refusals in between, so the hold is taken here and
|
|
648
|
+
* let go in the handler's `finally`. A mount without a worker holds
|
|
649
|
+
* nothing.
|
|
650
|
+
*/
|
|
651
|
+
function holdCommits(): Promise<() => void> {
|
|
652
|
+
if (!changeRequests.whileCommitsHeld) return Promise.resolve(() => {});
|
|
653
|
+
return new Promise((held, failed) => {
|
|
654
|
+
changeRequests.whileCommitsHeld!<void>(() => new Promise<void>((release) => held(release))).catch(failed);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* What this save does to the knowledge-base repository, and whether the
|
|
660
|
+
* admin has said yes to it.
|
|
661
|
+
*
|
|
662
|
+
* A MOVE IS ANY SAVE AFTER WHICH THE DEPLOYMENT READS ANOTHER REPOSITORY
|
|
663
|
+
* than the one it is running on, however it came about: another address,
|
|
664
|
+
* another way of having the repository (managed, GitHub, address and
|
|
665
|
+
* token), another repository on GitHub. So the question is asked of the
|
|
666
|
+
* one thing that knows where the repository is under every way — the
|
|
667
|
+
* repository source — comparing the address IN EFFECT with the one this
|
|
668
|
+
* save would choose. A mount without a source has the one way there
|
|
669
|
+
* always was, and the address is the setting.
|
|
670
|
+
*
|
|
671
|
+
* A move takes the working copies of the repository that is left out of
|
|
672
|
+
* use: one whose history the new repository holds is pointed at it and
|
|
673
|
+
* kept, any other is set aside and cloned fresh. Work that was never
|
|
674
|
+
* pushed leaves the app with a copy that is set aside. That is not
|
|
675
|
+
* something to discover afterwards, so the save is REFUSED (409) until the
|
|
676
|
+
* answer comes back with the admin's decision.
|
|
677
|
+
*
|
|
678
|
+
* Only a real move asks: an address that differs from the one in effect in
|
|
679
|
+
* nothing but its spelling — a trailing slash, a `.git` suffix, the case
|
|
680
|
+
* of the host — is the same repository (see `kb-fs/remote-url.ts`), and so
|
|
681
|
+
* is a first-run save, where no repository is in effect and nothing on
|
|
682
|
+
* disk could be left.
|
|
683
|
+
*
|
|
684
|
+
* Returns null when it has already answered the request.
|
|
685
|
+
*/
|
|
686
|
+
async function repositoryChangeFor(
|
|
687
|
+
entries: Record<string, string>,
|
|
688
|
+
confirmation: unknown,
|
|
689
|
+
/** How many open change requests the screen showed when the answer was given, when it says. */
|
|
690
|
+
seenOpenChangeRequests: unknown,
|
|
691
|
+
/** Whether the deployment is serving on the repository in effect. One that never served and has no open request is not asked. */
|
|
692
|
+
hasServed: boolean,
|
|
693
|
+
res: express.Response,
|
|
694
|
+
): Promise<{ changing: boolean; choice: RepositoryChangeChoice | null } | null> {
|
|
695
|
+
const unchanged = { changing: false, choice: null } as const;
|
|
696
|
+
const after = settings.resolveAfter(entries);
|
|
697
|
+
const now = (source ? source.url() : settings.resolve('kbRepoUrl')).trim();
|
|
698
|
+
const next = (source ? source.url(after) : after('kbRepoUrl')).trim();
|
|
699
|
+
if (now === '' || next === '' || sameRepository(now, next)) return unchanged;
|
|
700
|
+
// The count is what the choice is ABOUT, so it is read here rather than
|
|
701
|
+
// left for the screen to ask for separately: the two questions would
|
|
702
|
+
// otherwise be answered a round trip apart, and the number on screen
|
|
703
|
+
// would be the one from before whatever happened in between.
|
|
704
|
+
const openChangeRequests = await changeRequests.countOpen();
|
|
705
|
+
// An answer stands for the count it was given about. A request opened
|
|
706
|
+
// while the question stood was never offered "close", and the "keep" the
|
|
707
|
+
// screen sends when there is nothing to choose would decide it unasked:
|
|
708
|
+
// the question is put again, with the count as it is now.
|
|
709
|
+
const answered = confirmation === 'keep' || confirmation === 'close';
|
|
710
|
+
const answerIsCurrent = typeof seenOpenChangeRequests !== 'number' || seenOpenChangeRequests === openChangeRequests;
|
|
711
|
+
if (answered && answerIsCurrent) return { changing: true, choice: confirmation };
|
|
712
|
+
// A move all the same, and the phase treats it as one; nobody is asked.
|
|
713
|
+
// Open change requests are themselves proof that the deployment served,
|
|
714
|
+
// whatever `hasServed` says: a move that failed part-way leaves it gated
|
|
715
|
+
// with a failure standing, and the next save must still ask about them.
|
|
716
|
+
if (!answered && !hasServed && openChangeRequests === 0) return { changing: true, choice: null };
|
|
717
|
+
res.status(409).json({
|
|
718
|
+
error: 'This moves the deployment to another repository.',
|
|
719
|
+
repositoryChange: {
|
|
720
|
+
openChangeRequests,
|
|
721
|
+
// The way left and the way moved to, for the screen to name them.
|
|
722
|
+
// Equal when the move is within one way (another address, another
|
|
723
|
+
// repository on GitHub); absent from a mount with one way only.
|
|
724
|
+
...(source ? { from: source.mode(), to: source.mode(after) } : {}),
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
|
|
489
730
|
/**
|
|
490
731
|
* A repository the deployment keeps for itself is new, and empty, and has
|
|
491
732
|
* no branches for anyone to look up: the branch model is ours to name. It
|
|
@@ -649,7 +890,10 @@ export function createSetupRoutes(
|
|
|
649
890
|
// the environment, whose field the form cannot even edit) is that token's
|
|
650
891
|
// first and only pairing, not a change of it.
|
|
651
892
|
const tokenSupplied = Boolean(entries.gitToken?.trim());
|
|
652
|
-
|
|
893
|
+
// A different SPELLING of the configured address is the same repository,
|
|
894
|
+
// so the stored token is still the token it was saved for: asking for it
|
|
895
|
+
// again over a trailing slash would be a question with no answer.
|
|
896
|
+
if (now.url && !sameRepository(next.url, now.url) && next.token && !tokenSupplied) {
|
|
653
897
|
return settings.sourceOf('gitToken') === 'env'
|
|
654
898
|
? refuse({
|
|
655
899
|
kbRepoUrl:
|
|
@@ -12,6 +12,7 @@ import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
|
|
|
12
12
|
import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
|
|
13
13
|
import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
|
|
14
14
|
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
15
|
+
import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
|
|
15
16
|
import { SpillStore } from '../../workspace/spill-store.js';
|
|
16
17
|
import { DocExtractService } from '../../workspace/file-readers/doc-extract.service.js';
|
|
17
18
|
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
@@ -111,7 +112,7 @@ async function start(): Promise<string> {
|
|
|
111
112
|
registerWorkspaceTools(
|
|
112
113
|
registry, router, fakeAuth, toolHandler,
|
|
113
114
|
new SpillStore(join(tmpdir(), 'bevel-test-spills')), new DocExtractService(join(tmpdir(), 'bevel-test-doc-extract')), allowAll, testKbContext({ kbDirName: KB_DIR }),
|
|
114
|
-
{
|
|
115
|
+
{ recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() },
|
|
115
116
|
new RoutineWritePolicyService(),
|
|
116
117
|
{} as never,
|
|
117
118
|
checker,
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { ToolRegistry } from '../../tool-registry/tool-registry.js';
|
|
5
|
+
import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
|
|
6
|
+
import type { ToolContext } from '../../tool-helpers/tool.contract.js';
|
|
7
|
+
import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
|
|
8
|
+
import { BranchNotFoundError } from '../../../shared/domain-errors.js';
|
|
9
|
+
import { registerSkillsTools } from '../skills.tools.js';
|
|
10
|
+
import { unmergedSkillNotice } from '../skills.service.js';
|
|
11
|
+
import type { GetSkillOptions, ISkillService, ListSkillsOptions, Skill } from '../skills.contract.js';
|
|
12
|
+
import { AllowedToolsChecker } from '../allowed-tools-check.js';
|
|
13
|
+
import type { ToolManualDetail, ToolManualSummary } from '../../tool-manuals/tool-manuals.contract.js';
|
|
14
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The two skill tools reading a draft branch: the input reaches the service,
|
|
18
|
+
* the branch's own answer comes back with its mark and its first line, the
|
|
19
|
+
* `allowed-tools` warnings are computed from the BRANCH's version of the
|
|
20
|
+
* skill, a branch nobody pushed is the file tools' 404, and `branch` with
|
|
21
|
+
* `version` is refused rather than silently ranked.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const DRAFT = 'juan/skill-deck';
|
|
25
|
+
const KB_DIR = 'knowledge-base';
|
|
26
|
+
|
|
27
|
+
const hubspot: ToolManualSummary = { slug: 'hubspot', name: 'hubspot', path: 'Plugins/Sales/hubspot.tool', type: 'inline' };
|
|
28
|
+
const checker = new AllowedToolsChecker(
|
|
29
|
+
{ listExternal: async () => [] },
|
|
30
|
+
{
|
|
31
|
+
listAccessible: async () => [hubspot],
|
|
32
|
+
getDetail: async (): Promise<ToolManualDetail> => ({
|
|
33
|
+
...hubspot,
|
|
34
|
+
description: null,
|
|
35
|
+
capabilities: [{ name: 'search', description: null }],
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
testKbContext({ kbDirName: KB_DIR }),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/** The released skill and the draft's changed copy of it — only the draft names a tool nobody serves. */
|
|
42
|
+
const released: Skill = {
|
|
43
|
+
name: 'rfi',
|
|
44
|
+
description: 'RFI.',
|
|
45
|
+
path: 'Plugins/Sales/rfi',
|
|
46
|
+
body: '# RFI',
|
|
47
|
+
files: [],
|
|
48
|
+
allowedTools: ['hubspot.search'],
|
|
49
|
+
};
|
|
50
|
+
const onDraft: Skill = {
|
|
51
|
+
...released,
|
|
52
|
+
body: `${unmergedSkillNotice(DRAFT)}\n\n# RFI, rewritten`,
|
|
53
|
+
allowedTools: ['hubspot.search', 'hubspot.serch'],
|
|
54
|
+
unmerged: true,
|
|
55
|
+
branch: DRAFT,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** What the tools handed the service, so a test can see the branch arrive (or not). */
|
|
59
|
+
let listArgs: [string | undefined, ListSkillsOptions | undefined][] = [];
|
|
60
|
+
let getArgs: [string, string, string | undefined, GetSkillOptions | undefined][] = [];
|
|
61
|
+
|
|
62
|
+
const skillService: ISkillService = {
|
|
63
|
+
listSkills: async (userEmail?: string, options?: ListSkillsOptions) => {
|
|
64
|
+
listArgs.push([userEmail, options]);
|
|
65
|
+
if (options?.branch === 'juan/typo') throw new BranchNotFoundError('juan/typo');
|
|
66
|
+
if (options?.branch === DRAFT) {
|
|
67
|
+
return [
|
|
68
|
+
{ name: 'rfi', description: 'RFI.', path: released.path, unmerged: true, branch: DRAFT },
|
|
69
|
+
{ name: 'make-deck', description: 'Decks.', path: 'Plugins/make-deck', unmerged: true, branch: DRAFT },
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
return [{ name: 'rfi', description: 'RFI.', path: released.path }];
|
|
73
|
+
},
|
|
74
|
+
getSkill: async (userEmail: string, name: string, file?: string, options?: GetSkillOptions) => {
|
|
75
|
+
getArgs.push([userEmail, name, file, options]);
|
|
76
|
+
if (options?.branch === 'juan/typo') throw new BranchNotFoundError('juan/typo');
|
|
77
|
+
return { ok: true, kind: 'skill', skill: options?.branch === DRAFT ? onDraft : released };
|
|
78
|
+
},
|
|
79
|
+
invalidate: () => {},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let httpServer: HttpServer | undefined;
|
|
83
|
+
let registry: ToolRegistry;
|
|
84
|
+
|
|
85
|
+
async function start(): Promise<string> {
|
|
86
|
+
listArgs = [];
|
|
87
|
+
getArgs = [];
|
|
88
|
+
registry = new ToolRegistry();
|
|
89
|
+
const resolve = async (auth: ToolAuth, signal: AbortSignal): Promise<ToolContext> =>
|
|
90
|
+
({
|
|
91
|
+
user: { id: 'u', email: 'e@x', name: 'N' },
|
|
92
|
+
scope: auth.scope,
|
|
93
|
+
source: auth.source,
|
|
94
|
+
abortSignal: signal,
|
|
95
|
+
workspaceService: {} as never,
|
|
96
|
+
workflowService: {} as never,
|
|
97
|
+
events: {} as never,
|
|
98
|
+
getFilesystem: async () => ({}) as never,
|
|
99
|
+
}) as unknown as ToolContext;
|
|
100
|
+
const fakeAuth = (req: express.Request, _res: express.Response, next: express.NextFunction) => {
|
|
101
|
+
req.toolAuth = { source: 'external', userId: 'u', scope: 'read' };
|
|
102
|
+
next();
|
|
103
|
+
};
|
|
104
|
+
const app = express();
|
|
105
|
+
app.use(express.json());
|
|
106
|
+
const router = express.Router();
|
|
107
|
+
registerSkillsTools(registry, router, fakeAuth, createToolHandlerFactory(resolve), skillService, checker);
|
|
108
|
+
app.use('/api', router);
|
|
109
|
+
httpServer = await new Promise<HttpServer>((r) => {
|
|
110
|
+
const s = app.listen(0, () => r(s));
|
|
111
|
+
});
|
|
112
|
+
return `http://127.0.0.1:${(httpServer.address() as { port: number }).port}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const post = (url: string, body: unknown) =>
|
|
116
|
+
fetch(url, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer x' },
|
|
119
|
+
body: JSON.stringify(body),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(async () => {
|
|
123
|
+
if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
|
|
124
|
+
httpServer = undefined;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('the skill tools read a branch when asked', () => {
|
|
128
|
+
it('declares `branch` as an OPTIONAL input on both tools', async () => {
|
|
129
|
+
await start();
|
|
130
|
+
const defs = await registry.listExternal({ userEmail: 'e@x' });
|
|
131
|
+
for (const name of ['list_skills', 'get_skill']) {
|
|
132
|
+
const def = defs.find((t) => t.name === name)!;
|
|
133
|
+
// `toolDef` wraps a tool's flat inputs under `body`.
|
|
134
|
+
const inputs = (def.inputs as { properties: { body: { properties: Record<string, unknown>; required?: string[] } } })
|
|
135
|
+
.properties.body;
|
|
136
|
+
expect(inputs.properties.branch).toBeDefined();
|
|
137
|
+
expect(inputs.required ?? []).not.toContain('branch');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('list_skills passes the branch on and returns what that branch has, marked', async () => {
|
|
142
|
+
const base = await start();
|
|
143
|
+
const body = (await (await post(`${base}/api/agent/tools/list_skills`, { branch: DRAFT })).json()) as {
|
|
144
|
+
skills: { name: string; unmerged?: boolean; branch?: string }[];
|
|
145
|
+
};
|
|
146
|
+
expect(listArgs).toEqual([['e@x', { branch: DRAFT }]]);
|
|
147
|
+
expect(body.skills.map((s) => s.name)).toEqual(['rfi', 'make-deck']);
|
|
148
|
+
expect(body.skills.every((s) => s.unmerged === true && s.branch === DRAFT)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('list_skills without a branch asks for the released catalog', async () => {
|
|
152
|
+
const base = await start();
|
|
153
|
+
const body = (await (await post(`${base}/api/agent/tools/list_skills`, {})).json()) as {
|
|
154
|
+
skills: { name: string; unmerged?: boolean }[];
|
|
155
|
+
};
|
|
156
|
+
expect(listArgs).toEqual([['e@x', { branch: undefined }]]);
|
|
157
|
+
expect(body.skills).toEqual([{ name: 'rfi', description: 'RFI.', path: released.path }]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('a blank branch is no branch at all: the released catalog answers', async () => {
|
|
161
|
+
const base = await start();
|
|
162
|
+
await post(`${base}/api/agent/tools/list_skills`, { branch: ' ' });
|
|
163
|
+
await post(`${base}/api/agent/tools/get_skill`, { name: 'rfi', branch: '' });
|
|
164
|
+
expect(listArgs).toEqual([['e@x', { branch: undefined }]]);
|
|
165
|
+
expect(getArgs[0][3]).toEqual({ version: undefined, branch: undefined });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('get_skill passes the branch on, and the body keeps its unmerged first line', async () => {
|
|
169
|
+
const base = await start();
|
|
170
|
+
const body = (await (await post(`${base}/api/agent/tools/get_skill`, { name: 'rfi', branch: DRAFT })).json()) as {
|
|
171
|
+
skill: { body: string; unmerged?: boolean; branch?: string };
|
|
172
|
+
};
|
|
173
|
+
expect(getArgs).toEqual([['e@x', 'rfi', undefined, { version: undefined, branch: DRAFT }]]);
|
|
174
|
+
expect(body.skill.body.split('\n')[0]).toBe(unmergedSkillNotice(DRAFT));
|
|
175
|
+
expect(body.skill.unmerged).toBe(true);
|
|
176
|
+
expect(body.skill.branch).toBe(DRAFT);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('the allowed-tools check runs on the BRANCH version of the skill', async () => {
|
|
180
|
+
const base = await start();
|
|
181
|
+
const onBranch = (await (
|
|
182
|
+
await post(`${base}/api/agent/tools/get_skill`, { name: 'rfi', branch: DRAFT })
|
|
183
|
+
).json()) as { warnings: { entry: string; suggestion?: string }[] };
|
|
184
|
+
// `hubspot.serch` is only in the DRAFT's `allowed-tools`: the warning can
|
|
185
|
+
// only come from the copy the branch serves.
|
|
186
|
+
expect(onBranch.warnings.map((w) => w.entry)).toEqual(['hubspot.serch']);
|
|
187
|
+
expect(onBranch.warnings[0].suggestion).toBe('hubspot.search');
|
|
188
|
+
const releasedRes = (await (await post(`${base}/api/agent/tools/get_skill`, { name: 'rfi' })).json()) as {
|
|
189
|
+
warnings: unknown[];
|
|
190
|
+
};
|
|
191
|
+
expect(releasedRes.warnings).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('refuses `branch` together with `version`, naming both, without asking the service', async () => {
|
|
195
|
+
const base = await start();
|
|
196
|
+
const res = await post(`${base}/api/agent/tools/get_skill`, { name: 'rfi', branch: DRAFT, version: '1.4.0' });
|
|
197
|
+
expect(res.status).toBe(400);
|
|
198
|
+
const body = (await res.json()) as { error: string };
|
|
199
|
+
expect(body.error).toContain('`branch` or `version`, not both');
|
|
200
|
+
expect(getArgs).toEqual([]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('answers 404 naming a branch that does not exist, on both tools', async () => {
|
|
204
|
+
const base = await start();
|
|
205
|
+
for (const [tool, args] of [
|
|
206
|
+
['list_skills', { branch: 'juan/typo' }],
|
|
207
|
+
['get_skill', { name: 'rfi', branch: 'juan/typo' }],
|
|
208
|
+
] as const) {
|
|
209
|
+
const res = await post(`${base}/api/agent/tools/${tool}`, args);
|
|
210
|
+
expect(res.status).toBe(404);
|
|
211
|
+
expect(await res.json()).toMatchObject({
|
|
212
|
+
kind: 'branch-not-found',
|
|
213
|
+
branch: 'juan/typo',
|
|
214
|
+
error: 'There is no branch named juan/typo.',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|