@capybara-hq/plugin-sdk 0.1.0-rc.1 → 0.1.0-rc.3

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 (54) hide show
  1. package/README.md +1 -1
  2. package/agent-docs/agent-templates.md +47 -0
  3. package/agent-docs/concepts.md +58 -0
  4. package/agent-docs/connector-templates.md +47 -0
  5. package/agent-docs/extension-points/channel.md +15 -0
  6. package/agent-docs/extension-points/config-source.md +15 -0
  7. package/agent-docs/extension-points/connector.md +15 -0
  8. package/agent-docs/extension-points/counter.md +15 -0
  9. package/agent-docs/extension-points/directory.md +15 -0
  10. package/agent-docs/extension-points/file-preview.md +15 -0
  11. package/agent-docs/extension-points/identity-source.md +15 -0
  12. package/agent-docs/extension-points/model-provider.md +15 -0
  13. package/agent-docs/extension-points/object-store.md +15 -0
  14. package/agent-docs/extension-points/sandbox.md +15 -0
  15. package/agent-docs/extension-points/secret-store.md +15 -0
  16. package/agent-docs/extension-points/web-fetch.md +15 -0
  17. package/agent-docs/extension-points/web-search.md +15 -0
  18. package/agent-docs/extensions.md +63 -0
  19. package/agent-docs/plugin-format.md +60 -0
  20. package/agent-docs/skills.md +44 -0
  21. package/agent-docs/tool-naming.md +21 -0
  22. package/dist/agent-template.d.ts +85 -0
  23. package/dist/agent-template.js +361 -2
  24. package/dist/config-ui.d.ts +131 -0
  25. package/dist/config-ui.js +602 -0
  26. package/dist/ctx.d.ts +3 -1
  27. package/dist/directory.d.ts +11 -2
  28. package/dist/extension-point.d.ts +9 -0
  29. package/dist/extension-point.js +17 -12
  30. package/dist/file-preview.d.ts +8 -0
  31. package/dist/hooks.d.ts +14 -4
  32. package/dist/index.d.ts +4 -1
  33. package/dist/index.js +4 -1
  34. package/dist/marketplace.d.ts +29 -2
  35. package/dist/marketplace.js +30 -2
  36. package/dist/org-access.d.ts +27 -0
  37. package/dist/org-access.js +35 -0
  38. package/dist/plugin-manifest.d.ts +25 -2
  39. package/dist/plugin-manifest.js +20 -1
  40. package/dist/sandbox.d.ts +140 -60
  41. package/dist/sandbox.js +30 -6
  42. package/dist/secret.d.ts +4 -1
  43. package/dist/secret.js +5 -2
  44. package/dist/skill-invocation.d.ts +8 -5
  45. package/dist/skill-invocation.js +17 -8
  46. package/dist/skill.d.ts +20 -3
  47. package/dist/skill.js +28 -6
  48. package/dist/tool.d.ts +8 -0
  49. package/dist/ui.d.ts +8 -0
  50. package/dist/web.d.ts +105 -0
  51. package/dist/web.js +135 -0
  52. package/package.json +4 -2
  53. package/schemas/marketplace.v1.schema.json +36 -0
  54. package/schemas/plugin.v1.schema.json +49 -4
