@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
@@ -1,4 +1,5 @@
1
- import { type AgentTemplate, type ConnectorTemplateFile, type PluginManifest } from "@capybara-hq/plugin-sdk";
1
+ import { type AgentTemplate, type ConnectorTemplateFile, type ParsedTheme, type PluginManifest } from "@capybara-hq/plugin-sdk";
2
+ import { ApiError } from "../errors.js";
2
3
  import { LEGACY_FORMAT_MESSAGE, type PackageTgzLimits, packagePath } from "../packages/tgz.js";
3
4
  export { LEGACY_FORMAT_MESSAGE, packagePath };
4
5
  /**
@@ -13,13 +14,24 @@ export { LEGACY_FORMAT_MESSAGE, packagePath };
13
14
  * plugin.json) is rejected first, with {@link LEGACY_FORMAT_MESSAGE}: there is no reader for the
14
15
  * old formats.
15
16
  */
16
- /** Codes of {@link PluginPackageError}. */
17
- export type PluginPackageErrorCode = "catalog.invalid_manifest" | "catalog.legacy_format";
17
+ /**
18
+ * Codes of {@link PluginPackageError}: `theme.invalid` / `theme.unreadable` when the package's only
19
+ * problems are in its themes (themes spec §5: a failing theme refuses the whole plugin).
20
+ */
21
+ export type PluginPackageErrorCode = "catalog.invalid_manifest" | "catalog.legacy_format" | "theme.invalid" | "theme.unreadable";
22
+ /** The problems of one theme of a refused plugin (English; `unreadable:` lines for readability). */
23
+ export interface PluginThemeProblem {
24
+ /** The theme id (the file's base name). */
25
+ theme: string;
26
+ problems: string[];
27
+ }
18
28
  /** The plugin package is invalid; `issues` lists the problems (English, shown to the admin). */
19
29
  export declare class PluginPackageError extends Error {
20
30
  readonly code: PluginPackageErrorCode;
21
31
  readonly issues: string[];
22
- constructor(code: PluginPackageErrorCode, issues: string[]);
32
+ /** The failing themes of a `theme.*` code. */
33
+ readonly themes?: PluginThemeProblem[];
34
+ constructor(code: PluginPackageErrorCode, issues: string[], themes?: PluginThemeProblem[]);
23
35
  }
24
36
  /** A skill of the plugin. `dir` is relative to the package root; `files` are relative to `dir`. */
