@bevel-software/platform-core-backend 0.28.0 → 0.29.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/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 +7 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +1 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +25 -14
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +11 -6
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +13 -6
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +5 -2
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +36 -14
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +208 -85
- package/dist/modules/access/access-control.service.js.map +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/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/index.d.ts +7 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -0
- package/dist/modules/kb-fs/index.js +2 -0
- package/dist/modules/kb-fs/index.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/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-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/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 +13 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +24 -0
- 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.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/index.d.ts +10 -0
- package/dist/modules/workspace/index.d.ts.map +1 -0
- package/dist/modules/workspace/index.js +10 -0
- package/dist/modules/workspace/index.js.map +1 -0
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +106 -20
- 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/kb-template/access.md +2 -1
- package/mcp-app/page.html +142 -8
- package/package.json +3 -3
- package/src/core/__tests__/compose-embed.test.ts +65 -0
- package/src/core/__tests__/lifecycle.test.ts +29 -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 +7 -1
- package/src/core/create-core-services.ts +27 -21
- package/src/core/lifecycle.ts +24 -12
- package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +23 -3
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +303 -25
- package/src/modules/access/__tests__/access-control.download-read.test.ts +3 -1
- package/src/modules/access/__tests__/access-control.service.test.ts +77 -16
- package/src/modules/access/__tests__/change-read-gate.test.ts +36 -5
- package/src/modules/access/access-control.interface.ts +5 -2
- package/src/modules/access/access-control.service.ts +236 -95
- package/src/modules/auth/index.ts +29 -0
- 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/index.ts +6 -0
- package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +73 -6
- package/src/modules/plugins/join-request-jobs.service.ts +45 -8
- 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-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-registry/__tests__/tool-description-length.test.ts +36 -2
- 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.readFileAtRef.test.ts +42 -0
- package/src/modules/workflow/git/git.service.ts +23 -0
- package/src/modules/workflow/index.ts +7 -0
- package/src/modules/workflow/pending-commits.service.ts +4 -23
- package/src/modules/workflow/workflow.routes.ts +24 -1
- package/src/modules/workflow/workflow.service.ts +3 -2
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +21 -11
- package/src/modules/workspace/index.ts +14 -0
- package/src/modules/workspace/workspace.tools.ts +120 -20
- package/src/shared/workspace-id.ts +24 -0
package/kb-template/access.md
CHANGED
|
@@ -14,7 +14,8 @@ owner:
|
|
|
14
14
|
#
|
|
15
15
|
# Verbs are read, write, download and owner. `owner` implies the rest and
|
|
16
16
|
# `write` implies `read`. Read is default-deny: a folder with no `read:` grant
|
|
17
|
-
# is invisible to everyone except admins, who
|
|
17
|
+
# is invisible to everyone except admins, who may always change what is under
|
|
18
|
+
# the root and so may always see it, unless a nearer access.md excludes them.
|
|
18
19
|
#
|
|
19
20
|
# Each entry is a grant (a bare principal) or a denial (the lowercase word
|
|
20
21
|
# `deny`, a space, then the principal — capitalised forms like `Deny` are
|
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.29.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.29.0",
|
|
59
|
+
"@bevel-software/platform-shared": "0.29.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/adm-zip": "^0.5.8",
|
|
@@ -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,5 +1,8 @@
|
|
|
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';
|
|
5
8
|
|
|
@@ -254,12 +257,12 @@ describe('createShutdown', () => {
|
|
|
254
257
|
order.push('commitWorker.stop');
|
|
255
258
|
},
|
|
256
259
|
},
|
|
257
|
-
|
|
260
|
+
pluginJoinRequestJobs: {
|
|
258
261
|
stopSweeping() {
|
|
259
|
-
order.push('
|
|
262
|
+
order.push('pluginJoinRequestJobs.stopSweeping');
|
|
260
263
|
},
|
|
261
264
|
async drain() {
|
|
262
|
-
order.push('
|
|
265
|
+
order.push('pluginJoinRequestJobs.drain');
|
|
263
266
|
},
|
|
264
267
|
},
|
|
265
268
|
agentUploadStore: {
|
|
@@ -296,12 +299,12 @@ describe('createShutdown', () => {
|
|
|
296
299
|
expect(d.order).toEqual([
|
|
297
300
|
'server.close',
|
|
298
301
|
'server.closeAllConnections',
|
|
299
|
-
'
|
|
302
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
300
303
|
// The upload sweep is stopped in the same breath as the other timers,
|
|
301
304
|
// and drained before the pool goes: a graph that is stopping must not
|
|
302
305
|
// leave a sweep walking the staging root its replacement will stage into.
|
|
303
306
|
'agentUploadStore.stopSweeping',
|
|
304
|
-
'
|
|
307
|
+
'pluginJoinRequestJobs.drain',
|
|
305
308
|
'agentUploadStore.drainSweep',
|
|
306
309
|
'commitWorker.stop',
|
|
307
310
|
'db.end',
|
|
@@ -311,8 +314,8 @@ describe('createShutdown', () => {
|
|
|
311
314
|
it('waits for the jobs a tick already started before the pool goes, within the budget', async () => {
|
|
312
315
|
const d = deps();
|
|
313
316
|
let release: () => void = () => undefined;
|
|
314
|
-
d.deps.
|
|
315
|
-
d.order.push('
|
|
317
|
+
d.deps.pluginJoinRequestJobs!.drain = () => {
|
|
318
|
+
d.order.push('pluginJoinRequestJobs.drain');
|
|
316
319
|
return new Promise<void>((resolve) => {
|
|
317
320
|
release = resolve;
|
|
318
321
|
});
|
|
@@ -326,9 +329,9 @@ describe('createShutdown', () => {
|
|
|
326
329
|
expect(d.order).toEqual([
|
|
327
330
|
'server.close',
|
|
328
331
|
'server.closeAllConnections',
|
|
329
|
-
'
|
|
332
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
330
333
|
'agentUploadStore.stopSweeping',
|
|
331
|
-
'
|
|
334
|
+
'pluginJoinRequestJobs.drain',
|
|
332
335
|
]);
|
|
333
336
|
release();
|
|
334
337
|
await done;
|
|
@@ -348,7 +351,7 @@ describe('createShutdown', () => {
|
|
|
348
351
|
// `commitWorker.stop` — which awaits an in-flight commit and can take
|
|
349
352
|
// most of the shutdown budget — would start fresh clone/commit/push work
|
|
350
353
|
// that the process exit immediately after kills mid-git.
|
|
351
|
-
const stopped = d.order.indexOf('
|
|
354
|
+
const stopped = d.order.indexOf('pluginJoinRequestJobs.stopSweeping');
|
|
352
355
|
expect(stopped).toBeGreaterThanOrEqual(0);
|
|
353
356
|
expect(stopped).toBeLessThan(d.order.indexOf('commitWorker.stop'));
|
|
354
357
|
expect(stopped).toBeLessThan(d.order.indexOf('db.end'));
|
|
@@ -357,7 +360,7 @@ describe('createShutdown', () => {
|
|
|
357
360
|
it('shuts down cleanly when no background jobs were built yet', async () => {
|
|
358
361
|
const d = deps();
|
|
359
362
|
// A stop that lands mid-boot: the services exist, the jobs do not.
|
|
360
|
-
delete (d.deps as {
|
|
363
|
+
delete (d.deps as { pluginJoinRequestJobs?: unknown }).pluginJoinRequestJobs;
|
|
361
364
|
delete (d.deps as { agentUploadStore?: unknown }).agentUploadStore;
|
|
362
365
|
const shutdown = createShutdown(d.deps as never);
|
|
363
366
|
const done = shutdown('SIGTERM');
|
|
@@ -374,8 +377,8 @@ describe('createShutdown', () => {
|
|
|
374
377
|
|
|
375
378
|
it('does not wait past the budget for jobs that never finish, and still ends the pool', async () => {
|
|
376
379
|
const d = deps();
|
|
377
|
-
d.deps.
|
|
378
|
-
d.order.push('
|
|
380
|
+
d.deps.pluginJoinRequestJobs!.drain = () => {
|
|
381
|
+
d.order.push('pluginJoinRequestJobs.drain');
|
|
379
382
|
return new Promise<void>(() => undefined); // a clone that never returns
|
|
380
383
|
};
|
|
381
384
|
const shutdown = createShutdown(d.deps as never, { deadlineMs: 60 });
|
|
@@ -384,7 +387,7 @@ describe('createShutdown', () => {
|
|
|
384
387
|
d.finishClose();
|
|
385
388
|
await done;
|
|
386
389
|
expect(d.order.slice(-4)).toEqual([
|
|
387
|
-
'
|
|
390
|
+
'pluginJoinRequestJobs.drain',
|
|
388
391
|
'agentUploadStore.drainSweep',
|
|
389
392
|
'commitWorker.stop',
|
|
390
393
|
'db.end',
|
|
@@ -400,9 +403,9 @@ describe('createShutdown', () => {
|
|
|
400
403
|
expect(d.order).toEqual([
|
|
401
404
|
'server.close',
|
|
402
405
|
'server.closeAllConnections',
|
|
403
|
-
'
|
|
406
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
404
407
|
'agentUploadStore.stopSweeping',
|
|
405
|
-
'
|
|
408
|
+
'pluginJoinRequestJobs.drain',
|
|
406
409
|
'agentUploadStore.drainSweep',
|
|
407
410
|
'commitWorker.stop',
|
|
408
411
|
'db.end',
|
|
@@ -432,12 +435,12 @@ describe('stopCore', () => {
|
|
|
432
435
|
order.push('commitWorker.stop');
|
|
433
436
|
},
|
|
434
437
|
},
|
|
435
|
-
|
|
438
|
+
pluginJoinRequestJobs: {
|
|
436
439
|
stopSweeping() {
|
|
437
|
-
order.push('
|
|
440
|
+
order.push('pluginJoinRequestJobs.stopSweeping');
|
|
438
441
|
},
|
|
439
442
|
async drain() {
|
|
440
|
-
order.push('
|
|
443
|
+
order.push('pluginJoinRequestJobs.drain');
|
|
441
444
|
},
|
|
442
445
|
},
|
|
443
446
|
agentUploadStore: {
|
|
@@ -478,11 +481,11 @@ describe('stopCore', () => {
|
|
|
478
481
|
const g = graph();
|
|
479
482
|
await stopCore(g.core as never);
|
|
480
483
|
expect(g.order).toEqual([
|
|
481
|
-
'
|
|
484
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
482
485
|
'agentUploadStore.stopSweeping',
|
|
483
486
|
'startupRetry.stop',
|
|
484
487
|
'workflowService.stopTidying',
|
|
485
|
-
'
|
|
488
|
+
'pluginJoinRequestJobs.drain',
|
|
486
489
|
'agentUploadStore.drainSweep',
|
|
487
490
|
'workflowService.drainTidy',
|
|
488
491
|
'commitWorker.stop',
|
|
@@ -523,10 +526,10 @@ describe('stopCore', () => {
|
|
|
523
526
|
const core = { ...g.core, startupRetry: null, secretsScope: undefined };
|
|
524
527
|
await stopCore(core as never);
|
|
525
528
|
expect(g.order).toEqual([
|
|
526
|
-
'
|
|
529
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
527
530
|
'agentUploadStore.stopSweeping',
|
|
528
531
|
'workflowService.stopTidying',
|
|
529
|
-
'
|
|
532
|
+
'pluginJoinRequestJobs.drain',
|
|
530
533
|
'agentUploadStore.drainSweep',
|
|
531
534
|
'workflowService.drainTidy',
|
|
532
535
|
'commitWorker.stop',
|
|
@@ -539,11 +542,11 @@ describe('stopCore', () => {
|
|
|
539
542
|
const core = { ...g.core, commitWorker: { stop: () => new Promise<void>(() => undefined) } };
|
|
540
543
|
await stopCore(core as never, { deadlineMs: 50 });
|
|
541
544
|
expect(g.order).toEqual([
|
|
542
|
-
'
|
|
545
|
+
'pluginJoinRequestJobs.stopSweeping',
|
|
543
546
|
'agentUploadStore.stopSweeping',
|
|
544
547
|
'startupRetry.stop',
|
|
545
548
|
'workflowService.stopTidying',
|
|
546
|
-
'
|
|
549
|
+
'pluginJoinRequestJobs.drain',
|
|
547
550
|
'agentUploadStore.drainSweep',
|
|
548
551
|
'workflowService.drainTidy',
|
|
549
552
|
'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
|
/**
|
|
@@ -509,7 +509,13 @@ export async function createCoreServer(
|
|
|
509
509
|
// one-time link is the whole credential an agent's `curl` carries. Every
|
|
510
510
|
// file behind it was judged when `request_file_download` issued it.
|
|
511
511
|
toolsRouter.use(createAgentDownloadRoutes({ downloads: core.agentDownloadStore, identify: core.agentDownloadFetcher }));
|
|
512
|
-
registerSkillsTools(core.toolRegistry, toolsRouter, ta, th, core.skillService, allowedToolsChecker
|
|
512
|
+
registerSkillsTools(core.toolRegistry, toolsRouter, ta, th, core.skillService, allowedToolsChecker, {
|
|
513
|
+
accessControl: core.accessControl,
|
|
514
|
+
defaultWorkspaceId: () => core.kb.defaultWorkspaceId(),
|
|
515
|
+
// A skill's files as committed on the default branch, for the whole-skill
|
|
516
|
+
// verdict: the same clone and the same git the skill's history is read from.
|
|
517
|
+
committedFiles: core.gitService,
|
|
518
|
+
});
|
|
513
519
|
// Definitions only: the endpoints they describe are the app's own plugin
|
|
514
520
|
// creation routes, mounted below behind the key-or-session gate.
|
|
515
521
|
registerPluginsTools(core.toolRegistry, core.kb);
|
|
@@ -18,7 +18,8 @@ import { runCoreMigrations } from '../modules/database/migrate.js';
|
|
|
18
18
|
import { coreMigrationsDir } from '../assets.js';
|
|
19
19
|
import { WorkspaceService } from '../modules/workspace/workspace.service.js';
|
|
20
20
|
import { AccountLinkService } from '../modules/embed/account-link.service.js';
|
|
21
|
-
import { EmbedService } from '../modules/embed/embed.service.js';
|
|
21
|
+
import type { EmbedService } from '../modules/embed/embed.service.js';
|
|
22
|
+
import { composeEmbedService } from './compose-embed.js';
|
|
22
23
|
import { McpAppService, type IMcpAppService } from '../modules/embed/mcp-app.js';
|
|
23
24
|
import { createFileReaderRegistry } from '../modules/workspace/file-readers/file-reader.registry.js';
|
|
24
25
|
import { RoutineWritePolicyService } from '../modules/workspace/routine-write-policy.js';
|
|
@@ -1288,26 +1289,31 @@ export async function createCoreServices(
|
|
|
1288
1289
|
// judges them with the same access resolver the app file page uses, so what
|
|
1289
1290
|
// a reader may see and change inside a chat is what they may see and change
|
|
1290
1291
|
// in the app — never a second answer to the same question.
|
|
1291
|
-
const embedService =
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1292
|
+
const embedService = composeEmbedService(
|
|
1293
|
+
{
|
|
1294
|
+
// The RESOLVED checkout folder, not the env's: `config.kbDirName` is only
|
|
1295
|
+
// the environment value, empty on a deployment that took the default or
|
|
1296
|
+
// named it in setup, and every path the embed builds starts with it.
|
|
1297
|
+
config: { ...config, kbDirName },
|
|
1298
|
+
kb,
|
|
1299
|
+
workspaceService,
|
|
1300
|
+
accessControl,
|
|
1301
|
+
authService,
|
|
1302
|
+
workflowService,
|
|
1303
|
+
gitService,
|
|
1304
|
+
accountLinks: new AccountLinkService(db),
|
|
1305
|
+
// The SAME extension-to-reader registry `read_file` dispatches on, so the
|
|
1306
|
+
// embed answer to "is this text, or bytes a renderer fetches?" cannot
|
|
1307
|
+
// disagree with what a read of the file returns.
|
|
1308
|
+
readers: createFileReaderRegistry(docExtractService),
|
|
1309
|
+
// The SAME pre-disk gate the file editor and the agent tools run: a
|
|
1310
|
+
// `roles.yaml` that would not parse is refused before it is written, from
|
|
1311
|
+
// a chat exactly as from the app.
|
|
1312
|
+
validateWrite: makeRolesYamlWriteValidator(kbDirName),
|
|
1313
|
+
},
|
|
1314
|
+
// A node-id reference resolves only where a node graph is: the
|
|
1315
|
+
// distribution's, when it registers one (`ports.embedNodeIdResolver`).
|
|
1316
|
+
ports,
|
|
1311
1317
|
);
|
|
1312
1318
|
// The `ui://` view `open_page` carries, with the one origin it may frame:
|
|
1313
1319
|
// this deployment own public origin.
|