@@ -1,21 +1,26 @@
1
- const info = (cardinality, brokered = false, orgScoped = false) => ({
1
+ const info = (majors, description, cardinality, brokered = false, orgScoped = false, selection) => ({
2
+ majors,
3
+ description,
2
4
  cardinality,
3
5
  brokered,
4
6
  orgScoped,
7
+ ...(selection ? { selection } : {}),
5
8
  });
6
9
  /** The registered extension points. */
7
10
  export const EXTENSION_POINTS = {
8
- sandbox: info("single", true),
9
- "config-source": info("single"),
10
- "secret-store": info("single", true),
11
- "object-store": info("single", true),
12
- channel: info("multi", false, true),
13
- "model-provider": info("multi", true),
14
- "identity-source": info("multi"),
15
- directory: info("multi", false, true),
16
- counter: info("multi"),
17
- connector: info("multi", true),
18
- "file-preview": info("multi", false, true),
11
+ sandbox: info([2], "Runs an environment's instance and keeps its files for agents' commands (Docker, E2B).", "single", true),
12
+ "config-source": info([1], "Supplies deployment configuration from an external store such as Nacos.", "single"),
13
+ "secret-store": info([1], "Stores and resolves secrets (connector credentials, secret config) outside the database.", "single", true),
14
+ "object-store": info([1], "Stores files and artifacts in S3-compatible object storage, with presigned uploads.", "single", true),
15
+ channel: info([1], "Connects an instant-messaging platform (Slack, Lark): inbound events, replies and cards.", "multi", false, true),
16
+ "model-provider": info([1, 2], "Calls a model vendor's API and streams the result back to the runtime.", "multi", true),
17
+ "identity-source": info([1], "Signs people in through an external identity provider (SSO).", "multi"),
18
+ directory: info([1], "Syncs members and departments from an external company directory.", "multi", false, true),
19
+ counter: info([1], "Counts usage so quotas and reports can be built on it.", "multi"),
20
+ connector: info([1], "Speaks one protocol (MCP, OpenAPI) to external systems and exposes their tools to agents.", "multi", true),
21
+ "file-preview": info([1], "Renders previews of files of given types in the conversation UI.", "multi", false, true),
22
+ "web-search": info([1], "Searches the web for the WebSearch tool; each org picks a default provider.", "multi", true, true, "org-default"),
23
+ "web-fetch": info([1], "Fetches and extracts web pages for the WebFetch tool; each org picks a default provider.", "multi", true, true, "org-default"),
19
24
  };
20
25
  /** Cardinality of the registered extension points (derived from EXTENSION_POINTS). */
21
26
  export const EXTENSION_POINT_CARDINALITY = Object.fromEntries(Object.entries(EXTENSION_POINTS).map(([k, v]) => [k, v.cardinality]));
@@ -53,6 +53,14 @@ export interface FilePreviewArgs {
53
53
  * rejects a file over the previewer's `maxBytes`. The extension never gets a storage key or URL.
54
54
  */
55
55
  read(): Promise<Uint8Array>;
56
+ /**
57
+ * The extension's effective deployment config for this org: the platform's deployment config
58
+ * with the org's copy applied to the overridable fields (`orgAccess(…, "override")`), or the
59
+ * platform's config (equal to `ctx.config`) while the org follows it. Use it, not `ctx.config`,
60
+ * for work done for this org. Optional in the type so older extensions compile; the kernel
61
+ * always passes it.
62
+ */
63
+ config?: unknown;
56
64
  }
57
65
  /** Checks a `file-preview@1` provider; returns the problems (empty = valid). */
58
66
  export declare function validateFilePreviewProvider(p: unknown): string[];
package/dist/hooks.d.ts CHANGED
@@ -35,9 +35,10 @@ export interface HookScope {
35
35
  * What a model call is for: `step` is a run step's call (the agent's turn), `compaction` is the call
36
36
  * that summarizes earlier history before a step when the context grows too large, `title` is the call
37
37
  * that writes a conversation's title after its first completed run (no run: `runId` is "", `step` 0,
38
- * `attempt` 1).
38
+ * `attempt` 1), `web_fetch` is the call that extracts an answer from a fetched web page for the
39
+ * `WebFetch` tool.
39
40
  */
40
- export type ModelCallPurpose = "step" | "compaction" | "title";
41
+ export type ModelCallPurpose = "step" | "compaction" | "title" | "web_fetch";
41
42
  /** What a reported model call was for (the same values as {@link ModelCallPurpose}). */
42
43
  export type UsagePurpose = ModelCallPurpose;