25
37
  export interface PluginPackageSkill {
@@ -42,6 +54,18 @@ export interface PluginPackageAgentTemplate {
42
54
  path: string;
43
55
  template: AgentTemplate;
44
56
  }
57
+ /** A theme of the plugin (themes spec §4); `id` is the file's base name. */
58
+ export interface PluginPackageTheme {
59
+ id: string;
60
+ /** Relative to the package root (no leading `./`). */
61
+ path: string;
62
+ theme: ParsedTheme;
63
+ /**
64
+ * The theme's fonts and logo files after `validateThemeAssets` (the SVGs sanitized, the rest
65
+ * unchanged), keyed by their path relative to the package root (no leading `./`).
66
+ */
67
+ assets: Map<string, Uint8Array>;
68
+ }
45
69
  /** A validated plugin file tree. Paths are relative to the plugin root (no `package/`). */
46
70
  export interface PluginContents {
47
71
  /** `{}` when a git plugin has no package.json. */
@@ -52,6 +76,8 @@ export interface PluginContents {
52
76
  connectorTemplates: PluginPackageConnectorTemplate[];
53
77
  /** Agent templates (listed in plugin.json `agents`, else scanned from `agents/*.md`). */
54
78
  agentTemplates: PluginPackageAgentTemplate[];
79
+ /** Themes (listed in plugin.json `themes`, else scanned from `themes/*.json`). */
80
+ themes: PluginPackageTheme[];
55
81
  /** {@link contentDigest} of `files` (the same for a tgz and a git tree of the same files). */
56
82
  contentDigest: string;
57
83
  }
@@ -141,3 +167,14 @@ export interface ReadPluginFilesOptions {
141
167
  * {@link PluginPackageError} like {@link readPluginPackage}.
142
168
  */
143
169
  export declare function readPluginFiles(files: Map<string, Uint8Array>, opts: ReadPluginFilesOptions): PluginContents;
170
+ /**
171
+ * The refusal of a plugin whose only problems are in its themes: `theme.invalid` when any theme
172
+ * has a problem other than readability (those themes listed), else `theme.unreadable` (every
173
+ * failing theme listed with its failing pairs).
174
+ */
175
+ export declare function pluginThemeError(problems: PluginThemeProblem[]): PluginPackageError;
176
+ /**
177
+ * The API error of a `theme.*` {@link PluginPackageError}: 400 `theme.invalid` with details
178
+ * `[{ theme, errors }]`, or 400 `theme.unreadable` with details `[{ theme, issues }]`.
179
+ */
180
+ export declare function themeApiError(e: PluginPackageError): ApiError;
@@ -1,18 +1,25 @@
1
1
  import { createHash } from "node:crypto";
2
- import { AGENT_TEMPLATE_LIMITS, CATALOG_ITEM_NAME_RE, CONNECTOR_TEMPLATE_LIMITS, hostSdkProblem, PLUGIN_MANIFEST_FILE, PLUGIN_MANIFEST_LIMITS, parseAgentTemplate, parseConnectorTemplateFile, parsePluginManifest, parseSkillMd, SKILL_ENTRY_FILE, setupGuideFileProblem, validatePluginPackageJsonFor, } from "@capybara-hq/plugin-sdk";
2
+ import { AGENT_TEMPLATE_LIMITS, CATALOG_ITEM_NAME_RE, CONNECTOR_TEMPLATE_LIMITS, hostSdkProblem, PLUGIN_MANIFEST_FILE, PLUGIN_MANIFEST_LIMITS, parseAgentTemplate, parseConnectorTemplateFile, parsePluginManifest, parseSkillMd, parseThemeFile, SKILL_ENTRY_FILE, setupGuideFileProblem, THEME_LIMITS, themeIdOf, validatePluginPackageJsonFor, validateThemeAssets, } from "@capybara-hq/plugin-sdk";
3
+ import { ApiError } from "../errors.js";
3
4
  import { HOST_ZOD_VERSION, LEGACY_FORMAT_MESSAGE, PackageTgzError, packagePath, parsePackageJsonFile, unpackPackageTgz, } from "../packages/tgz.js";
4
5
  export { LEGACY_FORMAT_MESSAGE, packagePath };
5
6
  /** The plugin package is invalid; `issues` lists the problems (English, shown to the admin). */
6
7
  export class PluginPackageError extends Error {
7
8
  code;
8
9
  issues;
9
- constructor(code, issues) {
10
+ /** The failing themes of a `theme.*` code. */
11
+ themes;
12
+ constructor(code, issues, themes) {
10
13
  super(code === "catalog.legacy_format"
11
14
  ? (issues[0] ?? LEGACY_FORMAT_MESSAGE)
12
- : `Invalid plugin package: ${issues[0] ?? "unknown problem"}`);
15
+ : code === "theme.invalid" || code === "theme.unreadable"
16
+ ? `Theme ${themes?.[0]?.theme ?? "?"} ${code === "theme.invalid" ? "is invalid" : "is not readable"}: ${issues[0] ?? "unknown problem"}`
17
+ : `Invalid plugin package: ${issues[0] ?? "unknown problem"}`);
13
18
  this.name = "PluginPackageError";
14
19
  this.code = code;
15
20
  this.issues = issues;
21
+ if (themes !== undefined)
22
+ this.themes = themes;
16
23
  }
17
24
  }
18
25
  /**
@@ -345,6 +352,7 @@ export function readPluginFiles(files, opts) {
345
352
  agentNames.set(r.template.name, path);
346
353
  agentTemplates.push({ name: r.template.name, path, template: r.template });
347
354
  }
355
+ const { themes, themePaths, themeProblems } = readThemes(files, manifest);
348
356
  if (manifest.icon !== undefined) {
349
357
  const icon = files.get(packagePath(manifest.icon));
350
358
  const problem = icon === undefined
@@ -378,11 +386,21 @@ export function readPluginFiles(files, opts) {
378
386
  }
379
387
  }
380
388
  }
381
- else if (skillDirs.length === 0 && templatePaths.length === 0 && agentPaths.length === 0) {
382
- issues.push("a plugin must contain an extension, a skill, a connector template or an agent template");
389
+ else if (skillDirs.length === 0 &&
390
+ templatePaths.length === 0 &&
391
+ agentPaths.length === 0 &&
392
+ themePaths.length === 0) {
393
+ issues.push("a plugin must contain an extension, a skill, a connector template, an agent template or a theme");
383
394
  }
384
- if (issues.length > 0)
385
- invalid(issues);
395
+ if (issues.length > 0) {
396
+ // Other problems first: the theme problems are listed with them as plain issues
397
+ invalid([
398
+ ...issues,
399
+ ...themeProblems.flatMap((t) => t.problems.map((p) => `themes/${t.theme}: ${p}`)),
400
+ ]);
401
+ }
402
+ if (themeProblems.length > 0)
403
+ throw pluginThemeError(themeProblems);
386
404
  return {
387
405
  packageJson: (packageJson ?? {}),
388
406
  manifest,
@@ -390,6 +408,108 @@ export function readPluginFiles(files, opts) {
390
408
  skills,
391
409
  connectorTemplates,
392
410
  agentTemplates,
411
+ themes,
393
412
  contentDigest: contentDigest(files),
394
413
  };
395
414
  }
415
+ const UNREADABLE = "unreadable:";
416
+ /**
417
+ * The refusal of a plugin whose only problems are in its themes: `theme.invalid` when any theme
418
+ * has a problem other than readability (those themes listed), else `theme.unreadable` (every
419
+ * failing theme listed with its failing pairs).
420
+ */
421
+ export function pluginThemeError(problems) {
422
+ const invalidOnes = problems.filter((t) => t.problems.some((p) => !p.startsWith(UNREADABLE)));
423
+ const code = invalidOnes.length > 0 ? "theme.invalid" : "theme.unreadable";
424
+ const listed = invalidOnes.length > 0 ? invalidOnes : problems;
425
+ return new PluginPackageError(code, listed.flatMap((t) => t.problems), listed);
426
+ }
427
+ /**
428
+ * The API error of a `theme.*` {@link PluginPackageError}: 400 `theme.invalid` with details
429
+ * `[{ theme, errors }]`, or 400 `theme.unreadable` with details `[{ theme, issues }]`.
430
+ */
431
+ export function themeApiError(e) {
432
+ const code = e.code === "theme.unreadable" ? "theme.unreadable" : "theme.invalid";
433
+ const key = code === "theme.invalid" ? "errors" : "issues";
434
+ return new ApiError(code, e.message, (e.themes ?? []).map((t) => ({ theme: t.theme, [key]: t.problems })));
435
+ }
436
+ /**
437
+ * The plugin's themes (themes spec §4-§5): listed in `manifest.themes`, else scanned from
438
+ * `themes/*.json` (at most {@link THEME_LIMITS}`.perPlugin`); each parsed by `parseThemeFile`
439
+ * (shape, token grammar, readability) and its assets checked by `validateThemeAssets`. The asset
440
+ * limit is per plugin, so the sizes of the distinct asset paths are also summed over all themes.
441
+ * Problems are returned per theme, never thrown.
442
+ */
443
+ function readThemes(files, manifest) {
444
+ const themeProblems = [];
445
+ const problem = (theme, problems) => {
446
+ const t = themeProblems.find((x) => x.theme === theme);
447
+ if (t)
448
+ t.problems.push(...problems);
449
+ else
450
+ themeProblems.push({ theme, problems: [...problems] });
451
+ };
452
+ const themePaths = [];
453
+ if (manifest.themes !== undefined) {
454
+ for (const t of manifest.themes) {
455
+ const path = packagePath(t.path);
456
+ if (!files.has(path))
457
+ problem(themeIdOf(path), [`${t.path} not found in the package`]);
458
+ else if (!themePaths.includes(path))
459
+ themePaths.push(path);
460
+ }
461
+ }
462
+ else {
463
+ for (const p of [...files.keys()].sort()) {
464
+ if (/^themes\/[^/]+\.json$/.test(p))
465
+ themePaths.push(p);
466
+ }
467
+ if (themePaths.length > THEME_LIMITS.perPlugin) {
468
+ problem(themeIdOf(themePaths[THEME_LIMITS.perPlugin]), [
469
+ `themes/ holds ${themePaths.length} themes; a plugin may have at most ${THEME_LIMITS.perPlugin}`,
470
+ ]);
471
+ }
472
+ }
473
+ const themes = [];
474
+ const sizes = new Map();
475
+ for (const path of themePaths) {
476
+ const id = themeIdOf(path);
477
+ if (themes.some((t) => t.id === id)) {
478
+ problem(id, [`${path}: another theme of the plugin has the id "${id}"`]);
479
+ continue;
480
+ }
481
+ const bytes = files.get(path);
482
+ if (bytes.byteLength > THEME_LIMITS.fileBytes) {
483
+ problem(id, [`${path}: the theme file is larger than ${THEME_LIMITS.fileBytes} bytes`]);
484
+ continue;
485
+ }
486
+ const json = decode(bytes);
487
+ if (json === undefined) {
488
+ problem(id, [`${path}: not valid UTF-8 text`]);
489
+ continue;
490
+ }
491
+ const parsed = parseThemeFile(id, json);
492
+ if ("errors" in parsed) {
493
+ problem(id, parsed.errors);
494
+ continue;
495
+ }
496
+ const { assets, errors } = validateThemeAssets(parsed, files);
497
+ if (errors.length > 0) {
498
+ problem(id, errors);
499
+ continue;
500
+ }
501
+ let total = 0;
502
+ for (const p of assets.keys())
503
+ sizes.set(p, files.get(p).byteLength);
504
+ for (const n of sizes.values())
505
+ total += n;
506
+ if (total > THEME_LIMITS.assetsBytes) {
507
+ problem(id, [
508
+ `the plugin's theme assets are larger than ${THEME_LIMITS.assetsBytes} bytes in total (${total}); the limit covers all of the plugin's themes`,
509
+ ]);
510
+ continue;
511
+ }
512
+ themes.push({ id, path, theme: parsed, assets });
513
+ }
514
+ return { themes, themePaths, themeProblems };
515
+ }
@@ -11,6 +11,7 @@ import { LockService } from "../infra/lock/lock.service.js";
11
11
  import type { KernelLogger } from "../log.js";
12
12
  import { OrgLock } from "../orgs/org-lock.js";
13
13
  import { StorageService } from "../storage/storage.service.js";
14
+ import { type ThemePreview } from "../themes/theme-usage.js";
14
15
  import { ConnectorsService } from "../tools/connectors.service.js";
15
16
  import { type CatalogVersionFiles } from "./catalog-content.js";
16
17
  import { type ComponentKind } from "./component-state.js";
@@ -25,7 +26,7 @@ export interface PlatformUpdateComponent {
25
26
  kind: ComponentKind;
26
27
  /** The platform component (null: added by the target). */
27
28
  id: string | null;
28
- /** Skill or agent template name; a connector template's id. */
29
+ /** Skill or agent template name; a connector template's or a theme's id. */
29
30
  name: string;
30
31
  change: UpdateChange;
31
32
  /** The catalog version of the component's current version (null: added, or no catalog version). */
@@ -109,6 +110,11 @@ export interface PlatformUpdatePreview {
109
110
  needsApproval: boolean;
110
111
  } | null;
111
112
  connectorOrgs: PlatformUpdateConnectorOrg[];
113
+ /**
114
+ * The themes the update removes that are on in some org (themes spec §6): the orgs whose default
115
+ * each is and how many people picked it. Absent when no such theme is removed.
116
+ */
117
+ themes?: ThemePreview[];
112
118
  /**
113
119
  * An owner's update preview only ({@link PlatformPluginUpdater.previewForOwner}): the name of
114
120
  * every org the preview mentions, by id. An org admin cannot read other orgs, and these names
@@ -306,7 +312,10 @@ export declare class PlatformPluginUpdater {
306
312
  * extension removes it.
307
313
  */
308
314
  private diff;
309
- /** The target's skill names, agent templates and connector templates, from object storage. */
315
+ /**
316
+ * The target's skill names, agent templates, connector templates and themes, from object storage
317
+ * (a theme failing its checks -> 400 `theme.invalid`, themes spec §6).
318
+ */
310
319
  private readContents;
311
320
  /**
312
321
  * Whether the target's extension package asks for permissions the approved snapshot does not
@@ -328,6 +337,8 @@ export declare class PlatformPluginUpdater {
328
337
  private checkDependents;
329
338
  /** The plugin's live platform components with their current versions. */
330
339
  private liveComponents;
340
+ /** The `themes` block of the preview: the orgs each of the removed themes `ids` is on in. */
341
+ private themesLost;
331
342
  /** The orgs where the plugin is on: a live install (builtin: every live org). */
332
343
  private pluginOrgs;
333
344
  private liveOrgs;
@@ -34,11 +34,12 @@ import { KERNEL_LOGGER } from "../infra/tokens.js";
34
34
  import { OrgLock } from "../orgs/org-lock.js";
35
35
  import { packagePath } from "../packages/tgz.js";
36
36
  import { StorageService } from "../storage/storage.service.js";
37
+ import { assertNotPlatformDefault, pickedByIn, themesPreview, } from "../themes/theme-usage.js";
37
38
  import { canonicalJson } from "../tools/canonical-json.js";
38
39
  import { ConnectorsService } from "../tools/connectors.service.js";
39
- import { dirDigest, getPath, readCatalogAgentTemplates, readCatalogTemplates, setPath, skillFilesDigest, templatePluginProblem, } from "./catalog-content.js";
40
+ import { dirDigest, getPath, readCatalogAgentTemplates, readCatalogTemplates, readCatalogThemes, setPath, skillFilesDigest, templatePluginProblem, themeAssetsDigest, } from "./catalog-content.js";
40
41
  import { markVersionApplied, versionUsable } from "./catalog-items.js";
41
- import { agentsUsing, clashingOrgSkills, clashingPlatformSkills, componentOn, connectorsMadeFrom, connectorTemplatesOn, listedIn, loadScopeState, pluginFactsOf, switchOf, } from "./component-state.js";
42
+ import { agentsUsing, clashingOrgSkills, clashingPlatformSkills, componentOn, connectorsMadeFrom, connectorTemplatesOn, listedIn, loadComponents, loadScopeState, pluginFactsOf, switchOf, themeIdClashes, } from "./component-state.js";
42
43
  import { CatalogItemLock } from "./locks.js";
43
44
  import { itemPendingError, itemRemovedError } from "./ownership.js";
44
45
  import { PlanStale, PlatformComponents } from "./platform-components.js";
@@ -58,6 +59,7 @@ const KIND_ORDER = {
58
59
  skill: 0,
59
60
  agent_template: 1,
60
61
  connector_template: 2,
62
+ theme: 3,
61
63
  };
62
64
  /** How long one update holds its item (renewed while it runs). */
63
65
  const UPDATE_LOCK_TTL_MS = 60_000;
@@ -188,7 +190,17 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
188
190
  owner: { orgId },
189
191
  ...(staged !== undefined && { target: staged }),
190
192
  }));
191
- return { ...preview, orgNames: await this.orgNamesOf(preview) };
193
+ // The owner sees the names of the orgs whose default a removed theme is (named like the other
194
+ // orgs the preview mentions, P3-14 / P3-19), but people only of its own org: other orgs'
195
+ // member counts were never disclosed to it. Display only: `diffHash` covers the full preview.
196
+ const themes = preview.themes === undefined
197
+ ? undefined
198
+ : await Promise.all(preview.themes.map(async (t) => ({
199
+ ...t,
200
+ pickedBy: await pickedByIn(this.db, t.id, new Set([orgId])),
201
+ })));
202
+ const owned = { ...preview, ...(themes !== undefined && { themes }) };
203
+ return { ...owned, orgNames: await this.orgNamesOf(owned) };
192
204
  }
