@capybara-hq/kernel 0.1.0-rc.10 → 0.1.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/admin.schemas.d.ts +3 -0
- package/dist/agents/agents.schemas.d.ts +3 -0
- package/dist/agents/agents.service.js +11 -1
- package/dist/agents/definition.d.ts +1 -0
- package/dist/agents/definition.js +7 -3
- package/dist/agents/org-templates.js +4 -0
- package/dist/agents/pending-connectors.service.js +1 -1
- package/dist/catalog/catalog-content.d.ts +33 -1
- package/dist/catalog/catalog-content.js +65 -2
- package/dist/catalog/catalog-items.js +1 -0
- package/dist/catalog/catalog-scope.service.d.ts +29 -3
- package/dist/catalog/catalog-scope.service.js +82 -10
- package/dist/catalog/catalog.service.d.ts +7 -5
- package/dist/catalog/catalog.service.js +26 -5
- package/dist/catalog/claude/translate.js +3 -0
- package/dist/catalog/component-scope.controller.d.ts +18 -6
- package/dist/catalog/component-scope.controller.js +36 -13
- package/dist/catalog/component-scope.schemas.d.ts +4 -0
- package/dist/catalog/component-scope.service.d.ts +59 -9
- package/dist/catalog/component-scope.service.js +156 -27
- package/dist/catalog/component-state.d.ts +20 -3
- package/dist/catalog/component-state.js +40 -14
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-catalog.service.js +3 -0
- package/dist/catalog/owned-items.js +7 -0
- package/dist/catalog/platform-components.d.ts +23 -3
- package/dist/catalog/platform-components.js +118 -9
- package/dist/catalog/platform-migration.js +2 -1
- package/dist/catalog/plugin-import.d.ts +24 -4
- package/dist/catalog/plugin-import.js +41 -7
- package/dist/catalog/plugin-install.d.ts +20 -1
- package/dist/catalog/plugin-install.js +71 -1
- package/dist/catalog/plugin-package.d.ts +41 -4
- package/dist/catalog/plugin-package.js +127 -7
- package/dist/catalog/plugin-upgrade.d.ts +13 -2
- package/dist/catalog/plugin-upgrade.js +124 -6
- package/dist/catalog/unshare.js +1 -0
- package/dist/conversations/conversations.service.d.ts +9 -0
- package/dist/conversations/conversations.service.js +9 -2
- package/dist/conversations/events.d.ts +1 -0
- package/dist/db/kernel-schema.d.ts +54 -4
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +27 -0
- package/dist/http/api.module.js +7 -0
- package/dist/identity/accounts.d.ts +11 -4
- package/dist/identity/accounts.js +23 -10
- package/dist/identity/me.controller.d.ts +10 -10
- package/dist/identity/me.controller.js +8 -6
- package/dist/identity/me.schemas.d.ts +13 -3
- package/dist/identity/me.schemas.js +10 -2
- package/dist/identity/settings.d.ts +27 -0
- package/dist/identity/settings.js +64 -0
- package/dist/orgs/settings.service.d.ts +31 -0
- package/dist/orgs/settings.service.js +31 -1
- package/dist/platform/exclusions.js +26 -0
- package/dist/platform/operation.js +6 -2
- package/dist/platform/ops/admin.ops.d.ts +3 -1
- package/dist/platform/ops/admin.ops.js +25 -7
- package/dist/platform/ops/agents.ops.js +19 -6
- package/dist/platform/ops/components.ops.js +1 -0
- package/dist/platform/ops/org-settings.ops.d.ts +3 -1
- package/dist/platform/ops/org-settings.ops.js +33 -8
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/platform.ops.d.ts +3 -1
- package/dist/platform/ops/platform.ops.js +43 -16
- package/dist/platform/ops/themes.ops.d.ts +14 -0
- package/dist/platform/ops/themes.ops.js +47 -0
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/runtime/subagents/definition.d.ts +3 -2
- package/dist/runtime/subagents/definition.js +2 -1
- package/dist/themes/render-css.d.ts +12 -0
- package/dist/themes/render-css.js +132 -0
- package/dist/themes/theme-delivery.service.d.ts +34 -0
- package/dist/themes/theme-delivery.service.js +111 -0
- package/dist/themes/theme-state.d.ts +95 -0
- package/dist/themes/theme-state.js +203 -0
- package/dist/themes/theme-usage.d.ts +61 -0
- package/dist/themes/theme-usage.js +98 -0
- package/dist/themes/themes-public.controller.d.ts +28 -0
- package/dist/themes/themes-public.controller.js +92 -0
- package/dist/themes/themes.controller.d.ts +13 -0
- package/dist/themes/themes.controller.js +38 -0
- package/dist/themes/themes.module.d.ts +3 -0
- package/dist/themes/themes.module.js +21 -0
- package/dist/themes/themes.schemas.d.ts +3 -0
- package/dist/themes/themes.schemas.js +2 -0
- package/dist/themes/themes.service.d.ts +50 -0
- package/dist/themes/themes.service.js +99 -0
- package/dist/tools/connector-account-policies.d.ts +51 -0
- package/dist/tools/connector-account-policies.js +165 -0
- package/dist/tools/connectors.controller.d.ts +35 -4
- package/dist/tools/connectors.controller.js +66 -7
- package/dist/tools/connectors.schemas.d.ts +11 -0
- package/dist/tools/connectors.schemas.js +11 -0
- package/dist/tools/connectors.service.d.ts +2 -0
- package/dist/tools/connectors.service.js +1 -0
- package/dist/tools/pipeline.d.ts +1 -1
- package/dist/tools/policy.d.ts +23 -1
- package/dist/tools/policy.js +99 -24
- package/dist/tools/tools.module.js +3 -0
- package/package.json +3 -3
- package/src/migrations/0102_themes.sql +64 -0
- package/src/migrations/0103_ui_theme_setting.sql +7 -0
- package/src/migrations/0104_connector_tool_account_policies.sql +16 -0
|
@@ -50,6 +50,9 @@ export declare const DeploymentSettingsBody: z.ZodObject<{
|
|
|
50
50
|
deny: "deny";
|
|
51
51
|
}>>;
|
|
52
52
|
orgMarketplaceBrowse: z.ZodOptional<z.ZodBoolean>;
|
|
53
|
+
uiTheme: z.ZodOptional<z.ZodObject<{
|
|
54
|
+
id: z.ZodNullable<z.ZodString>;
|
|
55
|
+
}, z.core.$strict>>;
|
|
53
56
|
}, z.core.$strict>;
|
|
54
57
|
export declare const PurposeParams: z.ZodObject<{
|
|
55
58
|
purpose: z.ZodString;
|
|
@@ -33,6 +33,7 @@ export declare const AgentCreateDefinition: z.ZodObject<{
|
|
|
33
33
|
tools: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodArray<z.ZodString>]>;
|
|
34
34
|
}, z.core.$strict>], "kind">>>;
|
|
35
35
|
toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
36
|
+
allow: "allow";
|
|
36
37
|
ask: "ask";
|
|
37
38
|
deny: "deny";
|
|
38
39
|
}>>>;
|
|
@@ -116,6 +117,7 @@ export declare const AgentCreateBody: z.ZodObject<{
|
|
|
116
117
|
tools: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodArray<z.ZodString>]>;
|
|
117
118
|
}, z.core.$strict>], "kind">>>;
|
|
118
119
|
toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
120
|
+
allow: "allow";
|
|
119
121
|
ask: "ask";
|
|
120
122
|
deny: "deny";
|
|
121
123
|
}>>>;
|
|
@@ -231,6 +233,7 @@ export declare const AgentUpdateBody: z.ZodObject<{
|
|
|
231
233
|
tools: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodArray<z.ZodString>]>;
|
|
232
234
|
}, z.core.$strict>], "kind">>>;
|
|
233
235
|
toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
236
|
+
allow: "allow";
|
|
234
237
|
ask: "ask";
|
|
235
238
|
deny: "deny";
|
|
236
239
|
}>>>;
|
|
@@ -8,6 +8,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { parseToolKey } from "@capybara-hq/plugin-sdk";
|
|
11
12
|
import { Inject, Injectable } from "@nestjs/common";
|
|
12
13
|
import { sql } from "kysely";
|
|
13
14
|
import { ApiError } from "../errors.js";
|
|
@@ -310,7 +311,16 @@ let AgentsService = class AgentsService {
|
|
|
310
311
|
message: "subagentTools is set only on a sub-agent's definition at dispatch",
|
|
311
312
|
});
|
|
312
313
|
}
|
|
313
|
-
for (const key of Object.
|
|
314
|
+
for (const [key, policy] of Object.entries(def.toolPolicies)) {
|
|
315
|
+
// `allow` counts only for a connector tool (decision 2026-11-09): elsewhere a policy only
|
|
316
|
+
// tightens, so an `allow` there is refused rather than kept as a no-op
|
|
317
|
+
if (policy === "allow" && parseToolKey(key)?.kind !== "connector") {
|
|
318
|
+
details.push({
|
|
319
|
+
path: `toolPolicies.${key}`,
|
|
320
|
+
message: `"allow" applies only to a connector's tools; "${key}" can only be tightened (ask or deny)`,
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
314
324
|
if (!isToolPolicyKey(key, keys)) {
|
|
315
325
|
// an rc key (tool naming spec §8.3): saving writes current keys only, so name them
|
|
316
326
|
const renamed = legacyKeySaveText(key, keys);
|
|
@@ -196,6 +196,7 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
|
|
|
196
196
|
deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
197
197
|
}, z.core.$strict>>;
|
|
198
198
|
toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
199
|
+
allow: "allow";
|
|
199
200
|
ask: "ask";
|
|
200
201
|
deny: "deny";
|
|
201
202
|
}>>>;
|
|
@@ -207,10 +207,14 @@ export const AgentDefinitionSchema = z.object({
|
|
|
207
207
|
*/
|
|
208
208
|
subagentTools: SubagentToolsSchema.optional(),
|
|
209
209
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
210
|
+
* Policies by tool key; keys are tools the agent could have (on or turned off). `ask` / `deny`
|
|
211
|
+
* tighten any tool. `allow` is accepted only on a connector tool key and counts only for a
|
|
212
|
+
* personal-credential connector's tool (decision 2026-11-09): it overrides the org default and
|
|
213
|
+
* the member's own 询问, never the org's or the member's 禁止 (`tools/policy.ts`
|
|
214
|
+
* `connectorPolicy`); on any other tool it changes nothing. Older definitions without it read as
|
|
215
|
+
* empty.
|
|
212
216
|
*/
|
|
213
|
-
toolPolicies: z.record(z.string(), z.enum(["ask", "deny"])).default({}),
|
|
217
|
+
toolPolicies: z.record(z.string(), z.enum(["allow", "ask", "deny"])).default({}),
|
|
214
218
|
/**
|
|
215
219
|
* 技能(M5 T13):`orgSkills` 是引用的组织技能 id(保存时校验属于本组织且未删除,不可重复)。
|
|
216
220
|
* agent 自己的技能(`scope=agent`)自动对它生效,不进定义。可省略(较早保存的定义也没有),按空处理;
|
|
@@ -200,6 +200,10 @@ extensionTools = []) {
|
|
|
200
200
|
const toolPolicies = {};
|
|
201
201
|
const connectorPolicies = new Map();
|
|
202
202
|
for (const [k, v] of Object.entries(def.toolPolicies ?? {})) {
|
|
203
|
+
// a template only tightens: an agent's `allow` (a connector tool's, decision 2026-11-09)
|
|
204
|
+
// is not carried into it
|
|
205
|
+
if (v === "allow")
|
|
206
|
+
continue;
|
|
203
207
|
const parsed = parseToolKey(k);
|
|
204
208
|
if (parsed !== null && parsed.kind !== "connector" && TOOL_KEY_RE.test(k)) {
|
|
205
209
|
toolPolicies[k] = v;
|
|
@@ -134,7 +134,7 @@ let PendingConnectorsService = class PendingConnectorsService {
|
|
|
134
134
|
}
|
|
135
135
|
const k = connectorToolKey(c.slug, n);
|
|
136
136
|
const prev = toolPolicies[k];
|
|
137
|
-
if (prev === undefined || RANK[p] > RANK[prev])
|
|
137
|
+
if (prev === undefined || prev === "allow" || RANK[p] > RANK[prev])
|
|
138
138
|
toolPolicies[k] = p;
|
|
139
139
|
}
|
|
140
140
|
// tools are on by default (tools default-on spec §3): binding an entry that names some tools
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentTemplate, type Connector, type ConnectorTemplateFile, type ConnectorTemplateSpec, type LocalizedName, type PluginManifest } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { type AgentTemplate, type Connector, type ConnectorTemplateFile, type ConnectorTemplateSpec, type LocalizedName, type ParsedTheme, type PluginManifest } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import type { KernelSchemaDb } from "../db/kernel-schema.js";
|
|
3
3
|
import type { ExtensionRegistry } from "../extensions/registry.js";
|
|
4
4
|
import type { ExtensionUnavailableReason } from "../extensions/unavailable.js";
|
|
@@ -230,3 +230,35 @@ export declare function readCatalogAgentTemplates(storage: StorageService, v: Ca
|
|
|
230
230
|
path: string;
|
|
231
231
|
template: AgentTemplate;
|
|
232
232
|
}[]>;
|
|
233
|
+
/** A theme of a catalog version ({@link readCatalogThemes}). */
|
|
234
|
+
export interface CatalogTheme {
|
|
235
|
+
/** The theme id (the file's base name). */
|
|
236
|
+
id: string;
|
|
237
|
+
/** Relative to the plugin root. */
|
|
238
|
+
path: string;
|
|
239
|
+
theme: ParsedTheme;
|
|
240
|
+
/** The theme's fonts and logo files (paths relative to the plugin root), sanitized. */
|
|
241
|
+
assets: SkillPackageFile[];
|
|
242
|
+
/** {@link themeAssetsDigest} of the version's records of those files. */
|
|
243
|
+
assetsDigest: string;
|
|
244
|
+
}
|
|
245
|
+
/** The asset paths a parsed theme references (fonts, logo mark and favicon), without `./`. */
|
|
246
|
+
export declare function themeAssetPaths(theme: ParsedTheme): string[];
|
|
247
|
+
/**
|
|
248
|
+
* The digest of a theme's assets as recorded on a catalog version (`files`): equal digests = the
|
|
249
|
+
* same fonts and logo. With the canonical definition it decides whether a theme changed.
|
|
250
|
+
*/
|
|
251
|
+
export declare function themeAssetsDigest(files: {
|
|
252
|
+
path: string;
|
|
253
|
+
sha256: string;
|
|
254
|
+
}[], theme: ParsedTheme): string;
|
|
255
|
+
/**
|
|
256
|
+
* The themes of a catalog version (`manifest.themes`, listed explicitly since import), parsed from
|
|
257
|
+
* storage and their assets checked again (checked at import; again in case storage was changed).
|
|
258
|
+
* Grammar and token names are checked again (a token renamed in core stays loud, themes spec
|
|
259
|
+
* §4.1), but not readability: the version passed it at import, and a later change of the built-in
|
|
260
|
+
* tokens must not block an update or a rollback to it. A theme that fails -> 400 `theme.invalid`
|
|
261
|
+
* as at import (themes spec §6: a failing new version blocks the plugin update with the same
|
|
262
|
+
* refusal).
|
|
263
|
+
*/
|
|
264
|
+
export declare function readCatalogThemes(storage: StorageService, v: CatalogVersionFiles): Promise<CatalogTheme[]>;
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
// OrgCatalogService: kept out of org-catalog.service.ts so the installer's module never imports a
|
|
4
4
|
// module whose provider injects PluginInstaller (an ESM import cycle would run decorators early).
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { parseAgentTemplate, parseConnectorTemplateFile, } from "@capybara-hq/plugin-sdk";
|
|
6
|
+
import { parseAgentTemplate, parseConnectorTemplateFile, parseThemeFile, themeIdOf, validateThemeAssets, } from "@capybara-hq/plugin-sdk";
|
|
7
7
|
import { normalizeToolKeys } from "../agents/definition.js";
|
|
8
8
|
import { ApiError } from "../errors.js";
|
|
9
9
|
import { can } from "../orgs/roles.js";
|
|
10
10
|
import { packagePath } from "../packages/tgz.js";
|
|
11
11
|
import { checkSkillFiles, SkillArchiveError } from "../skills/package.js";
|
|
12
12
|
import { readDisplayFiles } from "./catalog-files.js";
|
|
13
|
-
import { iconProblem } from "./plugin-package.js";
|
|
13
|
+
import { iconProblem, pluginThemeError, themeApiError } from "./plugin-package.js";
|
|
14
14
|
import { satisfiesRange } from "./versions.js";
|
|
15
15
|
/**
|
|
16
16
|
* A connector template as an org sees it (the install wizard and the upgrade preview's inputs):
|
|
@@ -365,3 +365,66 @@ export async function readCatalogAgentTemplates(storage, v) {
|
|
|
365
365
|
return { path, template: normalizeToolKeys(parsed.template) };
|
|
366
366
|
});
|
|
367
367
|
}
|
|
368
|
+
/** The asset paths a parsed theme references (fonts, logo mark and favicon), without `./`. */
|
|
369
|
+
export function themeAssetPaths(theme) {
|
|
370
|
+
const refs = (theme.fonts ?? []).map((f) => f.file);
|
|
371
|
+
for (const ref of [theme.logo?.mark, theme.logo?.favicon])
|
|
372
|
+
if (ref !== undefined)
|
|
373
|
+
refs.push(ref);
|
|
374
|
+
return [...new Set(refs.map((r) => packagePath(r)))];
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The digest of a theme's assets as recorded on a catalog version (`files`): equal digests = the
|
|
378
|
+
* same fonts and logo. With the canonical definition it decides whether a theme changed.
|
|
379
|
+
*/
|
|
380
|
+
export function themeAssetsDigest(files, theme) {
|
|
381
|
+
const paths = new Set(themeAssetPaths(theme));
|
|
382
|
+
return skillFilesDigest(files.filter((f) => paths.has(f.path)));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The themes of a catalog version (`manifest.themes`, listed explicitly since import), parsed from
|
|
386
|
+
* storage and their assets checked again (checked at import; again in case storage was changed).
|
|
387
|
+
* Grammar and token names are checked again (a token renamed in core stays loud, themes spec
|
|
388
|
+
* §4.1), but not readability: the version passed it at import, and a later change of the built-in
|
|
389
|
+
* tokens must not block an update or a rollback to it. A theme that fails -> 400 `theme.invalid`
|
|
390
|
+
* as at import (themes spec §6: a failing new version blocks the plugin update with the same
|
|
391
|
+
* refusal).
|
|
392
|
+
*/
|
|
393
|
+
export async function readCatalogThemes(storage, v) {
|
|
394
|
+
const refs = v.manifest.themes ?? [];
|
|
395
|
+
if (refs.length === 0)
|
|
396
|
+
return [];
|
|
397
|
+
const read = async (paths) => (await readCatalogFiles(storage, v.object_prefix, v.files.filter((f) => paths.includes(f.path)))).map((f) => [f.path, f.data]);
|
|
398
|
+
const paths = refs.map((r) => packagePath(r.path));
|
|
399
|
+
const texts = new Map(await read(paths));
|
|
400
|
+
const parsed = paths.map((path) => {
|
|
401
|
+
const id = themeIdOf(path);
|
|
402
|
+
const data = texts.get(path);
|
|
403
|
+
if (data === undefined)
|
|
404
|
+
throw new ApiError("internal", `Stored theme ${path} is missing`);
|
|
405
|
+
const r = parseThemeFile(id, new TextDecoder().decode(data), { readability: false });
|
|
406
|
+
if ("errors" in r)
|
|
407
|
+
throw themeApiError(pluginThemeError([{ theme: id, problems: r.errors }]));
|
|
408
|
+
return { id, path, theme: r };
|
|
409
|
+
});
|
|
410
|
+
const files = new Map(await read([...new Set(parsed.flatMap((t) => themeAssetPaths(t.theme)))]));
|
|
411
|
+
const out = [];
|
|
412
|
+
for (const { id, path, theme } of parsed) {
|
|
413
|
+
const { assets, errors } = validateThemeAssets(theme, files);
|
|
414
|
+
if (errors.length > 0)
|
|
415
|
+
throw themeApiError(pluginThemeError([{ theme: id, problems: errors }]));
|
|
416
|
+
out.push({
|
|
417
|
+
id,
|
|
418
|
+
path,
|
|
419
|
+
theme,
|
|
420
|
+
assets: [...assets].map(([p, bytes]) => ({
|
|
421
|
+
path: p,
|
|
422
|
+
size: bytes.byteLength,
|
|
423
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
424
|
+
data: bytes,
|
|
425
|
+
})),
|
|
426
|
+
assetsDigest: themeAssetsDigest(v.files, theme),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
@@ -670,6 +670,7 @@ export function componentCountsOf(m) {
|
|
|
670
670
|
return {
|
|
671
671
|
extension: m?.extension === undefined ? 0 : 1,
|
|
672
672
|
skills: m?.skills?.length ?? 0,
|
|
673
|
+
themes: m?.themes?.length ?? 0,
|
|
673
674
|
connectorTemplates: m?.connectorTemplates?.length ?? 0,
|
|
674
675
|
agentTemplates: m?.agents?.length ?? 0,
|
|
675
676
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Queue } from "bullmq";
|
|
2
|
+
import { ApiError } from "../errors.js";
|
|
2
3
|
import { ExtensionAdminOps } from "../extensions/admin-ops.js";
|
|
3
4
|
import { ExtensionAdmin } from "../extensions/admin-view.js";
|
|
4
5
|
import { ExtensionLifecycle } from "../extensions/lifecycle.js";
|
|
@@ -8,6 +9,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
8
9
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
9
10
|
import type { KernelLogger } from "../log.js";
|
|
10
11
|
import { OrgLock } from "../orgs/org-lock.js";
|
|
12
|
+
import { type ThemePreview } from "../themes/theme-usage.js";
|
|
11
13
|
import { type ActorOpts, type CatalogItemView, CatalogService } from "./catalog.service.js";
|
|
12
14
|
import { type ComponentKind } from "./component-state.js";
|
|
13
15
|
import { type PluginConfirmResult, PluginImport } from "./plugin-import.js";
|
|
@@ -46,7 +48,9 @@ export interface ScopePreview {
|
|
|
46
48
|
}[];
|
|
47
49
|
/**
|
|
48
50
|
* The orgs the change would newly offer a skill of the plugin whose own skills already use its
|
|
49
|
-
* name
|
|
51
|
+
* name, or turn a theme of the plugin on in where another theme with its id is on (rule 10,
|
|
52
|
+
* themes spec §6; `kind: "theme"` with the clashing themes in `themes`, `skills` empty): the
|
|
53
|
+
* change is refused while there are any.
|
|
50
54
|
*/
|
|
51
55
|
conflicts: {
|
|
52
56
|
orgId: string;
|
|
@@ -54,7 +58,16 @@ export interface ScopePreview {
|
|
|
54
58
|
name: string;
|
|
55
59
|
skills: string[];
|
|
56
60
|
plugins: string[];
|
|
61
|
+
/** Set for a theme id clash; absent for a skill. */
|
|
62
|
+
kind?: "theme";
|
|
63
|
+
/** A theme id clash: the clashing themes on in the org. */
|
|
64
|
+
themes?: string[];
|
|
57
65
|
}[];
|
|
66
|
+
/**
|
|
67
|
+
* The plugin's themes the change takes away from some orgs (themes spec §6): the orgs among
|
|
68
|
+
* them whose default each is and how many people picked it. Absent when no theme is taken away.
|
|
69
|
+
*/
|
|
70
|
+
themes?: ThemePreview[];
|
|
58
71
|
scopeHash: string;
|
|
59
72
|
}
|
|
60
73
|
/** A scope change (`orgs` only with `named`) or a disable. */
|
|
@@ -64,6 +77,12 @@ export type ScopeTarget = {
|
|
|
64
77
|
} | {
|
|
65
78
|
disabled: true;
|
|
66
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* The refusal of a preview's conflicts: skill ones as `plugin.skill_conflict`, theme ones as
|
|
82
|
+
* `theme.id_taken`; both at once as `plugin.skill_conflict` listing every clash (the skill details
|
|
83
|
+
* first, then the theme ones `{ theme, orgId, themes, plugins }`), so none waits for a retry.
|
|
84
|
+
*/
|
|
85
|
+
export declare function conflictsError(conflicts: ScopePreview["conflicts"]): ApiError;
|
|
67
86
|
/**
|
|
68
87
|
* The super admin's plugin scope and deployment-level disable (plugin and component management
|
|
69
88
|
* spec §4.1, §5 rules 2-4 and 8), and the import confirm that enqueues the scope sync of the item it
|
|
@@ -234,6 +253,12 @@ export declare class CatalogScopeService {
|
|
|
234
253
|
enqueueSync(itemId: string): Promise<void>;
|
|
235
254
|
/** Enqueues the item's scope sync; a failure only warns (the next change or org sync converges). */
|
|
236
255
|
private enqueue;
|
|
256
|
+
/**
|
|
257
|
+
* 409 `theme.platform_default` when a live theme of the item is the platform default and `after`
|
|
258
|
+
* says the change leaves it narrower than `all` or gone (themes spec §7). In the caller's
|
|
259
|
+
* transaction, before anything is written.
|
|
260
|
+
*/
|
|
261
|
+
private assertKeepsPlatformDefault;
|
|
237
262
|
/** The live item (row locked with `forUpdate`); missing -> 404, builtin -> `plugin.builtin`. */
|
|
238
263
|
private item;
|
|
239
264
|
/** The `named` list of the item, sorted. */
|
|
@@ -247,8 +272,9 @@ export declare class CatalogScopeService {
|
|
|
247
272
|
* components with their own scope a scope change replaces; the orgs where a skill or agent
|
|
248
273
|
* template is on now and would be off after (agents merged into `affected`: a component with its
|
|
249
274
|
* own scope that goes back to following the plugin, or one cut by the disable); and the orgs a
|
|
250
|
-
* skill would newly be offered to whose own skills use its name
|
|
251
|
-
* is on
|
|
275
|
+
* skill would newly be offered to whose own skills use its name, or a theme would come on in
|
|
276
|
+
* where another theme with its id is on; and per theme the orgs it would go off in
|
|
277
|
+
* (`themesLost`). `onAfter`: the orgs the plugin is on in after the change.
|
|
252
278
|
*/
|
|
253
279
|
private componentChanges;
|
|
254
280
|
/**
|
|
@@ -26,10 +26,11 @@ import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
|
26
26
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
27
27
|
import { OrgLock } from "../orgs/org-lock.js";
|
|
28
28
|
import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
|
|
29
|
+
import { assertNotPlatformDefault, themesPreview, } from "../themes/theme-usage.js";
|
|
29
30
|
import { CatalogService } from "./catalog.service.js";
|
|
30
31
|
import { assertOrgsExist, markVersionApplied } from "./catalog-items.js";
|
|
31
32
|
import { nameConflictError } from "./component-scope.service.js";
|
|
32
|
-
import { agentsUsing, COMPONENT_KINDS, componentOffered, componentOn, listedIn, loadComponents, loadScopeState, pluginFactsOf, skillNameClashes, switchOf, } from "./component-state.js";
|
|
33
|
+
import { agentsUsing, COMPONENT_KINDS, componentOffered, componentOn, listedIn, loadComponents, loadScopeState, pluginFactsOf, skillNameClashes, switchOf, themeIdClashes, } from "./component-state.js";
|
|
33
34
|
import { ownedPluginCleanupJob } from "./job-ids.js";
|
|
34
35
|
import { assertNotAwaiting, assertOwnerListed, assertShareable, assertShareableTarget, itemRemovedError, newlySharedWith, ownerOf, ownersOf, PLATFORM_OWNER, packageOwner, pendingDigest, releaseExtensionClaim, } from "./ownership.js";
|
|
35
36
|
import { PluginImport } from "./plugin-import.js";
|
|
@@ -63,9 +64,30 @@ function scopeHash(target, p) {
|
|
|
63
64
|
needsConfig: [...p.needsConfig].sort(),
|
|
64
65
|
replaced: [...p.replaced].sort((a, b) => a.id.localeCompare(b.id)),
|
|
65
66
|
conflicts: [...p.conflicts].sort((a, b) => a.orgId.localeCompare(b.orgId)),
|
|
67
|
+
// only when present: a preview without themes keeps the hash it had before themes
|
|
68
|
+
...(p.themes !== undefined && { themes: p.themes }),
|
|
66
69
|
}))
|
|
67
70
|
.digest("hex");
|
|
68
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The refusal of a preview's conflicts: skill ones as `plugin.skill_conflict`, theme ones as
|
|
74
|
+
* `theme.id_taken`; both at once as `plugin.skill_conflict` listing every clash (the skill details
|
|
75
|
+
* first, then the theme ones `{ theme, orgId, themes, plugins }`), so none waits for a retry.
|
|
76
|
+
*/
|
|
77
|
+
export function conflictsError(conflicts) {
|
|
78
|
+
const skills = conflicts.filter((c) => c.kind !== "theme");
|
|
79
|
+
const themes = conflicts.filter((c) => c.kind === "theme").map(({ kind: _k, ...c }) => c);
|
|
80
|
+
if (themes.length === 0)
|
|
81
|
+
return nameConflictError(skills);
|
|
82
|
+
const t = nameConflictError(themes, "theme");
|
|
83
|
+
if (skills.length === 0)
|
|
84
|
+
return t;
|
|
85
|
+
const s = nameConflictError(skills);
|
|
86
|
+
return new ApiError(s.code, `${s.message}\n${t.message}`, [
|
|
87
|
+
...(s.details ?? []),
|
|
88
|
+
...(t.details ?? []),
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
69
91
|
/**
|
|
70
92
|
* The super admin's plugin scope and deployment-level disable (plugin and component management
|
|
71
93
|
* spec §4.1, §5 rules 2-4 and 8), and the import confirm that enqueues the scope sync of the item it
|
|
@@ -676,11 +698,14 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
676
698
|
// The owner stays listed (P3-17); widening goes through the share gate (P3-16)
|
|
677
699
|
assertOwnerListed(item, t);
|
|
678
700
|
assertShareableTarget(item, { scope: item.scope, orgs: from }, t);
|
|
701
|
+
// A scope change sends every theme back to following the plugin (rule 2): each is `all` after
|
|
702
|
+
// only when the plugin is (themes spec §7)
|
|
703
|
+
await this.assertKeepsPlatformDefault(item.id, (c) => c.disabled || item.disabled ? "gone" : t.scope === "all" ? "all" : "narrower");
|
|
679
704
|
const p = await this.compute(item, t);
|
|
680
705
|
if (p.scopeHash !== hash)
|
|
681
706
|
throw staleError();
|
|
682
707
|
if (p.conflicts.length > 0)
|
|
683
|
-
throw
|
|
708
|
+
throw conflictsError(p.conflicts);
|
|
684
709
|
if (item.scope === t.scope && from.join() === t.orgs.join() && p.replaced.length === 0)
|
|
685
710
|
return;
|
|
686
711
|
await this.followAgain(actor, itemId, p.replaced, opts);
|
|
@@ -723,13 +748,15 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
723
748
|
const item = await this.item(itemId, true);
|
|
724
749
|
if (item.disabled === disabled)
|
|
725
750
|
return;
|
|
751
|
+
if (disabled)
|
|
752
|
+
await this.assertKeepsPlatformDefault(item.id, () => "gone");
|
|
726
753
|
const p = await this.compute(item, disabled
|
|
727
754
|
? await this.targetOf(item, { disabled: true })
|
|
728
755
|
: { scope: item.scope, orgs: await this.listed(itemId), disabled, scopeChange: false });
|
|
729
756
|
if (disabled && p.scopeHash !== hash)
|
|
730
757
|
throw staleError();
|
|
731
758
|
if (p.conflicts.length > 0)
|
|
732
|
-
throw
|
|
759
|
+
throw conflictsError(p.conflicts);
|
|
733
760
|
await this.db
|
|
734
761
|
.updateTable("catalog_items")
|
|
735
762
|
.set({
|
|
@@ -780,6 +807,7 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
780
807
|
"platform_skills",
|
|
781
808
|
"platform_agent_templates",
|
|
782
809
|
"platform_connector_templates",
|
|
810
|
+
"platform_themes",
|
|
783
811
|
]) {
|
|
784
812
|
await this.db
|
|
785
813
|
.updateTable(table)
|
|
@@ -830,6 +858,15 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
830
858
|
this.log.warn("enqueueing the plugin scope sync failed", { itemId, err });
|
|
831
859
|
}
|
|
832
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* 409 `theme.platform_default` when a live theme of the item is the platform default and `after`
|
|
863
|
+
* says the change leaves it narrower than `all` or gone (themes spec §7). In the caller's
|
|
864
|
+
* transaction, before anything is written.
|
|
865
|
+
*/
|
|
866
|
+
async assertKeepsPlatformDefault(itemId, after) {
|
|
867
|
+
const themes = await loadComponents(this.db, "theme", { itemId });
|
|
868
|
+
await assertNotPlatformDefault(this.db, themes.map((c) => c.id), (id) => after(themes.find((c) => c.id === id) ?? { disabled: false }));
|
|
869
|
+
}
|
|
833
870
|
/** The live item (row locked with `forUpdate`); missing -> 404, builtin -> `plugin.builtin`. */
|
|
834
871
|
async item(itemId, forUpdate) {
|
|
835
872
|
if (!UUID_RE.test(itemId))
|
|
@@ -954,8 +991,15 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
954
991
|
else
|
|
955
992
|
onAfter.add(org.id);
|
|
956
993
|
}
|
|
957
|
-
const { replaced, conflicts } = await this.componentChanges(item, target, orgs, onAfter, affected);
|
|
958
|
-
const
|
|
994
|
+
const { replaced, conflicts, themesLost } = await this.componentChanges(item, target, orgs, onAfter, affected);
|
|
995
|
+
const themes = await themesPreview(db, themesLost);
|
|
996
|
+
const p = {
|
|
997
|
+
affected,
|
|
998
|
+
needsConfig,
|
|
999
|
+
replaced,
|
|
1000
|
+
conflicts,
|
|
1001
|
+
...(themes !== undefined && { themes }),
|
|
1002
|
+
};
|
|
959
1003
|
return { ...p, scopeHash: scopeHash(target, p) };
|
|
960
1004
|
}
|
|
961
1005
|
/**
|
|
@@ -963,13 +1007,15 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
963
1007
|
* components with their own scope a scope change replaces; the orgs where a skill or agent
|
|
964
1008
|
* template is on now and would be off after (agents merged into `affected`: a component with its
|
|
965
1009
|
* own scope that goes back to following the plugin, or one cut by the disable); and the orgs a
|
|
966
|
-
* skill would newly be offered to whose own skills use its name
|
|
967
|
-
* is on
|
|
1010
|
+
* skill would newly be offered to whose own skills use its name, or a theme would come on in
|
|
1011
|
+
* where another theme with its id is on; and per theme the orgs it would go off in
|
|
1012
|
+
* (`themesLost`). `onAfter`: the orgs the plugin is on in after the change.
|
|
968
1013
|
*/
|
|
969
1014
|
async componentChanges(item, target, orgs, onAfter, affected) {
|
|
970
1015
|
const db = this.db;
|
|
971
1016
|
const replaced = [];
|
|
972
1017
|
const conflicts = [];
|
|
1018
|
+
const themesLost = new Map();
|
|
973
1019
|
const lose = new Map();
|
|
974
1020
|
for (const kind of COMPONENT_KINDS) {
|
|
975
1021
|
const comps = await loadComponents(db, kind, { itemId: item.id });
|
|
@@ -997,7 +1043,31 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
997
1043
|
};
|
|
998
1044
|
const listed = listedIn(st, c.id, org.id);
|
|
999
1045
|
const s = switchOf(st, c.id, org.id);
|
|
1000
|
-
|
|
1046
|
+
const onNow = componentOn(c, pNow, listed, s);
|
|
1047
|
+
const onAfterChange = componentOn(cAfter, pAfter, listed, s);
|
|
1048
|
+
if (kind === "theme") {
|
|
1049
|
+
if (onNow && !onAfterChange) {
|
|
1050
|
+
const set = themesLost.get(c.id) ?? new Set();
|
|
1051
|
+
set.add(org.id);
|
|
1052
|
+
themesLost.set(c.id, set);
|
|
1053
|
+
}
|
|
1054
|
+
if (onAfterChange && !onNow) {
|
|
1055
|
+
const clash = await themeIdClashes(db, org.id, c, c.name);
|
|
1056
|
+
if (clash.length > 0) {
|
|
1057
|
+
conflicts.push({
|
|
1058
|
+
orgId: org.id,
|
|
1059
|
+
orgName: org.name,
|
|
1060
|
+
name: c.name,
|
|
1061
|
+
skills: [],
|
|
1062
|
+
themes: clash.map((x) => x.id),
|
|
1063
|
+
plugins: clash.map((x) => x.plugin ?? "platform"),
|
|
1064
|
+
kind: "theme",
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
if (onNow && !onAfterChange) {
|
|
1001
1071
|
for (const a of await agentsUsing(db, org.id, kind, [c.id])) {
|
|
1002
1072
|
const m = lose.get(org.id) ?? new Map();
|
|
1003
1073
|
m.set(a.id, a.name);
|
|
@@ -1031,7 +1101,7 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
1031
1101
|
e.agents.push({ id, name });
|
|
1032
1102
|
}
|
|
1033
1103
|
}
|
|
1034
|
-
return { replaced, conflicts };
|
|
1104
|
+
return { replaced, conflicts, themesLost };
|
|
1035
1105
|
}
|
|
1036
1106
|
/**
|
|
1037
1107
|
* Sends the components of the item with their own scope back to following it (a plugin scope
|
|
@@ -1044,7 +1114,9 @@ let CatalogScopeService = class CatalogScopeService {
|
|
|
1044
1114
|
? "platform_skills"
|
|
1045
1115
|
: r.kind === "agent_template"
|
|
1046
1116
|
? "platform_agent_templates"
|
|
1047
|
-
: "
|
|
1117
|
+
: r.kind === "theme"
|
|
1118
|
+
? "platform_themes"
|
|
1119
|
+
: "platform_connector_templates";
|
|
1048
1120
|
await this.db
|
|
1049
1121
|
.updateTable(table)
|
|
1050
1122
|
.set({
|
|
@@ -111,6 +111,7 @@ export interface ComponentCounts {
|
|
|
111
111
|
skills: number;
|
|
112
112
|
connectorTemplates: number;
|
|
113
113
|
agentTemplates: number;
|
|
114
|
+
themes: number;
|
|
114
115
|
}
|
|
115
116
|
/**
|
|
116
117
|
* A git source as recorded (spec §5.1: the actual host, repo and commit, and whether it follows a
|
|
@@ -329,8 +330,8 @@ export declare class CatalogService {
|
|
|
329
330
|
/**
|
|
330
331
|
* Deletes the item (soft delete, audit `catalog.removed`; requests still pending are cancelled in
|
|
331
332
|
* the same transaction): versions and stored files are kept (installed components still refer to
|
|
332
|
-
* them),
|
|
333
|
-
* item. Deleted / missing -> `catalog.item_not_found`. A platform plugin whose extension is
|
|
333
|
+
* them), its themes are soft-deleted (nothing refers to their rows), organizations no longer see
|
|
334
|
+
* it; the same name can later be imported again as a new item. Deleted / missing -> `catalog.item_not_found`. A platform plugin whose extension is
|
|
334
335
|
* still enabled on the deployment -> 409 `catalog.extension_enabled` (details
|
|
335
336
|
* `[{ extension, displayName }]`): disable it on the Extensions page first, as an uninstall
|
|
336
337
|
* requires. A disabled extension keeps its install record (the super admin uninstalls it on the
|
|
@@ -353,9 +354,10 @@ export declare class CatalogService {
|
|
|
353
354
|
* The soft delete of {@link remove} in the caller's transaction, without its super admin check
|
|
354
355
|
* (pre-flight F14: the super admin's remove and an owner's delete or an org deletion,
|
|
355
356
|
* `OwnedItems.transferOrRemove`, share it): `deleted_at` set, a pending version cleared, the
|
|
356
|
-
* extension name claim of a first upload still waiting released (ruling P3-37, F10),
|
|
357
|
-
*
|
|
358
|
-
*
|
|
357
|
+
* extension name claim of a first upload still waiting released (ruling P3-37, F10), its themes
|
|
358
|
+
* soft-deleted (refused with 409 `theme.platform_default` while one is the platform default),
|
|
359
|
+
* pending requests cancelled, audit `catalog.removed` (`actor` null: the kernel). Returns the
|
|
360
|
+
* removed item's name, or null when it is gone already.
|
|
359
361
|
*/
|
|
360
362
|
removeIn(actor: string | null, itemId: string, opts?: ActorOpts & {
|
|
361
363
|
reason?: string;
|
|
@@ -21,6 +21,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
21
21
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
22
22
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
23
23
|
import { sameRepository } from "../packages/provenance.js";
|
|
24
|
+
import { assertNotPlatformDefault } from "../themes/theme-usage.js";
|
|
24
25
|
import { countInstalls, itemConfirmations, itemScopeFacts, itemView, marketplaceLatest, versionUsable, } from "./catalog-items.js";
|
|
25
26
|
import { expandGitUrl, GitSourceError, gitErrorDetails, normalGitPath, readGitTreeAndOids, resolveGitCommit, } from "./git-source.js";
|
|
26
27
|
import { fetchMarketplaceArchive, marketplaceNetPolicy, marketplaceUrlProblem, } from "./marketplace-fetch.js";
|
|
@@ -463,8 +464,8 @@ let CatalogService = class CatalogService {
|
|
|
463
464
|
/**
|
|
464
465
|
* Deletes the item (soft delete, audit `catalog.removed`; requests still pending are cancelled in
|
|
465
466
|
* the same transaction): versions and stored files are kept (installed components still refer to
|
|
466
|
-
* them),
|
|
467
|
-
* item. Deleted / missing -> `catalog.item_not_found`. A platform plugin whose extension is
|
|
467
|
+
* them), its themes are soft-deleted (nothing refers to their rows), organizations no longer see
|
|
468
|
+
* it; the same name can later be imported again as a new item. Deleted / missing -> `catalog.item_not_found`. A platform plugin whose extension is
|
|
468
469
|
* still enabled on the deployment -> 409 `catalog.extension_enabled` (details
|
|
469
470
|
* `[{ extension, displayName }]`): disable it on the Extensions page first, as an uninstall
|
|
470
471
|
* requires. A disabled extension keeps its install record (the super admin uninstalls it on the
|
|
@@ -522,13 +523,25 @@ let CatalogService = class CatalogService {
|
|
|
522
523
|
* The soft delete of {@link remove} in the caller's transaction, without its super admin check
|
|
523
524
|
* (pre-flight F14: the super admin's remove and an owner's delete or an org deletion,
|
|
524
525
|
* `OwnedItems.transferOrRemove`, share it): `deleted_at` set, a pending version cleared, the
|
|
525
|
-
* extension name claim of a first upload still waiting released (ruling P3-37, F10),
|
|
526
|
-
*
|
|
527
|
-
*
|
|
526
|
+
* extension name claim of a first upload still waiting released (ruling P3-37, F10), its themes
|
|
527
|
+
* soft-deleted (refused with 409 `theme.platform_default` while one is the platform default),
|
|
528
|
+
* pending requests cancelled, audit `catalog.removed` (`actor` null: the kernel). Returns the
|
|
529
|
+
* removed item's name, or null when it is gone already.
|
|
528
530
|
*/
|
|
529
531
|
async removeIn(actor, itemId, opts = {}) {
|
|
530
532
|
const t = this.db;
|
|
531
533
|
const now = sql `now()`;
|
|
534
|
+
// Its themes go with it: refused while one is the platform default (themes spec §7), before
|
|
535
|
+
// anything is written
|
|
536
|
+
// (the theme rows FOR UPDATE first: the theme lock order is item, theme row, `uiTheme`)
|
|
537
|
+
const themes = await t
|
|
538
|
+
.selectFrom("platform_themes")
|
|
539
|
+
.select("id")
|
|
540
|
+
.where("catalog_item_id", "=", itemId)
|
|
541
|
+
.where("deleted_at", "is", null)
|
|
542
|
+
.forUpdate()
|
|
543
|
+
.execute();
|
|
544
|
+
await assertNotPlatformDefault(t, themes.map((r) => r.id), () => "gone");
|
|
532
545
|
const row = await t
|
|
533
546
|
.updateTable("catalog_items")
|
|
534
547
|
.set({
|
|
@@ -551,6 +564,14 @@ let CatalogService = class CatalogService {
|
|
|
551
564
|
await this.installLock.lock(row.name);
|
|
552
565
|
await releaseExtensionClaim(t, row.name, row.package_name);
|
|
553
566
|
}
|
|
567
|
+
// Its themes go with it (themes spec §6): nothing refers to a theme row (a theme picked by an
|
|
568
|
+
// org or a person is an id that falls back once no live theme has it)
|
|
569
|
+
await t
|
|
570
|
+
.updateTable("platform_themes")
|
|
571
|
+
.set({ deleted_at: now, updated_at: now })
|
|
572
|
+
.where("catalog_item_id", "=", itemId)
|
|
573
|
+
.where("deleted_at", "is", null)
|
|
574
|
+
.execute();
|
|
554
575
|
// Pending requests for a deleted item are pointless: cancel them in the same transaction
|
|
555
576
|
await cancelPendingRequests(t, { itemId }, actor, "item_removed", opts);
|
|
556
577
|
await audit(t, {
|
|
@@ -836,6 +836,9 @@ export function translateClaudePlugin(files, o) {
|
|
|
836
836
|
// the raw Claude agents/*.md the tree still holds and parse them as Capybara templates
|
|
837
837
|
// (batch 12 review I1); normalizedManifest drops an empty list, so digests are unchanged
|
|
838
838
|
agents: agents.agents.map((a) => ({ path: `./${a.path}` })),
|
|
839
|
+
// always empty, for the same reason: a Claude Code plugin's themes/*.json are not Capybara
|
|
840
|
+
// themes (themes spec §4); normalizedManifest drops the empty list
|
|
841
|
+
themes: [],
|
|
839
842
|
};
|
|
840
843
|
const checked = parsePluginManifest(manifest);
|
|
841
844
|
if ("errors" in checked) {
|