43
44
  /**
@@ -168,9 +169,18 @@ export type HookVerdict = void | {
168
169
  };
169
170
  /**
170
171
  * A hook handler. The payload is a deep-frozen copy (mutating it throws). `signal` aborts when the
171
- * hook runs past `hooks.timeoutMs` or the call it observes is aborted.
172
+ * hook runs past `hooks.timeoutMs` or the call it observes is aborted. `call.config` is the
173
+ * extension's effective deployment config for the payload's org (the platform's config with the
174
+ * org's copy applied to the overridable fields, or `ctx.config` while the org follows it); the
175
+ * kernel always passes it, and never calls a hook for an org where that config is not valid
176
+ * (observe hooks are skipped, policy hooks deny).
172
177
  */
173
- export type HookHandler<K extends AvailableHook> = (payload: HookPayloads[K], signal: AbortSignal) => Promise<HookVerdict> | HookVerdict;
178
+ export type HookHandler<K extends AvailableHook> = (payload: HookPayloads[K], signal: AbortSignal, call?: HookCall) => Promise<HookVerdict> | HookVerdict;
179
+ /** The per-call context of a hook handler (see {@link HookHandler}). */
180
+ export interface HookCall {
181
+ /** The extension's effective deployment config for the payload's org. */
182
+ config: unknown;
183
+ }
174
184
  /**
175
185
  * - `policy` (only on {@link POLICY_HOOKS}): policy hooks run serially by plugin name (registration
176
186
  * order within a plugin) before the call; the first `{ deny }`, error or timeout blocks the call and
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- export declare const SDK_VERSION = "0.1.0-rc.1";
1
+ export declare const SDK_VERSION = "0.1.0-rc.3";
2
2
  export * from "./agent-template.js";
3
3
  export * from "./bootstrap.js";
4
4
  export * from "./cards.js";
5
5
  export * from "./channel.js";
6
+ export * from "./config-ui.js";
6
7
  export * from "./connector.js";
7
8
  export * from "./connector-auth.js";
8
9
  export * from "./ctx.js";
@@ -15,6 +16,7 @@ export * from "./identity.js";
15
16
  export * from "./marketplace.js";
16
17
  export * from "./model.js";
17
18
  export * from "./model-params.js";
19
+ export * from "./org-access.js";
18
20
  export * from "./permissions.js";
19
21
  export * from "./plugin-manifest.js";
20
22
  export * from "./plugin-package.js";
@@ -26,3 +28,4 @@ export * from "./storage.js";
26
28
  export * from "./tool.js";
27
29
  export * from "./tool-key.js";
28
30
  export * from "./ui.js";
31
+ export * from "./web.js";
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- export const SDK_VERSION = "0.1.0-rc.1";
1
+ export const SDK_VERSION = "0.1.0-rc.3";
2
2
  export * from "./agent-template.js";
3
3
  export * from "./bootstrap.js";
4
4
  export * from "./cards.js";
5
5
  export * from "./channel.js";
6
+ export * from "./config-ui.js";
6
7
  export * from "./connector.js";
7
8
  export * from "./connector-auth.js";
8
9
  export * from "./ctx.js";
@@ -15,6 +16,7 @@ export * from "./identity.js";
15
16
  export * from "./marketplace.js";
16
17
  export * from "./model.js";
17
18
  export * from "./model-params.js";
19
+ export * from "./org-access.js";
18
20
  export * from "./permissions.js";
19
21
  export * from "./plugin-manifest.js";
20
22
  export * from "./plugin-package.js";
@@ -26,3 +28,4 @@ export * from "./storage.js";
26
28
  export * from "./tool.js";
27
29
  export * from "./tool-key.js";
28
30
  export * from "./ui.js";
31
+ export * from "./web.js";
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
2
  /**
3
3
  * The marketplace manifest `marketplace.json` (package ecosystem spec §5.2): a list of plugins,
4
- * each with one source. One entry is one version: a marketplace that offers several versions lists
5
- * the plugin once per version, so `(name, marketplaceEntryVersion)` is unique in a manifest.
4
+ * each with one source. One entry is one version. `capybara-plugin marketplace --write` lists one
5
+ * entry per plugin, its current version (plugin component management spec §13.2); readers still
6
+ * accept several versions of a plugin (marketplaces written in the older form), so
7
+ * `(name, marketplaceEntryVersion)` is unique in a manifest.
6
8
  *
7
9
  * Sources: a same-repo `path`, a `git` repo, an `npm` package (exact version + sha512, downloads
8
10
  * are verified against it, never npm metadata) or an `archive` URL (sha512 or sha256 SRI).
@@ -19,6 +21,10 @@ export declare const MARKETPLACE_MANIFEST_SCHEMA_VERSION = 1;
19
21
  export declare const MARKETPLACE_LIMITS: {
20
22
  readonly nameChars: 100;
21
23
  readonly summaryChars: 280;
24
+ /** Characters of each `displayName` language (as `plugin.json`'s). */
25
+ readonly displayNameChars: 100;
26
+ /** Characters of a `deprecated.reason`. */
27
+ readonly deprecationReasonChars: 280;
22
28
  readonly plugins: 5000;
