@capybara-hq/kernel 0.1.0-rc.14 → 0.1.0-rc.15

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 (99) hide show
  1. package/dist/admin/admin.schemas.d.ts +5 -13
  2. package/dist/catalog/admin-catalog.controller.d.ts +27 -4
  3. package/dist/catalog/admin-catalog.controller.js +53 -5
  4. package/dist/catalog/appearance.schemas.d.ts +10 -0
  5. package/dist/catalog/appearance.schemas.js +20 -0
  6. package/dist/catalog/appearance.service.d.ts +105 -0
  7. package/dist/catalog/appearance.service.js +306 -0
  8. package/dist/catalog/catalog-import.module.js +3 -0
  9. package/dist/catalog/catalog-items.d.ts +11 -0
  10. package/dist/catalog/catalog-items.js +21 -0
  11. package/dist/catalog/catalog-scope.service.d.ts +7 -0
  12. package/dist/catalog/catalog-scope.service.js +55 -15
  13. package/dist/catalog/catalog.service.js +10 -2
  14. package/dist/catalog/cli.js +3 -0
  15. package/dist/catalog/marketplaces.service.d.ts +1 -1
  16. package/dist/catalog/org-extension.controller.d.ts +27 -7
  17. package/dist/catalog/org-extension.controller.js +48 -6
  18. package/dist/catalog/plugin-import.d.ts +28 -4
  19. package/dist/catalog/plugin-import.js +95 -19
  20. package/dist/catalog/plugin-package.d.ts +4 -2
  21. package/dist/catalog/plugin-package.js +29 -3
  22. package/dist/client/client-access.service.d.ts +101 -0
  23. package/dist/client/client-access.service.js +294 -0
  24. package/dist/client/client-builds.service.d.ts +109 -0
  25. package/dist/client/client-builds.service.js +229 -0
  26. package/dist/client/client-delivery.controller.d.ts +25 -0
  27. package/dist/client/client-delivery.controller.js +66 -0
  28. package/dist/client/client-files.d.ts +48 -0
  29. package/dist/client/client-files.js +136 -0
  30. package/dist/client/client-reports.cron.d.ts +13 -0
  31. package/dist/client/client-reports.cron.js +42 -0
  32. package/dist/client/client-reports.service.d.ts +30 -0
  33. package/dist/client/client-reports.service.js +102 -0
  34. package/dist/client/client.controller.d.ts +45 -0
  35. package/dist/client/client.controller.js +121 -0
  36. package/dist/client/client.module.d.ts +7 -0
  37. package/dist/client/client.module.js +26 -0
  38. package/dist/client/client.schemas.d.ts +55 -0
  39. package/dist/client/client.schemas.js +27 -0
  40. package/dist/ctx/create-ctx.d.ts +4 -3
  41. package/dist/ctx/create-ctx.js +4 -3
  42. package/dist/db/kernel-schema.d.ts +47 -0
  43. package/dist/errors.d.ts +29 -0
  44. package/dist/errors.js +29 -0
  45. package/dist/extensions/admin-ops.d.ts +4 -1
  46. package/dist/extensions/admin-ops.js +22 -1
  47. package/dist/extensions/admin-view.d.ts +8 -0
  48. package/dist/extensions/admin-view.js +78 -0
  49. package/dist/extensions/blobs.d.ts +14 -1
  50. package/dist/extensions/blobs.js +48 -5
  51. package/dist/extensions/host.d.ts +6 -1
  52. package/dist/extensions/host.js +20 -9
  53. package/dist/extensions/package.d.ts +4 -3
  54. package/dist/extensions/package.js +5 -1
  55. package/dist/extensions/permission-set.d.ts +19 -3
  56. package/dist/extensions/permission-set.js +19 -2
  57. package/dist/extensions/permission-view.d.ts +38 -7
  58. package/dist/extensions/permission-view.js +27 -8
  59. package/dist/extensions/sources.d.ts +6 -0
  60. package/dist/extensions/sources.js +2 -0
  61. package/dist/extensions/state.module.js +9 -1
  62. package/dist/http/api.module.js +10 -0
  63. package/dist/identity/settings.d.ts +41 -26
  64. package/dist/identity/settings.js +87 -4
  65. package/dist/infra/throttler/named-throttlers.d.ts +4 -0
  66. package/dist/infra/throttler/named-throttlers.js +14 -0
  67. package/dist/kernel.module.js +3 -0
  68. package/dist/orgs/settings.service.d.ts +2 -26
  69. package/dist/platform/exclusions.js +27 -1
  70. package/dist/platform/ops/admin.ops.js +17 -2
  71. package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
  72. package/dist/platform/ops/catalog-admin.ops.js +85 -2
  73. package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
  74. package/dist/platform/ops/org-plugins.ops.js +63 -4
  75. package/dist/platform/ops/org-settings.ops.js +2 -2
  76. package/dist/platform/ops/orgs.ops.js +1 -0
  77. package/dist/platform/ops/ui.ops.js +1 -1
  78. package/dist/platform/platform-ops.module.js +2 -0
  79. package/dist/platform/route-schemas.js +3 -0
  80. package/dist/roles/worker.module.js +4 -0
  81. package/dist/themes/render-css.d.ts +2 -1
  82. package/dist/themes/render-css.js +5 -2
  83. package/dist/themes/theme-delivery.service.d.ts +4 -2
  84. package/dist/themes/theme-delivery.service.js +8 -3
  85. package/dist/themes/theme-state.d.ts +28 -7
  86. package/dist/themes/theme-state.js +24 -6
  87. package/dist/themes/themes-public.controller.d.ts +7 -2
  88. package/dist/themes/themes-public.controller.js +11 -5
  89. package/dist/themes/themes.service.js +2 -2
  90. package/dist/tools/pipeline.d.ts +3 -2
  91. package/dist/ui/arrangement.d.ts +41 -50
  92. package/dist/ui/arrangement.js +88 -15
  93. package/dist/ui/registry.d.ts +13 -3
  94. package/dist/ui/registry.js +15 -4
  95. package/dist/ui/ui.schemas.d.ts +4 -0
  96. package/dist/ui/views.d.ts +36 -9
  97. package/dist/ui/views.js +58 -28
  98. package/package.json +3 -3
  99. package/src/migrations/0107_extension_client_builds.sql +50 -0
