@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.
- package/agent-docs/extension-points/sandbox.md +2 -2
- package/agent-docs/extensions.md +1 -1
- package/agent-docs/plugin-format.md +6 -2
- package/dist/ctx.d.ts +8 -3
- package/dist/define-extension.d.ts +5 -1
- package/dist/define-extension.js +1 -0
- package/dist/device-protocol.d.ts +1 -1
- package/dist/device-protocol.js +1 -1
- package/dist/extension-point.js +1 -1
- package/dist/file-preview.d.ts +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +7 -1
- package/dist/permissions.d.ts +9 -4
- package/dist/permissions.js +38 -15
- package/dist/plugin-manifest.d.ts +10 -3
- package/dist/plugin-manifest.js +24 -5
- package/dist/sandbox.d.ts +9 -9
- package/dist/sandbox.js +3 -3
- package/dist/theme-assets.d.ts +39 -0
- package/dist/theme-assets.js +449 -0
- package/dist/theme-readability.d.ts +37 -0
- package/dist/theme-readability.js +145 -0
- package/dist/theme-tokens.generated.d.ts +275 -0
- package/dist/theme-tokens.generated.js +167 -0
- package/dist/theme.d.ts +152 -0
- package/dist/theme.js +392 -0
- package/dist/tool.d.ts +3 -3
- package/dist/ui-resolve.d.ts +40 -0
- package/dist/ui-resolve.js +54 -0
- package/dist/ui-slots.d.ts +43 -0
- package/dist/ui-slots.js +168 -0
- package/dist/ui.d.ts +114 -39
- package/dist/ui.js +0 -4
- package/package.json +2 -1
- package/schemas/plugin.v1.schema.json +19 -0
- package/schemas/theme.v1.schema.json +225 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- generated:extension-point -->
|
|
4
4
|
- Name: `sandbox` (a ref is `sandbox@<major>`)
|
|
5
|
-
- Runs
|
|
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`.
|
|
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.
|
package/agent-docs/extensions.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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;
|
package/dist/define-extension.js
CHANGED
|
@@ -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
|
|
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. */
|
package/dist/device-protocol.js
CHANGED
|
@@ -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
|
|
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. */
|
package/dist/extension-point.js
CHANGED
|
@@ -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
|
|
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),
|
package/dist/file-preview.d.ts
CHANGED
|
@@ -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",
|
|
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.
|
|
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.
|
|
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";
|
package/dist/permissions.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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[];
|
package/dist/permissions.js
CHANGED
|
@@ -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
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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
|
-
|
|
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
|
|
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`
|
|
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}.
|
package/dist/plugin-manifest.js
CHANGED
|
@@ -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
|
|
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`
|
|
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
|
-
|
|
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
|
|
8
|
-
* (`createFileSystem`) lives until the
|
|
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 (
|
|
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
|
|
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
|
|
97
|
-
* `destroyFileSystem` (the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8
|
-
* (`createFileSystem`) lives until the
|
|
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
|
|
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
|
+
};
|