@capybara-hq/plugin-sdk 0.1.0-rc.7 → 0.1.0-rc.9

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- generated:extension-point -->
4
4
  - Name: `sandbox` (a ref is `sandbox@<major>`)
5
- - Runs an environment's instance and keeps its files for agents' commands (Docker, E2B).
5
+ - Runs a device's instance and keeps its files for agents' commands (Docker, E2B).
6
6
  - Providers: one at a time
7
7
  - Enabled per org: no
8
8
  - Brokered by the platform: yes (extensions use it only through `ctx`)
@@ -10,6 +10,6 @@
10
10
 
11
11
  Contract: SandboxProvider (`sandbox.ts`)
12
12
 
13
- The kernel accepts only `sandbox@2`. An environment is one instance plus a file system that outlives it. Implement the file system (`createFileSystem`, `destroyFileSystem`, `destroyFileSystemByName`), the instance on it (`create`, which throws `FileSystemGoneError` when the file system is gone instead of making an empty one; `resume`, `suspend`; `destroy` and `destroyByName`, which drop the instance and keep the files), and the instance's `exec`, `readFile`, `writeFile`, `stat`, `killAll` and `killTagged` (`concurrentExec` is required: several conversations share one instance). Aborting a command must kill it. Locking, suspension timing and destruction are the kernel's job, not yours. Never publish a docker socket proxy on a host address.
13
+ The kernel accepts only `sandbox@2`. A device is one instance plus a file system that outlives it. Implement the file system (`createFileSystem`, `destroyFileSystem`, `destroyFileSystemByName`), the instance on it (`create`, which throws `FileSystemGoneError` when the file system is gone instead of making an empty one; `resume`, `suspend`; `destroy` and `destroyByName`, which drop the instance and keep the files), and the instance's `exec`, `readFile`, `writeFile`, `stat`, `killAll` and `killTagged` (`concurrentExec` is required: several conversations share one instance). Aborting a command must kill it. Locking, suspension timing and destruction are the kernel's job, not yours. Never publish a docker socket proxy on a host address.
14
14
 
15
15
  See `extensions` for the rules every extension follows.
@@ -44,7 +44,7 @@ look like `channel@1`: name and major version. The registered points:
44
44
  <!-- generated:extension-points -->
45
45
  | Extension point | Providers | Per org | What it is |
46
46
  |---|---|---|---|
47
- | `sandbox` | single | no | Runs an environment's instance and keeps its files for agents' commands (Docker, E2B). |
47
+ | `sandbox` | single | no | Runs a device's instance and keeps its files for agents' commands (Docker, E2B). |
48
48
  | `config-source` | single | no | Supplies deployment configuration from an external store such as Nacos. |
49
49
  | `secret-store` | single | no | Stores and resolves secrets (connector credentials, secret config) outside the database. |
50
50
  | `object-store` | single | no | Stores files and artifacts in S3-compatible object storage, with presigned uploads. |
