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

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 (107) 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/channels/channels.schemas.d.ts +4 -0
  23. package/dist/channels/channels.schemas.js +2 -0
  24. package/dist/channels/connections.controller.d.ts +5 -3
  25. package/dist/channels/connections.controller.js +6 -4
  26. package/dist/channels/connections.service.d.ts +26 -0
  27. package/dist/channels/connections.service.js +79 -6
  28. package/dist/client/client-access.service.d.ts +101 -0
  29. package/dist/client/client-access.service.js +294 -0
  30. package/dist/client/client-builds.service.d.ts +109 -0
  31. package/dist/client/client-builds.service.js +229 -0
  32. package/dist/client/client-delivery.controller.d.ts +25 -0
  33. package/dist/client/client-delivery.controller.js +66 -0
  34. package/dist/client/client-files.d.ts +48 -0
  35. package/dist/client/client-files.js +136 -0
  36. package/dist/client/client-reports.cron.d.ts +13 -0
  37. package/dist/client/client-reports.cron.js +42 -0
  38. package/dist/client/client-reports.service.d.ts +30 -0
  39. package/dist/client/client-reports.service.js +102 -0
  40. package/dist/client/client.controller.d.ts +45 -0
  41. package/dist/client/client.controller.js +121 -0
  42. package/dist/client/client.module.d.ts +7 -0
  43. package/dist/client/client.module.js +26 -0
  44. package/dist/client/client.schemas.d.ts +55 -0
  45. package/dist/client/client.schemas.js +27 -0
  46. package/dist/ctx/create-ctx.d.ts +4 -3
  47. package/dist/ctx/create-ctx.js +4 -3
  48. package/dist/db/kernel-schema.d.ts +47 -0
  49. package/dist/errors.d.ts +29 -0
  50. package/dist/errors.js +29 -0
  51. package/dist/extensions/admin-ops.d.ts +4 -1
  52. package/dist/extensions/admin-ops.js +22 -1
  53. package/dist/extensions/admin-view.d.ts +8 -0
  54. package/dist/extensions/admin-view.js +78 -0
  55. package/dist/extensions/blobs.d.ts +14 -1
  56. package/dist/extensions/blobs.js +48 -5
  57. package/dist/extensions/guide-places.d.ts +3 -1
  58. package/dist/extensions/guide-places.js +1 -1
  59. package/dist/extensions/host.d.ts +6 -1
  60. package/dist/extensions/host.js +20 -9
  61. package/dist/extensions/package.d.ts +4 -3
  62. package/dist/extensions/package.js +5 -1
  63. package/dist/extensions/permission-set.d.ts +19 -3
  64. package/dist/extensions/permission-set.js +19 -2
  65. package/dist/extensions/permission-view.d.ts +38 -7
  66. package/dist/extensions/permission-view.js +23 -7
  67. package/dist/extensions/sources.d.ts +6 -0
  68. package/dist/extensions/sources.js +2 -0
  69. package/dist/extensions/state.module.js +9 -1
  70. package/dist/http/api.module.js +10 -0
  71. package/dist/identity/settings.d.ts +41 -26
  72. package/dist/identity/settings.js +87 -4
  73. package/dist/infra/throttler/named-throttlers.d.ts +4 -0
  74. package/dist/infra/throttler/named-throttlers.js +14 -0
  75. package/dist/kernel.module.js +3 -0
  76. package/dist/orgs/settings.service.d.ts +2 -26
  77. package/dist/platform/exclusions.js +27 -1
  78. package/dist/platform/ops/admin.ops.js +17 -2
  79. package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
  80. package/dist/platform/ops/catalog-admin.ops.js +85 -2
  81. package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
  82. package/dist/platform/ops/org-plugins.ops.js +63 -4
  83. package/dist/platform/ops/org-settings.ops.js +2 -2
  84. package/dist/platform/ops/orgs.ops.js +1 -0
  85. package/dist/platform/ops/ui.ops.js +1 -1
  86. package/dist/platform/platform-ops.module.js +2 -0
  87. package/dist/platform/route-schemas.js +3 -0
  88. package/dist/roles/worker.module.js +4 -0
  89. package/dist/themes/render-css.d.ts +2 -1
  90. package/dist/themes/render-css.js +5 -2
  91. package/dist/themes/theme-delivery.service.d.ts +4 -2
  92. package/dist/themes/theme-delivery.service.js +8 -3
  93. package/dist/themes/theme-state.d.ts +28 -7
  94. package/dist/themes/theme-state.js +24 -6
  95. package/dist/themes/themes-public.controller.d.ts +7 -2
  96. package/dist/themes/themes-public.controller.js +11 -5
  97. package/dist/themes/themes.service.js +2 -2
  98. package/dist/tools/pipeline.d.ts +3 -2
  99. package/dist/ui/arrangement.d.ts +41 -50
  100. package/dist/ui/arrangement.js +88 -15
  101. package/dist/ui/registry.d.ts +13 -3
  102. package/dist/ui/registry.js +15 -4
  103. package/dist/ui/ui.schemas.d.ts +4 -0
  104. package/dist/ui/views.d.ts +36 -9
  105. package/dist/ui/views.js +58 -28
  106. package/package.json +3 -3
  107. package/src/migrations/0107_extension_client_builds.sql +50 -0