23
29
  readonly permissionKeys: 500;
24
30
  readonly urlChars: 2000;
@@ -53,6 +59,11 @@ export interface MarketplaceEntry {
53
59
  en: string;
54
60
  zh?: string;
55
61
  };
62
+ /** Display name (the plugin's `plugin.json` `displayName`); lists show it, else the name. */
63
+ displayName?: {
64
+ zh: string;
65
+ en: string;
66
+ };
56
67
  /** Exact semver; defaults to the npm source's version, otherwise the plugin's own. */
57
68
  version?: string;
58
69
  /** Exact semver. */
@@ -64,6 +75,15 @@ export interface MarketplaceEntry {
64
75
  };
65
76
  /** https URL. */
66
77
  homepage?: string;
78
+ /**
79
+ * This version is deprecated (`true`, or with a reason shown to admins): browsing lists it under
80
+ * the plugin's other versions, never as the one to import, and the kernel refuses importing it
81
+ * (`marketplace.entry_deprecated`). Set by hand on the entry by the marketplace maintainer;
82
+ * `capybara-plugin marketplace --write` keeps it. `false` is the same as absent.
83
+ */
84
+ deprecated?: boolean | {
85
+ reason?: string;
86
+ };
67
87
  source: MarketplaceSource;
68
88
  }
69
89
  /** `marketplace.json`. */
@@ -97,3 +117,10 @@ export declare function parseMarketplaceManifest(json: unknown, opts?: Marketpla
97
117
  };
98
118
  /** The entry's version: `version`, else the npm source's version, else `null` (the plugin's own). */
99
119
  export declare function marketplaceEntryVersion(e: MarketplaceEntry): string | null;
120
+ /**
121
+ * The entry's deprecation (see {@link MarketplaceEntry.deprecated}): `null` when it is not
122
+ * deprecated (absent or `false`), else its reason (`null` without one).
123
+ */
124
+ export declare function marketplaceEntryDeprecation(e: MarketplaceEntry): {
125
+ reason: string | null;
126
+ } | null;
@@ -2,8 +2,10 @@ import { z } from "zod";
2
2
  import { CATALOG_ITEM_NAME_RE, isSafePackagePath, NPM_PACKAGE_NAME_RE, SEMVER_VERSION_RE, SHA512_INTEGRITY_RE, } from "./plugin-package.js";