193
205
  /**
194
206
  * Locks, in sorted order and before the item (ruling P3-50), every live org the update may
@@ -234,6 +246,7 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
234
246
  ...(p.extension?.orgs ?? []),
235
247
  ...p.connectorOrgs.map((c) => c.orgId),
236
248
  ...p.components.flatMap((c) => [...c.pinnedOrgs, ...(c.kept ?? []).map((k) => k.orgId)]),
249
+ ...(p.themes ?? []).flatMap((t) => t.orgDefaults.map((o) => o.orgId)),
237
250
  ]);
238
251
  if (ids.size === 0)
239
252
  return {};
@@ -517,7 +530,9 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
517
530
  ? "platform_skills"
518
531
  : c.kind === "agent_template"
519
532
  ? "platform_agent_templates"
520
- : "platform_connector_templates";
533
+ : c.kind === "theme"
534
+ ? "platform_themes"
535
+ : "platform_connector_templates";
521
536
  await t
522
537
  .updateTable(table)
523
538
  .set({ deleted_at: now, updated_at: now })
@@ -984,6 +999,43 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
984
999
  pinnedOrgs: [],
985
1000
  });
986
1001
  }
1002
+ // Themes (by id, canonical definition and their assets' digest)
1003
+ const toThemes = contents.themes;
1004
+ for (const th of toThemes) {
1005
+ const l = live.themes.find((x) => x.name === th.id);
1006
+ if (!l) {
1007
+ components.push(added("theme", th.id));
1008
+ continue;
1009
+ }
1010
+ const same = canonicalJson(l.definition) === canonicalJson(th.theme) &&
1011
+ l.assetsDigest === th.assetsDigest;
1012
+ components.push({
1013
+ kind: "theme",
1014
+ id: l.id,
1015
+ name: th.id,
1016
+ change: same ? "same" : direction(l.catalogVersion),
1017
+ from: l.catalogVersion,
1018
+ to: to.version,
1019
+ updatedOnOwn: false,
1020
+ ownScope: l.ownScope,
1021
+ pinnedOrgs: [],
1022
+ });
1023
+ }
1024
+ for (const l of live.themes) {
1025
+ if (toThemes.some((x) => x.id === l.name))
1026
+ continue;
1027
+ components.push({
1028
+ kind: "theme",
1029
+ id: l.id,
1030
+ name: l.name,
1031
+ change: "removed",
1032
+ from: l.catalogVersion,
1033
+ to: null,
1034
+ updatedOnOwn: false,
1035
+ ownScope: l.ownScope,
1036
+ pinnedOrgs: [],
1037
+ });
1038
+ }
987
1039
  // The orgs where the plugin is on (a live install; builtin: every org)
988
1040
  const onOrgs = await this.pluginOrgs(itemId, item.builtin);
989
1041
  if (extension)
@@ -1005,6 +1057,32 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
1005
1057
  if (conflicts.length > 0) {
1006
1058
  throw new ApiError("plugin.skill_conflict", `The update adds skills whose names organizations already use where the plugin is on (${[...new Set(conflicts.map((c) => c.skill))].join(", ")}); remove or rename those skills first`, conflicts);
1007
1059
  }
1060
+ // Themes spec §7: an update never removes the platform default theme; rule 10 (§6): an added
1061
+ // theme must not take an id already on where the plugin is on
1062
+ const removedThemes = components
1063
+ .filter((c) => c.kind === "theme" && c.change === "removed" && c.id !== null)
1064
+ .map((c) => c.id);
1065
+ await assertNotPlatformDefault(db, removedThemes, () => "gone");
1066
+ const themeConflicts = [];
1067
+ for (const c of components) {
1068
+ if (c.kind !== "theme" || c.change !== "added")
1069
+ continue;
1070
+ for (const orgId of onOrgs) {
1071
+ const clash = await themeIdClashes(db, orgId, null, c.name);
1072
+ if (clash.length > 0) {
1073
+ themeConflicts.push({
1074
+ theme: c.name,
1075
+ orgId,
1076
+ themes: clash.map((x) => x.id),
1077
+ plugins: clash.map((x) => x.plugin ?? "platform"),
1078
+ });
1079
+ }
1080
+ }
1081
+ }
1082
+ if (themeConflicts.length > 0) {
1083
+ throw new ApiError("theme.id_taken", `The update adds themes whose ids are already on in organizations where the plugin is on (${[...new Set(themeConflicts.map((c) => c.theme))].join(", ")}); switch those themes off there first`, themeConflicts);
1084
+ }
1085
+ const themes = await this.themesLost(removedThemes);
1008
1086
  // Pinned copies (in orgs where the component is on) and removed components agents use
1009
1087
  for (const kind of ["skill", "agent_template"]) {
1010
1088
  const mine = components.filter((c) => c.kind === kind && c.id !== null);
@@ -1145,6 +1223,8 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
1145
1223
  reset: c.credentialsReset,
1146
1224
  })),
1147
1225
  connectorOrgs,
1226
+ // only when present: a diff without themes keeps the hash it had before themes
1227
+ ...(themes !== undefined && { themes }),
1148
1228
  }));
1149
1229
  return {
1150
1230
  itemId,
@@ -1155,18 +1235,23 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
1155
1235
  extension,
1156
1236
  connectors,
1157
1237
  connectorOrgs,
1238
+ ...(themes !== undefined && { themes }),
1158
1239
  suggest,
1159
1240
  contents,
1160
1241
  dependencies,
1161
1242
  diffHash,
1162
1243
  };
1163
1244
  }
1164
- /** The target's skill names, agent templates and connector templates, from object storage. */
1245
+ /**
1246
+ * The target's skill names, agent templates, connector templates and themes, from object storage
1247
+ * (a theme failing its checks -> 400 `theme.invalid`, themes spec §6).
1248
+ */
1165
1249
  async readContents(to) {
1166
1250
  return {
1167
1251
  skillNames: await catalogSkillNames(this.storage, to, to.manifest),
1168
1252
  agents: await readCatalogAgentTemplates(this.storage, to),
1169
1253
  templates: await readCatalogTemplates(this.storage, to),
1254
+ themes: await readCatalogThemes(this.storage, to),
1170
1255
  };
1171
1256
  }