package/dist/ui/views.js CHANGED
@@ -20,7 +20,7 @@ import { readUiArrangement } from "../identity/settings.js";
20
20
  import { KernelDb } from "../infra/database/kernel-db.js";
21
21
  import { KERNEL_LOGGER } from "../infra/tokens.js";
22
22
  import { readOrgSettings } from "../orgs/settings.service.js";
23
- import { arrange, arrangeBySlot, effectiveEntry, ORG_ARRANGEABLE, orderItems, } from "./arrangement.js";
23
+ import { arrange, arrangeBySlot, effectiveEntry, isSingle, keyOf, ORG_ARRANGEABLE, orderItems, resolveSingleSlot, storedUse, } from "./arrangement.js";
24
24
  import { callExtension } from "./call-extension.js";
25
25
  import { canUseSlot, UUID_RE } from "./registry.js";
26
26
  export { callExtension } from "./call-extension.js";
@@ -73,12 +73,14 @@ export function dropForeignOpens(view, panels) {
73
73
  return { ...rest, ...(open !== undefined && ok(open) && { open }), blocks };
74
74
  }
75
75
  /**
76
- * Lists contributions for a listing route: no alias, a view's render registered; base order
77
- * (spec §5): extension name, then manifest order (stable).
76
+ * Lists contributions for a listing route: no alias, a view's render registered (an embed or a
77
+ * component is listed without one: a component's server view is optional); base order (spec §5):
78
+ * extension name, then manifest order (stable).
78
79
  */