3
3
  /**
4
4
  * The marketplace manifest `marketplace.json` (package ecosystem spec §5.2): a list of plugins,
5
- * each with one source. One entry is one version: a marketplace that offers several versions lists
6
- * the plugin once per version, so `(name, marketplaceEntryVersion)` is unique in a manifest.
5
+ * each with one source. One entry is one version. `capybara-plugin marketplace --write` lists one
6
+ * entry per plugin, its current version (plugin component management spec §13.2); readers still
7
+ * accept several versions of a plugin (marketplaces written in the older form), so
8
+ * `(name, marketplaceEntryVersion)` is unique in a manifest.
7
9
  *
8
10
  * Sources: a same-repo `path`, a `git` repo, an `npm` package (exact version + sha512, downloads
9
11
  * are verified against it, never npm metadata) or an `archive` URL (sha512 or sha256 SRI).
@@ -20,6 +22,10 @@ export const MARKETPLACE_MANIFEST_SCHEMA_VERSION = 1;
20
22
  export const MARKETPLACE_LIMITS = {
21
23
  nameChars: 100,
22
24
  summaryChars: 280,
25
+ /** Characters of each `displayName` language (as `plugin.json`'s). */
26
+ displayNameChars: 100,
27
+ /** Characters of a `deprecated.reason`. */
28
+ deprecationReasonChars: 280,
23
29
  plugins: 5000,
24
30
  permissionKeys: 500,
25
31
  urlChars: 2000,
@@ -123,6 +129,12 @@ function buildSchema(opts) {
123
129
  en: z.string().min(1).max(M.summaryChars),
124
130
  zh: z.string().min(1).max(M.summaryChars).optional(),
125
131
  }),
132
+ displayName: z
133
+ .object({
134
+ zh: z.string().min(1).max(M.displayNameChars),
135
+ en: z.string().min(1).max(M.displayNameChars),
136
+ })
137
+ .optional(),
126
138
  version: exactVersion("version").optional(),
127
139
  minKernel: exactVersion("minKernel").optional(),
128
140
  permissions: z
@@ -132,6 +144,12 @@ function buildSchema(opts) {
132
144
  })
133
145
  .optional(),
134
146
  homepage: urlString(HTTPS_URL_RE).optional(),
147
+ deprecated: z
148
+ .union([
149
+ z.boolean(),
150
+ z.object({ reason: z.string().min(1).max(M.deprecationReasonChars).optional() }),
151
+ ])
152
+ .optional(),
135
153
  source,
136
154
  });
137
155
  return z
@@ -254,3 +272,13 @@ export function marketplaceEntryVersion(e) {
254
272
  return e.source.npm.version;
255
273
  return null;
256
274
  }