@@ -10,11 +10,12 @@ A plugin is a directory, packed into a `.tgz` with `capybara-plugin pack` (the p
10
10
  skills/<name>/SKILL.md skills (scanned unless plugin.json lists them)
11
11
  connectors/<id>.json connector templates (scanned unless listed)
12
12
  agents/<name>.md agent templates (scanned unless listed)
13
+ themes/<id>.json themes (scanned unless listed)
13
14
  src/index.ts the extension (optional; bundled by the packer)
14
15
  migrations/*.sql, ui/** extension migrations and UI files (optional)
15
16
  ```
16
17
 
17
- A plugin without `src/index.ts` is declarative: skills and templates only, no code runs.
18
+ A plugin without `src/index.ts` is declarative: skills, templates and themes only, no code runs.
18
19
 
19
20
  ## plugin.json
20
21
 
@@ -37,6 +38,7 @@ file is strict: an unknown key is an error. Fields (agent templates are listed u
37
38
  | `skills` | array of object | no |
38
39
  | `connectorTemplates` | array of object | no |
39
40
  | `agents` | array of object | no |
41
+ | `themes` | array of object | no |
40
42
  | `setupGuides` | array of object | no |
41
43
  | `extension` | object | no |
42
44
  <!-- /generated -->
@@ -48,10 +50,12 @@ Rules that the schema cannot show:
48
50
  release candidates, ranges on official packages are written `^0.1.0-rc.0`.
49
51
  - `description` is a string or `{ en, zh? }`; `displayName` is `{ zh, en }`.
50
52
  - `icon` is a `./` path to an `.svg` or `.png` inside the plugin.
51
- - `skills`, `connectorTemplates`, `agents` list paths starting with `./` that stay inside the plugin.
53
+ - `skills`, `connectorTemplates`, `agents`, `themes` list paths starting with `./` that stay inside the plugin.
52
54
  Omit them to scan the default folders. Names and ids are unique within the plugin.
53
55
  - `dependencies`: plugin name plus semver range; no self-dependency.
54
56
  - `agents`: at most 64 templates (see `agent-templates`).
57
+ - `themes`: at most 10 `<id>.json` files; the id is the file's base name. A theme file is checked
58
+ by `parseThemeFile` (schema `https://capybarahq.com/schemas/theme.v1.schema.json`).
55
59
  - `setupGuides` (at most 8): setup material shown where a person configures what the plugin
56
60
  brings. Each guide has an `id`, a `place` (`channel-connection`: 新建渠道 and the channel detail;
57
61
  the plugin's extension must provide `channel@1`), a `title` and `steps` (`{ en, zh? }`, plain
package/dist/ctx.d.ts CHANGED
@@ -198,15 +198,20 @@ export interface ExtensionContext {
198
198
  * 界面贡献的运行时(清单 `ui` + `permissions.ui`):
199
199
  * - `view` / `action`:只能在 `setup` 期间注册,id 是清单里 kind view 的贡献 id;
200
200
  * `action` 收到视图里按钮或表单提交(`args.actionId` 区分是哪一个);
201
- * - `invalidate`:告诉内核「这个贡献变了」,打开着它的网页会重新取视图(经 bus,跨副本);
201
+ * - `invalidate`:告诉内核「这个贡献变了」,打开着它的网页会重新取视图(经 bus,跨副本);a
202
+ * `platform.page` contribution takes no `orgId` (it refreshes the admin console), every other
203
+ * contribution requires one;
202
204
  * - `verifyEmbedToken`:校验 embed 页面拿到的嵌入令牌,只认签给本插件、未过期的,且此刻插件仍在该组织启用、
203
- * 调用者仍是 active 成员(组织页面还要求 member 以上),否则返回 null;返回的 `role` 是此刻的角色。
205
+ * 调用者仍是 active 成员,角色仍满足位置的 minRole,否则返回 null;返回的 `role` 是此刻的角色。A
206
+ * platform-scope token (`scope: "platform"`, no `orgId`) verifies only for a `platform.page`
207
+ * contribution while the account is still an active super admin.
204
208
  */
205
209
  ui: {
206
210
  view(id: string, render: (args: UiRenderArgs) => Promise<UiView>): void;
207
211
  action(id: string, handler: (args: UiActionArgs) => Promise<UiActionResult>): void;
208
212
  invalidate(target: {
209
- orgId: string;
213
+ /** Required, except for a `platform.page` contribution, where it must be omitted. */
214
+ orgId?: string;
210
215
  conversationId?: string;
211
216
  contribution: string;
212
217
  }): Promise<void>;
@@ -14,7 +14,11 @@ export interface ExtensionManifest {
14
14
  optional?: ExtensionPointRef[];
15
15
  /** 权限清单(治理而非沙箱)。 */
16
16
  permissions?: ExtensionPermissions;
17
- /** 界面贡献:对话页标签与组织页面;id 须同时列在 `permissions.ui` 里。 */
17
+ /**
18
+ * UI contributions, each in a slot of `UI_SLOTS`; every id is also listed in `permissions.ui`.
19
+ * Kept exactly as written: structural problems fail the extension, while a contribution to an
20
+ * unknown slot or version is only dropped when the host resolves the slots (`resolveUiSlots`).
21
+ */
18
22
  ui?: UiContribution[];
19
23
  config?: {
20
24
  deployment?: z.ZodTypeAny;
@@ -78,6 +78,7 @@ export function defineExtension(m) {
78
78
  if (errs.length > 0)
79
79
  fail(`invalid permissions: ${errs.join("; ")}`);
80
80
  }
81
+ // structural ui problems only; slot resolution never fails the extension (UI slots spec §3.2)
81
82
  const uiErrs = validateUiContributions(m.ui, m.permissions);
82
83
  if (uiErrs.length > 0)
83
84
  fail(`invalid ui: ${uiErrs.join("; ")}`);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Device protocol v1: the WebSocket frames between the kernel and a `capybara-device` program
3
- * (a connected machine that serves as an environment). Pure types and hand-written guards.
3
+ * (a connected machine that serves as a device). Pure types and hand-written guards.
4
4
  */
5
5
  import { type ExecResult, type SandboxActor, type SandboxLimits, type SandboxRoots } from "./sandbox.js";
6
6
  /** The protocol version carried in `X-Capybara-Device-Protocol` and the hello frame. */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Device protocol v1: the WebSocket frames between the kernel and a `capybara-device` program
3
- * (a connected machine that serves as an environment). Pure types and hand-written guards.
3
+ * (a connected machine that serves as a device). Pure types and hand-written guards.
4
4
  */
5
5
  import { validateSandboxPathShape, } from "./sandbox.js";
6
6
  /** The protocol version carried in `X-Capybara-Device-Protocol` and the hello frame. */
@@ -8,7 +8,7 @@ const info = (majors, description, cardinality, brokered = false, orgScoped = fa
8
8
  });
9
9
  /** The registered extension points. */
10
10
  export const EXTENSION_POINTS = {
11
- sandbox: info([2], "Runs an environment's instance and keeps its files for agents' commands (Docker, E2B).", "single", true),
11
+ sandbox: info([2], "Runs a device's instance and keeps its files for agents' commands (Docker, E2B).", "single", true),
12
12
  "config-source": info([1], "Supplies deployment configuration from an external store such as Nacos.", "single"),
13
13
  "secret-store": info([1], "Stores and resolves secrets (connector credentials, secret config) outside the database.", "single", true),
14
14
  "object-store": info([1], "Stores files and artifacts in S3-compatible object storage, with presigned uploads.", "single", true),
@@ -7,7 +7,7 @@ export declare const FILE_PREVIEW_MAX_BYTES: number;
7
7
  /**
8
8
  * One previewer of a `file-preview@1` provider: which files it previews and how. It renders either a
9
9
  * view (`render`, drawn by the web app like any `UiView`) or an embed page (`embed`, the id of a ui
10
- * contribution `{ kind: "embed", place: "file-preview" }` of the same extension, which receives the
10
+ * contribution `{ kind: "embed", slot: "file.preview" }` of the same extension, which receives the
11
11
  * file over the message bridge). Exactly one of the two.
12
12
  */
13
13
  export interface FilePreviewer {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "0.1.0-rc.7";
1
+ export declare const SDK_VERSION = "0.1.0-rc.9";
2
2
  export * from "./agent-template.js";
3
3
  export * from "./bootstrap.js";
4
4
  export * from "./cards.js";
@@ -27,7 +27,13 @@ export * from "./setup-guide.js";
27
27
  export * from "./skill.js";
28
28
  export * from "./skill-invocation.js";
29
29
  export * from "./storage.js";
30
+ export * from "./theme.js";
31
+ export * from "./theme-assets.js";
32
+ export * from "./theme-readability.js";
33
+ export * from "./theme-tokens.generated.js";
30
34
  export * from "./tool.js";
31
35
  export * from "./tool-key.js";
32
36
  export * from "./ui.js";
37
+ export * from "./ui-resolve.js";
38
+ export * from "./ui-slots.js";
33
39
  export * from "./web.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export const SDK_VERSION = "0.1.0-rc.7";
1
+ export const SDK_VERSION = "0.1.0-rc.9";
2
2
  export * from "./agent-template.js";
3
3
  export * from "./bootstrap.js";
4
4
  export * from "./cards.js";
@@ -27,7 +27,13 @@ export * from "./setup-guide.js";
27
27
  export * from "./skill.js";
28
28
  export * from "./skill-invocation.js";
29
29
  export * from "./storage.js";
30
+ export * from "./theme.js";
31
+ export * from "./theme-assets.js";
32
+ export * from "./theme-readability.js";
33
+ export * from "./theme-tokens.generated.js";
30
34
  export * from "./tool.js";
31
35
  export * from "./tool-key.js";
32
36
  export * from "./ui.js";
37
+ export * from "./ui-resolve.js";
38
+ export * from "./ui-slots.js";
33
39
  export * from "./web.js";
@@ -62,9 +62,14 @@ export declare class ExtensionPointUnavailableError extends Error {
62
62
  /** 校验权限清单,返回错误列表(空表示合法)。 */
63
63
  export declare function validatePermissions(plugin: string, p: unknown): string[];
64
64
  /**
65
- * 校验清单 `ui` 并与权限清单交叉核对,返回错误列表(空表示合法):
66
- * 每个贡献的 id 恰好出现在 `permissions.ui.views`(kind view)或 `permissions.ui.embeds`(kind embed)里,
67
- * `permissions.ui` 里也不能有清单 `ui` 没有的 id;embed 的 path 以 `/ui/` 开头并在 `permissions.http` 中声明为 GET。
68
- * 假定 `permissions` 已经过 {@link validatePermissions}。
65
+ * Checks the structure of the manifest's `ui` and cross-checks it with the permission manifest;
66
+ * returns the problems (empty = valid). Only structural problems (UI slots spec §3.2): each
67
+ * contribution's id is listed in exactly `permissions.ui.views` (kind view) or
68
+ * `permissions.ui.embeds` (kind embed), and `permissions.ui` lists no id the `ui` lacks; an embed's
69
+ * path starts with `/ui/` and is declared in `permissions.http` as GET; exactly one of `slot` (a
70
+ * well-formed slot reference) and `place` (a known place); for a slot the catalog knows at the
71
+ * version named, a kind it accepts and, unless an alias, the icon it requires. An unknown slot id or version is not a
72
+ * problem here: `resolveUiSlots` drops that contribution and the rest of the extension loads.
73
+ * Assumes `permissions` passed {@link validatePermissions}.
69
74
  */
70
75
  export declare function validateUiContributions(ui: unknown, permissions: unknown): string[];
@@ -1,5 +1,6 @@
1
1
  import { POLICY_HOOKS } from "./hooks.js";
2
2
  import { UI_ICONS } from "./ui.js";
3
+ import { PLACE_SLOTS, parseSlotRef, uiSlot } from "./ui-slots.js";
3
4
  /** hook 挂载点(封闭列表,见总设计 2.4)。 */
4
5
  export const HOOK_MOUNT_POINTS = [
5
6
  "session.start",
@@ -168,20 +169,18 @@ export function validatePermissions(plugin, p) {
168
169
  }
169
170
  const UI_ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
170
171
  const UI_TITLE_MAX = 100;
171
- const UI_PLACES = [
172
- "conversation",
173
- "panel",
174
- "section",
175
- "inline",
176
- "page",
177
- "file-preview",
178
- ];
172
+ const UI_PLACES = Object.keys(PLACE_SLOTS);
179
173
  const UI_EMBED_PREFIX = "/ui/";
180
174
  /**
181
- * 校验清单 `ui` 并与权限清单交叉核对,返回错误列表(空表示合法):
182
- * 每个贡献的 id 恰好出现在 `permissions.ui.views`(kind view)或 `permissions.ui.embeds`(kind embed)里,
183
- * `permissions.ui` 里也不能有清单 `ui` 没有的 id;embed 的 path 以 `/ui/` 开头并在 `permissions.http` 中声明为 GET。
184
- * 假定 `permissions` 已经过 {@link validatePermissions}。
175
+ * Checks the structure of the manifest's `ui` and cross-checks it with the permission manifest;
176
+ * returns the problems (empty = valid). Only structural problems (UI slots spec §3.2): each
177
+ * contribution's id is listed in exactly `permissions.ui.views` (kind view) or
178
+ * `permissions.ui.embeds` (kind embed), and `permissions.ui` lists no id the `ui` lacks; an embed's
179
+ * path starts with `/ui/` and is declared in `permissions.http` as GET; exactly one of `slot` (a
180
+ * well-formed slot reference) and `place` (a known place); for a slot the catalog knows at the
181
+ * version named, a kind it accepts and, unless an alias, the icon it requires. An unknown slot id or version is not a
182
+ * problem here: `resolveUiSlots` drops that contribution and the rest of the extension loads.
183
+ * Assumes `permissions` passed {@link validatePermissions}.
185
184
  */
186
185
  export function validateUiContributions(ui, permissions) {
187
186
  const perms = isObj(permissions) ? permissions : {};
@@ -219,22 +218,46 @@ export function validateUiContributions(ui, permissions) {
219
218
  if (!title ||
220
219
  [title.zh, title.en].some((t) => typeof t !== "string" || t === "" || t.length > UI_TITLE_MAX))
221
220
  errs.push(`ui "${id}" title needs non-empty zh and en of at most ${UI_TITLE_MAX} characters`);
222
- if (!UI_PLACES.includes(c.place))
221
+ // the slot it names, when the slot / place is well formed (known to the catalog or not)
222
+ let slotRef = null;
223
+ if ((c.slot === undefined) === (c.place === undefined)) {
224
+ errs.push(`ui "${id}" needs exactly one of slot and place`);
225
+ }
226
+ else if (c.slot !== undefined) {
227
+ slotRef = typeof c.slot === "string" ? parseSlotRef(c.slot) : null;
228
+ if (slotRef === null)
229
+ errs.push(`ui "${id}" slot must be a slot reference like "org.page" or "org.page@1"`);
230
+ }
231
+ else if (!UI_PLACES.includes(c.place)) {
223
232
  errs.push(`ui "${id}" place must be one of ${UI_PLACES.join(", ")}`);
233
+ }
234
+ else {
235
+ slotRef = { id: PLACE_SLOTS[c.place], version: 1 };
236
+ }
237
+ if (c.after !== undefined && typeof c.after !== "string")
238
+ errs.push(`ui "${id}" after must be a string`);
224
239
  if (c.kind !== "view" && c.kind !== "embed") {
225
240
  errs.push(`ui "${id}" kind must be view or embed`);
226
241
  return;
227
242
  }
228
243
  if (c.icon !== undefined && !UI_ICONS.includes(c.icon))
229
244
  errs.push(`ui "${id}" icon must be one of ${UI_ICONS.join(", ")}`);
245
+ // the slot checks describe only the catalog's version: another version is left to
246
+ // resolveUiSlots, which drops it (unsupported-version) without failing the extension
247
+ const known = slotRef === null ? undefined : uiSlot(slotRef.id);
248
+ const spec = known !== undefined && known.version === slotRef?.version ? known : undefined;
249
+ if (spec !== undefined) {
250
+ if (!spec.accepts.includes(c.kind))
251
+ errs.push(`ui "${id}" slot ${spec.id} does not accept kind ${c.kind}`);
252
+ if (spec.icon === "required" && c.icon === undefined && c.aliasOf === undefined)
253
+ errs.push(`ui "${id}" slot ${spec.id} needs an icon`);
254
+ }
230
255
  if (c.aliasOf !== undefined)
231
256
  aliases.push({ id, kind: c.kind, target: c.aliasOf });
232
257
  if (seen.has(id))
233
258
  errs.push(`ui has duplicate id "${id}"`);
234
259
  seen.set(id, c.kind);
235
260
  if (c.kind === "view") {
236
- if (c.place === "file-preview")
237
- errs.push(`ui "${id}" place file-preview is only allowed for kind embed`);
238
261
  if (c.path !== undefined)
239
262
  errs.push(`ui "${id}" path is only allowed for kind embed`);
240
263
  if (!views.includes(id))
@@ -2,9 +2,11 @@ import { z } from "zod";
2
2
  import { type JsonSchema } from "./config-ui.js";
3
3
  import { type ConnectorTemplateSpec, type LocalizedName, type StaticExtensionManifest } from "./plugin-package.js";
4
4
  import { type PluginSetupGuide } from "./setup-guide.js";
5
+ import { type PluginThemeRef } from "./theme.js";
5
6
  /**
6
7
  * The plugin manifest `.capybara-plugin/plugin.json` (package ecosystem spec §4.1). A plugin is a
7
- * bundle of at most one extension, skills and connector templates, packed as an npm-pack tgz:
8
+ * bundle of at most one extension, skills, connector templates, agent templates and themes, packed as
9
+ * an npm-pack tgz:
8
10
  *
9
11
  * ```
10
12
  * package/package.json npm metadata; version equals plugin.json version
@@ -12,13 +14,14 @@ import { type PluginSetupGuide } from "./setup-guide.js";
12
14
  * package/skills/<name>/SKILL.md skills
13
15
  * package/connectors/<id>.json connector templates ({@link ConnectorTemplateFile})
14
16
  * package/agents/<name>.md agent templates (parsed by `parseAgentTemplate`)
17
+ * package/themes/<id>.json themes (parsed by `parseThemeFile`)
15
18
  * package/dist/index.mjs extension entry (only with an extension)
16
19
  * package/migrations/*.sql, package/ui/**
17
20
  * ```
18
21
  *
19
22
  * Paths start with `./` and stay inside the plugin root. Omitting `skills` / `connectorTemplates`
20
- * / `agents` means "scan `skills/*\/SKILL.md` / `connectors/*.json` / `agents/*.md`" (done by the
21
- * kernel when it reads the package). `extension` is written by the packer, never by hand.
23
+ * / `agents` / `themes` means "scan `skills/*\/SKILL.md` / `connectors/*.json` / `agents/*.md` /
24
+ * `themes/*.json`" (done by the kernel when it reads the package). `extension` is written by the packer, never by hand.
22
25
  *
23
26
  * The committed JSON Schema (`schemas/plugin.v1.schema.json`) is generated from
24
27
  * {@link PluginManifestSchema} by `pnpm gen:package-schemas` and drift-checked by
@@ -40,6 +43,8 @@ export declare const PLUGIN_MANIFEST_LIMITS: {
40
43
  readonly connectorTemplates: 20;
41
44
  /** Agent templates ({@link AGENT_TEMPLATE_LIMITS}`.perPlugin`). */
42
45
  readonly agents: 64;
46
+ /** Themes ({@link THEME_LIMITS}`.perPlugin`). */
47
+ readonly themes: 10;
43
48
  readonly descriptionChars: 1024;
44
49
  readonly publisherChars: 100;
45
50
  readonly displayNameChars: 100;
@@ -124,6 +129,8 @@ export interface PluginManifest {
124
129
  connectorTemplates?: PluginConnectorTemplateRef[];
125
130
  /** Agent templates; omitted = scan `agents/*.md`. */
126
131
  agents?: PluginAgentRef[];
132
+ /** Themes; omitted = scan `themes/*.json`. */
133
+ themes?: PluginThemeRef[];
127
134
  /**
128
135
  * Setup guides (spec `2026-11-08-plugin-setup-guides-design.md`): steps and template files the
129
136
  * kernel renders for a place (`channel-connection`); see {@link PluginSetupGuide}.
@@ -6,9 +6,11 @@ import { defineExtension, EXTENSION_NAME_RE } from "./define-extension.js";
6
6
  import { CATALOG_ITEM_NAME_RE, CONNECTOR_TEMPLATE_LIMITS, isSafePackagePath, SEMVER_VERSION_RE, validateConnectorTemplate, } from "./plugin-package.js";
7
7
  import { hasUpperBound } from "./semver-range.js";
8
8
  import { SETUP_GUIDE_FILE_RE, SETUP_GUIDE_LANGUAGES, SETUP_GUIDE_LIMITS, SETUP_GUIDE_MODES, SETUP_GUIDE_PLACES, } from "./setup-guide.js";
9
+ import { THEME_LIMITS, themeIdOf } from "./theme.js";
9
10
  /**
10
11
  * The plugin manifest `.capybara-plugin/plugin.json` (package ecosystem spec §4.1). A plugin is a
11
- * bundle of at most one extension, skills and connector templates, packed as an npm-pack tgz:
12
+ * bundle of at most one extension, skills, connector templates, agent templates and themes, packed as
13
+ * an npm-pack tgz:
12
14
  *
13
15
  * ```
14
16
  * package/package.json npm metadata; version equals plugin.json version
@@ -16,13 +18,14 @@ import { SETUP_GUIDE_FILE_RE, SETUP_GUIDE_LANGUAGES, SETUP_GUIDE_LIMITS, SETUP_G
16
18
  * package/skills/<name>/SKILL.md skills
17
19
  * package/connectors/<id>.json connector templates ({@link ConnectorTemplateFile})
18
20
  * package/agents/<name>.md agent templates (parsed by `parseAgentTemplate`)
21
+ * package/themes/<id>.json themes (parsed by `parseThemeFile`)
19
22
  * package/dist/index.mjs extension entry (only with an extension)
20
23
  * package/migrations/*.sql, package/ui/**
21
24
  * ```
22
25
  *
23
26
  * Paths start with `./` and stay inside the plugin root. Omitting `skills` / `connectorTemplates`
24
- * / `agents` means "scan `skills/*\/SKILL.md` / `connectors/*.json` / `agents/*.md`" (done by the
25
- * kernel when it reads the package). `extension` is written by the packer, never by hand.
27
+ * / `agents` / `themes` means "scan `skills/*\/SKILL.md` / `connectors/*.json` / `agents/*.md` /
28
+ * `themes/*.json`" (done by the kernel when it reads the package). `extension` is written by the packer, never by hand.
26
29
  *
27
30
  * The committed JSON Schema (`schemas/plugin.v1.schema.json`) is generated from
28
31
  * {@link PluginManifestSchema} by `pnpm gen:package-schemas` and drift-checked by
@@ -44,6 +47,8 @@ export const PLUGIN_MANIFEST_LIMITS = {
44
47
  connectorTemplates: 20,
45
48
  /** Agent templates ({@link AGENT_TEMPLATE_LIMITS}`.perPlugin`). */
46
49
  agents: AGENT_TEMPLATE_LIMITS.perPlugin,
50
+ /** Themes ({@link THEME_LIMITS}`.perPlugin`). */
51
+ themes: THEME_LIMITS.perPlugin,
47
52
  descriptionChars: 1024,
48
53
  publisherChars: 100,
49
54
  displayNameChars: 100,
@@ -196,6 +201,12 @@ const schema = z
196
201
  }))
197
202
  .max(L.agents)
198
203
  .optional(),
204
+ themes: z
205
+ .array(z.strictObject({
206
+ path: dotPath("path").refine((p) => p.endsWith(".json") && CATALOG_ITEM_NAME_RE.test(themeIdOf(p)), { message: "path must be a <id>.json file with id matching ^[a-z0-9][a-z0-9-]{0,63}$" }),
207
+ }))
208
+ .max(L.themes)
209
+ .optional(),
199
210
  setupGuides: z.array(setupGuide).max(SETUP_GUIDE_LIMITS.guides).optional(),
200
211
  extension: extensionSection.optional(),
201
212
  })
@@ -235,6 +246,13 @@ const schema = z
235
246
  issue(["agents", i, "path"], `duplicate agent template path ${a.path}`);
236
247
  agentPaths.add(a.path);
237
248
  });
249
+ const themeIds = new Set();
250
+ m.themes?.forEach((t, i) => {
251
+ const id = themeIdOf(t.path);
252
+ if (themeIds.has(id))
253
+ issue(["themes", i, "path"], `duplicate theme id ${id}`);
254
+ themeIds.add(id);
255
+ });
238
256
  const guideIds = new Set();
239
257
  m.setupGuides?.forEach((g, i) => {
240
258
  if (guideIds.has(g.id))
@@ -269,8 +287,9 @@ const schema = z
269
287
  if (m.extension === undefined &&
270
288
  m.skills?.length === 0 &&
271
289
  m.connectorTemplates?.length === 0 &&
272
- (m.agents === undefined || m.agents.length === 0)) {
273
- issue([], "a plugin must contain an extension, a skill, a connector template or an agent template");
290
+ (m.agents === undefined || m.agents.length === 0) &&
291
+ (m.themes === undefined || m.themes.length === 0)) {
292
+ issue([], "a plugin must contain an extension, a skill, a connector template, an agent template or a theme");
274
293
  }
275
294
  const ext = m.extension;
276
295
  if (ext === undefined)
package/dist/sandbox.d.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * primitives below; the kernel builds `glob` / `grep` / `edit` from `exec` plus `readFile` /
5
5
  * `writeFile`.
6
6
  *
7
- * `@2` separates an environment's **file system** from its **instance**: the file system
8
- * (`createFileSystem`) lives until the environment is deleted (`destroyFileSystem`); instances
7
+ * `@2` separates a device's **file system** from its **instance**: the file system
8
+ * (`createFileSystem`) lives until the device is deleted (`destroyFileSystem`); instances
9
9
  * (`create`) attach to it and come and go (`destroy` drops the instance and keeps the files).
10
10
  */
11
11
  /** Sandbox extension point ref. */
@@ -38,7 +38,7 @@ export interface SandboxHandle {
38
38
  }
39
39
  /** The roots of an instance, falling back to {@link DEFAULT_SANDBOX_ROOTS}. */
40
40
  export declare function rootsOf(h: SandboxHandle): SandboxRoots;
41
- /** The provider's file system handle (an environment's files, independent of any instance). */
41
+ /** The provider's file system handle (a device's files, independent of any instance). */
42
42
  export interface FileSystemHandle {
43
43
  /** The provider's file system id. */
44
44
  id: string;
@@ -74,7 +74,7 @@ export declare function isSandboxGoneError(e: unknown): e is SandboxGoneError;
74
74
  /**
75
75
  * The file system is definitely gone on the provider's side (volume removed, E2B sandbox killed).
76
76
  * `create` throws it instead of attaching the instance to a new, empty file system; the kernel then
77
- * reports the environment's files as lost rather than handing out a silently empty workspace.
77
+ * reports the device's files as lost rather than handing out a silently empty workspace.
78
78
  */
79
79
  export declare class FileSystemGoneError extends Error {
80
80
  readonly fileSystemGone: true;
@@ -93,8 +93,8 @@ export declare const SANDBOX_TAG_RE: RegExp;
93
93
  /**
94
94
  * The `sandbox@2` implementation (provided by a sandbox plugin such as Docker or E2B).
95
95
  *
96
- * **File systems and instances.** `createFileSystem` makes an environment's files; they live until
97
- * `destroyFileSystem` (the environment is deleted). `create` makes an instance attached to an
96
+ * **File systems and instances.** `createFileSystem` makes a device's files; they live until
97
+ * `destroyFileSystem` (the device is deleted). `create` makes an instance attached to an
98
98
  * existing file system; `destroy` / `destroyByName` drop the instance only and keep the files, and
99
99
  * `suspend` keeps them too. When the file system is gone, `create` must throw
100
100
  * {@link FileSystemGoneError} and never create an empty one in its place (a Docker provider checks
@@ -126,7 +126,7 @@ export declare const SANDBOX_TAG_RE: RegExp;
126
126
  * `exitCode: null`, `timedOut: false` (and the output collected so far), it does not reject. An
127
127
  * `exec` called with an already aborted signal rejects at once.
128
128
  *
129
- * **Concurrent use** (required in `@2`): the sessions of one environment share its instance, so the
129
+ * **Concurrent use** (required in `@2`): the sessions of one device share its instance, so the
130
130
  * kernel calls `exec`, `readFile`, `writeFile` and `stat` concurrently on one instance. Those four
131
131
  * methods must be safe to call concurrently. Each `exec` labels its command with its `tag`, and an
132
132
  * aborted `exec` kills only its own command.
@@ -135,12 +135,12 @@ export interface SandboxProvider {
135
135
  /** `exec` / `readFile` / `writeFile` / `stat` may run concurrently on one instance (required). */
136
136
  readonly concurrentExec: true;
137
137
  /**
138
- * Whether the provider isolates the sessions of one environment from each other inside the
138
+ * Whether the provider isolates the sessions of one device from each other inside the
139
139
  * instance (a declared capability; absent means false).
140
140
  */
141
141
  readonly sessionIsolation?: boolean;
142
142
  /**
143
- * Creates an environment's file system. `name` looks like `cap-env-<environmentId>`. If a file
143
+ * Creates a device's file system. `name` looks like `cap-device-<deviceId>`. If a file
144
144
  * system with this name already exists, return it.
145
145
  */
146
146
  createFileSystem(args: {
package/dist/sandbox.js CHANGED
@@ -4,8 +4,8 @@
4
4
  * primitives below; the kernel builds `glob` / `grep` / `edit` from `exec` plus `readFile` /
5
5
  * `writeFile`.
6
6
  *
7
- * `@2` separates an environment's **file system** from its **instance**: the file system
8
- * (`createFileSystem`) lives until the environment is deleted (`destroyFileSystem`); instances
7
+ * `@2` separates a device's **file system** from its **instance**: the file system
8
+ * (`createFileSystem`) lives until the device is deleted (`destroyFileSystem`); instances
9
9
  * (`create`) attach to it and come and go (`destroy` drops the instance and keeps the files).
10
10
  */
11
11
  /** Sandbox extension point ref. */
@@ -45,7 +45,7 @@ export function isSandboxGoneError(e) {
45
45
  /**
46
46
  * The file system is definitely gone on the provider's side (volume removed, E2B sandbox killed).
47
47
  * `create` throws it instead of attaching the instance to a new, empty file system; the kernel then
48
- * reports the environment's files as lost rather than handing out a silently empty workspace.
48
+ * reports the device's files as lost rather than handing out a silently empty workspace.
49
49
  */
50
50
  export class FileSystemGoneError extends Error {
51
51
  fileSystemGone = true;
@@ -0,0 +1,39 @@
1
+ import { type ParsedTheme } from "./theme.js";
2
+ /**
3
+ * Parses a logo SVG (UTF-8, ≤ {@link THEME_LIMITS}`.svgBytes`) with a strict, namespace-aware
4
+ * XML parser and re-serializes it. The root must be an `svg` element in the SVG namespace with a
5
+ * numeric `viewBox`; its `width`, `height` and `class` are dropped. Rejected: a DOCTYPE (and so
6
+ * any entity declaration), processing instructions, a non-UTF-8 encoding, elements outside the
7
+ * SVG allowlist (`script`, `foreignObject`, `style`, …) or in the XHTML namespace, `style` and
8
+ * `on*` attributes, `href` / `xlink:href` not starting with `#`, `javascript:` / `vbscript:` in
9
+ * any value, quotes or a backslash in attribute values, any function other than `url(#id)`, colors
10
+ * (`rgb`, `rgba`, `hsl`, `hsla`) and transforms (`translate`, `rotate`, `scale`, `matrix`,
11
+ * `skewX`, `skewY`), a viewBox longer than 128 characters, and animation of `href`, `style` or event
12
+ * handlers. Comments, the XML declaration and elements / attributes in other namespaces (editor
13
+ * metadata) are dropped; CDATA becomes escaped text. Never throws.
14
+ */
15
+ export declare function sanitizeThemeSvg(bytes: Uint8Array): {
16
+ svg: string;
17
+ } | {
18
+ error: string;
19
+ };
20
+ /** A woff2 font: the `wOF2` signature and at most {@link THEME_LIMITS}`.fontBytes`. */
21
+ export declare function checkWoff2(bytes: Uint8Array): string | undefined;
22
+ /**
23
+ * A PNG logo: the PNG signature, an `IHDR` first chunk with a height of at least
24
+ * {@link THEME_LIMITS}`.pngMinHeight` pixels, and at most {@link THEME_LIMITS}`.pngBytes`.
25
+ */
26
+ export declare function checkPng(bytes: Uint8Array): string | undefined;
27
+ /**
28
+ * Resolves and checks the asset files of a parsed theme: each `fonts[].file` ({@link checkWoff2})
29
+ * and `logo.mark` / `logo.favicon` (`.svg` through {@link sanitizeThemeSvg}, `.png` through
30
+ * {@link checkPng}); the favicon must be square (SVG viewBox or PNG width = height). `files` holds the plugin's files keyed by their path without a leading
31
+ * `./`; the returned `assets` (the sanitized SVG bytes, the other files unchanged) use the same
32
+ * keys. Errors are `<field>: <reason>`, English; the total input size of the theme's distinct
33
+ * assets must stay within {@link THEME_LIMITS}`.assetsBytes`. That limit is per plugin, so a
34
+ * caller reading several themes also checks the sum over all of them (distinct paths).
35
+ */
36
+ export declare function validateThemeAssets(theme: ParsedTheme, files: Map<string, Uint8Array>): {
37
+ assets: Map<string, Uint8Array>;
38
+ errors: string[];
39
+ };