79
80
  function listed(entries) {
80
81
  return entries
81
- .filter((c) => c.contribution.aliasOf === undefined && (c.contribution.kind === "embed" || c.render))
82
+ .filter((c) => c.contribution.aliasOf === undefined &&
83
+ (c.contribution.kind === "embed" || c.contribution.kind === "component" || c.render))
82
84
  .map((c, i) => ({ c, i }))
83
85
  .sort((x, y) => (x.c.plugin === y.c.plugin ? x.i - y.i : x.c.plugin < y.c.plugin ? -1 : 1))
84
86
  .map(({ c: { plugin, contribution: c, slot: s, after } }) => ({
@@ -92,6 +94,8 @@ function listed(entries) {
92
94
  icon: c.icon ?? "puzzle",
93
95
  }));
94
96
  }
97
+ /** The candidates of a single slot in an arrangement listing. */
98
+ const candidatesOf = (items) => items.map((i) => ({ key: keyOf(i), title: i.title }));
95
99
  /** The items in effective order, each marked hidden or not (hidden ones stay in place). */
96
100
  function withHidden(items, entry) {
97
101
  const hidden = new Set(entry.hidden);
@@ -107,12 +111,15 @@ const TOAST_MAX = 500;
107
111
  const MAX_LOGGED_PROBLEMS = 10;
108
112
  const notFound = () => new ApiError("ui.not_found", "UI contribution not found");
109
113
  const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
110
- /** 校验动作结果,返回问题列表(空 = 合法)。 */
111
- function actionResultProblems(r) {
114
+ /**
115
+ * 校验动作结果,返回问题列表(空 = 合法)。A new `view` may carry `data` only for a component
116
+ * (`component`).
117
+ */
118
+ function actionResultProblems(r, component) {
112
119
  if (!isObj(r))
113
120
  return ["action result must be an object"];
114
121
  if (r.view !== undefined)
115
- return validateUiView(r.view).map((p) => `view: ${p}`);
122
+ return validateUiView(r.view, { component }).map((p) => `view: ${p}`);
116
123
  if (r.toast === undefined)
117
124
  return [];
118
125
  if (!isObj(r.toast))
@@ -133,6 +140,8 @@ function actionResultProblems(r) {
133
140
  * org; org: the role alone); refused is 404 `ui.not_found`, the same as not existing;
134
141
  * - a view takes only the context id of its scope (`conversationId` / `agentId`): a missing one or
135
142
  * one of another scope is 400 `validation_failed`;
143
+ * - a component's server view (`ctx.ui.view(<component id>)`) is served like a view of its slot;
144
+ * only its views carry `data` (a plain view's `data` is 502 `ui.invalid_view`);
136
145
  * - 调用带 5 秒时限与中止信号:render 抛错 502 `ui.render_failed`、action 抛错 502 `ui.action_failed`、
137
146
  * 超时 504 `ui.timeout`、返回非法视图 502
138
147
  * `ui.invalid_view`,都记告警日志、不写对话日志(这些不是模型可见的内容)。
@@ -160,9 +169,15 @@ let UiViews = class UiViews {
160
169
  }
161
170
  /**
162
171
  * The contributions the role may use in the org, of one slot or of every org-reachable slot:
163
- * the extension enabled and loaded, a view's render registered, the slot's `minRole`
164
- * ({@link canUseSlot}, so never `platform.page`); no alias, no `file.preview` contribution (the
165
- * file previewers list those). Base order (spec §5): extension name, then manifest order.
172
+ * the extension enabled and loaded, a view's render registered (a component is listed with or
173
+ * without a server view), the slot's `minRole` ({@link canUseSlot}, so never `platform.page`);
174
+ * no alias, no `file.preview` contribution (the file previewers list those). Base order (spec
175
+ * §5): extension name, then manifest order. A single slot lists at most its winner
176
+ * ({@link resolveSingleSlot} over what this role may use: none until one is chosen, and the web
177
+ * renders the built-in); display only, the routes that serve a contribution never read it. The
178
+ * winner is listed whether or not its client code may load (kill switch, per-plugin disable,
179
+ * client API support): the web renders the built-in when the org's client set has no such
180
+ * component, and any other consumer must do the same.
166
181
  */
167
182
  async contributions(orgId, role, slot) {
168
183
  const items = await this.orgBase(orgId, role, slot);
@@ -172,7 +187,16 @@ let UiViews = class UiViews {
172
187
  this.d.orgArrangement(orgId),
173
188
  this.d.platformArrangement(),
174
189
  ]);
175
- return arrangeBySlot(items, (s) => effectiveEntry(org, platform, s).entry);
190
+ const winners = new Map();
191
+ const winnerOf = (s) => {
192
+ if (!winners.has(s)) {
193
+ const available = new Set(items.filter((i) => i.slot === s).map(keyOf));
194
+ winners.set(s, resolveSingleSlot(s, org?.[s], platform[s], available));
195
+ }
196
+ return winners.get(s) ?? null;
197
+ };
198
+ const shown = items.filter((i) => !isSingle(i.slot) || keyOf(i) === winnerOf(i.slot));
199
+ return arrangeBySlot(shown, (s) => effectiveEntry(org, platform, s).entry);
176
200
  }
177
201
  /** {@link contributions} before the arrangement: what the role may use, in base order. */
178
202
  async orgBase(orgId, role, slot) {
@@ -187,7 +211,8 @@ let UiViews = class UiViews {
187
211
  }
188
212
  /**
189
213
  * The arrangement page of an org: every org-arrangeable slot with the contributions the role may
190
- * use, in effective order with the hidden ones in place, and where the entry comes from.
214
+ * use: a list slot in effective order with the hidden ones in place, and where the entry comes
215
+ * from; a single slot with its candidates, the stored `use` and whether it follows the platform.
191
216
  */
192
217
  async arrangementView(orgId, role) {
193
218
  // the page is for people who arrange or at least see the org's UI, not for a guest
@@ -199,17 +224,18 @@ let UiViews = class UiViews {
199
224
  this.d.platformArrangement(),
200
225
  ]);
201
226
  return ORG_ARRANGEABLE.map((slot) => {
227
+ const items = base.filter((i) => i.slot === slot);
228
+ if (isSingle(slot)) {
229
+ return { slot, candidates: candidatesOf(items), ...storedUse(org, platform, slot) };
230
+ }
202
231
  const { entry, source } = effectiveEntry(org, platform, slot);
203
- return {
204
- slot,
205
- source,
206
- items: withHidden(base.filter((i) => i.slot === slot), entry),
207
- };
232
+ return { slot, source, items: withHidden(items, entry) };
208
233
  });
209
234
  }
210
235
  /**
211
236
  * The platform's arrangement page: every arrangeable slot with the contributions of every running
212
- * extension (whatever its org enablement), ordered by the platform entry, hidden ones in place.
237
+ * extension (whatever its org enablement): a list slot ordered by the platform entry, hidden ones
238
+ * in place; a single slot with its candidates and the platform's `use`.
213
239
  */
214
240
  async platformArrangementView() {
215
241
  const [base, platform] = await Promise.all([
@@ -217,18 +243,19 @@ let UiViews = class UiViews {
217
243
  this.d.platformArrangement(),
218
244
  ]);
219
245
  return UI_ARRANGEABLE_SLOTS.map((slot) => {
246
+ const items = base.filter((i) => i.slot === slot);
247
+ if (isSingle(slot)) {
248
+ const { use } = storedUse(undefined, platform, slot);
249
+ return { slot, candidates: candidatesOf(items), use, followsPlatform: false };
250
+ }
220
251
  const { entry, source } = effectiveEntry(undefined, platform, slot);
221
- return {
222
- slot,
223
- source,
224
- items: withHidden(base.filter((i) => i.slot === slot), entry),
225
- };
252
+ return { slot, source, items: withHidden(items, entry) };
226
253
  });
227
254
  }
228
255
  /**
229
256
  * The `platform.page` contributions of the running extensions, for the admin console (super
230
- * admins; no org enablement applies): no alias, a view's render registered. Base order like
231
- * {@link contributions}.
257
+ * admins; no org enablement applies): no alias, a view's render registered (a component is
258
+ * listed with or without a server view). Base order like {@link contributions}.
232
259
  */
233
260
  async platformContributions() {
234
261
  const items = listed(this.d.host.uiContributions().filter((c) => c.slot.id === "platform.page"));
@@ -289,7 +316,8 @@ let UiViews = class UiViews {
289
316
  const { entry, args } = await this.resolve(t, "render");
290
317
  const render = entry.render;
291
318
  const view = await this.call(t, "render", (signal) => render({ ...args, signal }));
292
- this.check(t, "render", validateUiView(view));
319
+ // `data` only from a component's view (client extensions spec §5.2)
320
+ this.check(t, "render", validateUiView(view, { component: entry.contribution.kind === "component" }));
293
321
  return dropForeignOpens(view, this.panels(t.plugin));
294
322
  }
295
323
  /** 执行动作:调用插件 `action`(视图里的按钮 / 表单提交),校验返回的提示或新视图。 */
@@ -303,7 +331,7 @@ let UiViews = class UiViews {
303
331
  ...(input.payload !== undefined && { payload: input.payload }),
304
332
  ...(input.answer !== undefined && { answer: input.answer }),
305
333
  }));
306
- this.check(t, "action", actionResultProblems(r));
334
+ this.check(t, "action", actionResultProblems(r, entry.contribution.kind === "component"));
307
335
  const res = r;
308
336
  if (res.view !== undefined)
309
337
  return { view: dropForeignOpens(res.view, this.panels(t.plugin)) };
@@ -338,7 +366,9 @@ let UiViews = class UiViews {
338
366
  c.contribution.aliasOf === undefined &&
339
367
  (need === "embed"
340
368
  ? c.contribution.kind === "embed"
341
- : c.contribution.kind === "view" && c[need] !== undefined));
369
+ : // a view, or a component's server view (served like a view of its slot)
370
+ (c.contribution.kind === "view" || c.contribution.kind === "component") &&
371
+ c[need] !== undefined));
342
372
  if (t.scope === "platform") {
343
373
  const found = platformEntry(entry);
344
374
  return { entry: found, args: this.platformArgs(t, found) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/kernel",
3
- "version": "0.1.0-rc.14",
3
+ "version": "0.1.0-rc.16",
4
4
  "description": "Capybara kernel: config, database, plugin host and HTTP API of the Capybara agent platform",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -65,7 +65,7 @@
65
65
  "yaml": "^2.9.1",
66
66
  "yauzl": "3.4.0",
67
67
  "zod": "^4.6.7",
68
- "@capybara-hq/plugin-sdk": "^0.1.0-rc.10"
68
+ "@capybara-hq/plugin-sdk": "^0.1.0-rc.12"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@langchain/langgraph-checkpoint-validation": "^1.2.0",
@@ -74,7 +74,7 @@
74
74
  "@types/ws": "^8.18.2",
75
75
  "@types/yauzl": "3.4.0",
76
76
  "ws": "^8.22.0",
77
- "@capybara-hq/testing": "0.1.0-rc.13"
77
+ "@capybara-hq/testing": "0.1.0-rc.15"
78
78
  },
79
79
  "scripts": {
80
80
  "build": "tsc -b"
@@ -0,0 +1,50 @@
1
+ -- Client extensions (spec 2026-11-09-client-extensions-design.md §9.1, §10.4).
2
+ -- extension_client_builds: the client files of an extension package, keyed by the package sha256
3
+ -- (the build id). The files live in object storage under `deployment/<object_prefix>`; the
4
+ -- integrity values delivered to browsers come from this row, never from the object store.
5
+ CREATE TABLE IF NOT EXISTS extension_client_builds (
6
+ package_sha256 text PRIMARY KEY REFERENCES extension_packages(sha256) ON DELETE CASCADE,
7
+ plugin text NOT NULL,
8
+ entry text,
9
+ styles text[] NOT NULL,
10
+ assets text[] NOT NULL,
11
+ integrity jsonb NOT NULL,
12
+ sizes jsonb NOT NULL,
13
+ api_version int NOT NULL,
14
+ object_prefix text NOT NULL,
15
+ created_at timestamptz NOT NULL DEFAULT now()
16
+ );
17
+
18
+ -- extension_client_reports: crash and load reports of client code, one row per
19
+ -- (plugin, build, org, contribution, kind); org_id is null for admin pages. Upserted, pruned after
20
+ -- 30 days.
21
+ CREATE TABLE IF NOT EXISTS extension_client_reports (
22
+ plugin text NOT NULL,
23
+ build text NOT NULL,
24
+ org_id text NULL,
25
+ contribution text NULL,
26
+ kind text NOT NULL CHECK (kind IN ('load', 'render', 'event', 'api', 'budget')),
27
+ count int NOT NULL DEFAULT 1,
28
+ first_at timestamptz NOT NULL DEFAULT now(),
29
+ last_at timestamptz NOT NULL DEFAULT now(),
30
+ last_message text NOT NULL
31
+ );
32
+
33
+ CREATE UNIQUE INDEX IF NOT EXISTS extension_client_reports_key_uq
34
+ ON extension_client_reports (
35
+ plugin, build, (coalesce(org_id, '')), (coalesce(contribution, '')), kind
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS extension_client_reports_last_at
39
+ ON extension_client_reports (last_at);
40
+
41
+ -- extension_replica_status.serving_sha256: the package a replica actually serves for the plugin
42
+ -- (the row's own sha256 when loaded; the older package still running when the row's version is
43
+ -- pending approval or failed; null when none runs). Client code follows the server code that runs
44
+ -- (spec §9.3): a build is delivered while a live replica serves its package.
45
+ ALTER TABLE extension_replica_status ADD COLUMN IF NOT EXISTS serving_sha256 text NULL;
46
+
47
+ -- catalog_item_versions.appearance_order: the keys of plugin.json's
48
+ -- suggestedAppearance.replacements in the author's order (a jsonb object does not keep key order),
49
+ -- so the suggestion is listed and applied in that order (spec §6.5); null without replacements.
50
+ ALTER TABLE catalog_item_versions ADD COLUMN IF NOT EXISTS appearance_order text[] NULL;