1172
1257
  /**
@@ -1331,7 +1416,39 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
1331
1416
  file: r.file,
1332
1417
  ownScope: r.scope_mode === "own",
1333
1418
  }));
1334
- return { skills, agentTemplates, templates };
1419
+ const themes = (await db
1420
+ .selectFrom("platform_themes as p")
1421
+ .innerJoin("platform_theme_versions as v", (j) => j.onRef("v.theme_id", "=", "p.id").onRef("v.version", "=", "p.current_version"))
1422
+ .leftJoin("catalog_item_versions as cv", (j) => j
1423
+ .onRef("cv.item_id", "=", "p.catalog_item_id")
1424
+ .onRef("cv.version", "=", "v.catalog_version"))
1425
+ .select(["p.id", "p.name", "p.scope_mode", "v.definition", "v.catalog_version", "cv.files"])
1426
+ .where("p.catalog_item_id", "=", itemId)
1427
+ .where("p.deleted_at", "is", null)
1428
+ .execute()).map((r) => ({
1429
+ id: r.id,
1430
+ name: r.name,
1431
+ definition: r.definition,
1432
+ catalogVersion: r.catalog_version,
1433
+ // no recorded files (should not happen): never equal, the update writes a new version
1434
+ assetsDigest: r.files === null ? null : themeAssetsDigest(r.files, r.definition),
1435
+ ownScope: r.scope_mode === "own",
1436
+ }));
1437
+ return { skills, agentTemplates, templates, themes };
1438
+ }
1439
+ /** The `themes` block of the preview: the orgs each of the removed themes `ids` is on in. */
1440
+ async themesLost(ids) {
1441
+ if (ids.length === 0)
1442
+ return undefined;
1443
+ const comps = await loadComponents(this.db, "theme", { ids });
1444
+ const st = await loadScopeState(this.db, "theme", comps);
1445
+ const orgs = await this.liveOrgs();
1446
+ const lost = new Map();
1447
+ for (const c of comps) {
1448
+ const on = orgs.filter((orgId) => componentOn(c, pluginFactsOf(st, c.itemId, orgId), listedIn(st, c.id, orgId), switchOf(st, c.id, orgId)));
1449
+ lost.set(c.id, new Set(on));
1450
+ }
1451
+ return themesPreview(this.db, lost);
1335
1452
  }
