@capybara-hq/kernel 0.1.0-rc.10 → 0.1.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/admin.schemas.d.ts +3 -0
- package/dist/agents/agents.schemas.d.ts +3 -0
- package/dist/agents/agents.service.js +11 -1
- package/dist/agents/definition.d.ts +1 -0
- package/dist/agents/definition.js +7 -3
- package/dist/agents/org-templates.js +4 -0
- package/dist/agents/pending-connectors.service.js +1 -1
- package/dist/catalog/catalog-content.d.ts +33 -1
- package/dist/catalog/catalog-content.js +65 -2
- package/dist/catalog/catalog-items.js +1 -0
- package/dist/catalog/catalog-scope.service.d.ts +29 -3
- package/dist/catalog/catalog-scope.service.js +82 -10
- package/dist/catalog/catalog.service.d.ts +7 -5
- package/dist/catalog/catalog.service.js +26 -5
- package/dist/catalog/claude/translate.js +3 -0
- package/dist/catalog/component-scope.controller.d.ts +18 -6
- package/dist/catalog/component-scope.controller.js +36 -13
- package/dist/catalog/component-scope.schemas.d.ts +4 -0
- package/dist/catalog/component-scope.service.d.ts +59 -9
- package/dist/catalog/component-scope.service.js +156 -27
- package/dist/catalog/component-state.d.ts +20 -3
- package/dist/catalog/component-state.js +40 -14
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-catalog.service.js +3 -0
- package/dist/catalog/owned-items.js +7 -0
- package/dist/catalog/platform-components.d.ts +23 -3
- package/dist/catalog/platform-components.js +118 -9
- package/dist/catalog/platform-migration.js +2 -1
- package/dist/catalog/plugin-import.d.ts +24 -4
- package/dist/catalog/plugin-import.js +41 -7
- package/dist/catalog/plugin-install.d.ts +20 -1
- package/dist/catalog/plugin-install.js +71 -1
- package/dist/catalog/plugin-package.d.ts +41 -4
- package/dist/catalog/plugin-package.js +127 -7
- package/dist/catalog/plugin-upgrade.d.ts +13 -2
- package/dist/catalog/plugin-upgrade.js +124 -6
- package/dist/catalog/unshare.js +1 -0
- package/dist/conversations/conversations.service.d.ts +9 -0
- package/dist/conversations/conversations.service.js +9 -2
- package/dist/conversations/events.d.ts +1 -0
- package/dist/db/kernel-schema.d.ts +54 -4
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +27 -0
- package/dist/http/api.module.js +7 -0
- package/dist/identity/accounts.d.ts +11 -4
- package/dist/identity/accounts.js +23 -10
- package/dist/identity/me.controller.d.ts +10 -10
- package/dist/identity/me.controller.js +8 -6
- package/dist/identity/me.schemas.d.ts +13 -3
- package/dist/identity/me.schemas.js +10 -2
- package/dist/identity/settings.d.ts +27 -0
- package/dist/identity/settings.js +64 -0
- package/dist/orgs/settings.service.d.ts +31 -0
- package/dist/orgs/settings.service.js +31 -1
- package/dist/platform/exclusions.js +26 -0
- package/dist/platform/operation.js +6 -2
- package/dist/platform/ops/admin.ops.d.ts +3 -1
- package/dist/platform/ops/admin.ops.js +25 -7
- package/dist/platform/ops/agents.ops.js +19 -6
- package/dist/platform/ops/components.ops.js +1 -0
- package/dist/platform/ops/org-settings.ops.d.ts +3 -1
- package/dist/platform/ops/org-settings.ops.js +33 -8
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/platform.ops.d.ts +3 -1
- package/dist/platform/ops/platform.ops.js +43 -16
- package/dist/platform/ops/themes.ops.d.ts +14 -0
- package/dist/platform/ops/themes.ops.js +47 -0
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/runtime/subagents/definition.d.ts +3 -2
- package/dist/runtime/subagents/definition.js +2 -1
- package/dist/themes/render-css.d.ts +12 -0
- package/dist/themes/render-css.js +132 -0
- package/dist/themes/theme-delivery.service.d.ts +34 -0
- package/dist/themes/theme-delivery.service.js +111 -0
- package/dist/themes/theme-state.d.ts +95 -0
- package/dist/themes/theme-state.js +203 -0
- package/dist/themes/theme-usage.d.ts +61 -0
- package/dist/themes/theme-usage.js +98 -0
- package/dist/themes/themes-public.controller.d.ts +28 -0
- package/dist/themes/themes-public.controller.js +92 -0
- package/dist/themes/themes.controller.d.ts +13 -0
- package/dist/themes/themes.controller.js +38 -0
- package/dist/themes/themes.module.d.ts +3 -0
- package/dist/themes/themes.module.js +21 -0
- package/dist/themes/themes.schemas.d.ts +3 -0
- package/dist/themes/themes.schemas.js +2 -0
- package/dist/themes/themes.service.d.ts +50 -0
- package/dist/themes/themes.service.js +99 -0
- package/dist/tools/connector-account-policies.d.ts +51 -0
- package/dist/tools/connector-account-policies.js +165 -0
- package/dist/tools/connectors.controller.d.ts +35 -4
- package/dist/tools/connectors.controller.js +66 -7
- package/dist/tools/connectors.schemas.d.ts +11 -0
- package/dist/tools/connectors.schemas.js +11 -0
- package/dist/tools/connectors.service.d.ts +2 -0
- package/dist/tools/connectors.service.js +1 -0
- package/dist/tools/pipeline.d.ts +1 -1
- package/dist/tools/policy.d.ts +23 -1
- package/dist/tools/policy.js +99 -24
- package/dist/tools/tools.module.js +3 -0
- package/package.json +3 -3
- package/src/migrations/0102_themes.sql +64 -0
- package/src/migrations/0103_ui_theme_setting.sql +7 -0
- package/src/migrations/0104_connector_tool_account_policies.sql +16 -0
|
@@ -19,9 +19,11 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
19
19
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
20
20
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
21
21
|
import { OrgLock } from "../orgs/org-lock.js";
|
|
22
|
+
import { themeDefinitions, themeSwatches } from "../themes/theme-state.js";
|
|
23
|
+
import { assertNotPlatformDefault, platformDefaultThemeId, themesPreview, } from "../themes/theme-usage.js";
|
|
22
24
|
import { assertOrgsExist } from "./catalog-items.js";
|
|
23
25
|
import { ComponentPromotion, notOwnedError, PROMOTION_ATTEMPTS, PromotionStale, promotionStaleError, } from "./component-promotion.js";
|
|
24
|
-
import { agentsUsing, COMPONENT_KINDS, componentLocked, componentOffered, componentOn, componentPluginOn, connectorsMadeFrom, listedIn, loadComponents, loadScopeState, pluginFactsOf, skillNameClashes, switchOf, } from "./component-state.js";
|
|
26
|
+
import { agentsUsing, COMPONENT_KINDS, componentLocked, componentOffered, componentOn, componentPluginOn, connectorsMadeFrom, listedIn, loadComponents, loadScopeState, pluginFactsOf, skillNameClashes, switchOf, themeIdClashes, } from "./component-state.js";
|
|
25
27
|
import { assertOwnerListed, assertShareable, assertShareableTarget, newlySharedWith, ownerOf, ownersOf, } from "./ownership.js";
|
|
26
28
|
const notFound = () => new ApiError("component.not_found", "Component not found");
|
|
27
29
|
const staleError = () => new ApiError("plugin.scope_stale", "What this change affects has changed since the preview; preview it again and confirm the new hash");
|
|
@@ -31,14 +33,27 @@ const ACTION = {
|
|
|
31
33
|
skill: "skills.manage",
|
|
32
34
|
agent_template: "agents.manage",
|
|
33
35
|
connector_template: "connectors.manage",
|
|
36
|
+
theme: "org.update",
|
|
34
37
|
};
|
|
35
38
|
/**
|
|
36
39
|
* 409 `plugin.skill_conflict` listing the orgs whose own skills use a component's name (rule 10);
|
|
40
|
+
* for a theme 409 `theme.id_taken` listing the orgs where another theme with its id is on (details
|
|
41
|
+
* `[{ theme, orgId, themes, plugins }]`);
|
|
37
42
|
* for an agent template 409 `agent_template.name_taken` listing the orgs that already use its
|
|
38
43
|
* roster type (ruling P4-13; details `[{ orgId, type, templateId, owner }]`, one per clashing
|
|
39
44
|
* template, `owner` as save-as names it).
|
|
40
45
|
*/
|
|
41
46
|
export function nameConflictError(conflicts, kind = "skill") {
|
|
47
|
+
if (kind === "theme") {
|
|
48
|
+
return new ApiError("theme.id_taken", conflicts
|
|
49
|
+
.map((c) => `A theme with id ${c.name} is already on in organization ${c.orgName} (plugin "${c.plugins.join('", "')}"); one theme id cannot be on there twice. Switch that theme off there first.`)
|
|
50
|
+
.join("\n"), conflicts.map((c) => ({
|
|
51
|
+
theme: c.name,
|
|
52
|
+
orgId: c.orgId,
|
|
53
|
+
themes: c.themes ?? [],
|
|
54
|
+
plugins: c.plugins,
|
|
55
|
+
})));
|
|
56
|
+
}
|
|
42
57
|
if (kind === "agent_template") {
|
|
43
58
|
// the save-as wording (ruling P4-9), naming each clashing template's owner
|
|
44
59
|
const whose = (o, orgId) => o === undefined || o.kind === "platform"
|
|
@@ -76,6 +91,8 @@ function previewHash(c, target, p) {
|
|
|
76
91
|
.sort((a, b) => a.orgId.localeCompare(b.orgId))
|
|
77
92
|
.map((a) => ({ ...a, agents: [...a.agents].sort((x, y) => x.id.localeCompare(y.id)) })),
|
|
78
93
|
conflicts: [...p.conflicts].sort((a, b) => a.orgId.localeCompare(b.orgId)),
|
|
94
|
+
// only when present: a preview without themes keeps the hash it had before themes
|
|
95
|
+
...(p.themes !== undefined && { themes: p.themes }),
|
|
79
96
|
}))
|
|
80
97
|
.digest("hex");
|
|
81
98
|
}
|
|
@@ -95,10 +112,11 @@ function switchHash(orgId, c, preview) {
|
|
|
95
112
|
* An org admin switches one component on or off in the org (only that component, rule 5); one of a
|
|
96
113
|
* builtin plugin -> 409 `plugin.scope_locked` (an `all` one may go off, layering spec §4); switching
|
|
97
114
|
* off something in use is previewed with its agents first.
|
|
98
|
-
* Name clashes are checked for skills
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
115
|
+
* Name clashes are checked for skills, agent templates by their roster type, and themes by their
|
|
116
|
+
* id among the themes on in an org (themes spec §6). A change that would take the platform default
|
|
117
|
+
* theme away from an org is refused (409 `theme.platform_default`, themes spec §7). Components are
|
|
118
|
+
* resolved at read time (skills/usable.ts, agents/templates.ts), so nothing is enqueued.
|
|
119
|
+
* Stateless, shared by every replica.
|
|
102
120
|
*/
|
|
103
121
|
let ComponentScopeService = class ComponentScopeService {
|
|
104
122
|
kdb;
|
|
@@ -123,13 +141,13 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
123
141
|
/**
|
|
124
142
|
* What the admin routes act on (ruling P3-23): the platform component `id`, an org row the org
|
|
125
143
|
* promoted already (its platform row), or an org's own row not promoted yet. A connector
|
|
126
|
-
* template has no org rows; anything else -> 404 `component.not_found`.
|
|
144
|
+
* template or a theme has no org rows; anything else -> 404 `component.not_found`.
|
|
127
145
|
*/
|
|
128
146
|
async resolve(kind, id) {
|
|
129
147
|
const [c] = await loadComponents(this.db, kind, { ids: [id] });
|
|
130
148
|
if (c)
|
|
131
149
|
return { c };
|
|
132
|
-
if (kind === "connector_template")
|
|
150
|
+
if (kind === "connector_template" || kind === "theme")
|
|
133
151
|
throw notFound();
|
|
134
152
|
const org = await this.promotion.orgRow(kind, id);
|
|
135
153
|
if (!org)
|
|
@@ -166,7 +184,12 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
166
184
|
const st = await loadScopeState(this.db, kind, comps);
|
|
167
185
|
const orgs = await this.liveOrgs();
|
|
168
186
|
const owners = await ownersOf(this.db, comps.flatMap((c) => (c.ownerOrgId === null ? [] : [c.ownerOrgId])));
|
|
187
|
+
// a theme's name and swatches come from its current definition (the admin pickers, §10)
|
|
188
|
+
const defs = kind === "theme"
|
|
189
|
+
? await themeDefinitions(this.db, comps.map((c) => c.id))
|
|
190
|
+
: null;
|
|
169
191
|
const platform = comps.map((c) => {
|
|
192
|
+
const def = defs?.get(c.id)?.definition;
|
|
170
193
|
const plugin = c.itemId === null ? undefined : st.plugins.get(c.itemId);
|
|
171
194
|
const follow = c.scopeMode === "follow" && c.itemId !== null;
|
|
172
195
|
const scope = follow
|
|
@@ -181,6 +204,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
181
204
|
kind,
|
|
182
205
|
id: c.id,
|
|
183
206
|
name: c.name,
|
|
207
|
+
displayName: c.displayName,
|
|
184
208
|
description: c.description,
|
|
185
209
|
plugin: c.plugin,
|
|
186
210
|
itemId: c.itemId,
|
|
@@ -202,6 +226,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
202
226
|
offCount: scope === "all" && plugin?.builtin !== true
|
|
203
227
|
? orgs.filter((o) => switchOf(st, c.id, o.id) === false).length
|
|
204
228
|
: 0,
|
|
229
|
+
...(def !== undefined && { theme: { name: def.name, swatches: themeSwatches(def) } }),
|
|
205
230
|
};
|
|
206
231
|
});
|
|
207
232
|
const owned = await this.orgOwned(kind, orgs);
|
|
@@ -213,6 +238,8 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
213
238
|
*/
|
|
214
239
|
async orgList(orgId, filter) {
|
|
215
240
|
const out = [];
|
|
241
|
+
// the platform default theme is locked on in every org (themes spec §7)
|
|
242
|
+
const uiTheme = await platformDefaultThemeId(this.db);
|
|
216
243
|
for (const kind of filter.kind !== undefined ? [filter.kind] : COMPONENT_KINDS) {
|
|
217
244
|
const comps = await loadComponents(this.db, kind, filter.itemId !== undefined ? { itemId: filter.itemId } : {});
|
|
218
245
|
const st = await loadScopeState(this.db, kind, comps);
|
|
@@ -226,12 +253,13 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
226
253
|
const on = componentOn(c, p, listed, sw);
|
|
227
254
|
if (!on && !componentOffered(c, p, listed))
|
|
228
255
|
continue;
|
|
229
|
-
const locked = componentLocked(c, p);
|
|
256
|
+
const locked = componentLocked(c, p) || isUiTheme(c, uiTheme);
|
|
230
257
|
const pluginOn = componentPluginOn(c, p);
|
|
231
258
|
rows.push({
|
|
232
259
|
kind,
|
|
233
260
|
id: c.id,
|
|
234
261
|
name: c.name,
|
|
262
|
+
displayName: c.displayName,
|
|
235
263
|
description: c.description,
|
|
236
264
|
plugin: c.plugin,
|
|
237
265
|
itemId: c.itemId,
|
|
@@ -255,11 +283,11 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
255
283
|
/**
|
|
256
284
|
* The org's copies of platform skills or agent templates linked without a pin (ruling B2), by
|
|
257
285
|
* platform id: they resolve to it, so agents that select them use it (none for connector
|
|
258
|
-
* templates).
|
|
286
|
+
* templates and themes).
|
|
259
287
|
*/
|
|
260
288
|
async linkedCopies(orgId, kind) {
|
|
261
289
|
const out = new Map();
|
|
262
|
-
if (kind === "connector_template")
|
|
290
|
+
if (kind === "connector_template" || kind === "theme")
|
|
263
291
|
return out;
|
|
264
292
|
const rows = kind === "skill"
|
|
265
293
|
? await this.db
|
|
@@ -289,10 +317,11 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
289
317
|
}
|
|
290
318
|
/**
|
|
291
319
|
* The orgs' own skills (org scope) and agent templates, not linked to a platform component
|
|
292
|
-
* (ruling P2-11): listed read-only, on in their org only. Connector templates have none
|
|
320
|
+
* (ruling P2-11): listed read-only, on in their org only. Connector templates have none; an
|
|
321
|
+
* org's own theme is a platform_themes row with its org as owner, listed with the platform ones.
|
|
293
322
|
*/
|
|
294
323
|
async orgOwned(kind, orgs) {
|
|
295
|
-
if (kind === "connector_template")
|
|
324
|
+
if (kind === "connector_template" || kind === "theme")
|
|
296
325
|
return [];
|
|
297
326
|
const live = new Set(orgs.map((o) => o.id));
|
|
298
327
|
const rows = kind === "skill"
|
|
@@ -333,6 +362,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
333
362
|
kind,
|
|
334
363
|
id: r.id,
|
|
335
364
|
name: r.name,
|
|
365
|
+
displayName: null,
|
|
336
366
|
description: r.description,
|
|
337
367
|
plugin: r.plugin,
|
|
338
368
|
itemId: r.catalog_item_id,
|
|
@@ -428,7 +458,8 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
428
458
|
* The org admin's switch of one component (`PUT orgs/:orgId/components/:kind/:id/switch`): only
|
|
429
459
|
* this component (rule 5). A component not offered to the org -> 404. On: refused while the
|
|
430
460
|
* component or its plugin is disabled (`plugin.disabled`) or an own skill of the org has its name
|
|
431
|
-
* (`plugin.skill_conflict`, rule 10). Off:
|
|
461
|
+
* (`plugin.skill_conflict`, rule 10; a theme: `theme.id_taken`). Off: the platform default theme
|
|
462
|
+
* while it is on -> `theme.platform_default`; of a builtin plugin -> `plugin.scope_locked`; while it
|
|
432
463
|
* is on, without `confirm` the preview (agents that use it) and its hash are returned, with the
|
|
433
464
|
* current hash the switch is written (a changed preview -> `plugin.switch_stale`).
|
|
434
465
|
*/
|
|
@@ -459,7 +490,26 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
459
490
|
throw new ApiError("plugin.disabled", "This component is disabled by the super admin and cannot be switched on");
|
|
460
491
|
}
|
|
461
492
|
if (body.enabled) {
|
|
462
|
-
if (kind
|
|
493
|
+
if (kind === "theme") {
|
|
494
|
+
// theme ids are unique among the themes on in the org (themes spec §6)
|
|
495
|
+
if (!on && componentOn(c, p, listed, true)) {
|
|
496
|
+
const clash = await themeIdClashes(this.db, orgId, c, c.name);
|
|
497
|
+
if (clash.length > 0) {
|
|
498
|
+
const [org] = await this.liveOrgs(orgId);
|
|
499
|
+
throw nameConflictError([
|
|
500
|
+
{
|
|
501
|
+
orgId,
|
|
502
|
+
orgName: org?.name ?? orgId,
|
|
503
|
+
name: c.name,
|
|
504
|
+
skills: [],
|
|
505
|
+
themes: clash.map((x) => x.id),
|
|
506
|
+
plugins: clash.map((x) => x.plugin ?? "platform"),
|
|
507
|
+
},
|
|
508
|
+
], kind);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else if (kind !== "connector_template" && !on) {
|
|
463
513
|
const clash = kind === "skill"
|
|
464
514
|
? await skillNameClashes(this.db, orgId, c, c.name)
|
|
465
515
|
: await templateTypeClashes(this.db, orgId, c);
|
|
@@ -471,13 +521,22 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
471
521
|
}
|
|
472
522
|
}
|
|
473
523
|
else {
|
|
524
|
+
// The platform default theme is locked on in every org (themes spec §7): refused before the
|
|
525
|
+
// preview, inside this transaction
|
|
526
|
+
if (kind === "theme" && on)
|
|
527
|
+
await assertNotPlatformDefault(this.db, [c.id], () => "narrower");
|
|
474
528
|
if (componentLocked(c, p)) {
|
|
475
529
|
throw new ApiError("plugin.scope_locked", "This component belongs to a built-in plugin: it is on for every organization and cannot be switched off here");
|
|
476
530
|
}
|
|
477
531
|
if (on) {
|
|
532
|
+
const themes = kind === "theme"
|
|
533
|
+
? await themesPreview(this.db, new Map([[c.id, new Set([orgId])]]))
|
|
534
|
+
: undefined;
|
|
478
535
|
const preview = {
|
|
479
536
|
agents: await agentsUsing(this.db, orgId, kind, [c.id]),
|
|
480
537
|
connectorsKept: kind === "connector_template" ? await connectorsMadeFrom(this.db, orgId, c.id) : 0,
|
|
538
|
+
// a theme only: other kinds keep the preview (and the hash) they had before themes
|
|
539
|
+
...(themes !== undefined && { themes }),
|
|
481
540
|
};
|
|
482
541
|
const confirm = switchHash(orgId, c, preview);
|
|
483
542
|
if (body.confirm === undefined)
|
|
@@ -528,7 +587,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
528
587
|
plugin: c.plugin,
|
|
529
588
|
switch: body.enabled,
|
|
530
589
|
on: componentOn(c, p, listed, body.enabled),
|
|
531
|
-
locked: componentLocked(c, p),
|
|
590
|
+
locked: componentLocked(c, p) || isUiTheme(c, await platformDefaultThemeId(this.db)),
|
|
532
591
|
},
|
|
533
592
|
};
|
|
534
593
|
}
|
|
@@ -551,7 +610,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
551
610
|
const offered = componentOffered({ ...c, disabled: false }, p && { ...p, disabled: false }, listed);
|
|
552
611
|
if (!on && !offered)
|
|
553
612
|
throw notFound();
|
|
554
|
-
const locked = componentLocked(c, p);
|
|
613
|
+
const locked = componentLocked(c, p) || isUiTheme(c, await platformDefaultThemeId(this.db));
|
|
555
614
|
let preview = null;
|
|
556
615
|
let confirm = null;
|
|
557
616
|
if (on && !locked) {
|
|
@@ -578,6 +637,8 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
578
637
|
id = await this.promotion.promote(x.promote, actor);
|
|
579
638
|
const c = await this.component(kind, id, true);
|
|
580
639
|
const t = await this.targetOf(c, target);
|
|
640
|
+
if (kind === "theme")
|
|
641
|
+
await this.assertKeepsPlatformDefault(c, t);
|
|
581
642
|
const from = await this.listed(kind, id);
|
|
582
643
|
// Its own scope: the owner stays listed (P3-17), widening goes through the share gate (P3-16);
|
|
583
644
|
// following the plugin again takes the plugin's scope, which its own writer gates
|
|
@@ -615,8 +676,9 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
615
676
|
.values(t.orgs.map((org_id) => ({ kind, component_id: id, org_id })))
|
|
616
677
|
.execute();
|
|
617
678
|
}
|
|
618
|
-
// An own scope that newly shows an org-owned component tells its owner (P3-22)
|
|
619
|
-
|
|
679
|
+
// An own scope that newly shows an org-owned component tells its owner (P3-22); not a theme:
|
|
680
|
+
// the `item_shared` notice names plugins, skills and agent templates only
|
|
681
|
+
if (t.scopeMode === "own" && kind !== "connector_template" && kind !== "theme") {
|
|
620
682
|
const shared = newlySharedWith(before, t);
|
|
621
683
|
if (shared === "all_orgs" || shared.length > 0) {
|
|
622
684
|
await notifyItemShared(this.db, c.ownerOrgId, { kind, id }, c.name, shared);
|
|
@@ -643,7 +705,10 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
643
705
|
const c = await this.component(kind, id, true);
|
|
644
706
|
if (c.disabled === disabled)
|
|
645
707
|
return;
|
|
646
|
-
const
|
|
708
|
+
const t = await this.targetOf(c, { disabled });
|
|
709
|
+
if (kind === "theme")
|
|
710
|
+
await this.assertKeepsPlatformDefault(c, t);
|
|
711
|
+
const p = await this.compute(c, t);
|
|
647
712
|
if (disabled && p.scopeHash !== hash)
|
|
648
713
|
throw staleError();
|
|
649
714
|
if (p.conflicts.length > 0)
|
|
@@ -737,12 +802,19 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
737
802
|
.where("skill_id", "=", id)
|
|
738
803
|
.orderBy("version", "desc")
|
|
739
804
|
.execute()
|
|
740
|
-
:
|
|
741
|
-
.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
805
|
+
: kind === "theme"
|
|
806
|
+
? await this.db
|
|
807
|
+
.selectFrom("platform_theme_versions")
|
|
808
|
+
.select(["version", "catalog_version", "updated_on_own", "created_at"])
|
|
809
|
+
.where("theme_id", "=", id)
|
|
810
|
+
.orderBy("version", "desc")
|
|
811
|
+
.execute()
|
|
812
|
+
: await this.db
|
|
813
|
+
.selectFrom("platform_agent_template_versions")
|
|
814
|
+
.select(["version", "catalog_version", "updated_on_own", "created_at"])
|
|
815
|
+
.where("template_id", "=", id)
|
|
816
|
+
.orderBy("version", "desc")
|
|
817
|
+
.execute();
|
|
746
818
|
return rows.map((r) => ({
|
|
747
819
|
version: String(r.version),
|
|
748
820
|
catalogVersion: r.catalog_version,
|
|
@@ -860,6 +932,36 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
860
932
|
await assertOrgsExist(this.db, orgs);
|
|
861
933
|
return { scopeMode: "own", scope: target.scope, orgs, disabled: c.disabled };
|
|
862
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* 409 `theme.platform_default` when theme `c` is the platform default and `t` would leave it
|
|
937
|
+
* narrower than `all` or gone (themes spec §7), in the caller's transaction, before any write.
|
|
938
|
+
*/
|
|
939
|
+
async assertKeepsPlatformDefault(c, t) {
|
|
940
|
+
const after = await this.themeAfter(c, t);
|
|
941
|
+
await assertNotPlatformDefault(this.db, [c.id], () => after);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Theme `c`'s availability under `t`: gone when disabled (or its plugin is disabled or deleted
|
|
945
|
+
* while it follows it), else `all` when its effective scope (its own, or its plugin's; builtin:
|
|
946
|
+
* `all`) is `all`, else narrower.
|
|
947
|
+
*/
|
|
948
|
+
async themeAfter(c, t) {
|
|
949
|
+
if (t.disabled)
|
|
950
|
+
return "gone";
|
|
951
|
+
if (t.scopeMode === "own")
|
|
952
|
+
return t.scope === "all" ? "all" : "narrower";
|
|
953
|
+
if (c.itemId === null)
|
|
954
|
+
return "gone";
|
|
955
|
+
const item = await this.db
|
|
956
|
+
.selectFrom("catalog_items")
|
|
957
|
+
.select(["scope", "builtin", "disabled"])
|
|
958
|
+
.where("id", "=", c.itemId)
|
|
959
|
+
.where("deleted_at", "is", null)
|
|
960
|
+
.executeTakeFirst();
|
|
961
|
+
if (!item || item.disabled)
|
|
962
|
+
return "gone";
|
|
963
|
+
return item.builtin || item.scope === "all" ? "all" : "narrower";
|
|
964
|
+
}
|
|
863
965
|
/** Live orgs (`only`: that one), sorted by id. */
|
|
864
966
|
async liveOrgs(only) {
|
|
865
967
|
let q = this.db
|
|
@@ -894,11 +996,16 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
894
996
|
const listedAfter = new Set(target.orgs);
|
|
895
997
|
const affected = [];
|
|
896
998
|
const conflicts = [];
|
|
999
|
+
/** A theme: the orgs it is on in now and would be off in. */
|
|
1000
|
+
const lost = new Set();
|
|
897
1001
|
for (const org of await this.liveOrgs()) {
|
|
898
1002
|
const p = pluginFactsOf(st, c.itemId, org.id);
|
|
899
1003
|
const listed = listedIn(st, c.id, org.id);
|
|
900
1004
|
const sw = switchOf(st, c.id, org.id);
|
|
901
|
-
|
|
1005
|
+
const onNow = componentOn(c, p, listed, sw);
|
|
1006
|
+
const onAfter = componentOn(after, p, listedAfter.has(org.id), sw);
|
|
1007
|
+
if (onNow && !onAfter) {
|
|
1008
|
+
lost.add(org.id);
|
|
902
1009
|
const agents = await agentsUsing(this.db, org.id, c.kind, [c.id]);
|
|
903
1010
|
affected.push({
|
|
904
1011
|
orgId: org.id,
|
|
@@ -907,6 +1014,23 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
907
1014
|
connectorsKept: c.kind === "connector_template" ? await connectorsMadeFrom(this.db, org.id, c.id) : 0,
|
|
908
1015
|
});
|
|
909
1016
|
}
|
|
1017
|
+
if (c.kind === "theme") {
|
|
1018
|
+
// theme ids are unique among the themes on in an org (themes spec §6)
|
|
1019
|
+
if (onAfter && !onNow) {
|
|
1020
|
+
const clash = await themeIdClashes(this.db, org.id, c, c.name);
|
|
1021
|
+
if (clash.length > 0) {
|
|
1022
|
+
conflicts.push({
|
|
1023
|
+
orgId: org.id,
|
|
1024
|
+
orgName: org.name,
|
|
1025
|
+
name: c.name,
|
|
1026
|
+
skills: [],
|
|
1027
|
+
themes: clash.map((x) => x.id),
|
|
1028
|
+
plugins: clash.map((x) => x.plugin ?? "platform"),
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
910
1034
|
if (c.kind !== "connector_template" &&
|
|
911
1035
|
componentOffered(after, p, listedAfter.has(org.id)) &&
|
|
912
1036
|
!componentOffered(c, p, listed)) {
|
|
@@ -920,7 +1044,8 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
920
1044
|
}
|
|
921
1045
|
}
|
|
922
1046
|
}
|
|
923
|
-
const
|
|
1047
|
+
const themes = c.kind === "theme" ? await themesPreview(this.db, new Map([[c.id, lost]])) : undefined;
|
|
1048
|
+
const p = { affected, conflicts, ...(themes !== undefined && { themes }) };
|
|
924
1049
|
return { ...p, scopeHash: previewHash({ kind: c.kind, id: x.hashId ?? c.id }, target, p) };
|
|
925
1050
|
}
|
|
926
1051
|
};
|
|
@@ -945,6 +1070,8 @@ ComponentScopeService = __decorate([
|
|
|
945
1070
|
__param(4, Inject(TransactionHost))
|
|
946
1071
|
], ComponentScopeService);
|
|
947
1072
|
export { ComponentScopeService };
|
|
1073
|
+
/** Whether `c` is the platform default theme `uiTheme` (locked on in every org, themes spec §7). */
|
|
1074
|
+
const isUiTheme = (c, uiTheme) => c.kind === "theme" && c.id === uiTheme;
|
|
948
1075
|
/** The plugin row FOR SHARE (exclusive with its scope writers and an unshare, which hold it FOR UPDATE). */
|
|
949
1076
|
async function lockItemShared(db, itemId) {
|
|
950
1077
|
await db.selectFrom("catalog_items").select("id").where("id", "=", itemId).forShare().execute();
|
|
@@ -960,6 +1087,7 @@ const virtualOf = (o) => ({
|
|
|
960
1087
|
plugin: o.plugin,
|
|
961
1088
|
version: String(o.version),
|
|
962
1089
|
description: null,
|
|
1090
|
+
displayName: null,
|
|
963
1091
|
itemId: null,
|
|
964
1092
|
scopeMode: "own",
|
|
965
1093
|
scope: "named",
|
|
@@ -987,4 +1115,5 @@ const TABLE = {
|
|
|
987
1115
|
skill: "platform_skills",
|
|
988
1116
|
agent_template: "platform_agent_templates",
|
|
989
1117
|
connector_template: "platform_connector_templates",
|
|
1118
|
+
theme: "platform_themes",
|
|
990
1119
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { KernelSchemaDb } from "../db/kernel-schema.js";
|
|
2
2
|
import { type PluginScope } from "./scope.js";
|
|
3
3
|
/** The kinds of platform components. */
|
|
4
|
-
export type ComponentKind = "skill" | "agent_template" | "connector_template";
|
|
4
|
+
export type ComponentKind = "skill" | "agent_template" | "connector_template" | "theme";
|
|
5
|
+
/** The kinds the scope, switch, ownership and preview flows (and the component routes) walk. */
|
|
5
6
|
export declare const COMPONENT_KINDS: readonly ComponentKind[];
|
|
6
7
|
/** A component's own scope settings (ruling C2). */
|
|
7
8
|
export interface ComponentScopeFacts {
|
|
@@ -67,10 +68,18 @@ export interface ComponentRow extends ComponentScopeFacts {
|
|
|
67
68
|
/** The current version (a connector template: the catalog version that last changed it). */
|
|
68
69
|
version: string;
|
|
69
70
|
description: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* A connector template's localized display name (its file's `displayName`); null for skills and
|
|
73
|
+
* agent templates, whose name is what they are shown by.
|
|
74
|
+
*/
|
|
75
|
+
displayName: {
|
|
76
|
+
zh: string;
|
|
77
|
+
en: string;
|
|
78
|
+
} | null;
|
|
70
79
|
shownToOthers: boolean;
|
|
71
80
|
/** The owning org (null: the platform; ruling P3-1). */
|
|
72
81
|
ownerOrgId: string | null;
|
|
73
|
-
/** The org it was created in (skills, agent templates; null: the platform, F27). */
|
|
82
|
+
/** The org it was created in (skills, agent templates, themes; null: the platform, F27). */
|
|
74
83
|
originOrgId: string | null;
|
|
75
84
|
/** Its owner shares it with other orgs (the share gate, P3-1). */
|
|
76
85
|
shareable: boolean;
|
|
@@ -130,7 +139,8 @@ export declare function agentDefinitionRefs(db: KernelSchemaDb, orgId: string):
|
|
|
130
139
|
/**
|
|
131
140
|
* The ids an org's agents may reference a platform component by: its own id and the org's copies
|
|
132
141
|
* linked to it (ruling B2: unpinned ones resolve to it; ruling P2-18: pinned ones follow its
|
|
133
|
-
* availability). Connector templates: none (connectors made from one stay the org's own)
|
|
142
|
+
* availability). Connector templates: none (connectors made from one stay the org's own); themes:
|
|
143
|
+
* none (no agent references a theme, and a theme has no org copies).
|
|
134
144
|
*/
|
|
135
145
|
export declare function referenceIds(db: KernelSchemaDb, orgId: string, kind: ComponentKind, ids: readonly string[]): Promise<Map<string, string>>;
|
|
136
146
|
/**
|
|
@@ -167,6 +177,13 @@ export declare function skillNameClashes(db: KernelSchemaDb, orgId: string, comp
|
|
|
167
177
|
skills: string[];
|
|
168
178
|
plugins: string[];
|
|
169
179
|
}>;
|
|
180
|
+
/**
|
|
181
|
+
* The live themes with id `name` that are on in the org (themes spec §6: theme ids are unique in an
|
|
182
|
+
* org's usable set, rule 10), except `self` (null: a theme with no row yet, e.g. one an update adds).
|
|
183
|
+
*/
|
|
184
|
+
export declare function themeIdClashes(db: KernelSchemaDb, orgId: string, self: {
|
|
185
|
+
id: string;
|
|
186
|
+
} | null, name: string): Promise<ComponentRow[]>;
|
|
170
187
|
/** A connector template's id from its manifest path (`./connectors/<id>.json`). */
|
|
171
188
|
export declare const templateIdOfPath: (path: string) => string;
|
|
172
189
|
/**
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { visibleTo, wantedOn } from "./scope.js";
|
|
2
|
+
/** The kinds the scope, switch, ownership and preview flows (and the component routes) walk. */
|
|
2
3
|
export const COMPONENT_KINDS = [
|
|
3
4
|
"skill",
|
|
4
5
|
"agent_template",
|
|
5
6
|
"connector_template",
|
|
7
|
+
"theme",
|
|
6
8
|
];
|
|
7
9
|
/** The component or its plugin stops it everywhere: disabled, or the plugin disabled or deleted. */
|
|
8
10
|
function stopped(c, p) {
|
|
@@ -61,6 +63,11 @@ export function componentOffered(c, p, listed) {
|
|
|
61
63
|
export function componentLocked(_c, p) {
|
|
62
64
|
return p?.builtin === true;
|
|
63
65
|
}
|
|
66
|
+
/** A `{ zh, en }` display name from stored JSON (a template file's), else null. */
|
|
67
|
+
function localizedName(v) {
|
|
68
|
+
const n = v;
|
|
69
|
+
return typeof n?.zh === "string" && typeof n.en === "string" ? { zh: n.zh, en: n.en } : null;
|
|
70
|
+
}
|
|
64
71
|
/** The live platform components of `kind` (`ids` / `itemId`: only those). */
|
|
65
72
|
export async function loadComponents(db, kind, filter = {}) {
|
|
66
73
|
if (filter.ids !== undefined && filter.ids.length === 0)
|
|
@@ -103,19 +110,25 @@ export async function loadComponents(db, kind, filter = {}) {
|
|
|
103
110
|
q = q.where("p.template_id", "=", filter.name);
|
|
104
111
|
if (filter.forUpdate)
|
|
105
112
|
q = q.forUpdate("p");
|
|
106
|
-
return (await q.orderBy("p.id").execute()).map((r) =>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
return (await q.orderBy("p.id").execute()).map((r) => {
|
|
114
|
+
const file = r.file;
|
|
115
|
+
return {
|
|
116
|
+
...common(r),
|
|
117
|
+
...(file?.defaultEnabled === false && { defaultOff: true }),
|
|
118
|
+
name: r.template_id,
|
|
119
|
+
plugin: r.item_name,
|
|
120
|
+
version: r.current_version,
|
|
121
|
+
description: typeof file?.description === "string" ? file.description : null,
|
|
122
|
+
displayName: localizedName(file?.displayName),
|
|
123
|
+
originOrgId: null,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
117
126
|
}
|
|
118
|
-
const table = kind === "skill"
|
|
127
|
+
const table = kind === "skill"
|
|
128
|
+
? "platform_skills"
|
|
129
|
+
: kind === "theme"
|
|
130
|
+
? "platform_themes"
|
|
131
|
+
: "platform_agent_templates";
|
|
119
132
|
let q = db
|
|
120
133
|
.selectFrom(`${table} as p`)
|
|
121
134
|
.select([
|
|
@@ -148,6 +161,7 @@ export async function loadComponents(db, kind, filter = {}) {
|
|
|
148
161
|
plugin: r.plugin_name,
|
|
149
162
|
version: String(r.current_version),
|
|
150
163
|
description: r.description,
|
|
164
|
+
displayName: null,
|
|
151
165
|
originOrgId: r.origin_org_id,
|
|
152
166
|
}));
|
|
153
167
|
}
|
|
@@ -308,11 +322,12 @@ export async function agentDefinitionRefs(db, orgId) {
|
|
|
308
322
|
/**
|
|
309
323
|
* The ids an org's agents may reference a platform component by: its own id and the org's copies
|
|
310
324
|
* linked to it (ruling B2: unpinned ones resolve to it; ruling P2-18: pinned ones follow its
|
|
311
|
-
* availability). Connector templates: none (connectors made from one stay the org's own)
|
|
325
|
+
* availability). Connector templates: none (connectors made from one stay the org's own); themes:
|
|
326
|
+
* none (no agent references a theme, and a theme has no org copies).
|
|
312
327
|
*/
|
|
313
328
|
export async function referenceIds(db, orgId, kind, ids) {
|
|
314
329
|
const out = new Map();
|
|
315
|
-
if (kind === "connector_template" || ids.length === 0)
|
|
330
|
+
if (kind === "connector_template" || kind === "theme" || ids.length === 0)
|
|
316
331
|
return out;
|
|
317
332
|
for (const id of ids)
|
|
318
333
|
out.set(id, id);
|
|
@@ -402,6 +417,17 @@ export async function skillNameClashes(db, orgId, component, name) {
|
|
|
402
417
|
plugins: platform.map((c) => c.plugin ?? "platform"),
|
|
403
418
|
};
|
|
404
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* The live themes with id `name` that are on in the org (themes spec §6: theme ids are unique in an
|
|
422
|
+
* org's usable set, rule 10), except `self` (null: a theme with no row yet, e.g. one an update adds).
|
|
423
|
+
*/
|
|
424
|
+
export async function themeIdClashes(db, orgId, self, name) {
|
|
425
|
+
const others = (await loadComponents(db, "theme", { name })).filter((c) => c.id !== self?.id);
|
|
426
|
+
if (others.length === 0)
|
|
427
|
+
return [];
|
|
428
|
+
const st = await loadScopeState(db, "theme", others);
|
|
429
|
+
return others.filter((c) => componentOn(c, pluginFactsOf(st, c.itemId, orgId), listedIn(st, c.id, orgId), switchOf(st, c.id, orgId)));
|
|
430
|
+
}
|
|
405
431
|
/** A connector template's id from its manifest path (`./connectors/<id>.json`). */
|
|
406
432
|
export const templateIdOfPath = (path) => (path.split("/").pop() ?? path).replace(/\.json$/, "");
|
|
407
433
|
/**
|
|
@@ -188,7 +188,7 @@ export declare function headsToResolve(entries: readonly MarketplaceEntry[], imp
|
|
|
188
188
|
/** The host versions (an entry's `minKernel` is judged against them). */
|
|
189
189
|
export declare const HOST_VERSIONS: {
|
|
190
190
|
readonly kernel: string;
|
|
191
|
-
readonly sdk: "0.1.0-rc.
|
|
191
|
+
readonly sdk: "0.1.0-rc.8";
|
|
192
192
|
};
|
|
193
193
|
/** Whether this deployment satisfies the entry's `minKernel`. */
|
|
194
194
|
export declare const entryCompatible: (e: Pick<MarketplaceEntry, "minKernel">) => boolean;
|
|
@@ -479,6 +479,9 @@ let OrgCatalogService = class OrgCatalogService {
|
|
|
479
479
|
await this.recordSwitch(orgId, actor, itemId, false);
|
|
480
480
|
return { kind: "done", item: await this.itemOf(orgId, actor, itemId) };
|
|
481
481
|
}
|
|
482
|
+
// The platform default theme is locked on (themes spec §7): refused at the preview already;
|
|
483
|
+
// the uninstall's transaction checks again before it writes
|
|
484
|
+
await this.pluginInstaller.assertKeepsPlatformDefault(orgId, live.id);
|
|
482
485
|
const preview = await this.pluginInstaller.previewUninstall(orgId, live.id);
|
|
483
486
|
const confirm = uninstallPreviewHash(preview);
|
|
484
487
|
if (body.confirm === undefined)
|
|
@@ -29,6 +29,7 @@ import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
|
29
29
|
import { OrgLock } from "../orgs/org-lock.js";
|
|
30
30
|
import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
|
|
31
31
|
import { SkillsService } from "../skills/skills.service.js";
|
|
32
|
+
import { assertNotPlatformDefault } from "../themes/theme-usage.js";
|
|
32
33
|
import { CatalogService } from "./catalog.service.js";
|
|
33
34
|
import { CatalogScopeService } from "./catalog-scope.service.js";
|
|
34
35
|
import { notOwnedError } from "./component-promotion.js";
|
|
@@ -47,11 +48,13 @@ const COMPONENT_TABLES = [
|
|
|
47
48
|
"platform_skills",
|
|
48
49
|
"platform_agent_templates",
|
|
49
50
|
"platform_connector_templates",
|
|
51
|
+
"platform_themes",
|
|
50
52
|
];
|
|
51
53
|
const TABLE_OF = {
|
|
52
54
|
skill: "platform_skills",
|
|
53
55
|
agent_template: "platform_agent_templates",
|
|
54
56
|
connector_template: "platform_connector_templates",
|
|
57
|
+
theme: "platform_themes",
|
|
55
58
|
};
|
|
56
59
|
/** The hash of a delete preview: sha256 over its outcome and sorted ids. */
|
|
57
60
|
function previewHashOf(p) {
|
|
@@ -404,6 +407,10 @@ let OwnedItems = class OwnedItems {
|
|
|
404
407
|
afterCommit(() => this.enqueueCleanup(item.id));
|
|
405
408
|
return "removed";
|
|
406
409
|
}
|
|
410
|
+
// A transfer leaves none of its themes `all` (an `all` scope is narrowed, P3-39): refused
|
|
411
|
+
// while one is the platform default (themes spec §7), before anything changes
|
|
412
|
+
// (its theme rows FOR UPDATE first: the theme lock order is item, theme row, `uiTheme`)
|
|
413
|
+
await assertNotPlatformDefault(t, (await loadComponents(t, "theme", { itemId: item.id, forUpdate: true })).map((c) => c.id), () => "narrower");
|
|
407
414
|
// Read before anything changes: where each own-scope `all` component is on (P3-39)
|
|
408
415
|
const narrow = [];
|
|
409
416
|
for (const kind of COMPONENT_KINDS) {
|