@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
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ontology-session boundary — the touched-set store (Postgres + cache).
|
|
3
|
-
*
|
|
4
|
-
* Each session accumulates the SET of named ontologies it has touched. The gate
|
|
5
|
-
* calls `checkOperation` for every gated agent tool call:
|
|
6
|
-
*
|
|
7
|
-
* READ (isWrite=false) → always allowed; RECORD the touched ontology so it
|
|
8
|
-
* can poison later writes.
|
|
9
|
-
* WRITE (isWrite=true) → allowed only while the session is confined to a
|
|
10
|
-
* single named ontology equal to the target; once the
|
|
11
|
-
* session has touched ≥2 ontologies, every write is
|
|
12
|
-
* blocked. An allowed write also records its ontology.
|
|
13
|
-
*
|
|
14
|
-
* Neutral operations (`ontologyOf` → null) never record and never block.
|
|
15
|
-
*
|
|
16
|
-
* ┌── checkOperation(sessionId, wsPath, isWrite) ───────────────────────────┐
|
|
17
|
-
* │ ont = ontologyOf(wsPath) │
|
|
18
|
-
* │ ont === null → { allow:true } (neutral; no record) │
|
|
19
|
-
* │ touched = loadTouched(sessionId) (cache, else SELECT) │
|
|
20
|
-
* │ WRITE → decideWrite(touched, ont); if blocked, return block (no record) │
|
|
21
|
-
* │ record(sessionId, ont) (idempotent INSERT; update cache) │
|
|
22
|
-
* │ return { allow:true } │
|
|
23
|
-
* └──────────────────────────────────────────────────────────────────────────┘
|
|
24
|
-
*
|
|
25
|
-
* Postgres is the source of truth so the boundary survives a backend restart.
|
|
26
|
-
* The cache holds each session's touched set; a miss re-reads PG. See
|
|
27
|
-
* `docs/rp4-ontology-boundary-plan-v2.md`.
|
|
28
|
-
*
|
|
29
|
-
* Eviction: `delete(sessionId)` at run end is the PRIMARY reclamation;
|
|
30
|
-
* `sweepOlderThan` is the abandoned-run backstop; the MCP layer also calls
|
|
31
|
-
* `delete` from its `onEvict` hook.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { eq, lt } from 'drizzle-orm';
|
|
35
|
-
import { ontologyOf } from '../../shared/kb-layout.js';
|
|
36
|
-
import type { KbContext } from '../../shared/kb-context.js';
|
|
37
|
-
|
|
38
|
-
import type { Database } from '../database/connection.js';
|
|
39
|
-
import { sessionOntologyTouches } from '../database/schema.js';
|
|
40
|
-
import { recordTouch, decideWrite, type WriteDecision } from './session-ontology.policy.js';
|
|
41
|
-
import { WorkspaceMutex } from '../kb-fs/mutex.js';
|
|
42
|
-
|
|
43
|
-
export type OperationDecision =
|
|
44
|
-
| { allow: true; touched: string[] }
|
|
45
|
-
| { allow: false; reason: 'multi-ontology' | 'different-ontology'; touched: string[]; attempted: string };
|
|
46
|
-
|
|
47
|
-
export interface ISessionOntologyService {
|
|
48
|
-
/**
|
|
49
|
-
* Evaluate and record a gated agent operation. Reads always allow (and
|
|
50
|
-
* record); writes allow only while confined to a single ontology. Neutral
|
|
51
|
-
* paths always allow and record nothing.
|
|
52
|
-
*/
|
|
53
|
-
checkOperation(sessionId: string, wsPath: string, isWrite: boolean, kbDirName?: string): Promise<OperationDecision>;
|
|
54
|
-
/** Evaluate/record against an already-resolved ontology (used by graph tools). */
|
|
55
|
-
checkOntology(sessionId: string, ontology: string | null, isWrite: boolean): Promise<OperationDecision>;
|
|
56
|
-
/** The set of ontologies a session has touched. */
|
|
57
|
-
getTouched(sessionId: string): Promise<string[]>;
|
|
58
|
-
/** Reclaim a session's touched set (run end / MCP onEvict). */
|
|
59
|
-
delete(sessionId: string): Promise<void>;
|
|
60
|
-
/** Backstop: drop touch rows older than `cutoff` (abandoned runs). */
|
|
61
|
-
sweepOlderThan(cutoff: Date): Promise<number>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class SessionOntologyService implements ISessionOntologyService {
|
|
65
|
-
/**
|
|
66
|
-
* Write-through cache of each session's touched-ontology set. A present entry
|
|
67
|
-
* is authoritative for the session (we only ever add to it in-process); a
|
|
68
|
-
* miss re-reads Postgres. Keyed by the agent thread / sessionId.
|
|
69
|
-
*/
|
|
70
|
-
private readonly cache = new Map<string, Set<string>>();
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Serializes each session's read→decide→record path. Without it, two
|
|
74
|
-
* concurrent same-session writes (parallel tool calls in one turn) could both
|
|
75
|
-
* observe a singleton/empty touched set and both be allowed to different
|
|
76
|
-
* ontologies before either INSERT lands — defeating the boundary. Different
|
|
77
|
-
* sessions never contend.
|
|
78
|
-
*/
|
|
79
|
-
private readonly locks = new WorkspaceMutex();
|
|
80
|
-
|
|
81
|
-
constructor(
|
|
82
|
-
private readonly db: Database,
|
|
83
|
-
/** Names the knowledge-base root a path is classified under; read per call. */
|
|
84
|
-
private readonly kb: Pick<KbContext, 'layout'>,
|
|
85
|
-
) {}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Resolve `wsPath` to its ontology and evaluate/record the operation. Thin
|
|
89
|
-
* wrapper over {@link checkOntology} for the common path-based call site.
|
|
90
|
-
*/
|
|
91
|
-
async checkOperation(
|
|
92
|
-
sessionId: string,
|
|
93
|
-
wsPath: string,
|
|
94
|
-
isWrite: boolean,
|
|
95
|
-
kbDirName?: string,
|
|
96
|
-
): Promise<OperationDecision> {
|
|
97
|
-
return this.checkOntology(sessionId, ontologyOf(wsPath, this.kb.layout, kbDirName), isWrite);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Evaluate (and, when allowed, record) an operation against an
|
|
102
|
-
* already-resolved ontology. Neutral ops always allow and record nothing;
|
|
103
|
-
* reads always allow and record; writes allow only while the session stays
|
|
104
|
-
* confined to the single target ontology (see {@link decideWrite}). A blocked
|
|
105
|
-
* write records nothing.
|
|
106
|
-
*/
|
|
107
|
-
async checkOntology(sessionId: string, ontology: string | null, isWrite: boolean): Promise<OperationDecision> {
|
|
108
|
-
// Neutral operation: always allowed, records nothing. No state mutation, so
|
|
109
|
-
// it needs no per-session serialization.
|
|
110
|
-
if (ontology === null) {
|
|
111
|
-
const touched = await this.loadTouched(sessionId);
|
|
112
|
-
return { allow: true, touched: [...touched].sort() };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Serialize the decide+record path per session so concurrent same-session
|
|
116
|
-
// operations can't both pass the boundary on a stale touched set.
|
|
117
|
-
return this.locks.run(sessionId, async () => {
|
|
118
|
-
const touched = await this.loadTouched(sessionId);
|
|
119
|
-
|
|
120
|
-
if (isWrite) {
|
|
121
|
-
const decision: WriteDecision = decideWrite(touched, ontology);
|
|
122
|
-
if (!decision.allow) {
|
|
123
|
-
return {
|
|
124
|
-
allow: false,
|
|
125
|
-
reason: decision.reason,
|
|
126
|
-
touched: decision.touched,
|
|
127
|
-
attempted: decision.attempted,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Allowed (read, or a write confined to one ontology): record the touch.
|
|
133
|
-
await this.record(sessionId, ontology, touched);
|
|
134
|
-
return { allow: true, touched: [...recordTouch(touched, ontology)].sort() };
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** The sorted set of named ontologies this session has touched so far. */
|
|
139
|
-
async getTouched(sessionId: string): Promise<string[]> {
|
|
140
|
-
return [...(await this.loadTouched(sessionId))].sort();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Reclaim a session's touched set from both the cache and Postgres (run end / MCP onEvict). */
|
|
144
|
-
async delete(sessionId: string): Promise<void> {
|
|
145
|
-
this.cache.delete(sessionId);
|
|
146
|
-
await this.db.delete(sessionOntologyTouches).where(eq(sessionOntologyTouches.sessionId, sessionId));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Backstop sweep: drop touch rows older than `cutoff` (abandoned runs) and return how many were removed. */
|
|
150
|
-
async sweepOlderThan(cutoff: Date): Promise<number> {
|
|
151
|
-
const removed = await this.db
|
|
152
|
-
.delete(sessionOntologyTouches)
|
|
153
|
-
.where(lt(sessionOntologyTouches.touchedAt, cutoff))
|
|
154
|
-
.returning({ sessionId: sessionOntologyTouches.sessionId });
|
|
155
|
-
// A swept session may have other (newer) rows; drop its cache entry so the
|
|
156
|
-
// next op re-reads the authoritative remaining set from Postgres.
|
|
157
|
-
for (const r of removed) this.cache.delete(r.sessionId);
|
|
158
|
-
return removed.length;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Load a session's touched set from cache, else from Postgres (and cache it). */
|
|
162
|
-
private async loadTouched(sessionId: string): Promise<Set<string>> {
|
|
163
|
-
const cached = this.cache.get(sessionId);
|
|
164
|
-
if (cached) return cached;
|
|
165
|
-
const rows = await this.db
|
|
166
|
-
.select({ ontology: sessionOntologyTouches.ontology })
|
|
167
|
-
.from(sessionOntologyTouches)
|
|
168
|
-
.where(eq(sessionOntologyTouches.sessionId, sessionId));
|
|
169
|
-
const set = new Set(rows.map((r) => r.ontology));
|
|
170
|
-
this.cache.set(sessionId, set);
|
|
171
|
-
return set;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Record a touched ontology (idempotent) and update the cache. */
|
|
175
|
-
private async record(sessionId: string, ontology: string, current: Set<string>): Promise<void> {
|
|
176
|
-
if (current.has(ontology)) return; // already recorded; no DB write needed
|
|
177
|
-
await this.db
|
|
178
|
-
.insert(sessionOntologyTouches)
|
|
179
|
-
.values({ sessionId, ontology })
|
|
180
|
-
.onConflictDoNothing({ target: [sessionOntologyTouches.sessionId, sessionOntologyTouches.ontology] });
|
|
181
|
-
current.add(ontology);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
recordOntologyRead,
|
|
5
|
-
assertOntologyWriteAllowed,
|
|
6
|
-
assertShellAllowedWithinOntology,
|
|
7
|
-
MissingSessionError,
|
|
8
|
-
type SessionOntologyGate,
|
|
9
|
-
} from '../session-ontology.gate.js';
|
|
10
|
-
import type { ToolContext } from '../../tool-helpers/tool.contract.js';
|
|
11
|
-
import type { ISessionOntologyService, OperationDecision } from '../../workflow/session-ontology.service.js';
|
|
12
|
-
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
13
|
-
import { ontologyOf } from '../../../shared/kb-layout.js';
|
|
14
|
-
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
15
|
-
|
|
16
|
-
const KB = 'knowledge-base';
|
|
17
|
-
const kbContext = testKbContext({ kbDirName: KB });
|
|
18
|
-
const P_FILE = 'KnowledgeBase/Product/Knowledge/Foo.md';
|
|
19
|
-
const PL_FILE = 'KnowledgeBase/Platform/Knowledge/Bar.md';
|
|
20
|
-
const NEUTRAL = 'access.md';
|
|
21
|
-
const RECOVERY_EMAIL = 'recovery-bot@bevel.local';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* In-memory stand-in for the touched-set service — applies the real
|
|
25
|
-
* reads-record / writes-block-once-multi rule against a per-session set, so the
|
|
26
|
-
* gate's interaction with it is exercised end to end without a DB.
|
|
27
|
-
*/
|
|
28
|
-
function makeFakeService(): ISessionOntologyService {
|
|
29
|
-
const touched = new Map<string, Set<string>>();
|
|
30
|
-
const setFor = (s: string) => touched.get(s) ?? new Set<string>();
|
|
31
|
-
return {
|
|
32
|
-
async checkOperation(sessionId, wsPath, isWrite): Promise<OperationDecision> {
|
|
33
|
-
const ont = ontologyOf(wsPath, kbContext.layout, KB);
|
|
34
|
-
const set = setFor(sessionId);
|
|
35
|
-
if (ont === null) return { allow: true, touched: [...set].sort() };
|
|
36
|
-
if (isWrite) {
|
|
37
|
-
const spanned = new Set(set).add(ont);
|
|
38
|
-
if (spanned.size > 1) {
|
|
39
|
-
return {
|
|
40
|
-
allow: false,
|
|
41
|
-
reason: set.size >= 2 ? 'multi-ontology' : 'different-ontology',
|
|
42
|
-
touched: [...set].sort(),
|
|
43
|
-
attempted: ont,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
set.add(ont);
|
|
48
|
-
touched.set(sessionId, set);
|
|
49
|
-
return { allow: true, touched: [...set].sort() };
|
|
50
|
-
},
|
|
51
|
-
checkOntology: async () => ({ allow: true, touched: [] }),
|
|
52
|
-
getTouched: async (s) => [...setFor(s)].sort(),
|
|
53
|
-
delete: async (s) => { touched.delete(s); },
|
|
54
|
-
sweepOlderThan: async () => 0,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Build a gate wired the way the CORE composition wires it: the fake
|
|
60
|
-
* touched-set service plus an EMPTY hooks registry. Core registers no
|
|
61
|
-
* `preWrite` hook, so the gate only TRACKS touches and never blocks — the
|
|
62
|
-
* blocking assertions (the ontology-block hook the enterprise composition
|
|
63
|
-
* root registers) live with that hook in the enterprise repo.
|
|
64
|
-
*/
|
|
65
|
-
function makeGate(): SessionOntologyGate {
|
|
66
|
-
const service = makeFakeService();
|
|
67
|
-
const hooks = new WorkflowHooks();
|
|
68
|
-
return { service, enabled: true, kb: kbContext, recoveryBotEmail: RECOVERY_EMAIL, hooks };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function ctx(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
72
|
-
return {
|
|
73
|
-
user: { id: 'u1', email: 'dev@bevel.local', name: 'Dev' },
|
|
74
|
-
scope: 'write',
|
|
75
|
-
source: 'internal',
|
|
76
|
-
sessionId: 's1',
|
|
77
|
-
abortSignal: new AbortController().signal,
|
|
78
|
-
workspaceService: {} as never,
|
|
79
|
-
workflowService: {} as never,
|
|
80
|
-
events: {} as never,
|
|
81
|
-
getFilesystem: async () => ({} as never),
|
|
82
|
-
...overrides,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
describe('recordOntologyRead', () => {
|
|
87
|
-
let gate: SessionOntologyGate;
|
|
88
|
-
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
gate = makeGate();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('does nothing when the flag is disabled', async () => {
|
|
94
|
-
gate.enabled = false;
|
|
95
|
-
await expect(recordOntologyRead(gate, ctx(), P_FILE)).resolves.toBeUndefined();
|
|
96
|
-
expect(await gate.service.getTouched('s1')).toEqual([]);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('skips neutral paths (no record)', async () => {
|
|
100
|
-
await expect(recordOntologyRead(gate, ctx(), NEUTRAL)).resolves.toBeUndefined();
|
|
101
|
-
expect(await gate.service.getTouched('s1')).toEqual([]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('skips non-agent (browser session) callers', async () => {
|
|
105
|
-
await expect(
|
|
106
|
-
recordOntologyRead(gate, ctx({ source: 'session', sessionId: undefined }), P_FILE),
|
|
107
|
-
).resolves.toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('skips the recovery bot by identity', async () => {
|
|
111
|
-
await expect(
|
|
112
|
-
recordOntologyRead(
|
|
113
|
-
gate,
|
|
114
|
-
ctx({ user: { id: 'bot', email: 'Recovery-Bot@Bevel.Local', name: 'bot' }, sessionId: undefined }),
|
|
115
|
-
PL_FILE,
|
|
116
|
-
),
|
|
117
|
-
).resolves.toBeUndefined();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('fails closed when an agent read of a named path has no session id', async () => {
|
|
121
|
-
await expect(recordOntologyRead(gate, ctx({ sessionId: undefined }), P_FILE)).rejects.toBeInstanceOf(
|
|
122
|
-
MissingSessionError,
|
|
123
|
-
);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('allows a neutral read without a session id', async () => {
|
|
127
|
-
await expect(recordOntologyRead(gate, ctx({ sessionId: undefined }), NEUTRAL)).resolves.toBeUndefined();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('records a read and allows it', async () => {
|
|
131
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
132
|
-
expect(await gate.service.getTouched('s1')).toEqual(['KnowledgeBase/Product']);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('allows reads across ontologies (recording both)', async () => {
|
|
136
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
137
|
-
await expect(recordOntologyRead(gate, ctx(), PL_FILE)).resolves.toBeUndefined();
|
|
138
|
-
expect(await gate.service.getTouched('s1')).toEqual(['KnowledgeBase/Platform', 'KnowledgeBase/Product']);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('never blocks — a read into a second ontology still passes (delete_file relies on this)', async () => {
|
|
142
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
143
|
-
// A delete_file in a different ontology records its touch but never throws.
|
|
144
|
-
await expect(recordOntologyRead(gate, ctx(), PL_FILE)).resolves.toBeUndefined();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('assertOntologyWriteAllowed', () => {
|
|
149
|
-
let gate: SessionOntologyGate;
|
|
150
|
-
|
|
151
|
-
beforeEach(() => {
|
|
152
|
-
gate = makeGate();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('does nothing when the flag is disabled', async () => {
|
|
156
|
-
gate.enabled = false;
|
|
157
|
-
// Even a cross-ontology write must pass when disabled.
|
|
158
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
159
|
-
await expect(assertOntologyWriteAllowed(gate, ctx(), PL_FILE)).resolves.toBeUndefined();
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('skips neutral paths (no record, no block)', async () => {
|
|
163
|
-
await expect(assertOntologyWriteAllowed(gate, ctx(), NEUTRAL)).resolves.toBeUndefined();
|
|
164
|
-
expect(await gate.service.getTouched('s1')).toEqual([]);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('fails closed when an agent write of a named path has no session id', async () => {
|
|
168
|
-
await expect(assertOntologyWriteAllowed(gate, ctx({ sessionId: undefined }), P_FILE)).rejects.toBeInstanceOf(
|
|
169
|
-
MissingSessionError,
|
|
170
|
-
);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('allows a write confined to the one touched ontology', async () => {
|
|
174
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
175
|
-
await expect(assertOntologyWriteAllowed(gate, ctx(), P_FILE)).resolves.toBeUndefined();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// NOTE (core split): the "blocks a write to a different ontology" and
|
|
179
|
-
// "blocks ALL writes once the session has read across two ontologies" cases
|
|
180
|
-
// moved with the ontology-block hook (`registerOntologyBlockHook`) to the
|
|
181
|
-
// enterprise repo — core registers no preWrite hook, so a core-only
|
|
182
|
-
// deployment TRACKS but never blocks. The tests below pin exactly that.
|
|
183
|
-
|
|
184
|
-
it('core: allows a write to a different ontology (tracking only, no block hook)', async () => {
|
|
185
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
186
|
-
await expect(assertOntologyWriteAllowed(gate, ctx(), PL_FILE)).resolves.toBeUndefined();
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('core: still allows writes after reading across two ontologies (no block hook)', async () => {
|
|
190
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
191
|
-
await recordOntologyRead(gate, ctx(), PL_FILE);
|
|
192
|
-
await expect(assertOntologyWriteAllowed(gate, ctx(), P_FILE)).resolves.toBeUndefined();
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('assertShellAllowedWithinOntology (shell / no-path tools)', () => {
|
|
197
|
-
let gate: SessionOntologyGate;
|
|
198
|
-
|
|
199
|
-
beforeEach(() => {
|
|
200
|
-
gate = makeGate();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('allows a fresh session (nothing touched)', async () => {
|
|
204
|
-
await expect(assertShellAllowedWithinOntology(gate, ctx())).resolves.toBeUndefined();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('allows a session confined to a single ontology', async () => {
|
|
208
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
209
|
-
await expect(assertShellAllowedWithinOntology(gate, ctx())).resolves.toBeUndefined();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// NOTE (core split): the "blocks once the session has crossed into two
|
|
213
|
-
// ontologies" case moved with the enterprise ontology-block hook. Core
|
|
214
|
-
// registers no preWrite hook, so shell is never refused here.
|
|
215
|
-
it('core: allows shell even after crossing into two ontologies (no block hook)', async () => {
|
|
216
|
-
await recordOntologyRead(gate, ctx(), P_FILE);
|
|
217
|
-
await recordOntologyRead(gate, ctx(), PL_FILE);
|
|
218
|
-
await expect(assertShellAllowedWithinOntology(gate, ctx())).resolves.toBeUndefined();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('does nothing when the flag is disabled', async () => {
|
|
222
|
-
gate.enabled = false;
|
|
223
|
-
await gate.service.checkOperation('s1', P_FILE, false);
|
|
224
|
-
await gate.service.checkOperation('s1', PL_FILE, false);
|
|
225
|
-
await expect(assertShellAllowedWithinOntology(gate, ctx())).resolves.toBeUndefined();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('exempts the recovery bot', async () => {
|
|
229
|
-
await gate.service.checkOperation('s1', P_FILE, false);
|
|
230
|
-
await gate.service.checkOperation('s1', PL_FILE, false);
|
|
231
|
-
await expect(
|
|
232
|
-
assertShellAllowedWithinOntology(gate, ctx({ user: { id: 'bot', email: RECOVERY_EMAIL, name: 'bot' } })),
|
|
233
|
-
).resolves.toBeUndefined();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('allows when there is no session id (nothing to evaluate)', async () => {
|
|
237
|
-
await expect(assertShellAllowedWithinOntology(gate, ctx({ sessionId: undefined }))).resolves.toBeUndefined();
|
|
238
|
-
});
|
|
239
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ontology-session boundary — the per-call gate used by the agent tool handlers.
|
|
3
|
-
*
|
|
4
|
-
* One decision fn, called by every gated file/graph handler:
|
|
5
|
-
* READS (isWrite=false) record the touched ontology and always allow.
|
|
6
|
-
* WRITES (isWrite=true) run the registered BLOCKING `preWrite` hooks
|
|
7
|
-
* (`WorkflowHooks`), then record. The core registers
|
|
8
|
-
* no hook, so core alone only TRACKS; the enterprise
|
|
9
|
-
* overlay registers the ontology block (see
|
|
10
|
-
* `modules/kb/ontology-block.hook.ts`), restoring
|
|
11
|
-
* "allowed only while the session is confined to a
|
|
12
|
-
* single ontology; once it has touched ≥2, all
|
|
13
|
-
* writes are blocked".
|
|
14
|
-
*
|
|
15
|
-
* Skips entirely when: the flag is off, the path is neutral (`ontologyOf` →
|
|
16
|
-
* null), the caller is not an agent, or the caller is the recovery bot. Fails
|
|
17
|
-
* CLOSED for an agent named-path operation that carries no `sessionId`.
|
|
18
|
-
*
|
|
19
|
-
* Lives beside `assertCanRead` in the workspace tools — it does NOT touch
|
|
20
|
-
* `acquireLock` (that gate is protected-branch-only; this boundary applies on
|
|
21
|
-
* all branches and to reads). See `docs/rp4-ontology-boundary-plan-v2.md`.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { ontologyOf } from '../../shared/kb-layout.js';
|
|
25
|
-
import type { KbContext } from '../../shared/kb-context.js';
|
|
26
|
-
import type { JsonSchema } from '../tool-registry/tool.contract.js';
|
|
27
|
-
import { ToolError, type ToolContext } from '../tool-helpers/tool.contract.js';
|
|
28
|
-
import type { ISessionOntologyService } from '../workflow/session-ontology.service.js';
|
|
29
|
-
import type { WorkflowHooks } from '../workflow/workflow-hooks.js';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The `sessionId` input the ontology-session boundary reads: the agent run /
|
|
33
|
-
* conversation id. The in-process agent supplies its thread id automatically
|
|
34
|
-
* (via its per-run internal token); an external agent has no ambient run id,
|
|
35
|
-
* so it must first mint one with the `start_session` tool and then pass it
|
|
36
|
-
* EXPLICITLY on every gated call — the MCP proxy is a pure passthrough and
|
|
37
|
-
* never injects args.
|
|
38
|
-
*/
|
|
39
|
-
export const SESSION_ID_INPUT: JsonSchema = {
|
|
40
|
-
type: 'string',
|
|
41
|
-
description:
|
|
42
|
-
'The KnowledgeBase session id that scopes this run to one ontology. External agents (direct MCP calls and `call_tool_chain` alike): call the `start_session` tool ONCE at the very start of your work to obtain it, then pass that id here on EVERY gated call. The in-process agent runtime supplies this automatically (do not set it yourself).',
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Boundary warning appended to ontology-gated tool descriptions so the agent
|
|
47
|
-
* knows the restriction before it calls one. Action-then-why phrasing per
|
|
48
|
-
* docs/glossary.md.
|
|
49
|
-
*/
|
|
50
|
-
export const ONTOLOGY_BOUNDARY_NOTE =
|
|
51
|
-
' Stay within one KnowledgeBase ontology per conversation: once this run has read or written files in two different ontologies, every later content write (write_file/edit_file/mkdir/unzip, and both endpoints of move_file/copy_file) is blocked with a 403 and no override. Reads, delete_file and delete_folder never block — but each one records the ontology it touched and can poison a later content write, so do broad cross-ontology exploration only when you do not intend to write. Note move_file/copy_file are blocked outright when source and destination are different ontologies, since they would carry content across.';
|
|
52
|
-
|
|
53
|
-
/** Thrown when a write is refused because the session has crossed ontologies. */
|
|
54
|
-
export class OntologyWriteBlockedError extends ToolError {
|
|
55
|
-
constructor(touched: string[], attempted: string, reason: 'multi-ontology' | 'different-ontology') {
|
|
56
|
-
const msg =
|
|
57
|
-
reason === 'multi-ontology'
|
|
58
|
-
? `This conversation has already read across multiple ontologies (${touched.join(', ')}), so writing is no longer allowed. Start a new conversation to write to "${attempted}".`
|
|
59
|
-
: `This conversation is working in ${touched.join(', ')}, so it can't write to a different ontology ("${attempted}"). Start a new conversation to work in "${attempted}".`;
|
|
60
|
-
super(msg, 403);
|
|
61
|
-
this.name = 'OntologyWriteBlockedError';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Thrown when an agent op on a named ontology carries no session id (fail closed). */
|
|
66
|
-
export class MissingSessionError extends ToolError {
|
|
67
|
-
constructor() {
|
|
68
|
-
super(
|
|
69
|
-
'This operation needs a KnowledgeBase session id and none was provided. Call the `start_session` tool once at the start of your work to obtain a sessionId, then pass it explicitly as `sessionId` on every gated call (direct MCP calls and `call_tool_chain` alike).',
|
|
70
|
-
403,
|
|
71
|
-
);
|
|
72
|
-
this.name = 'MissingSessionError';
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface SessionOntologyGate {
|
|
77
|
-
service: ISessionOntologyService;
|
|
78
|
-
enabled: boolean;
|
|
79
|
-
/** The checkout folder and the layout a path is classified under. */
|
|
80
|
-
kb: Pick<KbContext, 'kbDirName' | 'layout'>;
|
|
81
|
-
/** Lowercased email of the recovery/merge bot, which is exempt. */
|
|
82
|
-
recoveryBotEmail: string;
|
|
83
|
-
/**
|
|
84
|
-
* The workflow lifecycle-hook registry whose BLOCKING `preWrite` hooks
|
|
85
|
-
* decide whether a gated write/shell op proceeds. Core registers none (the
|
|
86
|
-
* gate then only tracks); the enterprise composition root registers the
|
|
87
|
-
* ontology block against `workflowService.hooks` — pass that same instance
|
|
88
|
-
* here.
|
|
89
|
-
*/
|
|
90
|
-
hooks: WorkflowHooks;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Only internal/external agent callers are gated; `session` (browser JWT) and humans are not. */
|
|
94
|
-
function isAgentSource(ctx: ToolContext): boolean {
|
|
95
|
-
return ctx.source === 'internal' || ctx.source === 'external';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Shared gate preamble for the two per-path operations below. Returns `'skip'`
|
|
100
|
-
* when the boundary doesn't apply to this call (flag off, neutral path,
|
|
101
|
-
* non-agent caller, or recovery bot), otherwise the session id to evaluate
|
|
102
|
-
* against — throwing `MissingSessionError` (fail closed) when an agent op on a
|
|
103
|
-
* named ontology carries none.
|
|
104
|
-
*/
|
|
105
|
-
function resolveGatedSession(gate: SessionOntologyGate, ctx: ToolContext, wsPath: string): string | 'skip' {
|
|
106
|
-
if (!gate.enabled) return 'skip';
|
|
107
|
-
// Neutral paths never pin/record/block — skip before any identity work.
|
|
108
|
-
if (ontologyOf(wsPath, gate.kb.layout, gate.kb.kbDirName) === null) return 'skip';
|
|
109
|
-
// Humans (UI routes) and browser-JWT callers never hit these tools; only
|
|
110
|
-
// agent tool calls are gated.
|
|
111
|
-
if (!isAgentSource(ctx)) return 'skip';
|
|
112
|
-
// The recovery/merge bot legitimately spans ontologies — exempt by identity.
|
|
113
|
-
if (ctx.user.email.toLowerCase() === gate.recoveryBotEmail) return 'skip';
|
|
114
|
-
// Fail closed: an agent operation on a named ontology must carry a session id.
|
|
115
|
-
if (!ctx.sessionId) throw new MissingSessionError();
|
|
116
|
-
return ctx.sessionId;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Record a read of `wsPath`'s ontology into the session's touched set. Reads are
|
|
121
|
-
* always allowed — recording is the only effect — but each one widens the
|
|
122
|
-
* touched set and so can poison a later write (see {@link assertOntologyWriteAllowed}).
|
|
123
|
-
* Fails closed (`MissingSessionError`) for an agent read of a named ontology
|
|
124
|
-
* with no session id.
|
|
125
|
-
*/
|
|
126
|
-
export async function recordOntologyRead(gate: SessionOntologyGate, ctx: ToolContext, wsPath: string): Promise<void> {
|
|
127
|
-
const sessionId = resolveGatedSession(gate, ctx, wsPath);
|
|
128
|
-
if (sessionId === 'skip') return;
|
|
129
|
-
// checkOperation(isWrite=false) records the touch and always allows.
|
|
130
|
-
await gate.service.checkOperation(sessionId, wsPath, false, gate.kb.kbDirName);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Assert a write to `wsPath` is allowed, then record it. A write that would
|
|
135
|
-
* carry information into a second ontology is the thing the boundary blocks:
|
|
136
|
-
* permitted only while the session stays confined to the target ontology, and
|
|
137
|
-
* refused with `OntologyWriteBlockedError` once it has touched ≥2. Note `delete`
|
|
138
|
-
* is NOT a write here — removing a node propagates no cross-ontology
|
|
139
|
-
* information, so it goes through {@link recordOntologyRead}.
|
|
140
|
-
*
|
|
141
|
-
* The BLOCKING decision itself lives behind the `preWrite` hooks: the
|
|
142
|
-
* enterprise-registered ontology block evaluates-and-records atomically via
|
|
143
|
-
* `checkOperation(isWrite=true)` and throws to refuse (identical behavior to
|
|
144
|
-
* the previous in-gate decision). Core registers no hook, so with none the
|
|
145
|
-
* write is allowed and only the touch is recorded (tracking stays core). The
|
|
146
|
-
* follow-up read-record after the hooks is idempotent — a no-op when a hook
|
|
147
|
-
* already recorded the allowed write.
|
|
148
|
-
*/
|
|
149
|
-
export async function assertOntologyWriteAllowed(
|
|
150
|
-
gate: SessionOntologyGate,
|
|
151
|
-
ctx: ToolContext,
|
|
152
|
-
wsPath: string,
|
|
153
|
-
): Promise<void> {
|
|
154
|
-
const sessionId = resolveGatedSession(gate, ctx, wsPath);
|
|
155
|
-
if (sessionId === 'skip') return;
|
|
156
|
-
await gate.hooks.runPreWrite({ kind: 'file-write', sessionId, wsPath });
|
|
157
|
-
// Tracking stays core: record the (now-allowed) write's ontology touch.
|
|
158
|
-
// A blocked write never reaches this line, so it records nothing — same as
|
|
159
|
-
// before the hook split.
|
|
160
|
-
await gate.service.checkOperation(sessionId, wsPath, false, gate.kb.kbDirName);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Session-level write-eligibility gate for write-capable tools that have no
|
|
165
|
-
* single resolvable target path to check per-call — currently `execute_command`
|
|
166
|
-
* (arbitrary shell). Such a tool cannot be path-confined, so we enforce the
|
|
167
|
-
* boundary at the coarser session level: once the run has crossed into ≥2
|
|
168
|
-
* ontologies (the point at which EVERY path-checked write is already refused),
|
|
169
|
-
* we refuse the tool outright, so shell can't become a write path around the
|
|
170
|
-
* boundary. A fresh or single-ontology session is still allowed; a residual gap
|
|
171
|
-
* remains (a single-ontology session could shell-write a *different* ontology),
|
|
172
|
-
* accepted because arbitrary shell cannot be path-confined and blocking all
|
|
173
|
-
* shell after one KB read would gut its read-style diagnostic use (git/grep/ls).
|
|
174
|
-
*
|
|
175
|
-
* Skips when: the flag is off, the caller is not an agent, the caller is the
|
|
176
|
-
* recovery bot, or there is no session id — there is no named path here to fail
|
|
177
|
-
* closed on, and with no session there is no touched set to evaluate.
|
|
178
|
-
*
|
|
179
|
-
* Like {@link assertOntologyWriteAllowed}, the refusal itself is the
|
|
180
|
-
* enterprise-registered `preWrite` hook's (it evaluates the touched set and
|
|
181
|
-
* throws); core registers none, so shell is never refused in a core-only
|
|
182
|
-
* deployment.
|
|
183
|
-
*/
|
|
184
|
-
export async function assertShellAllowedWithinOntology(gate: SessionOntologyGate, ctx: ToolContext): Promise<void> {
|
|
185
|
-
if (!gate.enabled) return;
|
|
186
|
-
if (!isAgentSource(ctx)) return;
|
|
187
|
-
if (ctx.user.email.toLowerCase() === gate.recoveryBotEmail) return;
|
|
188
|
-
if (!ctx.sessionId) return;
|
|
189
|
-
|
|
190
|
-
await gate.hooks.runPreWrite({ kind: 'shell', sessionId: ctx.sessionId });
|
|
191
|
-
}
|