1336
1453
  /** The orgs where the plugin is on: a live install (builtin: every live org). */
1337
1454
  async pluginOrgs(itemId, builtin) {
@@ -1398,6 +1515,7 @@ function previewOf(d) {
1398
1515
  components: d.components,
1399
1516
  extension: extensionView(d.extension),
1400
1517
  connectorOrgs: d.connectorOrgs,
1518
+ ...(d.themes !== undefined && { themes: d.themes }),
1401
1519
  diffHash: d.diffHash,
1402
1520
  };
1403
1521
  }
@@ -115,4 +115,5 @@ const TABLE = {
115
115
  skill: "platform_skills",
116
116
  agent_template: "platform_agent_templates",
117
117
  connector_template: "platform_connector_templates",
118
+ theme: "platform_themes",
118
119
  };
@@ -1,5 +1,6 @@
1
1
  import { AgentLookup } from "../agents/agent-lookup.js";
2
2
  import type { ModelRef } from "../agents/definition.js";
3
+ import type { AgentAvatar } from "../agents/profile.js";
3
4
  import { BackgroundTaskStopper } from "../background/stop.js";
4
5
  import type { TitleSource } from "../db/kernel-schema.js";
5
6
  import { ConversationEnvironments, type NewConversationEnvironment } from "../environments/conversation-environments.js";