@@ -7,6 +7,7 @@ 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
+ import { EXTENSION_NAME_RE } from "@capybara-hq/plugin-sdk";
10
11
  import { Inject, Injectable } from "@nestjs/common";
11
12
  import { z } from "zod";
12
13
  import { ApiError } from "../errors.js";
@@ -74,6 +75,47 @@ export const UI_THEME_SETTING = "uiTheme";
74
75
  * {@link SettingsService.get}, both parsing with this schema.
75
76
  */
76
77
  export const UiThemeSetting = z.object({ id: z.string().min(1).max(200).nullable() });
78
+ /** The deployment setting key of the client code kill switch (client extensions spec §10.2). */
79
+ export const CLIENT_EXTENSIONS_SETTING = "clientExtensions";
80
+ /** The plugins listed in `clientExtensions.disabled`: at most this many. */
81
+ export const CLIENT_DISABLED_MAX = 200;
82
+ const ClientDisabledList = z
83
+ .array(z.string().regex(EXTENSION_NAME_RE, "must be an extension name"))
84
+ .max(CLIENT_DISABLED_MAX);
85
+ /**
86
+ * The client code kill switch (client extensions spec §10.2): `enabled: false` stops every plugin's
87
+ * client code (the shell reads list nothing, delivery is 404); `disabled` names the plugins whose
88
+ * client code alone is stopped while their server side keeps running. Every reader goes through
89
+ * {@link readClientExtensions} or {@link SettingsService.get}.
90
+ */
91
+ export const ClientExtensionsSetting = z.object({
92
+ enabled: z.boolean(),
93
+ disabled: ClientDisabledList,
94
+ });
95
+ /** The kill switch's default (and what a missing or invalid stored value reads as): everything on. */
96
+ export const clientExtensionsDefault = () => ({
97
+ enabled: true,
98
+ disabled: [],
99
+ });
100
+ /**
101
+ * Parses a stored `clientExtensions` value (the `deployment_settings` row's `value`, undefined when
102
+ * the row is missing): the default when missing; when invalid the default too, with a warning naming
103
+ * the key (a kill switch that fails open is worth a log line, as {@link SettingsService.get} logs
104
+ * every other invalid key). The one parser of the setting: the settings service and every access
105
+ * check read through it.
106
+ */
107
+ export function parseClientExtensions(value, log) {
108
+ if (value === undefined)
109
+ return clientExtensionsDefault();
110
+ const parsed = ClientExtensionsSetting.safeParse(value);
111
+ if (parsed.success)
112
+ return parsed.data;
113
+ log?.warn("invalid deployment setting; using default", {
114
+ key: CLIENT_EXTENSIONS_SETTING,
115
+ issues: parsed.error.issues.map((i) => i.message),
116
+ });
117
+ return clientExtensionsDefault();
118
+ }
77
119
  export const DEPLOYMENT_SETTINGS = z.object({
78
120
  orgCreation: z.enum(["anyone", "super_admin"]).default("anyone"),
79
121
  /** 按组织覆盖运行并发上限(orgId → 上限);未列出的组织用 `runtime.orgConcurrency`。 */
@@ -101,8 +143,13 @@ export const DEPLOYMENT_SETTINGS = z.object({
101
143
  orgMarketplaceBrowse: z.boolean().default(true),
102
144
  /** The platform default theme (themes spec §7); `{ id: null }`: the built-in theme. */
103
145
  uiTheme: UiThemeSetting.default({ id: null }),
104
- /** The platform default arrangement of UI slots (UI slots spec §5); orgs override per slot. */
146
+ /**
147
+ * The platform default arrangement of UI slots (UI slots spec §5): `{ hidden, order }` per list
148
+ * slot, `{ use }` per single slot (client extensions spec §6.3); orgs override per slot.
149
+ */
105
150
  uiArrangement: UiArrangementSchema.default({}),
151
+ /** The client code kill switch (client extensions spec §10.2). */
152
+ clientExtensions: ClientExtensionsSetting.default(clientExtensionsDefault),
106
153
  });
107
154
  const DEFAULTS = DEPLOYMENT_SETTINGS.parse({});
108
155
  /**
@@ -134,8 +181,22 @@ export const DEPLOYMENT_SETTINGS_PATCH = z
134
181
  * disabled (else 400 `theme.not_available`), or `{ id: null }` for the built-in theme.
135
182
  */
136
183
  uiTheme: UiThemeSetting.strict().optional(),
137
- /** Per slot, the entry to hide / order contributions with; null removes a slot's entry. */
184
+ /**
185
+ * Per slot, the entry to hide / order contributions with (`{ use }` on a single slot); null
186
+ * removes a slot's entry.
187
+ */
138
188
  uiArrangement: UiArrangementPatchSchema.optional(),
189
+ /**
190
+ * The client code kill switch: each given field replaces the stored one (`disabled` is the
191
+ * whole list, stored sorted without duplicates); at least one field.
192
+ */
193
+ clientExtensions: z
194
+ .object({ enabled: z.boolean().optional(), disabled: ClientDisabledList.optional() })
195
+ .strict()
196
+ .refine((o) => o.enabled !== undefined || o.disabled !== undefined, {
197
+ message: "give enabled, disabled or both",
198
+ })
199
+ .optional(),
139
200
  })
