@bevel-software/platform-core-backend 0.22.0 → 0.24.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 +46 -13
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +12 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +137 -24
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +46 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +99 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +16 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +28 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- 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 +17 -16
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +7 -3
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +38 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +39 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +2 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +2 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
- package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +4 -2
- package/dist/modules/audit/agent-audit.service.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-erasure.service.d.ts.map +1 -1
- package/dist/modules/auth/account-erasure.service.js +57 -16
- package/dist/modules/auth/account-erasure.service.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 +197 -18
- 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/code-mode/code-mode.tool.d.ts +20 -2
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +66 -35
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/connection.d.ts +16 -0
- package/dist/modules/database/connection.d.ts.map +1 -1
- package/dist/modules/database/connection.js +117 -0
- package/dist/modules/database/connection.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +337 -120
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +116 -58
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/database/migrate.d.ts +8 -0
- package/dist/modules/database/migrate.d.ts.map +1 -1
- package/dist/modules/database/migrate.js +271 -1
- package/dist/modules/database/migrate.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/mcp.service.d.ts +8 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +38 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-records.store.js +7 -4
- package/dist/modules/plugins/join-request-records.store.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 +12 -4
- 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/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +80 -0
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
- package/dist/modules/tool-registry/description-length.js +108 -0
- package/dist/modules/tool-registry/description-length.js.map +1 -0
- 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 +94 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +193 -3
- 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 +60 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +7 -3
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
- package/dist/modules/workflow/review-workflow/review-workflow.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 +115 -5
- 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/agent-upload.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.routes.js +210 -0
- package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.store.js +553 -0
- package/dist/modules/workspace/agent-upload.store.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/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +46 -4
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/upload-limits.d.ts +13 -0
- package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
- package/dist/modules/workspace/upload-limits.js +13 -0
- package/dist/modules/workspace/upload-limits.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +93 -4
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +128 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +316 -58
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +13 -11
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +791 -198
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/workspace/write-denial.d.ts +0 -6
- package/dist/modules/workspace/write-denial.d.ts.map +1 -1
- package/dist/modules/workspace/write-denial.js +0 -6
- package/dist/modules/workspace/write-denial.js.map +1 -1
- package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
- package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
- package/dist/modules/workspace/zip-entry-rules.js +154 -0
- package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
- 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/column-crypto.d.ts +194 -0
- package/dist/shared/column-crypto.d.ts.map +1 -0
- package/dist/shared/column-crypto.js +144 -0
- package/dist/shared/column-crypto.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/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -1
- package/dist/shared/token-crypto.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 +1 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/dist/tenancy/tenant-secrets.d.ts +5 -1
- package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
- package/dist/tenancy/tenant-secrets.js +4 -0
- package/dist/tenancy/tenant-secrets.js.map +1 -1
- package/kb-template/AGENTS.md +2 -0
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/0016_pii_encryption.sql +20 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/0016_snapshot.json +2327 -0
- package/migrations/meta/_journal.json +21 -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 +241 -5
- 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 +52 -16
- package/src/core/create-core-services.ts +156 -24
- package/src/core/lifecycle.ts +117 -13
- package/src/core-config.ts +31 -14
- package/src/index.ts +30 -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/__tests__/users-db-double.ts +21 -12
- 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 +17 -16
- package/src/modules/access/directory-sync-bot.ts +7 -3
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +11 -5
- package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
- package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
- package/src/modules/agent-instructions/compose.ts +50 -7
- package/src/modules/agent-instructions/index.ts +12 -0
- package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
- package/src/modules/audit/agent-audit.service.ts +4 -2
- package/src/modules/auth/__tests__/account-deactivation.test.ts +253 -0
- package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
- package/src/modules/auth/__tests__/account.routes.test.ts +87 -6
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +4 -2
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account-erasure.service.ts +69 -18
- 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 +206 -23
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
- package/src/modules/code-mode/code-mode.tool.ts +80 -34
- package/src/modules/database/__tests__/connection.test.ts +12 -0
- package/src/modules/database/__tests__/pii-backfill.pg.test.ts +561 -0
- package/src/modules/database/connection.ts +117 -0
- package/src/modules/database/core-schema.ts +116 -58
- package/src/modules/database/migrate.ts +353 -1
- 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.e2e.test.ts +4 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +115 -13
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/mcp.service.ts +46 -7
- 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/plugins/join-request-records.store.ts +7 -4
- 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 +12 -4
- 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/tool-manuals/tool-manuals.tools.ts +44 -31
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
- package/src/modules/tool-registry/description-length.ts +111 -0
- 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/__tests__/git.service.prFetchFailure.test.ts +170 -0
- package/src/modules/workflow/git/git.service.ts +215 -1
- package/src/modules/workflow/pending-commits.service.ts +64 -2
- package/src/modules/workflow/recovery-bot.ts +7 -3
- package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
- package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.service.ts +125 -5
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +394 -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.any-workspace-credentials.test.ts +156 -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 +402 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +77 -9
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +41 -39
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +766 -56
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/agent-upload.routes.ts +214 -0
- package/src/modules/workspace/agent-upload.store.ts +668 -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/startup/steps/__tests__/steps.test.ts +9 -9
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
- package/src/modules/workspace/startup/steps/template-source.ts +53 -5
- package/src/modules/workspace/upload-limits.ts +12 -0
- package/src/modules/workspace/workspace.routes.ts +96 -5
- package/src/modules/workspace/workspace.service.ts +319 -61
- package/src/modules/workspace/workspace.tools.ts +894 -214
- package/src/modules/workspace/write-denial.ts +0 -8
- package/src/modules/workspace/zip-entry-rules.ts +173 -0
- 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/__tests__/column-crypto.test.ts +217 -0
- package/src/shared/column-crypto.ts +218 -0
- package/src/shared/domain-errors.ts +31 -0
- package/src/shared/git.contract.ts +27 -0
- package/src/shared/token-crypto.ts +28 -1
- package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -1
- package/src/tenancy/static-tenant-source.ts +1 -3
- package/src/tenancy/tenant-secrets.ts +5 -1
- 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,6 +3,7 @@ import { logger } from '../../shared/logging.js';
|
|
|
3
3
|
|
|
4
4
|
const log = logger('skills');
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import { parseDocument } from 'yaml';
|
|
7
8
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
8
9
|
import type { KbContext } from '../../shared/kb-context.js';
|
|
@@ -13,6 +14,7 @@ import { TtlCache } from '../../shared/ttl-cache.js';
|
|
|
13
14
|
import type {
|
|
14
15
|
GetSkillOptions,
|
|
15
16
|
ISkillService,
|
|
17
|
+
ListSkillsOptions,
|
|
16
18
|
GetSkillResult,
|
|
17
19
|
Skill,
|
|
18
20
|
SkillHistorySource,
|
|
@@ -37,12 +39,43 @@ interface ParsedSkill {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
42
|
+
* One branch's skills and where they were read from. The workspace rides along
|
|
43
|
+
* because every later question — may this caller read the skill, what does its
|
|
44
|
+
* folder contain, what is in its history — has to be asked of the SAME clone
|
|
45
|
+
* the skills were scanned out of, never of whichever branch happens to be the
|
|
46
|
+
* default.
|
|
47
|
+
*/
|
|
48
|
+
interface Catalog {
|
|
49
|
+
wsId: string;
|
|
50
|
+
/** Absolute path of that workspace's KB clone; empty when there was none to read. */
|
|
51
|
+
kbRoot: string;
|
|
52
|
+
skills: ParsedSkill[];
|
|
53
|
+
/**
|
|
54
|
+
* Folder digests computed out of THIS catalog's clone, by skill folder —
|
|
55
|
+
* filled lazily by `digestOf`, so it lives exactly as long as the catalog
|
|
56
|
+
* does. On the released catalog that is the cache's TTL (and a merge to
|
|
57
|
+
* default drops both together); on a branch catalog, which is never cached,
|
|
58
|
+
* it is one call.
|
|
59
|
+
*/
|
|
60
|
+
digests: Map<string, Promise<string | null>>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reads skills from the DEFAULT-branch workspace, so the catalog is one global,
|
|
65
|
+
* released set. Results are cached briefly and on a merge to default the cache
|
|
66
|
+
* should be dropped via `invalidate()`.
|
|
67
|
+
*
|
|
68
|
+
* A caller may instead NAME a branch (`branch` on either method) and read the
|
|
69
|
+
* skills as they are on that draft — what an agent needs to try a skill it has
|
|
70
|
+
* just written, before anyone has approved it. That read is deliberately
|
|
71
|
+
* austere: no cache (the draft changes under the agent's own hands), access
|
|
72
|
+
* judged in that branch's own clone with that branch's rules, a 404 for a
|
|
73
|
+
* branch nobody pushed, and everything the default branch does not already
|
|
74
|
+
* serve marked `unmerged` — with one line at the top of a body saying it is
|
|
75
|
+
* not approved.
|
|
43
76
|
*/
|
|
44
77
|
export class SkillService implements ISkillService {
|
|
45
|
-
private readonly cache: TtlCache<
|
|
78
|
+
private readonly cache: TtlCache<Catalog>;
|
|
46
79
|
/**
|
|
47
80
|
* Where a `version` is read from — git, attached by the composition root
|
|
48
81
|
* once the git service exists (it is built after this one). Without it a
|
|
@@ -73,16 +106,21 @@ export class SkillService implements ISkillService {
|
|
|
73
106
|
this.cache.invalidate();
|
|
74
107
|
}
|
|
75
108
|
|
|
76
|
-
async listSkills(userEmail?: string): Promise<SkillSummary[]> {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
109
|
+
async listSkills(userEmail?: string, options: ListSkillsOptions = {}): Promise<SkillSummary[]> {
|
|
110
|
+
const { catalog, branch } = await this.catalogFor(options.branch);
|
|
111
|
+
const visible = userEmail ? await this.readableSkills(catalog, userEmail) : catalog.skills;
|
|
112
|
+
if (branch === undefined) return visible.map((s) => s.summary);
|
|
113
|
+
// Marking reads both copies of a folder, so it happens only here, on a
|
|
114
|
+
// branch read that asked for it — the released catalog every other surface
|
|
115
|
+
// takes never pays for it. The released catalog is resolved ONCE for the
|
|
116
|
+
// whole listing: per skill, a cold cache would start one scan of the
|
|
117
|
+
// default branch per skill.
|
|
118
|
+
const released = await this.defaultCatalog();
|
|
119
|
+
return Promise.all(
|
|
120
|
+
visible.map(async (s) =>
|
|
121
|
+
(await this.differs(released, catalog, s)) ? { ...s.summary, unmerged: true as const, branch } : s.summary,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
86
124
|
}
|
|
87
125
|
|
|
88
126
|
async getSkill(
|
|
@@ -92,10 +130,12 @@ export class SkillService implements ISkillService {
|
|
|
92
130
|
options: GetSkillOptions = {},
|
|
93
131
|
): Promise<GetSkillResult> {
|
|
94
132
|
if (!isSafeSkillName(name)) return { ok: false, error: 'not_found' };
|
|
95
|
-
const
|
|
133
|
+
const { catalog, branch } = await this.catalogFor(options.branch);
|
|
134
|
+
const found = catalog.skills.find((s) => s.summary.name === name);
|
|
135
|
+
// A skill the branch deleted is not a skill as far as this answer goes:
|
|
136
|
+
// the same `not_found` a name nobody ever used gets.
|
|
96
137
|
if (!found) return { ok: false, error: 'not_found' };
|
|
97
138
|
|
|
98
|
-
const wsId = this.kb.defaultWorkspaceId();
|
|
99
139
|
// Through `readable`, not a bare `canRead`: the two gates must never
|
|
100
140
|
// disagree. `canRead` reads a file's own frontmatter rules off disk on
|
|
101
141
|
// every call while `canReadBatch` resolves them through a per-workspace
|
|
@@ -109,19 +149,51 @@ export class SkillService implements ISkillService {
|
|
|
109
149
|
// rules would be authorized against the previous verdict until the memo
|
|
110
150
|
// expires. It is not: `registerCatalogCacheInvalidation` drops the gate on
|
|
111
151
|
// the same default-branch signal that drops this catalog, so the listing
|
|
112
|
-
// and the load are refreshed by one event or by neither.
|
|
113
|
-
|
|
152
|
+
// and the load are refreshed by one event or by neither. A branch read
|
|
153
|
+
// caches no catalog of its own, so the same pair of gates still answers
|
|
154
|
+
// for it — in that branch's workspace, where its own rules live.
|
|
155
|
+
const allowed = await this.readable(catalog.wsId, userEmail, [found.summary.path]);
|
|
114
156
|
if (allowed.get(`${found.summary.path}/SKILL.md`) !== true) {
|
|
115
|
-
|
|
157
|
+
// On the default branch: `forbidden` — the skill is released, everyone
|
|
158
|
+
// knows the catalog has it, and naming the refusal is what sends the
|
|
159
|
+
// caller to ask for access.
|
|
160
|
+
//
|
|
161
|
+
// On a draft: the same answer a name nobody ever used gets. A skill
|
|
162
|
+
// being written on a branch the caller cannot read there is one they
|
|
163
|
+
// must not learn exists either, and the listing already hides it — a
|
|
164
|
+
// load that answered `forbidden` would confirm it by the back door.
|
|
165
|
+
return { ok: false, error: branch === undefined ? 'forbidden' : 'not_found' };
|
|
116
166
|
}
|
|
117
167
|
|
|
168
|
+
const result = await this.serve(catalog, found, name, file, options.version);
|
|
169
|
+
if (branch === undefined || !result.ok) return result;
|
|
170
|
+
// The mark and the line say "nobody approved this", so they go on exactly
|
|
171
|
+
// what the default branch does not already serve. A skill the branch never
|
|
172
|
+
// touched IS the released one — read through a draft's clone, but the same
|
|
173
|
+
// bytes — and claiming otherwise would teach agents to ignore the warning.
|
|
174
|
+
if (!(await this.differs(await this.defaultCatalog(), catalog, found))) return result;
|
|
175
|
+
return asUnmerged(result, branch);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The skill (or the one bundled file, or the copy a `version` names) out of
|
|
180
|
+
* the catalog it was found in. Everything the caller is allowed to see has
|
|
181
|
+
* been settled by `getSkill`; this only reads.
|
|
182
|
+
*/
|
|
183
|
+
private async serve(
|
|
184
|
+
catalog: Catalog,
|
|
185
|
+
found: ParsedSkill,
|
|
186
|
+
name: string,
|
|
187
|
+
file: string | undefined,
|
|
188
|
+
version: string | undefined,
|
|
189
|
+
): Promise<GetSkillResult> {
|
|
118
190
|
// A version other than the one on disk now is read out of history —
|
|
119
191
|
// after the access check above, which is the caller's access to the skill
|
|
120
192
|
// as it is now. The version on disk IS the latest, whatever it is called,
|
|
121
193
|
// so asking for it by name answers from disk like asking for nothing.
|
|
122
|
-
const wanted =
|
|
194
|
+
const wanted = version?.trim();
|
|
123
195
|
if (wanted !== undefined && wanted.length > 0 && wanted !== found.summary.version) {
|
|
124
|
-
return this.getSkillAtVersion(wsId, found, name, wanted, file);
|
|
196
|
+
return this.getSkillAtVersion(catalog.wsId, found, name, wanted, file);
|
|
125
197
|
}
|
|
126
198
|
|
|
127
199
|
if (file !== undefined) {
|
|
@@ -131,7 +203,7 @@ export class SkillService implements ISkillService {
|
|
|
131
203
|
// the ignore rules, so a path that is not in it is one they hid.
|
|
132
204
|
if (!found.files.includes(repoPath)) return { ok: false, error: 'not_found' };
|
|
133
205
|
try {
|
|
134
|
-
const content = await this.workspaceService.readFile(wsId, `${this.kbDirName}/${repoPath}`);
|
|
206
|
+
const content = await this.workspaceService.readFile(catalog.wsId, `${this.kbDirName}/${repoPath}`);
|
|
135
207
|
return { ok: true, kind: 'file', file: { name, file, path: repoPath, content } };
|
|
136
208
|
} catch {
|
|
137
209
|
return { ok: false, error: 'not_found' };
|
|
@@ -150,8 +222,9 @@ export class SkillService implements ISkillService {
|
|
|
150
222
|
// --- internal ---------------------------------------------------------------
|
|
151
223
|
|
|
152
224
|
/**
|
|
153
|
-
* The skill as it was at the most recent
|
|
154
|
-
* `SKILL.md` declared `wanted` —
|
|
225
|
+
* The skill as it was at the most recent commit of `wsId`'s clone whose
|
|
226
|
+
* `SKILL.md` declared `wanted` — the default branch's history, or a named
|
|
227
|
+
* branch's own — walking that file's history newest first,
|
|
155
228
|
* so a version that was published, superseded and republished answers with
|
|
156
229
|
* its latest copy. Every version the walk met is collected on the way, so
|
|
157
230
|
* a miss can say what there is to ask for.
|
|
@@ -213,40 +286,150 @@ export class SkillService implements ISkillService {
|
|
|
213
286
|
* The ONE read gate both surfaces resolve through, keyed by each skill's
|
|
214
287
|
* `SKILL.md`. Shared so `listSkills` and `getSkill` can only ever give the
|
|
215
288
|
* same verdict about the same skill — see the note at the `getSkill` call.
|
|
289
|
+
*
|
|
290
|
+
* `wsId` is the workspace the skills were READ from, so a branch's answer is
|
|
291
|
+
* judged by the `roles.yaml`, groups and `access.md` tree that branch has —
|
|
292
|
+
* which is the point: a draft may be where a skill's access rules are being
|
|
293
|
+
* changed, and judging it by the default branch's rules would answer about a
|
|
294
|
+
* tree the caller is not reading.
|
|
216
295
|
*/
|
|
217
|
-
private async readable(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
);
|
|
296
|
+
private async readable(
|
|
297
|
+
wsId: string,
|
|
298
|
+
userEmail: string,
|
|
299
|
+
skillFolders: string[],
|
|
300
|
+
): Promise<Map<string, boolean>> {
|
|
301
|
+
return this.accessControl.canReadBatch(wsId, userEmail, skillFolders.map((p) => `${p}/SKILL.md`));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The skills of `catalog` this caller may read, by the gate above. */
|
|
305
|
+
private async readableSkills(catalog: Catalog, userEmail: string): Promise<ParsedSkill[]> {
|
|
306
|
+
const allowed = await this.readable(catalog.wsId, userEmail, catalog.skills.map((s) => s.summary.path));
|
|
307
|
+
// Fail closed: keep a skill only on an explicit `true` verdict — a missing
|
|
308
|
+
// entry counts as denied, matching the KB's default-deny read model (and the
|
|
309
|
+
// tool-manuals catalog). `!== false` would silently EXPOSE a skill any time
|
|
310
|
+
// the checker skips a path.
|
|
311
|
+
return catalog.skills.filter((s) => allowed.get(`${s.summary.path}/SKILL.md`) === true);
|
|
223
312
|
}
|
|
224
313
|
|
|
225
|
-
|
|
314
|
+
/**
|
|
315
|
+
* The catalog an answer comes out of, and the branch to MARK it with —
|
|
316
|
+
* `branch` undefined when the answer is the released one, so every surface
|
|
317
|
+
* that names no branch is answered exactly as it was before this input
|
|
318
|
+
* existed.
|
|
319
|
+
*/
|
|
320
|
+
private async catalogFor(branch?: string): Promise<{ catalog: Catalog; branch?: string }> {
|
|
321
|
+
const named = branch?.trim();
|
|
322
|
+
// A blank branch is a missing one, and the safe reading of a missing
|
|
323
|
+
// branch is the released catalog: it serves only approved skills. A name
|
|
324
|
+
// that is REAL but unknown is not reinterpreted that way — it is the 404
|
|
325
|
+
// `readBranch` lets through. Naming the default branch is the same answer
|
|
326
|
+
// as naming nothing, mark included: there is nothing unmerged about it.
|
|
327
|
+
if (!named || named === this.kb.defaultBranch) return { catalog: await this.defaultCatalog() };
|
|
328
|
+
return { catalog: await this.readBranch(named), branch: named };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async defaultCatalog(): Promise<Catalog> {
|
|
226
332
|
const cached = this.cache.get();
|
|
227
333
|
if (cached) return cached;
|
|
228
|
-
// Token first: a merge's `invalidate()` can land while
|
|
334
|
+
// Token first: a merge's `invalidate()` can land while the scan is still
|
|
229
335
|
// reading the pre-merge tree, and storing that read afterwards would undo
|
|
230
336
|
// the drop for a full TTL.
|
|
231
337
|
const token = this.cache.begin();
|
|
232
|
-
const
|
|
233
|
-
this.cache.set(
|
|
234
|
-
return
|
|
338
|
+
const catalog = await this.readDefault();
|
|
339
|
+
this.cache.set(catalog, token);
|
|
340
|
+
return catalog;
|
|
235
341
|
}
|
|
236
342
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
343
|
+
/**
|
|
344
|
+
* The default branch's catalog. A workspace that cannot be resolved degrades
|
|
345
|
+
* to an empty one — the manual/tools must never break because skills can't
|
|
346
|
+
* be read. A NAMED branch does not degrade: see `readBranch`.
|
|
347
|
+
*/
|
|
348
|
+
private async readDefault(): Promise<Catalog> {
|
|
242
349
|
let wsId: string;
|
|
243
350
|
try {
|
|
244
351
|
wsId = (await this.workspaceService.getOrCreateForBranch(this.kb.defaultBranch)).id;
|
|
245
352
|
} catch {
|
|
246
|
-
return [];
|
|
353
|
+
return { wsId: this.kb.defaultWorkspaceId(), kbRoot: '', skills: [], digests: new Map() };
|
|
247
354
|
}
|
|
248
355
|
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
356
|
+
return { wsId, kbRoot, skills: await this.scanTree(kbRoot), digests: new Map() };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* One branch's clone, scanned. Deliberately NOT cached and deliberately NOT
|
|
361
|
+
* degraded:
|
|
362
|
+
* - no cache, because the caller of a branch read is usually the agent that
|
|
363
|
+
* just wrote the skill, and a minute-old answer would hide its own work;
|
|
364
|
+
* - no degrading, because a branch nobody ever pushed must answer the 404
|
|
365
|
+
* the file tools answer (`BranchNotFoundError`, naming the branch) — an
|
|
366
|
+
* empty list would read as "that branch has no skills", which is a
|
|
367
|
+
* different and untrue statement.
|
|
368
|
+
*
|
|
369
|
+
* Resolving the workspace clones the branch on first use, so a call naming a
|
|
370
|
+
* branch for the first time waits for a clone.
|
|
371
|
+
*/
|
|
372
|
+
private async readBranch(branch: string): Promise<Catalog> {
|
|
373
|
+
const wsId = (await this.workspaceService.getOrCreateForBranch(branch)).id;
|
|
374
|
+
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
375
|
+
return { wsId, kbRoot, skills: await this.scanTree(kbRoot), digests: new Map() };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Does this skill, as the branch has it, differ from the `released` copy —
|
|
380
|
+
* by the content of its FOLDER? It is absent from the released catalog (or
|
|
381
|
+
* listed there at another path), or one of the files the two catalogs list
|
|
382
|
+
* for it differs byte for byte.
|
|
383
|
+
*
|
|
384
|
+
* Reading both folders is the price of saying something true, and it is paid
|
|
385
|
+
* only on a branch read: a skill folder is instructions plus a few assets.
|
|
386
|
+
* A file that cannot be read on either side counts as a difference — unable
|
|
387
|
+
* to prove the branch's copy is the released one, the honest answer is that
|
|
388
|
+
* it is not approved.
|
|
389
|
+
*
|
|
390
|
+
* Both sides go through `digestOf`, so the RELEASED side is read once per
|
|
391
|
+
* cached catalog rather than once per skill per call: a listing of N skills
|
|
392
|
+
* used to re-hash all N released folders on every call, and each `getSkill`
|
|
393
|
+
* re-hashed one of them again.
|
|
394
|
+
*/
|
|
395
|
+
private async differs(released: Catalog, branchCatalog: Catalog, skill: ParsedSkill): Promise<boolean> {
|
|
396
|
+
const mirror = released.skills.find((s) => s.summary.name === skill.summary.name);
|
|
397
|
+
if (!mirror || mirror.summary.path !== skill.summary.path) return true;
|
|
398
|
+
const [onBranch, onDefault] = await Promise.all([
|
|
399
|
+
this.digestOf(branchCatalog, skill),
|
|
400
|
+
this.digestOf(released, mirror),
|
|
401
|
+
]);
|
|
402
|
+
return onBranch === null || onDefault === null || onBranch !== onDefault;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* One skill folder's digest in one catalog's clone, computed at most once
|
|
407
|
+
* for that catalog. The folder path is a unique key within a catalog — a
|
|
408
|
+
* folder holds one `SKILL.md`, so it yields one skill — and the file list
|
|
409
|
+
* the digest covers comes from that same catalog's scan.
|
|
410
|
+
*
|
|
411
|
+
* Memoizing means a released folder's ASSETS are now as stale as the rest
|
|
412
|
+
* of the catalog that names them: both are refreshed by the TTL and dropped
|
|
413
|
+
* together by `invalidate()` on a merge to the default branch. A branch
|
|
414
|
+
* catalog is built per call and so is its memo, which is what a draft read
|
|
415
|
+
* needs — the agent's own last write must show.
|
|
416
|
+
*/
|
|
417
|
+
private digestOf(catalog: Catalog, skill: ParsedSkill): Promise<string | null> {
|
|
418
|
+
const key = skill.summary.path;
|
|
419
|
+
const memo = catalog.digests.get(key);
|
|
420
|
+
if (memo) return memo;
|
|
421
|
+
// The promise, not the value: skills asked for at once share one read.
|
|
422
|
+
// A rejection is dropped rather than kept — `folderDigest` answers `null`
|
|
423
|
+
// for a file it cannot read, so a throw here is a defect, and holding it
|
|
424
|
+
// would answer with it for the catalog's whole life.
|
|
425
|
+
const pending = folderDigest(catalog.kbRoot, skill);
|
|
426
|
+
catalog.digests.set(key, pending);
|
|
427
|
+
pending.catch(() => catalog.digests.delete(key));
|
|
428
|
+
return pending;
|
|
429
|
+
}
|
|
249
430
|
|
|
431
|
+
/** Scan one clone's `Skills/` and `Plugins/` roots into a catalog's skills. */
|
|
432
|
+
private async scanTree(kbRoot: string): Promise<ParsedSkill[]> {
|
|
250
433
|
// Skills may be grouped in category subfolders (each carrying its own
|
|
251
434
|
// access.md), so a SKILL.md can live at any depth under either root. Walk
|
|
252
435
|
// the tree and treat every folder that directly contains a SKILL.md as a
|
|
@@ -344,6 +527,60 @@ export class SkillService implements ISkillService {
|
|
|
344
527
|
|
|
345
528
|
// --- helpers ------------------------------------------------------------------
|
|
346
529
|
|
|
530
|
+
/**
|
|
531
|
+
* The one line a skill read from an unmerged branch begins with. Exported
|
|
532
|
+
* because the tool surface documents it and the tests assert it: there is one
|
|
533
|
+
* sentence, in one place, and an agent that has read it once recognises it.
|
|
534
|
+
*/
|
|
535
|
+
export function unmergedSkillNotice(branch: string): string {
|
|
536
|
+
return `This skill is read from the unmerged branch "${branch}" and is not approved.`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Say, on the answer itself, that it came off a branch that changed this skill.
|
|
541
|
+
*
|
|
542
|
+
* The body gets the line because the body is what an agent reads and acts on;
|
|
543
|
+
* a bundled file gets the flags beside its content, never a sentence inside it
|
|
544
|
+
* (prepending prose to a script is a syntax error, not a warning).
|
|
545
|
+
*/
|
|
546
|
+
function asUnmerged(result: Extract<GetSkillResult, { ok: true }>, branch: string): GetSkillResult {
|
|
547
|
+
if (result.kind === 'file') {
|
|
548
|
+
return { ...result, file: { ...result.file, unmerged: true, branch } };
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
...result,
|
|
552
|
+
skill: {
|
|
553
|
+
...result.skill,
|
|
554
|
+
unmerged: true,
|
|
555
|
+
branch,
|
|
556
|
+
body: `${unmergedSkillNotice(branch)}\n\n${result.skill.body}`,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* A digest of a skill folder as its own catalog lists it: `SKILL.md` plus
|
|
563
|
+
* every bundled file, each hashed under its relative name so a renamed,
|
|
564
|
+
* added or dropped asset is a difference too — and so a file one branch's
|
|
565
|
+
* ignore rules hide is one as well. `null` when a listed file cannot be read,
|
|
566
|
+
* which the caller treats as "cannot prove it is the released copy".
|
|
567
|
+
*/
|
|
568
|
+
async function folderDigest(kbRoot: string, skill: ParsedSkill): Promise<string | null> {
|
|
569
|
+
if (!kbRoot) return null;
|
|
570
|
+
const folder = skill.summary.path;
|
|
571
|
+
const rels = ['SKILL.md', ...skill.files.map((f) => f.slice(folder.length + 1))].sort();
|
|
572
|
+
const hash = createHash('sha256');
|
|
573
|
+
for (const rel of rels) {
|
|
574
|
+
hash.update(`${rel}\0`);
|
|
575
|
+
try {
|
|
576
|
+
hash.update(await fs.readFile(path.join(kbRoot, folder, ...rel.split('/'))));
|
|
577
|
+
} catch {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return hash.digest('hex');
|
|
582
|
+
}
|
|
583
|
+
|
|
347
584
|
/**
|
|
348
585
|
* Skill names are folder names; reject anything that could escape the folder.
|
|
349
586
|
*
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import type { Router, RequestHandler } from 'express';
|
|
2
2
|
import type { IToolRegistry, UtcpTool } from '../tool-registry/tool.contract.js';
|
|
3
|
-
import type
|
|
3
|
+
import { ToolError, type ToolContext } from '../tool-helpers/tool.contract.js';
|
|
4
4
|
import { toolDef } from '../tool-helpers/tool-def.js';
|
|
5
5
|
import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
|
|
6
6
|
import type { ISkillService } from './skills.contract.js';
|
|
7
7
|
import type { IAllowedToolsChecker } from './allowed-tools-check.js';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The optional `branch` both skill tools declare. Optional on purpose, unlike
|
|
11
|
+
* the required `branch` of the KB file tools: a skill read is answered by the
|
|
12
|
+
* released catalog by default, and naming a branch is the caller asking for
|
|
13
|
+
* something nobody has approved yet.
|
|
14
|
+
*/
|
|
15
|
+
const BRANCH_SKILLS_INPUT = {
|
|
16
|
+
type: 'string' as const,
|
|
17
|
+
description:
|
|
18
|
+
'Optional: a draft branch to read the skills from instead of the released (default) branch — ' +
|
|
19
|
+
'the branch you are working on. You must be able to read the branch and the skill on it; ' +
|
|
20
|
+
'a branch that does not exist answers 404 naming it. Omit it for the approved skills.',
|
|
21
|
+
};
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* Registers the two skill tools (both surfaces) and hosts their endpoints.
|
|
11
25
|
*
|
|
@@ -14,6 +28,13 @@ import type { IAllowedToolsChecker } from './allowed-tools-check.js';
|
|
|
14
28
|
* what exists right in the tool catalog, no `list_skills` round-trip needed, and
|
|
15
29
|
* it auto-updates as the default-branch catalog changes. The hosted routes are
|
|
16
30
|
* static; only the description text is dynamic.
|
|
31
|
+
*
|
|
32
|
+
* Both tools take an optional `branch`. Without it they answer from the
|
|
33
|
+
* released (default-branch) catalog — what the descriptions name, what the
|
|
34
|
+
* browser menu lists and what the MCP prompt surface serves. With it they read
|
|
35
|
+
* the skills as they are on that draft and say which of them nobody has
|
|
36
|
+
* approved, so an agent can try the skill it just wrote without waiting for a
|
|
37
|
+
* merge.
|
|
17
38
|
*/
|
|
18
39
|
export function registerSkillsTools(
|
|
19
40
|
registry: IToolRegistry,
|
|
@@ -32,8 +53,8 @@ export function registerSkillsTools(
|
|
|
32
53
|
router.post(
|
|
33
54
|
'/agent/tools/list_skills',
|
|
34
55
|
toolAuth,
|
|
35
|
-
toolHandler(async (
|
|
36
|
-
skills: await skillService.listSkills(ctx.user.email),
|
|
56
|
+
toolHandler(async (args, ctx: ToolContext) => ({
|
|
57
|
+
skills: await skillService.listSkills(ctx.user.email, { branch: branchArg(args) }),
|
|
37
58
|
})),
|
|
38
59
|
);
|
|
39
60
|
|
|
@@ -44,14 +65,38 @@ export function registerSkillsTools(
|
|
|
44
65
|
const name = typeof args.name === 'string' ? args.name : '';
|
|
45
66
|
const file = typeof args.file === 'string' ? args.file : undefined;
|
|
46
67
|
const version = typeof args.version === 'string' ? args.version : undefined;
|
|
68
|
+
const branch = branchArg(args);
|
|
47
69
|
if (!name) return { error: 'missing_name' };
|
|
48
|
-
|
|
70
|
+
// Refused rather than ranked: a `version` is a point in the released
|
|
71
|
+
// skill's history, a `branch` is a draft nobody has released — asked for
|
|
72
|
+
// together, neither answer is the one the caller meant, and guessing
|
|
73
|
+
// would serve instructions under a label that does not describe them.
|
|
74
|
+
if (branch !== undefined && version !== undefined && version.trim().length > 0) {
|
|
75
|
+
throw new ToolError(
|
|
76
|
+
'get_skill takes `branch` or `version`, not both: `branch` loads the skill as that draft has it ' +
|
|
77
|
+
'now, `version` loads a version the released skill declared. Pass one of them.',
|
|
78
|
+
400,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const result = await skillService.getSkill(ctx.user.email, name, file, { version, branch });
|
|
49
82
|
if (!allowedTools || !result.ok || result.kind !== 'skill') return result;
|
|
50
83
|
return { ...result, warnings: await allowedTools.check(ctx.user.email, result.skill.allowedTools) };
|
|
51
84
|
}),
|
|
52
85
|
);
|
|
53
86
|
}
|
|
54
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The `branch` a tool call named, or undefined when it named none (the
|
|
90
|
+
* released catalog). A non-string or blank value is NOT a branch: it is taken
|
|
91
|
+
* as absent, which answers from the default branch — the approved one. The
|
|
92
|
+
* resolution of a real name, and the 404 for one the platform never heard of,
|
|
93
|
+
* belong to the workspace layer, exactly as on the file tools.
|
|
94
|
+
*/
|
|
95
|
+
function branchArg(args: Record<string, unknown>): string | undefined {
|
|
96
|
+
const branch = args.branch;
|
|
97
|
+
return typeof branch === 'string' && branch.trim().length > 0 ? branch : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
/** "Currently available skills: `a`, `b`." (or a no-skills note), filtered to what the caller may read. */
|
|
56
101
|
async function availableSkillsLine(skillService: ISkillService, userEmail?: string): Promise<string> {
|
|
57
102
|
const skills = await skillService.listSkills(userEmail);
|
|
@@ -67,9 +112,16 @@ async function buildListSkillsDef(skillService: ISkillService, userEmail?: strin
|
|
|
67
112
|
'for a skill that declares one, its current `version` (its SKILL.md `metadata.version`, else a ' +
|
|
68
113
|
'top-level `version`, else `lifecycle.version`). ' +
|
|
69
114
|
'Discover what skills exist before specialist work, then `get_skill` to load one. ' +
|
|
115
|
+
'Pass `branch` to list the skills as they are on a draft branch instead of the released set — ' +
|
|
116
|
+
'what you need to try a skill you just wrote there; each skill that differs from the released ' +
|
|
117
|
+
'one comes back with `unmerged: true` and that branch, meaning nobody has approved it. ' +
|
|
70
118
|
(await availableSkillsLine(skillService, userEmail)),
|
|
71
119
|
path: '/api/agent/tools/list_skills',
|
|
72
|
-
inputs: {
|
|
120
|
+
inputs: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: { branch: BRANCH_SKILLS_INPUT },
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
},
|
|
73
125
|
outputs: {
|
|
74
126
|
type: 'object',
|
|
75
127
|
properties: {
|
|
@@ -88,6 +140,13 @@ async function buildListSkillsDef(skillService: ISkillService, userEmail?: strin
|
|
|
88
140
|
'else `lifecycle.version` in its SKILL.md); absent when it declares none.',
|
|
89
141
|
},
|
|
90
142
|
path: { type: 'string' },
|
|
143
|
+
unmerged: {
|
|
144
|
+
type: 'boolean',
|
|
145
|
+
description:
|
|
146
|
+
'Only when `branch` was passed, and only on a skill that differs from the released one ' +
|
|
147
|
+
'(it exists only on that branch, or was changed there): nobody has approved what it says.',
|
|
148
|
+
},
|
|
149
|
+
branch: { type: 'string', description: 'With `unmerged`: the branch the skill was read from.' },
|
|
91
150
|
},
|
|
92
151
|
},
|
|
93
152
|
},
|
|
@@ -104,7 +163,7 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
|
|
|
104
163
|
'Load a skill by name: returns its full instructions (SKILL.md body) to follow, plus the skill ' +
|
|
105
164
|
'folder path and the list of bundled files. Pass `file` to fetch a bundled file’s content ' +
|
|
106
165
|
'(e.g. a script) instead of the body. Loads the latest copy unless `version` names an earlier ' +
|
|
107
|
-
'one the skill declared. ' +
|
|
166
|
+
'one the skill declared, or `branch` names a draft to load it from. ' +
|
|
108
167
|
(await availableSkillsLine(skillService, userEmail)),
|
|
109
168
|
path: '/api/agent/tools/get_skill',
|
|
110
169
|
inputs: {
|
|
@@ -126,6 +185,7 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
|
|
|
126
185
|
'recent commit that declared that version; a version the skill never declared answers ' +
|
|
127
186
|
'`version_not_found` with the versions it did declare.',
|
|
128
187
|
},
|
|
188
|
+
branch: BRANCH_SKILLS_INPUT,
|
|
129
189
|
},
|
|
130
190
|
required: ['name'],
|
|
131
191
|
additionalProperties: false,
|
|
@@ -134,8 +194,20 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
|
|
|
134
194
|
type: 'object',
|
|
135
195
|
description: 'On success carries `skill` (or `file` when `file` was passed); on failure carries `error`.',
|
|
136
196
|
properties: {
|
|
137
|
-
skill: {
|
|
138
|
-
|
|
197
|
+
skill: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
description:
|
|
200
|
+
'The loaded skill: name, description, body, files, …. Read from a `branch` that changed it, ' +
|
|
201
|
+
'it also carries `unmerged: true` and that branch, and its `body` BEGINS with one line ' +
|
|
202
|
+
'saying the skill comes from that unmerged branch and is not approved — treat it as a ' +
|
|
203
|
+
'proposal you are testing, not as approved instructions.',
|
|
204
|
+
},
|
|
205
|
+
file: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
description:
|
|
208
|
+
'A bundled file: name, file, path, content — plus `unmerged` and `branch` when it came off ' +
|
|
209
|
+
'a branch that changed the skill (the note stays beside the content, never inside it).',
|
|
210
|
+
},
|
|
139
211
|
warnings: {
|
|
140
212
|
type: 'array',
|
|
141
213
|
description:
|
|
@@ -8,6 +8,7 @@ import { InternalTokenService } from '../internal-token.service.js';
|
|
|
8
8
|
import { createManualAuthMiddleware } from '../tool-auth.middleware.js';
|
|
9
9
|
import type { IExternalApiKeyService } from '../external-api-key.interface.js';
|
|
10
10
|
import type { AuthService } from '../../auth/auth.service.js';
|
|
11
|
+
import { AuthBackendError } from '../../auth/account-admission.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The manual endpoints (`/agent/utcp`, `/agent/internal/utcp`) accept a browser
|
|
@@ -17,6 +18,8 @@ import type { AuthService } from '../../auth/auth.service.js';
|
|
|
17
18
|
*/
|
|
18
19
|
const KEY = 'bevel_validkey';
|
|
19
20
|
const JWT = 'browser-jwt';
|
|
21
|
+
/** A valid session, presented while its account cannot be looked up. */
|
|
22
|
+
const JWT_DURING_OUTAGE = 'browser-jwt-during-outage';
|
|
20
23
|
|
|
21
24
|
const fakeExternalApiKeys = {
|
|
22
25
|
looksLikeExternalApiKey: (t: string) => typeof t === 'string' && t.startsWith('bevel_'),
|
|
@@ -25,10 +28,12 @@ const fakeExternalApiKeys = {
|
|
|
25
28
|
} as unknown as IExternalApiKeyService;
|
|
26
29
|
|
|
27
30
|
const fakeAuth = {
|
|
28
|
-
|
|
31
|
+
resolveSession: async (t: string) => {
|
|
29
32
|
if (t === JWT) return { userId: 'u-jwt', email: 'jwt@x' };
|
|
33
|
+
if (t === JWT_DURING_OUTAGE) throw new AuthBackendError(new Error('the database is down'));
|
|
30
34
|
throw new Error('bad jwt');
|
|
31
35
|
},
|
|
36
|
+
isActive: async () => true,
|
|
32
37
|
} as unknown as AuthService;
|
|
33
38
|
|
|
34
39
|
let server: HttpServer | undefined;
|
|
@@ -75,6 +80,14 @@ describe('createManualAuthMiddleware (read-only manual endpoints)', () => {
|
|
|
75
80
|
expect((await get(base, '/api/agent/utcp', 'garbage')).status).toBe(401);
|
|
76
81
|
});
|
|
77
82
|
|
|
83
|
+
it('answers 500, not 401, when the session’s account cannot be looked up', async () => {
|
|
84
|
+
const { base } = await start();
|
|
85
|
+
const r = await get(base, '/api/agent/utcp', JWT_DURING_OUTAGE);
|
|
86
|
+
// A 401 would sign a valid caller out over an outage.
|
|
87
|
+
expect(r.status).toBe(500);
|
|
88
|
+
expect(r.headers.get('www-authenticate')).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
78
91
|
it('refuses the INTERNAL manual to a browser JWT (403) but allows an internal token', async () => {
|
|
79
92
|
const { base, intToken } = await start();
|
|
80
93
|
expect((await get(base, '/api/agent/internal/utcp', JWT)).status).toBe(403);
|