@capybara-hq/kernel 0.1.0-rc.10 → 0.1.0-rc.11
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/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.schemas.d.ts +4 -0
- package/dist/catalog/component-scope.service.d.ts +48 -9
- package/dist/catalog/component-scope.service.js +152 -27
- package/dist/catalog/component-state.d.ts +12 -3
- package/dist/catalog/component-state.js +21 -3
- 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 +37 -6
- 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/db/kernel-schema.d.ts +41 -4
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +25 -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 +16 -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/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/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/package.json +3 -3
- package/src/migrations/0102_themes.sql +64 -0
- package/src/migrations/0103_ui_theme_setting.sql +7 -0
|
@@ -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) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentTemplate, type ConnectorTemplateFile } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { type AgentTemplate, type ConnectorTemplateFile, type ParsedTheme } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import type { SkillManifest, SourceFormat } from "../db/kernel-schema.js";
|
|
3
3
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
4
4
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
@@ -6,9 +6,9 @@ import type { KernelLogger } from "../log.js";
|
|
|
6
6
|
import { type SkillPackageFile } from "../skills/package.js";
|
|
7
7
|
import { type PrefixedStorage, StorageService } from "../storage/storage.service.js";
|
|
8
8
|
import { CatalogItemLock } from "./locks.js";
|
|
9
|
-
/** A skill
|
|
9
|
+
/** A skill, agent template or theme version a sync wrote (`from` null: the component was created). */
|
|
10
10
|
export interface ChangedRef {
|
|
11
|
-
kind: "skill" | "agent_template";
|
|
11
|
+
kind: "skill" | "agent_template" | "theme";
|
|
12
12
|
id: string;
|
|
13
13
|
name: string;
|
|
14
14
|
from: number | null;
|
|
@@ -31,11 +31,17 @@ export interface PlatformSyncResult {
|
|
|
31
31
|
skills: ChangedRef[];
|
|
32
32
|
agentTemplates: ChangedRef[];
|
|
33
33
|
connectorTemplates: ChangedConnectorTemplateRef[];
|
|
34
|
+
themes: ChangedRef[];
|
|
34
35
|
}
|
|
35
36
|
/** The prefix (in `deployment/`) of a platform skill version's files. */
|
|
36
37
|
export declare const platformSkillPrefix: (id: string, version: number) => string;
|
|
37
38
|
/** The prefix (in `deployment/`) of a platform agent template version's file. */
|
|
38
39
|
export declare const platformAgentTemplatePrefix: (id: string, version: number) => string;
|
|
40
|
+
/**
|
|
41
|
+
* The prefix (in `deployment/`) of a platform theme's files (themes spec §6); a version's fonts
|
|
42
|
+
* and logo are under `<prefix><version>-<catalog version>/` ({@link syncVersionPrefix}).
|
|
43
|
+
*/
|
|
44
|
+
export declare const platformThemePrefix: (id: string) => string;
|
|
39
45
|
/**
|
|
40
46
|
* A fresh prefix (in `deployment/`) for a version written before its transaction (ruling P3-45):
|
|
41
47
|
* `<id>/<version>-<uuid>/`, so two writers racing for one version number never overwrite each
|
|
@@ -73,6 +79,19 @@ interface TemplateStep {
|
|
|
73
79
|
file: SkillPackageFile;
|
|
74
80
|
template: AgentTemplate;
|
|
75
81
|
}
|
|
82
|
+
interface ThemeStep {
|
|
83
|
+
/** The theme id. */
|
|
84
|
+
name: string;
|
|
85
|
+
base: Base<number> | undefined;
|
|
86
|
+
/** The platform row id. */
|
|
87
|
+
id: string;
|
|
88
|
+
to: number;
|
|
89
|
+
/** null: the theme has no assets. */
|
|
90
|
+
prefix: string | null;
|
|
91
|
+
/** Fonts and logo, sanitized (paths relative to the plugin root). */
|
|
92
|
+
files: SkillPackageFile[];
|
|
93
|
+
theme: ParsedTheme;
|
|
94
|
+
}
|
|
76
95
|
interface ConnectorTemplateStep {
|
|
77
96
|
name: string;
|
|
78
97
|
base: Base<string> | undefined;
|
|
@@ -88,6 +107,7 @@ export interface SyncPlan {
|
|
|
88
107
|
skills: SkillStep[];
|
|
89
108
|
agentTemplates: TemplateStep[];
|
|
90
109
|
connectorTemplates: ConnectorTemplateStep[];
|
|
110
|
+
themes: ThemeStep[];
|
|
91
111
|
}
|
|
92
112
|
/** A component changed between the plan and its write (a concurrent sync): plan again. */
|
|
93
113
|
export declare class PlanStale extends Error {
|
|
@@ -7,10 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
// Platform components (plugin scope phase 2, rulings A2 and C2): the skills, agent templates
|
|
11
|
-
// connector templates of a catalog item, stored once for the deployment. Files live
|
|
12
|
-
// `deployment/platform/{skills,agent-templates}/<id>/<version
|
|
13
|
-
// are purged by prefix).
|
|
10
|
+
// Platform components (plugin scope phase 2, rulings A2 and C2): the skills, agent templates,
|
|
11
|
+
// connector templates and themes of a catalog item, stored once for the deployment. Files live
|
|
12
|
+
// under `deployment/platform/{skills,agent-templates,themes}/<id>/<version>…/`, never under an org
|
|
13
|
+
// prefix (orgs are purged by prefix).
|
|
14
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
15
15
|
import { parseAgentTemplate, } from "@capybara-hq/plugin-sdk";
|
|
16
16
|
import { Inject, Injectable } from "@nestjs/common";
|
|
@@ -24,7 +24,7 @@ import { packagePath } from "../packages/tgz.js";
|
|
|
24
24
|
import { readSkillPackage, skillManifest } from "../skills/package.js";
|
|
25
25
|
import { StorageService } from "../storage/storage.service.js";
|
|
26
26
|
import { canonicalJson } from "../tools/canonical-json.js";
|
|
27
|
-
import { readCatalogAgentTemplates, readCatalogFiles, readCatalogTemplates, skillFilesDigest, skillOf, } from "./catalog-content.js";
|
|
27
|
+
import { readCatalogAgentTemplates, readCatalogFiles, readCatalogTemplates, readCatalogThemes, skillFilesDigest, skillOf, themeAssetsDigest, } from "./catalog-content.js";
|
|
28
28
|
import { putCatalogFiles } from "./catalog-items.js";
|
|
29
29
|
import { CatalogItemLock } from "./locks.js";
|
|
30
30
|
import { assertNotAwaiting } from "./ownership.js";
|
|
@@ -32,6 +32,11 @@ import { assertNotAwaiting } from "./ownership.js";
|
|
|
32
32
|
export const platformSkillPrefix = (id, version) => `platform/skills/${id}/${version}/`;
|
|
33
33
|
/** The prefix (in `deployment/`) of a platform agent template version's file. */
|
|
34
34
|
export const platformAgentTemplatePrefix = (id, version) => `platform/agent-templates/${id}/${version}/`;
|
|
35
|
+
/**
|
|
36
|
+
* The prefix (in `deployment/`) of a platform theme's files (themes spec §6); a version's fonts
|
|
37
|
+
* and logo are under `<prefix><version>-<catalog version>/` ({@link syncVersionPrefix}).
|
|
38
|
+
*/
|
|
39
|
+
export const platformThemePrefix = (id) => `platform/themes/${id}/`;
|
|
35
40
|
/**
|
|
36
41
|
* A fresh prefix (in `deployment/`) for a version written before its transaction (ruling P3-45):
|
|
37
42
|
* `<id>/<version>-<uuid>/`, so two writers racing for one version number never overwrite each
|
|
@@ -94,9 +99,10 @@ let PlatformComponents = class PlatformComponents {
|
|
|
94
99
|
const plan = await this.plan(itemId, version);
|
|
95
100
|
if (plan.skills.length === 0 &&
|
|
96
101
|
plan.agentTemplates.length === 0 &&
|
|
97
|
-
plan.connectorTemplates.length === 0
|
|
102
|
+
plan.connectorTemplates.length === 0 &&
|
|
103
|
+
plan.themes.length === 0) {
|
|
98
104
|
await this.markSynced(itemId, version);
|
|
99
|
-
return { skills: [], agentTemplates: [], connectorTemplates: [] };
|
|
105
|
+
return { skills: [], agentTemplates: [], connectorTemplates: [], themes: [] };
|
|
100
106
|
}
|
|
101
107
|
await this.writeFiles(plan);
|
|
102
108
|
try {
|
|
@@ -109,6 +115,7 @@ let PlatformComponents = class PlatformComponents {
|
|
|
109
115
|
skills: r.skills.length,
|
|
110
116
|
agentTemplates: r.agentTemplates.length,
|
|
111
117
|
connectorTemplates: r.connectorTemplates.length,
|
|
118
|
+
themes: r.themes.length,
|
|
112
119
|
});
|
|
113
120
|
return r;
|
|
114
121
|
}
|
|
@@ -449,6 +456,7 @@ let PlatformComponents = class PlatformComponents {
|
|
|
449
456
|
skills: [],
|
|
450
457
|
agentTemplates: [],
|
|
451
458
|
connectorTemplates: [],
|
|
459
|
+
themes: [],
|
|
452
460
|
};
|
|
453
461
|
for (const s of manifest.skills ?? []) {
|
|
454
462
|
const files = await readCatalogFiles(this.files, row.object_prefix, row.files, packagePath(s.path));
|
|
@@ -524,17 +532,53 @@ let PlatformComponents = class PlatformComponents {
|
|
|
524
532
|
file: t.file,
|
|
525
533
|
});
|
|
526
534
|
}
|
|
535
|
+
// Themes (by plugin and id): changed when the definition or an asset changed
|
|
536
|
+
for (const t of await readCatalogThemes(this.files, v)) {
|
|
537
|
+
const live = await this.db
|
|
538
|
+
.selectFrom("platform_themes as p")
|
|
539
|
+
.innerJoin("platform_theme_versions as pv", (j) => j.onRef("pv.theme_id", "=", "p.id").onRef("pv.version", "=", "p.current_version"))
|
|
540
|
+
.leftJoin("catalog_item_versions as cv", (j) => j
|
|
541
|
+
.onRef("cv.item_id", "=", "p.catalog_item_id")
|
|
542
|
+
.onRef("cv.version", "=", "pv.catalog_version"))
|
|
543
|
+
.select(["p.id", "p.current_version", "pv.definition", "cv.files"])
|
|
544
|
+
.where("p.plugin_name", "=", plugin)
|
|
545
|
+
.where("p.name", "=", t.id)
|
|
546
|
+
.where("p.deleted_at", "is", null)
|
|
547
|
+
.executeTakeFirst();
|
|
548
|
+
if (live &&
|
|
549
|
+
canonicalJson(live.definition) === canonicalJson(t.theme) &&
|
|
550
|
+
live.files !== null &&
|
|
551
|
+
themeAssetsDigest(live.files, live.definition) === t.assetsDigest) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const id = live?.id ?? randomUUID();
|
|
555
|
+
const to = (live?.current_version ?? 0) + 1;
|
|
556
|
+
plan.themes.push({
|
|
557
|
+
name: t.id,
|
|
558
|
+
base: live && { id: live.id, version: live.current_version },
|
|
559
|
+
id,
|
|
560
|
+
to,
|
|
561
|
+
prefix: t.assets.length > 0
|
|
562
|
+
? syncVersionPrefix(`${platformThemePrefix(id)}${to}/`, version)
|
|
563
|
+
: null,
|
|
564
|
+
files: t.assets,
|
|
565
|
+
theme: t.theme,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
527
568
|
return plan;
|
|
528
569
|
}
|
|
529
570
|
/** Copies the planned versions' files to their platform prefixes. */
|
|
530
571
|
async writeFiles(plan) {
|
|
531
|
-
|
|
572
|
+
const themes = plan.themes.filter((t) => t.prefix !== null);
|
|
573
|
+
if (plan.skills.length === 0 && plan.agentTemplates.length === 0 && themes.length === 0)
|
|
532
574
|
return;
|
|
533
575
|
this.files.assertAvailable();
|
|
534
576
|
for (const s of plan.skills)
|
|
535
577
|
await putCatalogFiles(this.files, s.prefix, s.files);
|
|
536
578
|
for (const a of plan.agentTemplates)
|
|
537
579
|
await putCatalogFiles(this.files, a.prefix, [a.file]);
|
|
580
|
+
for (const t of themes)
|
|
581
|
+
await putCatalogFiles(this.files, t.prefix, t.files);
|
|
538
582
|
}
|
|
539
583
|
/**
|
|
540
584
|
* Writes the planned rows under the item name's lock (the lock of the import confirm), after
|
|
@@ -543,7 +587,12 @@ let PlatformComponents = class PlatformComponents {
|
|
|
543
587
|
async apply(plan) {
|
|
544
588
|
await this.itemLock.lock(plan.plugin);
|
|
545
589
|
const t = this.db;
|
|
546
|
-
const out = {
|
|
590
|
+
const out = {
|
|
591
|
+
skills: [],
|
|
592
|
+
agentTemplates: [],
|
|
593
|
+
connectorTemplates: [],
|
|
594
|
+
themes: [],
|
|
595
|
+
};
|
|
547
596
|
// New component rows carry the plugin's owner and 可共享 flag (ruling P3-2); an org plugin's
|
|
548
597
|
// skills and agent templates were created in its owner org (`origin_org_id`, immutable, F27),
|
|
549
598
|
// and like the plugin they are not shown to other orgs until the super admin shows them
|
|
@@ -725,6 +774,66 @@ let PlatformComponents = class PlatformComponents {
|
|
|
725
774
|
to: plan.version,
|
|
726
775
|
});
|
|
727
776
|
}
|
|
777
|
+
for (const th of plan.themes) {
|
|
778
|
+
const live = await t
|
|
779
|
+
.selectFrom("platform_themes")
|
|
780
|
+
.select(["id", "current_version"])
|
|
781
|
+
.where("plugin_name", "=", plan.plugin)
|
|
782
|
+
.where("name", "=", th.name)
|
|
783
|
+
.where("deleted_at", "is", null)
|
|
784
|
+
.forUpdate()
|
|
785
|
+
.executeTakeFirst();
|
|
786
|
+
assertBase(live && { id: live.id, version: live.current_version }, th.base);
|
|
787
|
+
const description = th.theme.description?.en ?? null;
|
|
788
|
+
const descriptionZh = th.theme.description?.zh ?? null;
|
|
789
|
+
if (live) {
|
|
790
|
+
await t
|
|
791
|
+
.updateTable("platform_themes")
|
|
792
|
+
.set({
|
|
793
|
+
current_version: th.to,
|
|
794
|
+
description,
|
|
795
|
+
description_zh: descriptionZh,
|
|
796
|
+
catalog_item_id: plan.itemId,
|
|
797
|
+
updated_at: new Date(),
|
|
798
|
+
})
|
|
799
|
+
.where("id", "=", th.id)
|
|
800
|
+
.execute();
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
await t
|
|
804
|
+
.insertInto("platform_themes")
|
|
805
|
+
.values({
|
|
806
|
+
id: th.id,
|
|
807
|
+
catalog_item_id: plan.itemId,
|
|
808
|
+
plugin_name: plan.plugin,
|
|
809
|
+
name: th.name,
|
|
810
|
+
description,
|
|
811
|
+
description_zh: descriptionZh,
|
|
812
|
+
current_version: th.to,
|
|
813
|
+
scope_mode: "follow",
|
|
814
|
+
...owned,
|
|
815
|
+
...origin,
|
|
816
|
+
})
|
|
817
|
+
.execute();
|
|
818
|
+
}
|
|
819
|
+
await t
|
|
820
|
+
.insertInto("platform_theme_versions")
|
|
821
|
+
.values({
|
|
822
|
+
theme_id: th.id,
|
|
823
|
+
version: th.to,
|
|
824
|
+
definition: JSON.stringify(th.theme),
|
|
825
|
+
object_prefix: th.prefix,
|
|
826
|
+
catalog_version: plan.version,
|
|
827
|
+
})
|
|
828
|
+
.execute();
|
|
829
|
+
out.themes.push({
|
|
830
|
+
kind: "theme",
|
|
831
|
+
id: th.id,
|
|
832
|
+
name: th.name,
|
|
833
|
+
from: th.base?.version ?? null,
|
|
834
|
+
to: th.to,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
728
837
|
return out;
|
|
729
838
|
}
|
|
730
839
|
};
|
|
@@ -203,7 +203,8 @@ let PlatformMigration = class PlatformMigration {
|
|
|
203
203
|
const version = await this.platformVersion(item.id, item.latest_version);
|
|
204
204
|
try {
|
|
205
205
|
const r = await this.platform.syncFromVersion(item.id, version, { initial: false });
|
|
206
|
-
|
|
206
|
+
const changed = r.skills.length + r.agentTemplates.length + r.connectorTemplates.length + r.themes.length;
|
|
207
|
+
if (changed > 0) {
|
|
207
208
|
stats.itemsSynced.push(`${item.name}@${version}`);
|
|
208
209
|
}
|
|
209
210
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentTemplate, type AgentTemplateTier, type LocalizedName, type PluginManifest } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { type AgentTemplate, type AgentTemplateTier, type LocalizedName, type PluginManifest, type ThemeScheme } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import type { Queue } from "bullmq";
|
|
3
3
|
import { ApiError } from "../errors.js";
|
|
4
4
|
import { ExtensionAdminOps } from "../extensions/admin-ops.js";
|
|
@@ -134,6 +134,18 @@ export interface PluginReviewAgentTemplate {
|
|
|
134
134
|
maxSteps?: number;
|
|
135
135
|
skills?: string[];
|
|
136
136
|
}
|
|
137
|
+
/** One theme in a review (themes spec §6): what the super admin sees before confirming. */
|
|
138
|
+
export interface PluginReviewTheme {
|
|
139
|
+
/** The theme id (the file's base name). */
|
|
140
|
+
id: string;
|
|
141
|
+
name: LocalizedName;
|
|
142
|
+
/** The schemes it defines. */
|
|
143
|
+
schemes: ThemeScheme[];
|
|
144
|
+
/** Number of fonts. */
|
|
145
|
+
fonts: number;
|
|
146
|
+
/** It replaces the logo mark, the wordmark or the favicon. */
|
|
147
|
+
logo: boolean;
|
|
148
|
+
}
|
|
137
149
|
/** The source of a staged import. */
|
|
138
150
|
export interface PluginImportSource {
|
|
139
151
|
/**
|
|
@@ -215,6 +227,11 @@ export interface PluginReview {
|
|
|
215
227
|
* {@link pluginReviewHash} when empty, so their digests are unchanged.
|
|
216
228
|
*/
|
|
217
229
|
agentTemplates?: PluginReviewAgentTemplate[];
|
|
230
|
+
/**
|
|
231
|
+
* Themes (themes spec §6). Absent without any; left out of {@link pluginReviewHash} when empty,
|
|
232
|
+
* so the digests of plugins without themes are unchanged.
|
|
233
|
+
*/
|
|
234
|
+
themes?: PluginReviewTheme[];
|
|
218
235
|
/**
|
|
219
236
|
* Each dependency with the highest imported catalog version satisfying its range (null when
|
|
220
237
|
* nothing does yet: the import still completes, an org install is refused until it is imported).
|
|
@@ -333,7 +350,9 @@ export interface StagedTarget {
|
|
|
333
350
|
}
|
|
334
351
|
/**
|
|
335
352
|
* The API error of a plugin package that cannot be read in a catalog path: the pre-0.1.0 format ->
|
|
336
|
-
* `catalog.legacy_format
|
|
353
|
+
* `catalog.legacy_format`; a failing theme (themes spec §5) -> `theme.invalid` (details
|
|
354
|
+
* `[{ theme, errors }]`) or `theme.unreadable` (details `[{ theme, issues }]`, the failing pairs);
|
|
355
|
+
* anything else -> `catalog.invalid_manifest` (issues as details).
|
|
337
356
|
*/
|
|
338
357
|
export declare function catalogPackageError(e: PluginPackageError | ExtensionPackageError): ApiError;
|
|
339
358
|
export declare class PluginImport {
|
|
@@ -629,10 +648,11 @@ export declare class PluginImport {
|
|
|
629
648
|
* current version / package, its own review digest), every template field, each skill's name / path
|
|
630
649
|
* / file count, the dependency resolution, the sha256 of every stored file, the format, the
|
|
631
650
|
* translation report's items, the sha256 of the license text and the two review flags
|
|
632
|
-
* (`claudeManifestIgnored`, `licenseTruncated`),
|
|
651
|
+
* (`claudeManifestIgnored`, `licenseTruncated`), the agent templates when there are any and the
|
|
652
|
+
* themes when there are any.
|
|
633
653
|
* Exported for tests. `repository` is display only.
|
|
634
654
|
*/
|
|
635
|
-
export declare function pluginReviewHash(r: Pick<PluginReview, "name" | "version" | "contentDigest" | "extension" | "connectorTemplates" | "agentTemplates" | "skills" | "dependencies" | "format" | "translation" | "licenseText" | "claudeManifestIgnored" | "licenseTruncated">, files: {
|
|
655
|
+
export declare function pluginReviewHash(r: Pick<PluginReview, "name" | "version" | "contentDigest" | "extension" | "connectorTemplates" | "agentTemplates" | "themes" | "skills" | "dependencies" | "format" | "translation" | "licenseText" | "claudeManifestIgnored" | "licenseTruncated">, files: {
|
|
636
656
|
path: string;
|
|
637
657
|
sha256: string;
|
|
638
658
|
}[]): string;
|
|
@@ -52,7 +52,7 @@ import { CatalogItemLock } from "./locks.js";
|
|
|
52
52
|
import { assertNameFree, assertPackageFree, assertShareable, packageOwner, releaseExtensionClaim, shareGate, usableBy, } from "./ownership.js";
|
|
53
53
|
import { PlatformComponents } from "./platform-components.js";
|
|
54
54
|
import { PLATFORM_MIGRATION_JOB } from "./platform-migration.js";
|
|
55
|
-
import { PluginPackageError, packagePath, readPluginFiles, readPluginPackage, } from "./plugin-package.js";
|
|
55
|
+
import { PluginPackageError, packagePath, readPluginFiles, readPluginPackage, themeApiError, } from "./plugin-package.js";
|
|
56
56
|
import { hasImportRequests, importRequestOf, resolveImportRequests } from "./request-rules.js";
|
|
57
57
|
import { assertNameNotReserved } from "./reserved-names.js";
|
|
58
58
|
import { addScopeOrg } from "./scope-orgs.js";
|
|
@@ -112,12 +112,17 @@ const reviewStale = (review) => new ApiError("plugin.review_stale", `The review
|
|
|
112
112
|
const mismatch = (what) => new ApiError("marketplace.package_mismatch", `The downloaded package does not match the marketplace entry: ${what}`);
|
|
113
113
|
/**
|
|
114
114
|
* The API error of a plugin package that cannot be read in a catalog path: the pre-0.1.0 format ->
|
|
115
|
-
* `catalog.legacy_format
|
|
115
|
+
* `catalog.legacy_format`; a failing theme (themes spec §5) -> `theme.invalid` (details
|
|
116
|
+
* `[{ theme, errors }]`) or `theme.unreadable` (details `[{ theme, issues }]`, the failing pairs);
|
|
117
|
+
* anything else -> `catalog.invalid_manifest` (issues as details).
|
|
116
118
|
*/
|
|
117
119
|
export function catalogPackageError(e) {
|
|
118
120
|
const legacy = e.code === "catalog.legacy_format" || e.code === "extension.legacy_format";
|
|
119
121
|
if (legacy)
|
|
120
122
|
return new ApiError("catalog.legacy_format", e.message, e.issues);
|
|
123
|
+
if (e.code === "theme.invalid" || e.code === "theme.unreadable") {
|
|
124
|
+
return themeApiError(e);
|
|
125
|
+
}
|
|
121
126
|
return invalid(e.issues);
|
|
122
127
|
}
|
|
123
128
|
/** Runs a package read, mapping its errors with {@link catalogPackageError}. */
|
|
@@ -380,7 +385,7 @@ let PluginImport = class PluginImport {
|
|
|
380
385
|
const objectFiles = content.files.filter((f) => content.objectPaths.has(f.path));
|
|
381
386
|
if (objectFiles.length > 0 && retry === undefined) {
|
|
382
387
|
if (source.kind === "bootstrap" && !this.files.available) {
|
|
383
|
-
throw new ApiError("storage.unavailable", `Bootstrap plugin ${m.name} ships skills, connector templates
|
|
388
|
+
throw new ApiError("storage.unavailable", `Bootstrap plugin ${m.name} ships skills, connector templates, agent templates or themes, which are kept in object storage, and object storage is not running yet (it may be a bootstrap plugin itself); a required bootstrap plugin may ship only an icon and setup guides, an optional one is retried once the extensions have loaded`);
|
|
384
389
|
}
|
|
385
390
|
this.files.assertAvailable();
|
|
386
391
|
}
|
|
@@ -430,6 +435,7 @@ let PluginImport = class PluginImport {
|
|
|
430
435
|
skills: content.skills,
|
|
431
436
|
connectorTemplates: content.templates,
|
|
432
437
|
...(content.agentTemplates.length > 0 && { agentTemplates: content.agentTemplates }),
|
|
438
|
+
...(content.themes.length > 0 && { themes: content.themes }),
|
|
433
439
|
dependencies,
|
|
434
440
|
source: src,
|
|
435
441
|
provenance: (opts.provenance ?? NOT_NPM).provenance,
|
|
@@ -1456,6 +1462,23 @@ let PluginImport = class PluginImport {
|
|
|
1456
1462
|
const { name, description, instructions, model, ...rest } = a.template;
|
|
1457
1463
|
return { name, path: a.path, description, instructions, model: model ?? "inherit", ...rest };
|
|
1458
1464
|
});
|
|
1465
|
+
// Themes: the file as written, the assets as sanitized by the reader (never the uploaded
|
|
1466
|
+
// originals: a shared path, e.g. the plugin icon, is stored sanitized too)
|
|
1467
|
+
const themes = pkg.themes.map((t) => {
|
|
1468
|
+
keepObject(t.path);
|
|
1469
|
+
for (const [path, data] of t.assets) {
|
|
1470
|
+
stored.set(path, fileOf(path, data));
|
|
1471
|
+
objectPaths.add(path);
|
|
1472
|
+
}
|
|
1473
|
+
const logo = t.theme.logo;
|
|
1474
|
+
return {
|
|
1475
|
+
id: t.id,
|
|
1476
|
+
name: t.theme.name,
|
|
1477
|
+
schemes: ["dark", "light"].filter((k) => t.theme.schemes[k] !== undefined),
|
|
1478
|
+
fonts: t.theme.fonts?.length ?? 0,
|
|
1479
|
+
logo: logo !== undefined && Object.keys(logo).length > 0,
|
|
1480
|
+
};
|
|
1481
|
+
});
|
|
1459
1482
|
if (problems.length > 0)
|
|
1460
1483
|
throw invalid(problems);
|
|
1461
1484
|
return {
|
|
@@ -1465,6 +1488,7 @@ let PluginImport = class PluginImport {
|
|
|
1465
1488
|
templates,
|
|
1466
1489
|
skills,
|
|
1467
1490
|
agentTemplates,
|
|
1491
|
+
themes,
|
|
1468
1492
|
notLoaded,
|
|
1469
1493
|
templatePaths,
|
|
1470
1494
|
};
|
|
@@ -1639,7 +1663,7 @@ export { PluginImport };
|
|
|
1639
1663
|
*/
|
|
1640
1664
|
function normalizedManifest(pkg) {
|
|
1641
1665
|
const pinned = new Map((pkg.manifest.skills ?? []).map((s) => [packagePath(s.path), s.pinned]));
|
|
1642
|
-
const { agents: _, ...manifest } = pkg.manifest;
|
|
1666
|
+
const { agents: _, themes: __, ...manifest } = pkg.manifest;
|
|
1643
1667
|
return {
|
|
1644
1668
|
...manifest,
|
|
1645
1669
|
skills: pkg.skills.map((s) => {
|
|
@@ -1651,6 +1675,7 @@ function normalizedManifest(pkg) {
|
|
|
1651
1675
|
...(pkg.agentTemplates.length > 0 && {
|
|
1652
1676
|
agents: pkg.agentTemplates.map((a) => ({ path: `./${a.path}` })),
|
|
1653
1677
|
}),
|
|
1678
|
+
...(pkg.themes.length > 0 && { themes: pkg.themes.map((t) => ({ path: `./${t.path}` })) }),
|
|
1654
1679
|
};
|
|
1655
1680
|
}
|
|
1656
1681
|
/** package.json `name`, or null without one (a git plugin; never the string "undefined"). */
|
|
@@ -1682,7 +1707,8 @@ function actionOf(s, sha256, version) {
|
|
|
1682
1707
|
* current version / package, its own review digest), every template field, each skill's name / path
|
|
1683
1708
|
* / file count, the dependency resolution, the sha256 of every stored file, the format, the
|
|
1684
1709
|
* translation report's items, the sha256 of the license text and the two review flags
|
|
1685
|
-
* (`claudeManifestIgnored`, `licenseTruncated`),
|
|
1710
|
+
* (`claudeManifestIgnored`, `licenseTruncated`), the agent templates when there are any and the
|
|
1711
|
+
* themes when there are any.
|
|
1686
1712
|
* Exported for tests. `repository` is display only.
|
|
1687
1713
|
*/
|
|
1688
1714
|
export function pluginReviewHash(r, files) {
|
|
@@ -1715,6 +1741,10 @@ export function pluginReviewHash(r, files) {
|
|
|
1715
1741
|
...((r.agentTemplates ?? []).length > 0
|
|
1716
1742
|
? [(r.agentTemplates ?? []).map((a) => [a.path, canonicalJson(a)])]
|
|
1717
1743
|
: []),
|
|
1744
|
+
// themes, only when there are any, tagged (never read as agent templates)
|
|
1745
|
+
...((r.themes ?? []).length > 0
|
|
1746
|
+
? [{ themes: (r.themes ?? []).map((t) => [t.id, canonicalJson(t)]) }]
|
|
1747
|
+
: []),
|
|
1718
1748
|
]);
|
|
1719
1749
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
1720
1750
|
}
|
|
@@ -1785,7 +1815,8 @@ function ownStaging(owner) {
|
|
|
1785
1815
|
/**
|
|
1786
1816
|
* An org upload's custom deny, by content (ruling P3-15'): skills or agent templates ->
|
|
1787
1817
|
* `customSkills`, connector templates -> `customConnectors`; each `deny` -> 403
|
|
1788
|
-
* `catalog.custom_disabled`. A package carrying neither passes
|
|
1818
|
+
* `catalog.custom_disabled`. A package carrying neither passes; themes are covered by neither
|
|
1819
|
+
* switch (themes spec §6), so a themes-only upload passes.
|
|
1789
1820
|
*/
|
|
1790
1821
|
async function assertCustomContent(db, content) {
|
|
1791
1822
|
if (content.skills.length > 0 || content.agentTemplates.length > 0) {
|
|
@@ -11,6 +11,7 @@ import { OrgLock } from "../orgs/org-lock.js";
|
|
|
11
11
|
import type { OrgAction } from "../orgs/roles.js";
|
|
12
12
|
import { type SkillConflict, SkillsService } from "../skills/skills.service.js";
|
|
13
13
|
import { StorageService } from "../storage/storage.service.js";
|
|
14
|
+
import { type ThemePreview } from "../themes/theme-usage.js";
|
|
14
15
|
import type { ConnectorDetail, CreateConnectorInput, ToolIssue } from "../tools/connectors.service.js";
|
|
15
16
|
import { ConnectorsService } from "../tools/connectors.service.js";
|
|
16
17
|
import { type CatalogVersionFiles, type TemplateDeps } from "./catalog-content.js";
|
|
@@ -42,7 +43,7 @@ export interface PluginInstallInput {
|
|
|
42
43
|
* agent template (ref = its agent_templates id; like a skill, `skill_version` records the version
|
|
43
44
|
* an install or upgrade added to an existing template).
|
|
44
45
|
*/
|
|
45
|
-
export type ComponentKind = "extension" | "connector" | "skill" | "plugin" | "agent_template";
|
|
46
|
+
export type ComponentKind = "extension" | "connector" | "skill" | "plugin" | "agent_template" | "theme";
|
|
46
47
|
export interface UninstallReport {
|
|
47
48
|
removed: {
|
|
48
49
|
kind: ComponentKind;
|
|
@@ -84,6 +85,11 @@ export interface UninstallPreview {
|
|
|
84
85
|
ref: string;
|
|
85
86
|
}[];
|
|
86
87
|
}[];
|
|
88
|
+
/**
|
|
89
|
+
* The plugin's themes on in the org through it (`follow`) that go off with it (themes spec §6):
|
|
90
|
+
* whether it is the org's default and how many people picked it. Absent when there are none.
|
|
91
|
+
*/
|
|
92
|
+
themes?: ThemePreview[];
|
|
87
93
|
}
|
|
88
94
|
/** The confirm hash of an uninstall preview: sha256 over its sorted JSON (the scheme of `diffHash`). */
|
|
89
95
|
export declare function uninstallPreviewHash(p: UninstallPreview): string;
|
|
@@ -303,6 +309,12 @@ export declare class PluginInstaller {
|
|
|
303
309
|
* and a name two plugins of the same plan both bring (the one installed first is the source).
|
|
304
310
|
*/
|
|
305
311
|
private assertNoSkillConflicts;
|
|
312
|
+
/**
|
|
313
|
+
* Refuses installing a plugin whose themes would come on in the org (they follow it) while
|
|
314
|
+
* another theme with the same id is on there (themes spec §6, rule 10): 409 `theme.id_taken`,
|
|
315
|
+
* details `[{ theme, orgId, themes, plugins }]`, before anything is written.
|
|
316
|
+
*/
|
|
317
|
+
private assertNoThemeClashes;
|
|
306
318
|
/** The connector checks of templates against what this replica loads right now. */
|
|
307
319
|
templateDeps(): TemplateDeps;
|
|
308
320
|
/**
|
|
@@ -453,6 +465,13 @@ export declare class PluginInstaller {
|
|
|
453
465
|
* nothing written), and keeps the connectors it created as the org's own (`org_owned`).
|
|
454
466
|
*/
|
|
455
467
|
private writeUninstall;
|
|
468
|
+
/**
|
|
469
|
+
* 409 `theme.platform_default` when uninstalling `installId` would switch the platform default
|
|
470
|
+
* theme off in the org (one of the plugin's `follow` themes on there through it, themes spec §7),
|
|
471
|
+
* in the caller's transaction before anything is written. A missing install passes (the
|
|
472
|
+
* uninstall reports it).
|
|
473
|
+
*/
|
|
474
|
+
assertKeepsPlatformDefault(orgId: string, installId: string): Promise<void>;
|
|
456
475
|
/**
|
|
457
476
|
* The oldest other live install (not in `removing`) that has component `kind` / `ref`, or
|
|
458
477
|
* undefined.
|