@@ -53,9 +54,17 @@ export interface ConversationView {
53
54
  export declare function escapeLike(s: string): string;
54
55
  /** One agent's group in the sidebar list: the caller's newest conversations with that agent. */
55
56
  export interface ConversationGroup {
57
+ /**
58
+ * The agent's name and the avatar half of its profile, in the agents API's shape (the sidebar
59
+ * draws the group title from it without reading the agents list).
60
+ */
56
61
  agent: {
57
62
  id: string;
58
63
  name: string;
64
+ profile: {
65
+ avatar: AgentAvatar;
66
+ updatedAt: string;
67
+ };
59
68
  };
60
69
  /** max(updated_at) of the group's conversations. */
61
70
  latestAt: Date;
@@ -482,7 +482,7 @@ let ConversationsService = class ConversationsService {
482
482
  LIMIT ${opts.limit + 1}
483
483
  )
484
484
  SELECT page.agent_id, page.latest_at, v.definition ->> 'name' AS agent_name,
485
- cs.id, cs.org_id, cs.account_id, cs.title, cs.title_source, cs.selection, cs.last_seq,
485
+ a.profile_avatar, a.profile_updated_at, cs.id, cs.org_id, cs.account_id, cs.title, cs.title_source, cs.selection, cs.last_seq,
486
486
  cs.created_at, cs.updated_at, cs.parent_conversation_id, cs.subagent_type,
487
487
  cs.archived_at, cs.environment_id, cs.activity
488
488
  FROM page
@@ -506,7 +506,14 @@ let ConversationsService = class ConversationsService {
506
506
  for (const r of rows) {
507
507
  if (current?.agent.id !== r.agent_id) {
508
508
  current = {
509
- agent: { id: r.agent_id, name: r.agent_name ?? "" },
509
+ agent: {
510
+ id: r.agent_id,
511
+ name: r.agent_name ?? "",
512
+ profile: {
513
+ avatar: r.profile_avatar,
514
+ updatedAt: r.profile_updated_at.toISOString(),
515
+ },
516
+ },
510
517
  latestAt: r.latest_at,
511
518
  conversations: [],
512
519
  more: false,
@@ -809,6 +809,7 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
809
809
  deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
810
810
  }, z.core.$strict>>;
811
811
  toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
812
+ allow: "allow";
812
813
  ask: "ask";
813
814
  deny: "deny";
814
815
  }>>>;