275
+ /**
276
+ * The entry's deprecation (see {@link MarketplaceEntry.deprecated}): `null` when it is not
277
+ * deprecated (absent or `false`), else its reason (`null` without one).
278
+ */
279
+ export function marketplaceEntryDeprecation(e) {
280
+ const d = e.deprecated;
281
+ if (d === undefined || d === false)
282
+ return null;
283
+ return { reason: d === true ? null : (d.reason ?? null) };
284
+ }
@@ -0,0 +1,27 @@
1
+ import type { z } from "zod";
2
+ /**
3
+ * How an org may treat a deployment config field: `readonly` (the org sees it, never sets it) or
4
+ * `override` (an org may set its own value). A field without a level is deployment-only.
5
+ */
6
+ export type OrgAccess = "readonly" | "override";
7
+ /**
8
+ * Marks a deployment config field with an org access level (Zod 4 meta `{ org: access }`), merging
9
+ * with existing meta (`secret`, `hot`, `ui`), so `orgAccess(secret(x), …)` and
10
+ * `secret(orgAccess(x, …))` are the same. `configJsonSchema` publishes it as `"x-org"`.
11
+ *
12
+ * The author's promise for `override`: the extension reads the field from the per-call `config`
13
+ * the kernel passes with each request, not from `ctx.config`, and the field may be absent from
14
+ * `ctx.config` (the deployment value is the default, which an org may replace per call).
15
+ *
16
+ * The per-call config of an org that has its own copy still contains the platform's hidden
17
+ * (platform-only) secrets, which an org cannot set; it never contains an overridable platform
18
+ * secret. Never send a hidden secret to an endpoint taken from an overridable field: an org could
19
+ * point that endpoint at a server it controls.
20
+ */
21
+ export declare function orgAccess<T extends z.ZodTypeAny>(schema: T, access: OrgAccess): T;
22
+ /**
23
+ * The org access level of a schema's meta, or undefined when it has none. It sees through
24
+ * `.optional()`, `.default()`, `.nullable()`, `.prefault()`, `.catch()` and `.readonly()` (the
25
+ * outermost level wins), so it agrees with the `"x-org"` that `configJsonSchema` publishes.
26
+ */
27
+ export declare function orgAccessOf(schema: z.ZodTypeAny): OrgAccess | undefined;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Marks a deployment config field with an org access level (Zod 4 meta `{ org: access }`), merging
3
+ * with existing meta (`secret`, `hot`, `ui`), so `orgAccess(secret(x), …)` and
4
+ * `secret(orgAccess(x, …))` are the same. `configJsonSchema` publishes it as `"x-org"`.
5
+ *
6
+ * The author's promise for `override`: the extension reads the field from the per-call `config`
7
+ * the kernel passes with each request, not from `ctx.config`, and the field may be absent from
8
+ * `ctx.config` (the deployment value is the default, which an org may replace per call).
9
+ *
10
+ * The per-call config of an org that has its own copy still contains the platform's hidden
11
+ * (platform-only) secrets, which an org cannot set; it never contains an overridable platform
12
+ * secret. Never send a hidden secret to an endpoint taken from an overridable field: an org could
13
+ * point that endpoint at a server it controls.
14
+ */
15
+ export function orgAccess(schema, access) {
16
+ return schema.meta({ ...(schema.meta() ?? {}), org: access });
17
+ }
18
+ /** The wrappers a field's org level is read through, as `configJsonSchema` inherits meta. */
19
+ const WRAPPERS = new Set(["default", "prefault", "optional", "nullable", "readonly", "catch"]);
20
+ /**
21
+ * The org access level of a schema's meta, or undefined when it has none. It sees through
22
+ * `.optional()`, `.default()`, `.nullable()`, `.prefault()`, `.catch()` and `.readonly()` (the
23
+ * outermost level wins), so it agrees with the `"x-org"` that `configJsonSchema` publishes.
24
+ */
25
+ export function orgAccessOf(schema) {
26
+ let node = schema;
27
+ while (node) {
28
+ const v = node.meta()?.org;
29
+ if (v === "readonly" || v === "override")
30
+ return v;
31
+ const def = node._zod.def;
32
+ node = WRAPPERS.has(def.type) ? def.innerType : undefined;
33
+ }
34
+ return undefined;
35
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { type JsonSchema } from "./config-ui.js";
2
3
  import { type ConnectorTemplateSpec, type LocalizedName, type StaticExtensionManifest } from "./plugin-package.js";
3
4
  /**
4
5
  * The plugin manifest `.capybara-plugin/plugin.json` (package ecosystem spec §4.1). A plugin is a
@@ -74,7 +75,29 @@ export interface PluginExtensionSection {
74
75
  migrations?: string;
75
76
  /** `./ui`. */
76
77
  ui?: string;
78
+ /**
79
+ * The config JSON Schemas (`configJsonSchema`, with `x-ui` hints), written by the packer so an
80
+ * extension not loaded on a replica still gets its form; each ≤
81
+ * {@link CONFIG_UI_LIMITS}`.configSchemaBytes`. Not part of the `manifest_mismatch` comparison.
82
+ */
83
+ configSchemas?: {
84
+ deployment?: JsonSchema;
85
+ org?: JsonSchema;
86
+ };
77
87
  }
88
+ /**
89
+ * A plugin's description: a string (English) or `{ en, zh? }`, each ≤ 1024 characters. Read it
90
+ * through {@link pluginDescription}.
91
+ */
92
+ export type PluginDescription = string | {
93
+ en: string;
94
+ zh?: string;
95
+ };
96
+ /** A plugin description as `{ en, zh? }` (a plain string is the English text). */
97
+ export declare function pluginDescription(d: PluginDescription): {
98
+ en: string;
99
+ zh?: string;
100
+ };
78
101
  /** `.capybara-plugin/plugin.json`. */