140
201
  .strict();
141
202
  /**
@@ -258,6 +319,19 @@ export async function readUiArrangement(db) {
258
319
  const parsed = UiArrangementSchema.safeParse(row?.value);
259
320
  return parsed.success ? parsed.data : {};
260
321
  }
322
+ /**
323
+ * The client code kill switch, read live from PG on every request (in the caller's transaction when
324
+ * there is one; never cached: every replica sees a change at once), through
325
+ * {@link parseClientExtensions} (missing or invalid -> everything on; invalid also warns on `log`).
326
+ */
327
+ export async function readClientExtensions(db, log) {
328
+ const row = await db
329
+ .selectFrom("deployment_settings")
330
+ .select("value")
331
+ .where("key", "=", CLIENT_EXTENSIONS_SETTING)
332
+ .executeTakeFirst();
333
+ return parseClientExtensions(row?.value, log);
334
+ }
261
335
  /** 按组织合并覆盖:值为 null 删除该组织,其余整项替换;未列出的组织不变。 */
262
336
  function mergeByOrg(current, patch) {
263
337
  const merged = { ...current };
@@ -349,12 +423,21 @@ let SettingsService = class SettingsService {
349
423
  }
350
424
  next.uiTheme = { id: patch.uiTheme.id };
351
425
  }
352
- const { runConcurrencyOverrides: runPatch, sandboxLimitOverrides: sandboxPatch, subagentLimitOverrides: subagentPatch, uiArrangement: arrangementPatch, } = patch;
426
+ const { runConcurrencyOverrides: runPatch, sandboxLimitOverrides: sandboxPatch, subagentLimitOverrides: subagentPatch, uiArrangement: arrangementPatch, clientExtensions: clientPatch, } = patch;
353
427
  if (runPatch !== undefined ||
354
428
  sandboxPatch !== undefined ||
355
429
  subagentPatch !== undefined ||
356
- arrangementPatch !== undefined) {
430
+ arrangementPatch !== undefined ||
431
+ clientPatch !== undefined) {
357
432
  const current = await this.get();
433
+ if (clientPatch !== undefined) {
434
+ next.clientExtensions = {
435
+ enabled: clientPatch.enabled ?? current.clientExtensions.enabled,
436
+ disabled: clientPatch.disabled === undefined
437
+ ? current.clientExtensions.disabled
438
+ : [...new Set(clientPatch.disabled)].sort(),
439
+ };
440
+ }
358
441
  if (arrangementPatch !== undefined) {
359
442
  next.uiArrangement = mergeArrangement(current.uiArrangement, arrangementPatch);
360
443
  }
@@ -29,6 +29,10 @@ export interface NamedThrottler {
29
29
  * | `POST /api/v1/invitations/:token/accept` | `invitation`, `invitation-ip` |
30
30
  * | `GET /api/v1/device/pending/:userCode`, `POST /api/v1/device/confirm`, `POST /api/v1/device/deny` | `device-code-account` |
31
31
  *
32
+ * `POST /api/v1/orgs/:orgId/ui/client/reports` and `POST /api/v1/admin/ui/client/reports` opt in to
33
+ * `client-reports` with `@Throttled` (20 per minute per account: a crash loop in a browser must
34
+ * not flood the table).
35
+ *
32
36
  * `POST /api/v1/device/authorize` and `POST /api/v1/device/token` opt in to `device-authorize-ip`
33
37
  * and `device-token-ip` with `@Throttled` (every request counts; neither checks a guessable
34
38
  * credential: the device code is 32 random bytes). `POST …/guides/:guideId/checks/:checkId/run`
@@ -30,6 +30,10 @@ const LOGIN_MSG = "Too many failed login attempts; try again later";
30
30
  * | `POST /api/v1/invitations/:token/accept` | `invitation`, `invitation-ip` |
31
31
  * | `GET /api/v1/device/pending/:userCode`, `POST /api/v1/device/confirm`, `POST /api/v1/device/deny` | `device-code-account` |
32
32
  *
33
+ * `POST /api/v1/orgs/:orgId/ui/client/reports` and `POST /api/v1/admin/ui/client/reports` opt in to
34
+ * `client-reports` with `@Throttled` (20 per minute per account: a crash loop in a browser must
35
+ * not flood the table).
36
+ *
33
37
  * `POST /api/v1/device/authorize` and `POST /api/v1/device/token` opt in to `device-authorize-ip`
34
38
  * and `device-token-ip` with `@Throttled` (every request counts; neither checks a guessable
35
39
  * credential: the device code is 32 random bytes). `POST …/guides/:guideId/checks/:checkId/run`
@@ -153,6 +157,16 @@ export const NAMED_THROTTLERS = [
153
157
  message: "Too many device code attempts; try again later",
154
158
  getTracker: (req) => sha256(principalAccountId(req)),
155
159
  },
160
+ // client code crash reports per signed-in account (counted before the handler): 20 / minute
161
+ {
162
+ name: "client-reports",
163
+ group: "client-reports",
164
+ onSuccess: "clear",
165
+ limit: 20,
166
+ ttl: 60_000,
167
+ message: "Too many client reports; try again later",
168
+ getTracker: (req) => sha256(principalAccountId(req)),
169
+ },
156
170
  // setup check runs per signed-in person (setup guides v2 §10.5): 30 a minute, every request
157
171
  // counts (@Throttled); a cached answer costs no third-party call but still counts, as the page
158
172
  // asks at most once per row and stale result. The edge of the route (with `retry-after`): the
@@ -9,6 +9,7 @@ import { Global, Module } from "@nestjs/common";
9
9
  import { APP_INTERCEPTOR } from "@nestjs/core";
10
10
  import { CatalogModule } from "./catalog/catalog.module.js";
11
11
  import { ChannelsModule } from "./channels/channels.module.js";
12
+ import { ClientModule } from "./client/client.module.js";
12
13
  import { TitlesModule } from "./conversations/titles.js";
13
14
  import { ExtensionsModule } from "./extensions/extensions.module.js";
14
15
  import { ApiModule } from "./http/api.module.js";
@@ -111,6 +112,8 @@ let KernelModule = KernelModule_1 = class KernelModule {
111
112
  TitlesModule,
112
113
  UsageReportModule,
113
114
  ChannelsModule,
115
+ // client extensions: the client builds stored with extension packages
116
+ ClientModule,
114
117
  // the @local sandbox provider, registered on every role (any replica may relay to a machine)
115
118
  LocalDevicesModule,
116
119
  // the Platform tool's operation registry and actor facts
@@ -87,19 +87,7 @@ export declare const OrgSettingsSchema: z.ZodObject<{
87
87
  locked: "locked";
88
88
  }>;
89
89
  }, z.core.$strict>>>;
90
- uiArrangement: z.ZodCatch<z.ZodOptional<z.ZodRecord<z.ZodEnum<{
91
- "agent.detail.section": "agent.detail.section";
92
- "conversation.panel": "conversation.panel";
93
- "conversation.properties.section": "conversation.properties.section";
94
- "conversation.tool-result.card": "conversation.tool-result.card";
95
- "file.preview": "file.preview";
96
- "org.page": "org.page";
97
- "org.settings.section": "org.settings.section";
98
- "platform.page": "platform.page";
99
- }> & z.core.$partial, z.ZodObject<{
100
- hidden: z.ZodArray<z.ZodString>;
101
- order: z.ZodArray<z.ZodString>;
102
- }, z.core.$strict>>>>;
90
+ uiArrangement: z.ZodCatch<z.ZodOptional<z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry>>, unknown>>>>;
103
91
  }, z.core.$strip>;
104
92
  export type OrgSettings = z.infer<typeof OrgSettingsSchema>;
105
93
  /** PATCH 体:只改给出的字段;未知字段 → 400。 */
@@ -137,19 +125,7 @@ export declare const OrgSettingsPatchSchema: z.ZodObject<{
137
125
  locked: "locked";
138
126
  }>;
139
127
  }, z.core.$strict>>>;
