@bevel-software/platform-core-backend 0.28.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-guide/access-control.md +15 -0
- package/agent-guide/skills.md +8 -0
- package/dist/__tests__/test-repositories.d.ts +3 -0
- package/dist/__tests__/test-repositories.d.ts.map +1 -0
- package/dist/__tests__/test-repositories.js +18 -0
- package/dist/__tests__/test-repositories.js.map +1 -0
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +4 -2
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/compose-embed.d.ts +35 -0
- package/dist/core/compose-embed.d.ts.map +1 -0
- package/dist/core/compose-embed.js +14 -0
- package/dist/core/compose-embed.js.map +1 -0
- package/dist/core/core-ports.d.ts +10 -0
- 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.map +1 -1
- package/dist/core/create-core-server.js +8 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +4 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +57 -18
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +19 -6
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +17 -6
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +20 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +68 -15
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +282 -58
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +20 -6
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
- package/dist/modules/auth/index.d.ts +15 -0
- package/dist/modules/auth/index.d.ts.map +1 -0
- package/dist/modules/auth/index.js +20 -0
- package/dist/modules/auth/index.js.map +1 -0
- package/dist/modules/diff/diff.service.d.ts.map +1 -1
- package/dist/modules/diff/diff.service.js +7 -1
- package/dist/modules/diff/diff.service.js.map +1 -1
- package/dist/modules/embed/embed.interface.d.ts +22 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -1
- package/dist/modules/embed/embed.service.d.ts +28 -14
- package/dist/modules/embed/embed.service.d.ts.map +1 -1
- package/dist/modules/embed/embed.service.js +38 -17
- package/dist/modules/embed/embed.service.js.map +1 -1
- package/dist/modules/embed/embed.tools.d.ts.map +1 -1
- package/dist/modules/embed/embed.tools.js +10 -2
- package/dist/modules/embed/embed.tools.js.map +1 -1
- package/dist/modules/kb-fs/git-dir.d.ts +21 -0
- package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
- package/dist/modules/kb-fs/git-dir.js +58 -0
- package/dist/modules/kb-fs/git-dir.js.map +1 -0
- package/dist/modules/kb-fs/index.d.ts +9 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -0
- package/dist/modules/kb-fs/index.js +3 -0
- package/dist/modules/kb-fs/index.js.map +1 -0
- package/dist/modules/kb-fs/ref-files.d.ts +63 -0
- package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
- package/dist/modules/kb-fs/ref-files.js +249 -0
- package/dist/modules/kb-fs/ref-files.js.map +1 -0
- package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-jobs.service.js +43 -8
- package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
- package/dist/modules/plugins/teams.routes.d.ts +4 -0
- package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
- package/dist/modules/plugins/teams.routes.js +9 -1
- package/dist/modules/plugins/teams.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +8 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +115 -11
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/default-branch-write.d.ts +69 -0
- package/dist/modules/tool-helpers/default-branch-write.d.ts.map +1 -0
- package/dist/modules/tool-helpers/default-branch-write.js +63 -0
- package/dist/modules/tool-helpers/default-branch-write.js.map +1 -0
- package/dist/modules/tool-helpers/index.d.ts +1 -0
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +1 -0
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts +2 -2
- package/dist/modules/workflow/file-lock.service.d.ts +27 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +70 -18
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +42 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +201 -24
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/index.d.ts +8 -0
- package/dist/modules/workflow/index.d.ts.map +1 -0
- package/dist/modules/workflow/index.js +8 -0
- package/dist/modules/workflow/index.js.map +1 -0
- package/dist/modules/workflow/pending-commits.service.d.ts +2 -17
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +4 -24
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +20 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +5 -3
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
- package/dist/modules/workspace/agent-access.gate.js +2 -2
- package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
- package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
- package/dist/modules/workspace/bounded-matcher.js +261 -0
- package/dist/modules/workspace/bounded-matcher.js.map +1 -0
- package/dist/modules/workspace/fetch-instant.d.ts +9 -0
- package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
- package/dist/modules/workspace/fetch-instant.js +13 -0
- package/dist/modules/workspace/fetch-instant.js.map +1 -0
- package/dist/modules/workspace/index.d.ts +17 -0
- package/dist/modules/workspace/index.d.ts.map +1 -0
- package/dist/modules/workspace/index.js +16 -0
- package/dist/modules/workspace/index.js.map +1 -0
- package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
- package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.contract.js +19 -0
- package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
- package/dist/modules/workspace/kb-repository.d.ts +236 -0
- package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.js +811 -0
- package/dist/modules/workspace/kb-repository.js.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
- package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +0 -5
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
- package/dist/modules/workspace/set-aside-clone.js +139 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +68 -32
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +240 -161
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +13 -4
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +268 -356
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/workspace-id.d.ts +17 -0
- package/dist/shared/workspace-id.d.ts.map +1 -1
- package/dist/shared/workspace-id.js +24 -0
- package/dist/shared/workspace-id.js.map +1 -1
- package/dist/test-setup.js +4 -0
- package/dist/test-setup.js.map +1 -1
- package/mcp-app/page.html +142 -8
- package/package.json +3 -3
- package/src/__tests__/test-repositories.ts +22 -0
- package/src/__tests__/workspace-service.ts +69 -0
- package/src/core/__tests__/compose-embed.test.ts +65 -0
- package/src/core/__tests__/lifecycle.test.ts +45 -26
- package/src/core/catalog-revision.ts +4 -2
- package/src/core/compose-embed.ts +50 -0
- package/src/core/core-ports.ts +10 -0
- package/src/core/create-core-server.ts +8 -2
- package/src/core/create-core-services.ts +63 -24
- package/src/core/lifecycle.ts +34 -12
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +443 -25
- package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
- package/src/modules/access/access-control.interface.ts +22 -0
- package/src/modules/access/access-control.service.ts +302 -58
- package/src/modules/access/access.routes.ts +22 -6
- package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
- package/src/modules/auth/index.ts +29 -0
- package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
- package/src/modules/diff/diff.service.ts +6 -4
- package/src/modules/embed/__tests__/embed.service.test.ts +76 -3
- package/src/modules/embed/__tests__/embed.tools.test.ts +29 -0
- package/src/modules/embed/embed.interface.ts +24 -0
- package/src/modules/embed/embed.service.ts +44 -19
- package/src/modules/embed/embed.tools.ts +10 -2
- package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
- package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
- package/src/modules/kb-fs/git-dir.ts +53 -0
- package/src/modules/kb-fs/index.ts +8 -0
- package/src/modules/kb-fs/ref-files.ts +279 -0
- package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +73 -6
- package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
- package/src/modules/plugins/join-request-jobs.service.ts +45 -8
- package/src/modules/plugins/teams.routes.ts +14 -1
- package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +1 -1
- package/src/modules/skills/skills.contract.ts +4 -0
- package/src/modules/skills/skills.tools.ts +120 -10
- package/src/modules/tool-auth/internal-token.service.ts +1 -1
- package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
- package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +581 -0
- package/src/modules/tool-helpers/default-branch-write.ts +90 -0
- package/src/modules/tool-helpers/index.ts +5 -0
- package/src/modules/tool-helpers/tool-handler.ts +2 -2
- package/src/modules/tool-helpers/tool.contract.ts +2 -2
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +42 -3
- package/src/modules/workflow/__tests__/fake-file-lock-db.ts +13 -2
- package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +168 -1
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +102 -6
- package/src/modules/workflow/file-lock.service.ts +79 -18
- package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
- package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +42 -0
- package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
- package/src/modules/workflow/git/git.service.ts +207 -22
- package/src/modules/workflow/index.ts +7 -0
- package/src/modules/workflow/pending-commits.service.ts +4 -23
- package/src/modules/workflow/workflow-hooks.ts +1 -1
- package/src/modules/workflow/workflow.routes.ts +24 -1
- package/src/modules/workflow/workflow.service.ts +3 -2
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
- package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
- package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
- package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
- package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +174 -309
- package/src/modules/workspace/agent-access.gate.ts +2 -2
- package/src/modules/workspace/bounded-matcher.ts +328 -0
- package/src/modules/workspace/fetch-instant.ts +13 -0
- package/src/modules/workspace/index.ts +26 -0
- package/src/modules/workspace/kb-repository.contract.ts +124 -0
- package/src/modules/workspace/kb-repository.ts +844 -0
- package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
- package/src/modules/workspace/routine-write-policy.ts +0 -15
- package/src/modules/workspace/set-aside-clone.ts +140 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
- package/src/modules/workspace/workspace.service.ts +247 -169
- package/src/modules/workspace/workspace.tools.ts +299 -385
- package/src/shared/workspace-id.ts +24 -0
- package/src/test-setup.ts +5 -0
package/mcp-app/page.html
CHANGED
|
@@ -72,6 +72,26 @@
|
|
|
72
72
|
* owns the view; later ones are ignored.
|
|
73
73
|
*/
|
|
74
74
|
var loading = false;
|
|
75
|
+
/**
|
|
76
|
+
* What the view calls `open_page` with to renew its token: the path
|
|
77
|
+
* and heading the result that owns the view echoed. Null until then,
|
|
78
|
+
* and when the result echoed no path (nothing to ask for).
|
|
79
|
+
*/
|
|
80
|
+
var pageArgs = null;
|
|
81
|
+
/** The deployment the owning result named; a renewed token must name the same one. */
|
|
82
|
+
var deployment = null;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the host runs a server tool for the view (`tools/call`,
|
|
85
|
+
* app to host): true or false when the handshake's reply said so in
|
|
86
|
+
* `hostCapabilities.serverTools`, null when it said nothing — then
|
|
87
|
+
* the view tries once, and a refusal is the answer.
|
|
88
|
+
*/
|
|
89
|
+
var serverTools = null;
|
|
90
|
+
/** Replies the view is waiting for, by request id. */
|
|
91
|
+
var pending = {};
|
|
92
|
+
var callCount = 0;
|
|
93
|
+
/** The one renewal in flight, shared by every call refused meanwhile. */
|
|
94
|
+
var renewing = null;
|
|
75
95
|
|
|
76
96
|
/** The name the deployment's Vite build gives the embed entry. */
|
|
77
97
|
var EMBED_ENTRY = 'embed';
|
|
@@ -90,6 +110,14 @@
|
|
|
90
110
|
* part of the conversation rather than the whole screen.
|
|
91
111
|
*/
|
|
92
112
|
var PREFERRED_HEIGHT = 640;
|
|
113
|
+
/** The tool that minted the token, and the one that renews it. */
|
|
114
|
+
var OPEN_PAGE_TOOL = 'open_page';
|
|
115
|
+
/**
|
|
116
|
+
* How long a renewal waits for the host before it gives up and the
|
|
117
|
+
* view says it has expired. A host that never answers must not leave
|
|
118
|
+
* a save spinning.
|
|
119
|
+
*/
|
|
120
|
+
var RENEW_TIMEOUT_MS = 20000;
|
|
93
121
|
|
|
94
122
|
function send(message) {
|
|
95
123
|
// The host is this view's parent; it told us its origin on the
|
|
@@ -128,6 +156,87 @@
|
|
|
128
156
|
});
|
|
129
157
|
}
|
|
130
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Ask the host to call one of this server's tools for the view, over
|
|
161
|
+
* the connection the chat already holds — and so as the identity
|
|
162
|
+
* that connection authenticated. Resolves with the CallToolResult;
|
|
163
|
+
* rejects when the host refuses or does not answer in time.
|
|
164
|
+
*/
|
|
165
|
+
function callServerTool(name, args) {
|
|
166
|
+
return new Promise(function (resolve, reject) {
|
|
167
|
+
callCount += 1;
|
|
168
|
+
var id = 'call-' + callCount;
|
|
169
|
+
var timer = setTimeout(function () {
|
|
170
|
+
delete pending[id];
|
|
171
|
+
var silent = new Error('the chat app did not answer');
|
|
172
|
+
silent.timedOut = true;
|
|
173
|
+
reject(silent);
|
|
174
|
+
}, RENEW_TIMEOUT_MS);
|
|
175
|
+
pending[id] = function (message) {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
delete pending[id];
|
|
178
|
+
if (message.error || !message.result) reject(message.error || new Error('no result'));
|
|
179
|
+
else resolve(message.result);
|
|
180
|
+
};
|
|
181
|
+
send({ jsonrpc: '2.0', id: id, method: 'tools/call', params: { name: name, arguments: args } });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A fresh token for the page this view shows, or null when there is
|
|
187
|
+
* none to be had. The view cannot mint one — minting needs the
|
|
188
|
+
* identity, which lives on the chat's connection — so it asks the
|
|
189
|
+
* host to call `open_page` again for the same path and heading and
|
|
190
|
+
* takes the token out of the `embedUrl` it answers. That call goes
|
|
191
|
+
* through `open_page`'s own gate: a reader whose access was
|
|
192
|
+
* withdrawn is refused there exactly as on a first call, and gets
|
|
193
|
+
* null here.
|
|
194
|
+
*
|
|
195
|
+
* Null without asking when the host said it runs no server tools for
|
|
196
|
+
* a view, or after a host that said nothing refused once. The bundle
|
|
197
|
+
* calls this at most once per refused call (see `embed-config.ts`).
|
|
198
|
+
*/
|
|
199
|
+
function renew() {
|
|
200
|
+
if (renewing) return renewing;
|
|
201
|
+
if (serverTools === false || !pageArgs || !deployment) return Promise.resolve(null);
|
|
202
|
+
// Every Hexis tool takes its arguments under `body`; flat ones are
|
|
203
|
+
// refused ("takes its arguments under body") and no token comes back.
|
|
204
|
+
renewing = callServerTool(OPEN_PAGE_TOOL, { body: pageArgs })
|
|
205
|
+
.then(
|
|
206
|
+
function (result) {
|
|
207
|
+
var data = (result && result.structuredContent) || {};
|
|
208
|
+
if (result && result.isError) return null;
|
|
209
|
+
var url;
|
|
210
|
+
try {
|
|
211
|
+
url = new URL(data.embedUrl);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
// The same deployment, or nothing: the bundle already runs
|
|
216
|
+
// against this one, and its API is the token's.
|
|
217
|
+
if (baseOf(url) !== deployment) return null;
|
|
218
|
+
var token = url.searchParams.get('token');
|
|
219
|
+
if (!token) return null;
|
|
220
|
+
var handoff = window[HANDOFF_GLOBAL];
|
|
221
|
+
if (handoff && typeof handoff === 'object') handoff.token = token;
|
|
222
|
+
return token;
|
|
223
|
+
},
|
|
224
|
+
function (err) {
|
|
225
|
+
// The host refused the call: one that said nothing about
|
|
226
|
+
// server tools has now said no. One that did not answer in
|
|
227
|
+
// time has said nothing, and is asked again on the next
|
|
228
|
+
// refused call.
|
|
229
|
+
if (serverTools === null && !(err && err.timedOut)) serverTools = false;
|
|
230
|
+
return null;
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
.then(function (token) {
|
|
234
|
+
renewing = null;
|
|
235
|
+
return token;
|
|
236
|
+
});
|
|
237
|
+
return renewing;
|
|
238
|
+
}
|
|
239
|
+
|
|
131
240
|
/** The text blocks of a CallToolResult, one line each. */
|
|
132
241
|
function textOf(result) {
|
|
133
242
|
var out = [];
|
|
@@ -226,8 +335,15 @@
|
|
|
226
335
|
}
|
|
227
336
|
if (loading) return;
|
|
228
337
|
loading = true;
|
|
338
|
+
deployment = base;
|
|
339
|
+
// What the view calls `open_page` with again when the token runs
|
|
340
|
+
// out: the arguments this result was answered for, as it echoed them.
|
|
341
|
+
if (typeof data.path === 'string' && data.path) {
|
|
342
|
+
pageArgs = { path: data.path };
|
|
343
|
+
if (typeof data.heading === 'string' && data.heading) pageArgs.heading = data.heading;
|
|
344
|
+
}
|
|
229
345
|
// The handoff, before the bundle: it reads this the moment it runs.
|
|
230
|
-
window[HANDOFF_GLOBAL] = { baseUrl: base, token: token, openLink: openLink };
|
|
346
|
+
window[HANDOFF_GLOBAL] = { baseUrl: base, token: token, openLink: openLink, renew: renew };
|
|
231
347
|
|
|
232
348
|
fetch(base + MANIFEST_PATH, { credentials: 'omit' })
|
|
233
349
|
.then(function (res) {
|
|
@@ -302,6 +418,13 @@
|
|
|
302
418
|
if (event.source !== window.parent) return;
|
|
303
419
|
if (hostOrigin === null && event.origin) hostOrigin = event.origin;
|
|
304
420
|
|
|
421
|
+
// A reply to a request the view sent (a server tool call): it has
|
|
422
|
+
// the request's id and no method of its own.
|
|
423
|
+
if (!message.method && typeof message.id === 'string' && pending[message.id]) {
|
|
424
|
+
pending[message.id](message);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
305
428
|
if (message.method === 'ui/notifications/tool-result') {
|
|
306
429
|
// The params ARE the CallToolResult. A `result` wrapper is
|
|
307
430
|
// accepted too, for a host that wrapped it.
|
|
@@ -314,16 +437,25 @@
|
|
|
314
437
|
// whole of the layout; nothing to recompute.
|
|
315
438
|
return;
|
|
316
439
|
}
|
|
317
|
-
// The handshake's reply carries the host's capabilities.
|
|
318
|
-
// here
|
|
319
|
-
//
|
|
320
|
-
|
|
440
|
+
// The handshake's reply carries the host's capabilities. The one
|
|
441
|
+
// read here is `serverTools`: whether the host runs this server's
|
|
442
|
+
// tools for the view, which is how the view renews its token. And a
|
|
443
|
+
// host that sends the first tool result only after the handshake
|
|
444
|
+
// completes needs us to have sent it.
|
|
445
|
+
if (!message.method && message.id === INITIALIZE_ID) {
|
|
321
446
|
// A rejected handshake is not advertised as ready — and the
|
|
322
447
|
// reader is told, rather than left at "Opening the page…".
|
|
323
448
|
if (message.error || !message.result) {
|
|
324
449
|
if (!shown) say('The chat app refused to open this page.');
|
|
325
450
|
return;
|
|
326
451
|
}
|
|
452
|
+
// Only a `serverTools` the host named decides: present is yes,
|
|
453
|
+
// false or null is no. Capabilities that leave it out have said
|
|
454
|
+
// nothing about it, and the view tries once.
|
|
455
|
+
var caps = message.result.hostCapabilities;
|
|
456
|
+
if (caps && typeof caps === 'object' && caps.serverTools !== undefined) {
|
|
457
|
+
serverTools = !!caps.serverTools;
|
|
458
|
+
}
|
|
327
459
|
send({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
|
|
328
460
|
if (!shown) say('Opening the page…');
|
|
329
461
|
}
|
|
@@ -337,9 +469,11 @@
|
|
|
337
469
|
params: {
|
|
338
470
|
protocolVersion: '2026-01-26',
|
|
339
471
|
appInfo: { name: 'hexis-knowledge-page', version: '1.0.0' },
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
// host
|
|
472
|
+
// Declares nothing. The extension has no way for a view to say
|
|
473
|
+
// it CALLS server tools (`tools` here would mean the view
|
|
474
|
+
// exposes tools of its own); whether the host runs them for a
|
|
475
|
+
// view is the host's to say, in the reply's `serverTools`.
|
|
476
|
+
// Links go out through ui/open-link, which every host offers.
|
|
343
477
|
appCapabilities: {},
|
|
344
478
|
},
|
|
345
479
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bevel-software/platform-core-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, workflow (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
56
56
|
"yaml": "^2.9.0",
|
|
57
57
|
"zod": "^3.24.0",
|
|
58
|
-
"@bevel-software/platform-mcp-core": "0.
|
|
59
|
-
"@bevel-software/platform-shared": "0.
|
|
58
|
+
"@bevel-software/platform-mcp-core": "0.30.0",
|
|
59
|
+
"@bevel-software/platform-shared": "0.30.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/adm-zip": "^0.5.8",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The repository roots `testKbRepository` placed beside a suite's workspaces
|
|
5
|
+
* root (`<workspacesRoot>.repository`). A suite removes its fixture root, not
|
|
6
|
+
* what sits next to it, so `test-setup.ts` removes these once the file's
|
|
7
|
+
* suites are done. Kept free of imports so the setup file can load it without
|
|
8
|
+
* pulling in a module a suite means to mock.
|
|
9
|
+
*/
|
|
10
|
+
const roots = new Set<string>();
|
|
11
|
+
|
|
12
|
+
export function recordTestRepositoryRoot(root: string): void {
|
|
13
|
+
roots.add(root);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function removeTestRepositoryRoots(): Promise<void> {
|
|
17
|
+
const pending = [...roots];
|
|
18
|
+
roots.clear();
|
|
19
|
+
await Promise.all(
|
|
20
|
+
pending.map((root) => fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { NodeGitRunner } from '../modules/workflow/git/node-git-runner.js';
|
|
3
|
+
import type { IGitRunner } from '../shared/git.contract.js';
|
|
4
|
+
import type { ITreeWalker } from '../shared/fs.contract.js';
|
|
5
|
+
import type { KbContext } from '../shared/kb-context.js';
|
|
6
|
+
import { KbRepository, repositoryDirFor } from '../modules/workspace/kb-repository.js';
|
|
7
|
+
import type { IKbRepository } from '../modules/workspace/kb-repository.contract.js';
|
|
8
|
+
import { WorkspaceService } from '../modules/workspace/workspace.service.js';
|
|
9
|
+
import { KbStartupRunner, type KbStartupRunnerOptions } from '../modules/workspace/startup/kb-startup-runner.js';
|
|
10
|
+
import { recordTestRepositoryRoot } from './test-repositories.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The repository a suite's services share when it builds them by hand: one
|
|
14
|
+
* next to the workspaces root (see `repositoryDirFor`), as the composition
|
|
15
|
+
* root would place it there for a deployment with no backups root. Removed
|
|
16
|
+
* once the file's suites are done (see `test-repositories.ts`): a suite's
|
|
17
|
+
* teardown removes its fixture root, not what sits beside it.
|
|
18
|
+
*/
|
|
19
|
+
export function testKbRepository(
|
|
20
|
+
workspacesRoot: string,
|
|
21
|
+
kbDirName: string,
|
|
22
|
+
kbRepoUrl: string | (() => string),
|
|
23
|
+
gitRunner?: IGitRunner,
|
|
24
|
+
): IKbRepository {
|
|
25
|
+
const dir = repositoryDirFor(workspacesRoot, kbDirName);
|
|
26
|
+
recordTestRepositoryRoot(path.dirname(dir));
|
|
27
|
+
return new KbRepository({
|
|
28
|
+
dir,
|
|
29
|
+
kbRepoUrl: typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl,
|
|
30
|
+
gitRunner: gitRunner ?? new NodeGitRunner(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A WorkspaceService as a suite builds it: the constructor's arguments, with
|
|
36
|
+
* the repository — which only the composition root builds in the product —
|
|
37
|
+
* made here when the suite passes none.
|
|
38
|
+
*/
|
|
39
|
+
export function newWorkspaceService(
|
|
40
|
+
workspacesRoot: string,
|
|
41
|
+
kbRepoUrl: string | (() => string),
|
|
42
|
+
kb: Pick<KbContext, 'kbDirName' | 'defaultBranch'>,
|
|
43
|
+
disk: ITreeWalker,
|
|
44
|
+
gitRunner: IGitRunner = new NodeGitRunner(),
|
|
45
|
+
setAsideRoot?: string,
|
|
46
|
+
aroundSetAside?: (workspaceId: string, move: () => Promise<void>) => Promise<void>,
|
|
47
|
+
repository?: IKbRepository,
|
|
48
|
+
): WorkspaceService {
|
|
49
|
+
return new WorkspaceService(
|
|
50
|
+
workspacesRoot,
|
|
51
|
+
kbRepoUrl,
|
|
52
|
+
kb,
|
|
53
|
+
disk,
|
|
54
|
+
gitRunner,
|
|
55
|
+
setAsideRoot,
|
|
56
|
+
aroundSetAside,
|
|
57
|
+
repository ?? testKbRepository(workspacesRoot, kb.kbDirName, kbRepoUrl, gitRunner),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A KbStartupRunner as a suite builds it — the repository made here when the suite passes none. */
|
|
62
|
+
export function newKbStartupRunner(
|
|
63
|
+
opts: Omit<KbStartupRunnerOptions, 'repository'> & { repository?: IKbRepository },
|
|
64
|
+
): KbStartupRunner {
|
|
65
|
+
return new KbStartupRunner({
|
|
66
|
+
...opts,
|
|
67
|
+
repository: opts.repository ?? testKbRepository(opts.workspacesRoot, opts.kbDirName, opts.kbRepoUrl, opts.gitRunner),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { testKbContext, TEST_BRANCH_MODEL } from '../../__tests__/kb-context.js';
|
|
3
|
+
import { makeRolesYamlWriteValidator } from '../../modules/access-model/roles-yaml-guard.js';
|
|
4
|
+
import { createFileReaderRegistry } from '../../modules/workspace/file-readers/file-reader.registry.js';
|
|
5
|
+
import type { DocExtractOutcome, DocExtractService } from '../../modules/workspace/file-readers/doc-extract.service.js';
|
|
6
|
+
import { composeEmbedService, type EmbedComposition } from '../compose-embed.js';
|
|
7
|
+
import { EmbedRefParseError } from '../../modules/embed/embed-link.js';
|
|
8
|
+
|
|
9
|
+
const KB = 'knowledge-base';
|
|
10
|
+
const BRANCH = TEST_BRANCH_MODEL.defaultBranch;
|
|
11
|
+
const REPO = 'Data/Thing.md';
|
|
12
|
+
const USER = { id: 'u-1', email: 'alice@bevel.software', name: 'Alice' };
|
|
13
|
+
|
|
14
|
+
/** The composition root's deps, as mocks: enough to mint and to say who is looking. */
|
|
15
|
+
function deps(): EmbedComposition {
|
|
16
|
+
const extract = async (): Promise<DocExtractOutcome> => ({ ok: false, message: 'no documents here' });
|
|
17
|
+
return {
|
|
18
|
+
config: { jwtSecret: 'test-jwt-secret', embedSharedSecret: '', publicFrontendUrl: 'https://hexis.example', kbDirName: KB },
|
|
19
|
+
kb: testKbContext({ kbDirName: KB }),
|
|
20
|
+
workspaceService: {
|
|
21
|
+
getOrCreateForBranch: vi.fn(async (branch: string) => ({ id: encodeURIComponent(branch), kbDirName: KB })),
|
|
22
|
+
isFile: vi.fn(async (_id: string, wsPath: string) => wsPath === `${KB}/${REPO}`),
|
|
23
|
+
readFileBinary: vi.fn(async () => Buffer.from('# Thing\n')),
|
|
24
|
+
writeFile: vi.fn(async () => undefined),
|
|
25
|
+
withPathTurn: vi.fn(async (_id: string, _p: string, op: () => Promise<unknown>) => op()),
|
|
26
|
+
},
|
|
27
|
+
accessControl: { canRead: vi.fn(async () => true), canWrite: vi.fn(async () => true) } as never,
|
|
28
|
+
authService: { getUserById: vi.fn(async () => USER), isEmailDomainAllowed: () => true, isActive: async () => true },
|
|
29
|
+
workflowService: {} as never,
|
|
30
|
+
gitService: {} as never,
|
|
31
|
+
accountLinks: { getUserId: async () => null, link: async () => undefined, listForUser: async () => [], unlink: async () => true },
|
|
32
|
+
readers: createFileReaderRegistry({ extract } satisfies Pick<DocExtractService, 'extract'> as DocExtractService),
|
|
33
|
+
validateWrite: makeRolesYamlWriteValidator(KB),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The composition root is the one place the distribution's ports reach the
|
|
39
|
+
* embed. Pinned here through that composition, not by handing the service a
|
|
40
|
+
* resolver directly: a node-id reference resolves through the port when one
|
|
41
|
+
* is registered, and is refused when none is — which is what a core-only
|
|
42
|
+
* deployment ships.
|
|
43
|
+
*/
|
|
44
|
+
describe('composeEmbedService', () => {
|
|
45
|
+
// The app's copy-link form: the full address of a node, which is its id.
|
|
46
|
+
const NODE_LINK = `https://hexis.example/workspace/${encodeURIComponent(BRANCH)}/abc-123`;
|
|
47
|
+
|
|
48
|
+
it('resolves a node-id reference through the registered port, and mints for the file it names', async () => {
|
|
49
|
+
const embedNodeIdResolver = vi.fn(async (id: string) => (id === 'abc-123' ? REPO : null));
|
|
50
|
+
const service = composeEmbedService(deps(), { embedNodeIdResolver });
|
|
51
|
+
const { token } = await service.mintForUser({ userId: USER.id, reference: NODE_LINK });
|
|
52
|
+
expect(embedNodeIdResolver).toHaveBeenCalledWith('abc-123');
|
|
53
|
+
expect((await service.viewerOf(token)).repoRelative).toBe(REPO);
|
|
54
|
+
// An id the graph does not know is refused as a dead reference.
|
|
55
|
+
await expect(service.mintForUser({ userId: USER.id, reference: 'nope-1' })).rejects.toThrow(EmbedRefParseError);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('refuses a node-id reference when no port is registered — core has no graph', async () => {
|
|
59
|
+
const service = composeEmbedService(deps(), {});
|
|
60
|
+
await expect(service.mintForUser({ userId: USER.id, reference: NODE_LINK })).rejects.toThrow(EmbedRefParseError);
|
|
61
|
+
// A path reference is untouched by the absence of a resolver.
|
|
62
|
+
const { token } = await service.mintForUser({ userId: USER.id, reference: REPO });
|
|
63
|
+
expect((await service.viewerOf(token)).repoRelative).toBe(REPO);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createShutdown,
|
|
2
|
+
import { createShutdown, stopCore, withStartupTask, type LeasedWorker } from '../../index.js';
|
|
3
|
+
// The lease helpers are the module's own, not on the entry point: their
|
|
4
|
+
// ordering is pinned here, where the module is the unit under test.
|
|
5
|
+
import { holdCommitWorkerLease, holdable } from '../lifecycle.js';
|
|
3
6
|
import { BevelSecretsVariableLoader, registerBevelSecretsVariableLoader } from '../../modules/secrets-vault/secrets-variable-loader.js';
|
|
4
7
|
import type { AdvisoryLease } from '../../modules/database/advisory-lock.js';
|
|
8
|
+
import { WorkerPatternMatcher } from '../../modules/workspace/bounded-matcher.js';
|
|
5
9
|
|
|
6
10
|
/** A lease the suite scripts: what each `tryAcquire` answers, and a way to lose it. */
|
|
7
11
|
function fakeLease(answers: boolean[]) {
|
|
@@ -254,12 +258,12 @@ describe('createShutdown', () => {
|
|
|
254
258
|
order.push('commitWorker.stop');
|
|
255
259
|
},
|
|
256
260
|
},
|
|
257
|
-
|
|
261
|
+
pluginJoinRequestJobs: {
|
|
258
262
|
stopSweeping() {
|
|
259
|
-
order.push('
|
|
263
|
+
order.push('pluginJoinRequestJobs.stopSweeping');
|
|
260
264
|
},
|
|
261
265
|
async drain() {
|
|
262
|
-
order.push('
|
|
266
|
+
order.push('pluginJoinRequestJobs.drain');
|
|
263
267
|
},
|
|
264
268
|
},
|
|
265
269
|
agentUploadStore: {
|
|
@@ -296,12 +300,12 @@ describe('createShutdown', () => {
|
|
|
296
300
|
expect(d.order).toEqual([
|
|
297
301
|
'server.close',
|
|
298
302
|
'server.closeAllConnections',
|
|
299
|
-
'
|
|
303
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
300
304
|
// The upload sweep is stopped in the same breath as the other timers,
|
|
301
305
|
// and drained before the pool goes: a graph that is stopping must not
|
|
302
306
|
// leave a sweep walking the staging root its replacement will stage into.
|
|
303
307
|
'agentUploadStore.stopSweeping',
|
|
304
|
-
'
|
|
308
|
+
'pluginJoinRequestJobs.drain',
|
|
305
309
|
'agentUploadStore.drainSweep',
|
|
306
310
|
'commitWorker.stop',
|
|
307
311
|
'db.end',
|
|
@@ -311,8 +315,8 @@ describe('createShutdown', () => {
|
|
|
311
315
|
it('waits for the jobs a tick already started before the pool goes, within the budget', async () => {
|
|
312
316
|
const d = deps();
|
|
313
317
|
let release: () => void = () => undefined;
|
|
314
|
-
d.deps.
|
|
315
|
-
d.order.push('
|
|
318
|
+
d.deps.pluginJoinRequestJobs!.drain = () => {
|
|
319
|
+
d.order.push('pluginJoinRequestJobs.drain');
|
|
316
320
|
return new Promise<void>((resolve) => {
|
|
317
321
|
release = resolve;
|
|
318
322
|
});
|
|
@@ -326,9 +330,9 @@ describe('createShutdown', () => {
|
|
|
326
330
|
expect(d.order).toEqual([
|
|
327
331
|
'server.close',
|
|
328
332
|
'server.closeAllConnections',
|
|
329
|
-
'
|
|
333
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
330
334
|
'agentUploadStore.stopSweeping',
|
|
331
|
-
'
|
|
335
|
+
'pluginJoinRequestJobs.drain',
|
|
332
336
|
]);
|
|
333
337
|
release();
|
|
334
338
|
await done;
|
|
@@ -348,7 +352,7 @@ describe('createShutdown', () => {
|
|
|
348
352
|
// `commitWorker.stop` — which awaits an in-flight commit and can take
|
|
349
353
|
// most of the shutdown budget — would start fresh clone/commit/push work
|
|
350
354
|
// that the process exit immediately after kills mid-git.
|
|
351
|
-
const stopped = d.order.indexOf('
|
|
355
|
+
const stopped = d.order.indexOf('pluginJoinRequestJobs.stopSweeping');
|
|
352
356
|
expect(stopped).toBeGreaterThanOrEqual(0);
|
|
353
357
|
expect(stopped).toBeLessThan(d.order.indexOf('commitWorker.stop'));
|
|
354
358
|
expect(stopped).toBeLessThan(d.order.indexOf('db.end'));
|
|
@@ -357,7 +361,7 @@ describe('createShutdown', () => {
|
|
|
357
361
|
it('shuts down cleanly when no background jobs were built yet', async () => {
|
|
358
362
|
const d = deps();
|
|
359
363
|
// A stop that lands mid-boot: the services exist, the jobs do not.
|
|
360
|
-
delete (d.deps as {
|
|
364
|
+
delete (d.deps as { pluginJoinRequestJobs?: unknown }).pluginJoinRequestJobs;
|
|
361
365
|
delete (d.deps as { agentUploadStore?: unknown }).agentUploadStore;
|
|
362
366
|
const shutdown = createShutdown(d.deps as never);
|
|
363
367
|
const done = shutdown('SIGTERM');
|
|
@@ -374,8 +378,8 @@ describe('createShutdown', () => {
|
|
|
374
378
|
|
|
375
379
|
it('does not wait past the budget for jobs that never finish, and still ends the pool', async () => {
|
|
376
380
|
const d = deps();
|
|
377
|
-
d.deps.
|
|
378
|
-
d.order.push('
|
|
381
|
+
d.deps.pluginJoinRequestJobs!.drain = () => {
|
|
382
|
+
d.order.push('pluginJoinRequestJobs.drain');
|
|
379
383
|
return new Promise<void>(() => undefined); // a clone that never returns
|
|
380
384
|
};
|
|
381
385
|
const shutdown = createShutdown(d.deps as never, { deadlineMs: 60 });
|
|
@@ -384,7 +388,7 @@ describe('createShutdown', () => {
|
|
|
384
388
|
d.finishClose();
|
|
385
389
|
await done;
|
|
386
390
|
expect(d.order.slice(-4)).toEqual([
|
|
387
|
-
'
|
|
391
|
+
'pluginJoinRequestJobs.drain',
|
|
388
392
|
'agentUploadStore.drainSweep',
|
|
389
393
|
'commitWorker.stop',
|
|
390
394
|
'db.end',
|
|
@@ -400,9 +404,9 @@ describe('createShutdown', () => {
|
|
|
400
404
|
expect(d.order).toEqual([
|
|
401
405
|
'server.close',
|
|
402
406
|
'server.closeAllConnections',
|
|
403
|
-
'
|
|
407
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
404
408
|
'agentUploadStore.stopSweeping',
|
|
405
|
-
'
|
|
409
|
+
'pluginJoinRequestJobs.drain',
|
|
406
410
|
'agentUploadStore.drainSweep',
|
|
407
411
|
'commitWorker.stop',
|
|
408
412
|
'db.end',
|
|
@@ -432,12 +436,12 @@ describe('stopCore', () => {
|
|
|
432
436
|
order.push('commitWorker.stop');
|
|
433
437
|
},
|
|
434
438
|
},
|
|
435
|
-
|
|
439
|
+
pluginJoinRequestJobs: {
|
|
436
440
|
stopSweeping() {
|
|
437
|
-
order.push('
|
|
441
|
+
order.push('pluginJoinRequestJobs.stopSweeping');
|
|
438
442
|
},
|
|
439
443
|
async drain() {
|
|
440
|
-
order.push('
|
|
444
|
+
order.push('pluginJoinRequestJobs.drain');
|
|
441
445
|
},
|
|
442
446
|
},
|
|
443
447
|
agentUploadStore: {
|
|
@@ -478,11 +482,11 @@ describe('stopCore', () => {
|
|
|
478
482
|
const g = graph();
|
|
479
483
|
await stopCore(g.core as never);
|
|
480
484
|
expect(g.order).toEqual([
|
|
481
|
-
'
|
|
485
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
482
486
|
'agentUploadStore.stopSweeping',
|
|
483
487
|
'startupRetry.stop',
|
|
484
488
|
'workflowService.stopTidying',
|
|
485
|
-
'
|
|
489
|
+
'pluginJoinRequestJobs.drain',
|
|
486
490
|
'agentUploadStore.drainSweep',
|
|
487
491
|
'workflowService.drainTidy',
|
|
488
492
|
'commitWorker.stop',
|
|
@@ -523,10 +527,10 @@ describe('stopCore', () => {
|
|
|
523
527
|
const core = { ...g.core, startupRetry: null, secretsScope: undefined };
|
|
524
528
|
await stopCore(core as never);
|
|
525
529
|
expect(g.order).toEqual([
|
|
526
|
-
'
|
|
530
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
527
531
|
'agentUploadStore.stopSweeping',
|
|
528
532
|
'workflowService.stopTidying',
|
|
529
|
-
'
|
|
533
|
+
'pluginJoinRequestJobs.drain',
|
|
530
534
|
'agentUploadStore.drainSweep',
|
|
531
535
|
'workflowService.drainTidy',
|
|
532
536
|
'commitWorker.stop',
|
|
@@ -534,16 +538,31 @@ describe('stopCore', () => {
|
|
|
534
538
|
]);
|
|
535
539
|
});
|
|
536
540
|
|
|
541
|
+
it('ends the grep matcher\'s workers, so an evicted graph leaves no search running behind it', async () => {
|
|
542
|
+
const g = graph();
|
|
543
|
+
const patternMatcher = new WorkerPatternMatcher();
|
|
544
|
+
// A search holding a worker for its whole 5 s budget when the graph is stopped.
|
|
545
|
+
const running = patternMatcher.search('^(a+)+$', '').matchingLines([`${'a'.repeat(39)}!`], 1);
|
|
546
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
547
|
+
const started = Date.now();
|
|
548
|
+
await stopCore({ ...g.core, patternMatcher } as never);
|
|
549
|
+
await expect(running).rejects.toMatchObject({ status: 503, details: { code: 'search_busy' } });
|
|
550
|
+
expect(Date.now() - started).toBeLessThan(1_000);
|
|
551
|
+
// Closed for good: nothing after the stop starts a worker.
|
|
552
|
+
await expect(patternMatcher.search('b', '').matchingLines(['b'], 1)).rejects.toMatchObject({ status: 503 });
|
|
553
|
+
expect(g.order.at(-1)).toBe('db.end');
|
|
554
|
+
});
|
|
555
|
+
|
|
537
556
|
it('does not wait past the budget for a worker that never stops, and still ends the pool', async () => {
|
|
538
557
|
const g = graph();
|
|
539
558
|
const core = { ...g.core, commitWorker: { stop: () => new Promise<void>(() => undefined) } };
|
|
540
559
|
await stopCore(core as never, { deadlineMs: 50 });
|
|
541
560
|
expect(g.order).toEqual([
|
|
542
|
-
'
|
|
561
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
543
562
|
'agentUploadStore.stopSweeping',
|
|
544
563
|
'startupRetry.stop',
|
|
545
564
|
'workflowService.stopTidying',
|
|
546
|
-
'
|
|
565
|
+
'pluginJoinRequestJobs.drain',
|
|
547
566
|
'agentUploadStore.drainSweep',
|
|
548
567
|
'workflowService.drainTidy',
|
|
549
568
|
'db.end',
|
|
@@ -29,10 +29,12 @@ const log = logger('catalog-revision');
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* A skill's line:
|
|
32
|
+
* A skill's line: what `list_skills` shows about the skill ITSELF on an AGENT
|
|
33
33
|
* surface. Not `plugins`, which only the browser route decorates onto the
|
|
34
34
|
* catalog — a field no agent-facing listing carries cannot be a change one
|
|
35
|
-
* owes a refresh for.
|
|
35
|
+
* owes a refresh for. Not `canWrite` either: it is a verdict about the caller,
|
|
36
|
+
* judged per call by the tool, not part of the skill, and it is never on the
|
|
37
|
+
* summaries this digest reads.
|
|
36
38
|
*/
|
|
37
39
|
function skillFingerprint(s: SkillSummary): string {
|
|
38
40
|
return [s.name, s.path, s.version ?? '', s.description].join('\u0000');
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { IGitService, IWorkflowService } from '@bevel-software/platform-shared';
|
|
2
|
+
import type { KbContext } from '../shared/kb-context.js';
|
|
3
|
+
import type { IAccessControl } from '../modules/access/access-control.interface.js';
|
|
4
|
+
import type { WriteValidator } from '../modules/kb-fs/index.js';
|
|
5
|
+
import type { FileReaderRegistry } from '../modules/workspace/index.js';
|
|
6
|
+
import type { CorePorts } from './core-ports.js';
|
|
7
|
+
import type { IAccountLinkService } from '../modules/embed/account-link.service.js';
|
|
8
|
+
import type { EmbedAuthPort, EmbedWorkspacePort } from '../modules/embed/embed.interface.js';
|
|
9
|
+
import { EmbedService, type EmbedConfig } from '../modules/embed/embed.service.js';
|
|
10
|
+
|
|
11
|
+
/** What the composition root hands the embed: the services it reads and writes through. */
|
|
12
|
+
export interface EmbedComposition {
|
|
13
|
+
config: EmbedConfig;
|
|
14
|
+
kb: KbContext;
|
|
15
|
+
workspaceService: EmbedWorkspacePort;
|
|
16
|
+
accessControl: IAccessControl;
|
|
17
|
+
authService: EmbedAuthPort;
|
|
18
|
+
workflowService: IWorkflowService;
|
|
19
|
+
gitService: IGitService;
|
|
20
|
+
accountLinks: IAccountLinkService;
|
|
21
|
+
/** The SAME extension-to-reader registry `read_file` dispatches on. */
|
|
22
|
+
readers: FileReaderRegistry;
|
|
23
|
+
/** The SAME pre-disk gate the file editor and the agent tools run. */
|
|
24
|
+
validateWrite: WriteValidator;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The embed service as the composition root builds it — the one place the
|
|
29
|
+
* distribution's ports reach it. Lives in `core/`, beside the composition
|
|
30
|
+
* root it serves: the ports are core's contract with a distribution, and a
|
|
31
|
+
* module never reaches up into core. A node-id reference (`/workspace/<branch>/
|
|
32
|
+
* <id>`, the app's copy-link form) resolves only where a node graph is: the
|
|
33
|
+
* distribution's, when it registers `ports.embedNodeIdResolver`; core has
|
|
34
|
+
* none and refuses such a reference.
|
|
35
|
+
*/
|
|
36
|
+
export function composeEmbedService(deps: EmbedComposition, ports: Pick<CorePorts, 'embedNodeIdResolver'>): EmbedService {
|
|
37
|
+
return new EmbedService(
|
|
38
|
+
deps.config,
|
|
39
|
+
deps.kb,
|
|
40
|
+
deps.workspaceService,
|
|
41
|
+
deps.accessControl,
|
|
42
|
+
deps.authService,
|
|
43
|
+
deps.workflowService,
|
|
44
|
+
deps.gitService,
|
|
45
|
+
deps.accountLinks,
|
|
46
|
+
deps.readers,
|
|
47
|
+
deps.validateWrite,
|
|
48
|
+
ports.embedNodeIdResolver ?? null,
|
|
49
|
+
);
|
|
50
|
+
}
|
package/src/core/core-ports.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { IAccountAdmission } from '../modules/auth/account-admission.js';
|
|
|
10
10
|
import type { IWriteAccess } from '../modules/write-access/write-access.js';
|
|
11
11
|
import type { OnServerStart } from '../modules/workspace/startup/on-server-start.js';
|
|
12
12
|
import type { AgentGuideHook } from '../modules/agent-guide/agent-guide.js';
|
|
13
|
+
import type { EmbedNodeIdResolver } from '../modules/embed/embed.interface.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Every seam the enterprise overlay can fill in the CORE composition
|
|
@@ -138,6 +139,15 @@ export interface CorePorts {
|
|
|
138
139
|
* `modules/write-access/write-access.ts`.
|
|
139
140
|
*/
|
|
140
141
|
writeAccess?: IWriteAccess;
|
|
142
|
+
/**
|
|
143
|
+
* How an embed reference that is a bare NODE ID (`/workspace/<branch>/<id>`,
|
|
144
|
+
* the app's copy-link form) becomes the repository path it names. Core has
|
|
145
|
+
* no node graph, so core refuses such a reference; a distribution with one
|
|
146
|
+
* answers from it, and the Atlassian connector's links to nodes mint as
|
|
147
|
+
* they did when that distribution served the embed itself. Returns null
|
|
148
|
+
* for an id that names nothing. See `modules/embed/embed.interface.ts`.
|
|
149
|
+
*/
|
|
150
|
+
embedNodeIdResolver?: EmbedNodeIdResolver;
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
/**
|