79
102
  export interface PluginManifest {
80
103
  $schema?: string;
@@ -84,8 +107,8 @@ export interface PluginManifest {
84
107
  /** Exact semver version. */
85
108
  version: string;
86
109
  publisher: string;
87
- /** ≤ 1024 characters. */
88
- description: string;
110
+ /** A string (English) or `{ en, zh? }`, each ≤ 1024 characters; see {@link pluginDescription}. */
111
+ description: PluginDescription;
89
112
  displayName?: LocalizedName;
90
113
  /** `./…` .svg / .png inside the plugin. */
91
114
  icon?: string;
@@ -1,6 +1,7 @@
1
1
  import semver from "semver";
2
2
  import { z } from "zod";
3
3
  import { AGENT_TEMPLATE_LIMITS } from "./agent-template.js";
4
+ import { CONFIG_UI_LIMITS } from "./config-ui.js";
4
5
  import { defineExtension, EXTENSION_NAME_RE } from "./define-extension.js";
5
6
  import { CATALOG_ITEM_NAME_RE, CONNECTOR_TEMPLATE_LIMITS, isSafePackagePath, SEMVER_VERSION_RE, validateConnectorTemplate, } from "./plugin-package.js";
6
7
  import { hasUpperBound } from "./semver-range.js";
@@ -47,6 +48,10 @@ export const PLUGIN_MANIFEST_LIMITS = {
47
48
  displayNameChars: 100,
48
49
  pathChars: 512,
49
50
  };
51
+ /** A plugin description as `{ en, zh? }` (a plain string is the English text). */
52
+ export function pluginDescription(d) {
53
+ return typeof d === "string" ? { en: d } : { en: d.en, ...(d.zh !== undefined && { zh: d.zh }) };
54
+ }
50
55
  const L = PLUGIN_MANIFEST_LIMITS;
51
56
  const MAX_ERRORS = 50;
52
57
  const ICON_RE = /\.(?:svg|png)$/i;
@@ -87,6 +92,11 @@ const staticExtensionManifest = z.strictObject({
87
92
  ui: z.array(z.record(z.string(), z.unknown())).optional(),
88
93
  config: z.strictObject({ deployment: configFields, org: configFields }),
89
94
  });
95
+ const configSchemaJson = z
96
+ .record(z.string(), z.unknown())
97
+ .refine((v) => new TextEncoder().encode(JSON.stringify(v)).length <= CONFIG_UI_LIMITS.configSchemaBytes, {
98
+ message: `must be at most ${CONFIG_UI_LIMITS.configSchemaBytes} bytes of JSON (${CONFIG_UI_LIMITS.configSchemaBytes / 1024} KiB)`,
99
+ });
90
100
  const extensionSection = z.strictObject({
91
101
  entry: dotPath("entry").refine((p) => p.endsWith(".mjs"), {
92
102
  message: "entry must be a .mjs file",
@@ -94,6 +104,9 @@ const extensionSection = z.strictObject({
94
104
  manifest: staticExtensionManifest,
95
105
  migrations: dotPath("migrations").optional(),
96
106
  ui: dotPath("ui").optional(),
107
+ configSchemas: z
108
+ .strictObject({ deployment: configSchemaJson.optional(), org: configSchemaJson.optional() })
109
+ .optional(),
97
110
  });
98
111
  const schema = z
99
112
  .strictObject({
@@ -107,7 +120,13 @@ const schema = z
107
120
  .max(256)
108
121
  .regex(SEMVER_VERSION_RE, { message: "version must be an exact semver version" }),
109
122
  publisher: text(L.publisherChars, "publisher"),
110
- description: text(L.descriptionChars, "description"),
123
+ description: z.union([
124
+ text(L.descriptionChars, "description"),
125
+ z.strictObject({
126
+ en: text(L.descriptionChars, "description.en"),
127
+ zh: text(L.descriptionChars, "description.zh").optional(),
128
+ }),
129
+ ]),
111
130
  displayName: localized.optional(),
112
131
  icon: dotPath("icon")
113
132
  .regex(ICON_RE, { message: "icon must be a .svg or .png file" })