140
- uiArrangement: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
141
- "agent.detail.section": "agent.detail.section";
142
- "conversation.panel": "conversation.panel";
143
- "conversation.properties.section": "conversation.properties.section";
144
- "conversation.tool-result.card": "conversation.tool-result.card";
145
- "file.preview": "file.preview";
146
- "org.page": "org.page";
147
- "org.settings.section": "org.settings.section";
148
- "platform.page": "platform.page";
149
- }> & z.core.$partial, z.ZodNullable<z.ZodObject<{
150
- hidden: z.ZodArray<z.ZodString>;
151
- order: z.ZodArray<z.ZodString>;
152
- }, z.core.$strict>>>>;
128
+ uiArrangement: z.ZodOptional<z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry | null>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry | null>>, unknown>>>;
153
129
  }, z.core.$strict>;
154
130
  export type OrgSettingsPatch = z.infer<typeof OrgSettingsPatchSchema>;
155
131
  /** 部署的沙箱出站(`sandbox.limits.egress` + `allowHosts`)。 */
@@ -714,9 +714,35 @@ export const PLATFORM_EXCLUSIONS = [
714
714
  category: "transport",
715
715
  reason: "Server-sent event stream of UI invalidations",
716
716
  },
717
+ // client extensions (client extensions spec §9.2-§9.3)
718
+ {
719
+ route: "GET /api/v1/client/:plugin/:build/*",
720
+ category: "transport",
721
+ reason: "Serves a plugin's client code, stylesheets and assets to the browser that runs them",
722
+ },
723
+ {
724
+ route: "GET /api/v1/orgs/:orgId/ui/client",
725
+ category: "internal",
726
+ reason: "The web shell's list of client code to load (file URLs and integrity); nothing an agent acts on",
727
+ },
728
+ {
729
+ route: "GET /api/v1/admin/ui/client",
730
+ category: "internal",
731
+ reason: "The admin console's list of platform page client code to load; nothing an agent acts on",
732
+ },
733
+ {
734
+ route: "POST /api/v1/orgs/:orgId/ui/client/reports",
735
+ category: "transport",
736
+ reason: "Fire-and-forget crash report sent by the browser that ran the client code",
737
+ },
738
+ {
739
+ route: "POST /api/v1/admin/ui/client/reports",
740
+ category: "transport",
741
+ reason: "Fire-and-forget crash report sent by the admin console that ran the client code",
742
+ },
717
743
  // themes (themes spec §8-§9)
718
744
  {
719
- route: "GET /api/v1/themes/:id/:version/theme.css",
745
+ route: "GET /api/v1/themes/:id/:version/:revision/theme.css",
720
746
  category: "transport",
721
747
  reason: "Serves a theme version's stylesheet to the browser; an agent reads themes.list",
722
748
  },
@@ -41,6 +41,7 @@ const SettingsInput = DEPLOYMENT_SETTINGS_PATCH.pick({
41
41
  orgMarketplaceBrowse: true,
42
42
  uiTheme: true,
43
43
  uiArrangement: true,
44
+ clientExtensions: true,
44
45
  }).strict();
45
46
  const ProviderId = ModelProviderParams.shape.provider.describe("The model provider (models.platform-list), e.g. the plugin name of its extension.");
46
47
  /** `provider/model` of a purpose target, or null. */
@@ -122,7 +123,7 @@ let AdminOperations = class AdminOperations {
122
123
  }),
