@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.
Files changed (106) hide show
  1. package/dist/admin/admin.schemas.d.ts +3 -0
  2. package/dist/agents/agents.schemas.d.ts +3 -0
  3. package/dist/agents/agents.service.js +11 -1
  4. package/dist/agents/definition.d.ts +1 -0
  5. package/dist/agents/definition.js +7 -3
  6. package/dist/agents/org-templates.js +4 -0
  7. package/dist/agents/pending-connectors.service.js +1 -1
  8. package/dist/catalog/catalog-content.d.ts +33 -1
  9. package/dist/catalog/catalog-content.js +65 -2
  10. package/dist/catalog/catalog-items.js +1 -0
  11. package/dist/catalog/catalog-scope.service.d.ts +29 -3
  12. package/dist/catalog/catalog-scope.service.js +82 -10
  13. package/dist/catalog/catalog.service.d.ts +7 -5
  14. package/dist/catalog/catalog.service.js +26 -5
  15. package/dist/catalog/claude/translate.js +3 -0
  16. package/dist/catalog/component-scope.controller.d.ts +18 -6
  17. package/dist/catalog/component-scope.controller.js +36 -13
  18. package/dist/catalog/component-scope.schemas.d.ts +4 -0
  19. package/dist/catalog/component-scope.service.d.ts +59 -9
  20. package/dist/catalog/component-scope.service.js +156 -27
  21. package/dist/catalog/component-state.d.ts +20 -3
  22. package/dist/catalog/component-state.js +40 -14
  23. package/dist/catalog/marketplaces.service.d.ts +1 -1
  24. package/dist/catalog/org-catalog.service.js +3 -0
  25. package/dist/catalog/owned-items.js +7 -0
  26. package/dist/catalog/platform-components.d.ts +23 -3
  27. package/dist/catalog/platform-components.js +118 -9
  28. package/dist/catalog/platform-migration.js +2 -1
  29. package/dist/catalog/plugin-import.d.ts +24 -4
  30. package/dist/catalog/plugin-import.js +41 -7
  31. package/dist/catalog/plugin-install.d.ts +20 -1
  32. package/dist/catalog/plugin-install.js +71 -1
  33. package/dist/catalog/plugin-package.d.ts +41 -4
  34. package/dist/catalog/plugin-package.js +127 -7
  35. package/dist/catalog/plugin-upgrade.d.ts +13 -2
  36. package/dist/catalog/plugin-upgrade.js +124 -6
  37. package/dist/catalog/unshare.js +1 -0
  38. package/dist/conversations/conversations.service.d.ts +9 -0
  39. package/dist/conversations/conversations.service.js +9 -2
  40. package/dist/conversations/events.d.ts +1 -0
  41. package/dist/db/kernel-schema.d.ts +54 -4
  42. package/dist/errors.d.ts +27 -0
  43. package/dist/errors.js +27 -0
  44. package/dist/http/api.module.js +7 -0
  45. package/dist/identity/accounts.d.ts +11 -4
  46. package/dist/identity/accounts.js +23 -10
  47. package/dist/identity/me.controller.d.ts +10 -10
  48. package/dist/identity/me.controller.js +8 -6
  49. package/dist/identity/me.schemas.d.ts +13 -3
  50. package/dist/identity/me.schemas.js +10 -2
  51. package/dist/identity/settings.d.ts +27 -0
  52. package/dist/identity/settings.js +64 -0
  53. package/dist/orgs/settings.service.d.ts +31 -0
  54. package/dist/orgs/settings.service.js +31 -1
  55. package/dist/platform/exclusions.js +26 -0
  56. package/dist/platform/operation.js +6 -2
  57. package/dist/platform/ops/admin.ops.d.ts +3 -1
  58. package/dist/platform/ops/admin.ops.js +25 -7
  59. package/dist/platform/ops/agents.ops.js +19 -6
  60. package/dist/platform/ops/components.ops.js +1 -0
  61. package/dist/platform/ops/org-settings.ops.d.ts +3 -1
  62. package/dist/platform/ops/org-settings.ops.js +33 -8
  63. package/dist/platform/ops/orgs.ops.js +1 -0
  64. package/dist/platform/ops/platform.ops.d.ts +3 -1
  65. package/dist/platform/ops/platform.ops.js +43 -16
  66. package/dist/platform/ops/themes.ops.d.ts +14 -0
  67. package/dist/platform/ops/themes.ops.js +47 -0
  68. package/dist/platform/platform-ops.module.d.ts +2 -1
  69. package/dist/platform/platform-ops.module.js +5 -0
  70. package/dist/platform/route-schemas.js +3 -0
  71. package/dist/runtime/subagents/definition.d.ts +3 -2
  72. package/dist/runtime/subagents/definition.js +2 -1
  73. package/dist/themes/render-css.d.ts +12 -0
  74. package/dist/themes/render-css.js +132 -0
  75. package/dist/themes/theme-delivery.service.d.ts +34 -0
  76. package/dist/themes/theme-delivery.service.js +111 -0
  77. package/dist/themes/theme-state.d.ts +95 -0
  78. package/dist/themes/theme-state.js +203 -0
  79. package/dist/themes/theme-usage.d.ts +61 -0
  80. package/dist/themes/theme-usage.js +98 -0
  81. package/dist/themes/themes-public.controller.d.ts +28 -0
  82. package/dist/themes/themes-public.controller.js +92 -0
  83. package/dist/themes/themes.controller.d.ts +13 -0
  84. package/dist/themes/themes.controller.js +38 -0
  85. package/dist/themes/themes.module.d.ts +3 -0
  86. package/dist/themes/themes.module.js +21 -0
  87. package/dist/themes/themes.schemas.d.ts +3 -0
  88. package/dist/themes/themes.schemas.js +2 -0
  89. package/dist/themes/themes.service.d.ts +50 -0
  90. package/dist/themes/themes.service.js +99 -0
  91. package/dist/tools/connector-account-policies.d.ts +51 -0
  92. package/dist/tools/connector-account-policies.js +165 -0
  93. package/dist/tools/connectors.controller.d.ts +35 -4
  94. package/dist/tools/connectors.controller.js +66 -7
  95. package/dist/tools/connectors.schemas.d.ts +11 -0
  96. package/dist/tools/connectors.schemas.js +11 -0
  97. package/dist/tools/connectors.service.d.ts +2 -0
  98. package/dist/tools/connectors.service.js +1 -0
  99. package/dist/tools/pipeline.d.ts +1 -1
  100. package/dist/tools/policy.d.ts +23 -1
  101. package/dist/tools/policy.js +99 -24
  102. package/dist/tools/tools.module.js +3 -0
  103. package/package.json +3 -3
  104. package/src/migrations/0102_themes.sql +64 -0
  105. package/src/migrations/0103_ui_theme_setting.sql +7 -0
  106. 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 only: an org agent template always carries its plugin's
99
- * type, so one with a platform template's type is a copy of it. Components are resolved at read
100
- * time (skills/usable.ts, agents/templates.ts), so nothing is enqueued. Stateless, shared by every
101
- * replica.
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: of a builtin plugin -> `plugin.scope_locked`; while it
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 !== "connector_template" && !on) {
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
- if (t.scopeMode === "own" && kind !== "connector_template") {
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 p = await this.compute(c, await this.targetOf(c, { disabled }));
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
- : await this.db
741
- .selectFrom("platform_agent_template_versions")
742
- .select(["version", "catalog_version", "updated_on_own", "created_at"])
743
- .where("template_id", "=", id)
744
- .orderBy("version", "desc")
745
- .execute();
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
- if (componentOn(c, p, listed, sw) && !componentOn(after, p, listedAfter.has(org.id), sw)) {
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 p = { affected, conflicts };
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
- ...common(r),
108
- ...(r.file?.defaultEnabled === false && {
109
- defaultOff: true,
110
- }),
111
- name: r.template_id,
112
- plugin: r.item_name,
113
- version: r.current_version,
114
- description: null,
115
- originOrgId: null,
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" ? "platform_skills" : "platform_agent_templates";
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.7";
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) {