123
124
  defineOperation({
124
125
  id: "admin.update-settings",
125
- description: "Change deployment settings (super admin); give only the keys to change: orgCreation (anyone or super_admin), priceCurrency (ISO 4217 code), customConnectors / customSkills (allow or deny: whether orgs may create their own instead of installing from the catalog), orgMarketplaceBrowse (true or false), uiTheme ({ id }: the platform default theme, a platform theme whose scope is all, or null for the built-in theme; it is what every org falls back to and what the sign-in pages show), uiArrangement ({ <slot>: { hidden, order } }: the platform default arrangement of UI slots, with contribution keys <extension>/<contribution>; hidden ones are not listed but still open by URL; null for a slot removes its entry). Per-org limits are orgs.set-limits.",
126
+ description: "Change deployment settings (super admin); give only the keys to change: orgCreation (anyone or super_admin), priceCurrency (ISO 4217 code), customConnectors / customSkills (allow or deny: whether orgs may create their own instead of installing from the catalog), orgMarketplaceBrowse (true or false), uiTheme ({ id }: the platform default theme, a platform theme whose scope is all, or null for the built-in theme; it is what every org falls back to and what the sign-in pages show), uiArrangement ({ <slot>: { hidden, order } } for a list slot, { <slot>: { use } } for a single slot: the platform default arrangement of UI slots, with contribution keys <extension>/<contribution>; hidden ones are not listed but still open by URL; use names the contribution that replaces the built-in, null for the built-in; null for a slot removes its entry), clientExtensions ({ enabled?, disabled? }: the kill switch of plugin client code; enabled false stops every plugin's interface code in every browser, disabled is the whole list of plugins whose interface code alone is stopped while their tools keep working; read the current list from admin.settings first). Per-org limits are orgs.set-limits.",
126
127
  input: SettingsInput.refine((b) => Object.keys(b).length > 0, {
127
128
  message: "at least one setting is required",
128
129
  }),
@@ -149,7 +150,7 @@ let AdminOperations = class AdminOperations {
149
150
  : { ok: false, code: "validation_failed", message: r.error.issues[0]?.message ?? "" };
150
151
  },
151
152
  summary: async (i) => {
152
- const { uiTheme, uiArrangement, ...rest } = i;
153
+ const { uiTheme, uiArrangement, clientExtensions, ...rest } = i;
153
154
  const parts = Object.entries(rest).map(([k, v]) => `${k}=${String(v)}`);
154
155
  const zh = [...parts];
155
156
  const en = [...parts];
@@ -164,6 +165,20 @@ let AdminOperations = class AdminOperations {
164
165
  zh.push(`uiArrangement(${slots})`);
165
166
  en.push(`uiArrangement (${slots})`);
166
167
  }
168
+ if (clientExtensions !== undefined) {
169
+ const { enabled, disabled } = clientExtensions;
170
+ if (enabled !== undefined) {
171
+ zh.push(enabled ? "开启插件界面代码" : "停用全部插件界面代码");
172
+ en.push(enabled ? "plugin interface code on" : "all plugin interface code stopped");
173
+ }
174
+ if (disabled !== undefined) {
175
+ const list = disabled.length === 0 ? null : disabled.join(", ");
176
+ zh.push(list === null ? "不再单独停用任何插件的界面代码" : `停用界面代码的插件:${list}`);
177
+ en.push(list === null
178
+ ? "no plugin's interface code stopped on its own"
179
+ : `interface code stopped for: ${list}`);
180
+ }
181
+ }
167
182
  return {
168
183
  zh: `修改平台部署设置:${zh.join(",")}`,
169
184
  en: `Change the deployment settings: ${en.join(", ")}`,
@@ -1,8 +1,10 @@
1
1
  import { type OnModuleInit } from "@nestjs/common";
2
+ import { type AppearanceResult, AppearanceService } from "../../catalog/appearance.service.js";
2
3
  import { CatalogService } from "../../catalog/catalog.service.js";
3
4
  import { CatalogScopeService } from "../../catalog/catalog-scope.service.js";
4
5
  import { PluginImport, type PluginReview } from "../../catalog/plugin-import.js";
5
6
  import { KernelDb } from "../../infra/database/kernel-db.js";
7
+ import { ThemesService } from "../../themes/themes.service.js";
6
8
  import { PlatformOperations } from "../registry.js";
7
9
  /** `plugins.set-shown` on a plugin an org owns. */
8
10
  export declare const ORG_OWNED = "An org owns this plugin and manages who sees it; the super admin changes that in the admin console, not here.";
@@ -41,6 +43,20 @@ export declare const reviewBrief: (r: PluginReview) => {
41
43
  resolved: string | null;
42
44
  }[];
43
45
  };
46
+ /**
47
+ * The card of an "apply this plugin's appearance" operation (client extensions spec §6.5), from the
48
+ * route's dry run: the theme it sets (its name), the slots it replaces and how many were skipped.
49
+ */
50
+ export declare function appearanceSummary(level: "platform" | "org", plugin: {
51
+ zh: string;
52
+ en: string;
53
+ }, r: AppearanceResult, theme: {
54
+ zh: string;
55
+ en: string;
56
+ } | null): {
57
+ zh: string;
58
+ en: string;
59
+ };
44
60
  /**
45
61
  * The super admin's platform catalog operations of the `Platform` tool (spec §14.2, §14.5 item
46
62
  * 1): prepare an import from npm or git and read its review (a person confirms it on the web: the
@@ -55,7 +71,9 @@ export declare class CatalogAdminOperations implements OnModuleInit {
55
71
  private readonly scope;
56
72
  private readonly pluginImport;
57
73
  private readonly kdb;
58
- constructor(registry: PlatformOperations, catalog: CatalogService, scope: CatalogScopeService, pluginImport: PluginImport, kdb: KernelDb);
74
+ private readonly appearance;
75
+ private readonly themes;
76
+ constructor(registry: PlatformOperations, catalog: CatalogService, scope: CatalogScopeService, pluginImport: PluginImport, kdb: KernelDb, appearance: AppearanceService, themes: ThemesService);
59
77
  /** The precheck refusal of another org's private plugin (the phase-1 rule), else null. */
60
78
  private otherOrgs;
61
79
  /** Whether an org owns the item (its owner org manages it, not the super admin's agent). */
@@ -11,11 +11,13 @@ import { ToolError } from "@capybara-hq/plugin-sdk";
11
11
  import { Inject, Injectable } from "@nestjs/common";
12
12
  import { z } from "zod";
13
13
  import { CatalogDisabledBody, CatalogGitImportBody, CatalogIdPathParams, CatalogNpmImportBody, CatalogScope, CatalogScopeBody, CatalogScopeOrgs, CatalogShownBody, CatalogStagingPathParams, } from "../../catalog/admin-catalog.schemas.js";
14
+ import { AppearanceService } from "../../catalog/appearance.service.js";
14
15
  import { CatalogService } from "../../catalog/catalog.service.js";
15
16
  import { CatalogScopeService } from "../../catalog/catalog-scope.service.js";
16
17
  import { PluginImport } from "../../catalog/plugin-import.js";
17
18
  import { ApiError } from "../../errors.js";
18
19
  import { KernelDb } from "../../infra/database/kernel-db.js";
20
+ import { ThemesService } from "../../themes/themes.service.js";
19
21
  import { defineOperation } from "../operation.js";
20
22
  import { PlatformOperations } from "../registry.js";
21
23
  import { precheckBy, viaAgent } from "./errors.js";
@@ -64,6 +66,37 @@ export const reviewBrief = (r) => ({
64
66
  const conflictText = (cs) => cs
65
67
  .map((c) => `skill "${c.name}" is already used in org ${c.orgName} (${c.plugins.length > 0 ? `${c.plugins.length} plugin(s)` : "its own skill"})`)
66
68
  .join("; ");
69
+ /**
70
+ * The card of an "apply this plugin's appearance" operation (client extensions spec §6.5), from the
71
+ * route's dry run: the theme it sets (its name), the slots it replaces and how many were skipped.
72
+ */
73
+ export function appearanceSummary(level, plugin, r, theme) {
74
+ const zh = [];
75
+ const en = [];
76
+ if (r.theme !== null && theme !== null) {
77
+ zh.push(`${level === "platform" ? "平台" : "本组织"}默认主题改为「${theme.zh}」`);
78
+ en.push(`${level === "platform" ? "platform" : "org"} default theme becomes "${theme.en}"`);
79
+ }
80
+ if (r.replacements.length > 0) {
81
+ const slots = r.replacements.map((x) => x.slot).join(", ");
82
+ zh.push(`替换 ${slots}`);
83
+ en.push(`replaces ${slots}`);
84
+ }
85
+ if (r.skipped.length > 0) {
86
+ zh.push(`跳过 ${r.skipped.length} 项`);
87
+ en.push(`${r.skipped.length} skipped`);
88
+ }
89
+ if (zh.length === 0) {
90
+ return {
91
+ zh: `插件「${plugin.zh}」的外观已经应用,没有改动`,
92
+ en: `The appearance of plugin "${plugin.en}" is applied already; nothing changes`,
93
+ };
94
+ }
95
+ return {
96
+ zh: `应用插件「${plugin.zh}」的外观:${zh.join(";")}`,
97
+ en: `Apply the appearance of plugin "${plugin.en}": ${en.join("; ")}`,
98
+ };
99
+ }
67
100
  /** A scope preview as the admin console shows it (orgs named); conflicting plugins counted. */
68
101
  const previewBrief = (p) => ({
69
102
  affected: p.affected,
@@ -93,12 +126,16 @@ let CatalogAdminOperations = class CatalogAdminOperations {
93
126
  scope;
94
127
  pluginImport;
95
128
  kdb;
96
- constructor(registry, catalog, scope, pluginImport, kdb) {
129
+ appearance;
130
+ themes;
131
+ constructor(registry, catalog, scope, pluginImport, kdb, appearance, themes) {
97
132
  this.registry = registry;
98
133
  this.catalog = catalog;
99
134
  this.scope = scope;
100
135
  this.pluginImport = pluginImport;
101
136
  this.kdb = kdb;
137
+ this.appearance = appearance;
138
+ this.themes = themes;
102
139
  }
103
140
  /** The precheck refusal of another org's private plugin (the phase-1 rule), else null. */
104
141
  async otherOrgs(itemId, orgId) {
@@ -585,6 +622,50 @@ let CatalogAdminOperations = class CatalogAdminOperations {
585
622
  return { deleted: input.itemId };
586
623
  },
587
624
  }),
625
+ defineOperation({
626
+ id: "plugins.platform-apply-appearance",
627
+ description: "Apply a platform plugin's suggested appearance (super admin; the admin console's 应用此插件的外观): in one step, its theme becomes the platform default theme and each component it suggests replaces the built-in block of its slot (shell.brand, conversation.empty, conversation.composer.frame) in the platform arrangement; orgs that set their own keep theirs. The card shows exactly what changes; a theme that cannot be the platform default (scope not all, or off in an org) refuses the whole call, a component that is not available is skipped. Asked every time, never remembered.",
628
+ input: z.object({ itemId: ItemId }).strict(),
629
+ example: { itemId: "3f1c2a9e-5b7d-4e21-9c0a-1d2e3f4a5b6c" },
630
+ authz: { superAdmin: true },
631
+ mirrors: ["POST /api/v1/admin/catalog/items/:id/appearance"],
632
+ renames: { itemId: "id" },
633
+ // the route's dry run is this operation's card and precheck, never a confirmed call
634
+ omits: ["dryRun"],
635
+ confirm: "platform",
636
+ rememberable: false,
637
+ errors: [
638
+ {
639
+ code: "catalog.item_not_found",
640
+ next: "Check the plugin id with plugins.platform-list; only a plugin whose plugin.json has suggestedAppearance can be applied.",
641
+ },
642
+ {
643
+ code: "theme.not_available",
644
+ next: "The plugin's theme cannot be the platform default: give it scope all (plugins.component-scope-preview, then plugins.set-component-scope) and make sure no org switched it off, then apply again.",
645
+ },
646
+ ],
647
+ precheck: async (i, ctx) => {
648
+ const other = await this.otherOrgs(i.itemId, ctx.orgId);
649
+ if (other !== null)
650
+ return other;
651
+ return precheckBy(() => this.appearance.apply({ platform: true }, i.itemId, { accountId: ctx.actorAccountId }, true));
652
+ },
653
+ summary: async (i, ctx) => {
654
+ const [n, r] = await Promise.all([
655
+ this.names(i.itemId),
656
+ this.appearance.apply({ platform: true }, i.itemId, { accountId: ctx.actorAccountId }, true),
657
+ ]);
658
+ const theme = r.theme && localized(await this.themes.nameOf(r.theme.to), r.theme.to);
659
+ return appearanceSummary("platform", n, r, theme);
660
+ },
661
+ run: async (input, ctx) => {
662
+ await assertNotOtherOrgs(this.kdb.db, input.itemId, ctx.orgId);
663
+ return this.appearance.apply({ platform: true }, input.itemId, {
664
+ accountId: ctx.actorAccountId,
665
+ auditDetail: viaAgent("plugins.platform-apply-appearance", ctx),
666
+ }, false);
667
+ },
668
+ }),
588
669
  ]);
589
670
  }
590
671
  };
@@ -594,6 +675,8 @@ CatalogAdminOperations = __decorate([
594
675
  __param(1, Inject(CatalogService)),
595
676
  __param(2, Inject(CatalogScopeService)),
596
677
  __param(3, Inject(PluginImport)),
597
- __param(4, Inject(KernelDb))
678
+ __param(4, Inject(KernelDb)),
679
+ __param(5, Inject(AppearanceService)),
680
+ __param(6, Inject(ThemesService))
598
681
  ], CatalogAdminOperations);
599
682
  export { CatalogAdminOperations };
@@ -1,10 +1,12 @@
1
1
  import { type OnModuleInit } from "@nestjs/common";
2
+ import { AppearanceService } from "../../catalog/appearance.service.js";
2
3
  import { CatalogScopeService } from "../../catalog/catalog-scope.service.js";
3
4
  import { OrgCatalogService } from "../../catalog/org-catalog.service.js";
4
5
  import { OrgPluginUploads } from "../../catalog/org-plugin-uploads.js";
5
6
  import { OwnedItems } from "../../catalog/owned-items.js";
6
7
  import { PluginInstaller } from "../../catalog/plugin-install.js";
7
8
  import { KernelDb } from "../../infra/database/kernel-db.js";
9
+ import { ThemesService } from "../../themes/themes.service.js";
8
10
  import { PlatformOperations } from "../registry.js";
9
11
  /**
10
12
  * The org's own plugin operations of the `Platform` tool (spec §14.2): the org's plugin installs
@@ -20,7 +22,11 @@ export declare class OrgPluginsOperations implements OnModuleInit {
20
22
  private readonly owned;
21
23
  private readonly scope;
22
24
  private readonly kdb;
23
- constructor(registry: PlatformOperations, installer: PluginInstaller, orgCatalog: OrgCatalogService, uploads: OrgPluginUploads, owned: OwnedItems, scope: CatalogScopeService, kdb: KernelDb);
25
+ private readonly appearance;
26
+ private readonly themes;
27
+ constructor(registry: PlatformOperations, installer: PluginInstaller, orgCatalog: OrgCatalogService, uploads: OrgPluginUploads, owned: OwnedItems, scope: CatalogScopeService, kdb: KernelDb, appearance: AppearanceService, themes: ThemesService);
28
+ /** The display name of the live plugin of `extension`, or the name when it cannot be read. */
29
+ private pluginNames;
24
30
  /** The org's own live plugin `id`, or null (another org's, a platform one, deleted, unknown). */
25
31
  private ownedItem;
26
32
  /** The precheck refusal of a plugin the org does not own. */
@@ -9,20 +9,23 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
9
9
  };
10
10
  import { Inject, Injectable } from "@nestjs/common";
11
11
  import { z } from "zod";
12
+ import { AppearanceService } from "../../catalog/appearance.service.js";
12
13
  import { CatalogScopeService } from "../../catalog/catalog-scope.service.js";
13
14
  import { PluginInstallPathParams } from "../../catalog/org-catalog.schemas.js";
14
15
  import { OrgCatalogService } from "../../catalog/org-catalog.service.js";
16
+ import { OrgExtensionParams } from "../../catalog/org-extension.schemas.js";
15
17
  import { OrgPluginUploads } from "../../catalog/org-plugin-uploads.js";
16
18
  import { OrgOwnedDeleteBody, OrgOwnedPluginParams, OrgOwnedShareableBody, OrgUploadStagingParams, } from "../../catalog/org-plugin-uploads.schemas.js";
17
19
  import { OwnedItems } from "../../catalog/owned-items.js";
18
20
  import { PluginInstaller } from "../../catalog/plugin-install.js";
19
21
  import { KernelDb } from "../../infra/database/kernel-db.js";
22
+ import { ThemesService } from "../../themes/themes.service.js";
20
23
  import { defineOperation } from "../operation.js";
21
24
  import { PlatformOperations } from "../registry.js";
22
- import { reviewBrief } from "./catalog-admin.ops.js";
25
+ import { appearanceSummary, reviewBrief } from "./catalog-admin.ops.js";
23
26
  import { precheckBy, viaAgent } from "./errors.js";
24
27
  import { Cursor, pageOf, startOf } from "./paging.js";
25
- import { named } from "./summary.js";
28
+ import { localized, named, orElse } from "./summary.js";
26
29
  const InstallId = PluginInstallPathParams.shape.installId.max(100);
27
30
  /** An org-owned plugin's catalog item id (`:id` of the owned-plugins routes). */
28
31
  const OwnedId = OrgOwnedPluginParams.shape.id;
@@ -51,7 +54,9 @@ let OrgPluginsOperations = class OrgPluginsOperations {
51
54
  owned;
52
55
  scope;
53
56
  kdb;
54
- constructor(registry, installer, orgCatalog, uploads, owned, scope, kdb) {
57
+ appearance;
58
+ themes;
59
+ constructor(registry, installer, orgCatalog, uploads, owned, scope, kdb, appearance, themes) {
55
60
  this.registry = registry;
56
61
  this.installer = installer;
57
62
  this.orgCatalog = orgCatalog;
@@ -59,6 +64,20 @@ let OrgPluginsOperations = class OrgPluginsOperations {
59
64
  this.owned = owned;
60
65
  this.scope = scope;
61
66
  this.kdb = kdb;
67
+ this.appearance = appearance;
68
+ this.themes = themes;
69
+ }
70
+ /** The display name of the live plugin of `extension`, or the name when it cannot be read. */
71
+ pluginNames(extension) {
72
+ return orElse(async () => {
73
+ const row = await this.kdb.db
74
+ .selectFrom("catalog_items")
75
+ .select("display_name")
76
+ .where("name", "=", extension)
77
+ .where("deleted_at", "is", null)
78
+ .executeTakeFirst();
79
+ return localized(row?.display_name, extension);
80
+ }, { zh: named(extension), en: named(extension) });
62
81
  }
63
82
  /** The org's own live plugin `id`, or null (another org's, a platform one, deleted, unknown). */
64
83
  async ownedItem(orgId, id) {
@@ -313,6 +332,44 @@ let OrgPluginsOperations = class OrgPluginsOperations {
313
332
  return { withdrawn: input.itemId };
314
333
  },
315
334
  }),
335
+ defineOperation({
336
+ id: "plugins.apply-appearance",
337
+ description: "Apply a plugin's suggested appearance in this org (org admins; the 扩展 page's 应用此插件的外观), for a plugin whose extension is on here: in one step, its theme becomes the org's default theme (whether members may pick their own stays as it is) and each component it suggests replaces the built-in block of its slot (shell.brand, conversation.empty, conversation.composer.frame) in the org's arrangement. The card shows exactly what changes; a theme that is not on in the org refuses the whole call, a component that is not available is skipped. Asked every time, never remembered.",
338
+ input: z.object({ extension: OrgExtensionParams.shape.extension }).strict(),
339
+ example: { extension: "theme-cyberpunk" },
340
+ authz: { orgAction: "org.update" },
341
+ mirrors: ["POST /api/v1/orgs/:orgId/extensions/:extension/appearance"],
342
+ // the route's dry run is this operation's card and precheck, never a confirmed call
343
+ omits: ["dryRun"],
344
+ confirm: "org",
345
+ rememberable: false,
346
+ errors: [
347
+ {
348
+ code: "extension.not_found",
349
+ next: "Check the extension with extensions.list: it must be on in this org and its plugin must suggest an appearance.",
350
+ },
351
+ {
352
+ code: "theme.not_available",
353
+ next: {
354
+ orgAdmin: "The plugin's theme is not on in this org: switch it on in the org's appearance settings on the web, or ask the super admin to offer it to this org, then apply again.",
355
+ member: "Ask an org admin to switch the plugin's theme on in this org first.",
356
+ },
357
+ },
358
+ ],
359
+ precheck: (i, ctx) => precheckBy(() => this.appearance.apply({ orgId: ctx.orgId }, i.extension, { accountId: ctx.actorAccountId }, true)),
360
+ summary: async (i, ctx) => {
361
+ const [n, r] = await Promise.all([
362
+ this.pluginNames(i.extension),
363
+ this.appearance.apply({ orgId: ctx.orgId }, i.extension, { accountId: ctx.actorAccountId }, true),
364
+ ]);
365
+ const theme = r.theme && localized(await this.themes.nameIn(ctx.orgId, r.theme.to), r.theme.to);
366
+ return appearanceSummary("org", n, r, theme);
367
+ },
368
+ run: async (input, ctx) => this.appearance.apply({ orgId: ctx.orgId }, input.extension, {
369
+ accountId: ctx.actorAccountId,
370
+ auditDetail: viaAgent("plugins.apply-appearance", ctx),
371
+ }, false),
372
+ }),
316
373
  ]);
317
374
  }
318
375
  };
@@ -324,6 +381,8 @@ OrgPluginsOperations = __decorate([
324
381
  __param(3, Inject(OrgPluginUploads)),
325
382
  __param(4, Inject(OwnedItems)),
326
383
  __param(5, Inject(CatalogScopeService)),
327
- __param(6, Inject(KernelDb))
384
+ __param(6, Inject(KernelDb)),
385
+ __param(7, Inject(AppearanceService)),
386
+ __param(8, Inject(ThemesService))
328
387
  ], OrgPluginsOperations);
329
388
  export { OrgPluginsOperations };