@capybara-hq/plugin-sdk 0.1.0-rc.2 → 0.1.0-rc.4

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 +2 -1
  25. package/dist/config-ui.js +25 -7
  26. package/dist/ctx.d.ts +65 -1
  27. package/dist/directory.d.ts +11 -2
  28. package/dist/extension-point.d.ts +4 -0
  29. package/dist/extension-point.js +16 -14
  30. package/dist/file-preview.d.ts +8 -0
  31. package/dist/hooks.d.ts +73 -14
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +2 -1
  34. package/dist/marketplace.d.ts +29 -2
  35. package/dist/marketplace.js +30 -2
  36. package/dist/model.d.ts +60 -2
  37. package/dist/model.js +22 -0
  38. package/dist/org-access.d.ts +27 -0
  39. package/dist/org-access.js +35 -0
  40. package/dist/permissions.d.ts +4 -1
  41. package/dist/plugin-manifest.d.ts +15 -2
  42. package/dist/plugin-manifest.js +11 -1
  43. package/dist/sandbox.d.ts +140 -60
  44. package/dist/sandbox.js +30 -6
  45. package/dist/skill-invocation.d.ts +8 -5
  46. package/dist/skill-invocation.js +17 -8
  47. package/dist/skill.d.ts +20 -3
  48. package/dist/skill.js +28 -6
  49. package/dist/tool.d.ts +8 -0
  50. package/dist/ui.d.ts +8 -0
  51. package/dist/web.d.ts +30 -5
  52. package/package.json +4 -2
  53. package/schemas/marketplace.v1.schema.json +36 -0
  54. package/schemas/plugin.v1.schema.json +29 -4
@@ -15,11 +15,20 @@ export interface Directory {
15
15
  * treats the directory as "not configured": the hourly scan, `directory_changed` and the
16
16
  * pre-removal recheck are skipped silently (no job, no audit, no warning), and a manual sync by an
17
17
  * admin returns 409 `directory.not_configured`. When not implemented, any config that satisfies
18
- * the schema counts as configured. Must be a pure function (no requests, never throws).
18
+ * the schema counts as configured. Must be a pure function (no requests, never throws). `config`
19
+ * is the effective deployment config for the org (see `listMembers`' `config`).
19
20
  */
20
- configured?(orgConfig: unknown): boolean;
21
+ configured?(orgConfig: unknown, config?: unknown): boolean;
21
22
  listMembers(args: {
22
23
  orgConfig: unknown;
24
+ /**
25
+ * The extension's effective deployment config for this org: the platform's deployment config
26
+ * with the org's copy applied to the overridable fields (`orgAccess(…, "override")`), or the
27
+ * platform's config (equal to `ctx.config`) while the org follows it. Use it, not `ctx.config`,
28
+ * for work done for this org. Optional in the type so older extensions compile; the kernel
29
+ * always passes it.
30
+ */
31
+ config?: unknown;
23
32
  cursor?: string;
24
33
  signal: AbortSignal;
25
34
  }): Promise<{
@@ -4,11 +4,15 @@ export type ExtensionPointRef = `${string}@${number}`;
4
4
  export type Cardinality = "single" | "multi";
5
5
  /** Registry entry of an extension point. */
6
6
  export interface ExtensionPointInfo {
7
+ /** What the extension point is for, in one sentence (served to agents by the Platform tool). */
8
+ description: string;
7
9
  cardinality: Cardinality;
8
10
  /** Brokered by the platform API (extensions may not `requires` it; they use it only through `ctx`). */
9
11
  brokered: boolean;
10
12
  /** Enabled per org (consumers may pass `orgId` to check that the org enabled the provider). */
11
13
  orgScoped: boolean;
14
+ /** The majors of this point the kernel accepts; a plugin providing any other major is refused. */
15
+ majors: readonly number[];
12
16
  /**
13
17
  * How the kernel picks one provider among several: `org-default` means each org chooses its
14
18
  * default provider (absent: no selection, every enabled provider is used).
@@ -1,4 +1,6 @@
1
- const info = (cardinality, brokered = false, orgScoped = false, selection) => ({
1
+ const info = (majors, description, cardinality, brokered = false, orgScoped = false, selection) => ({
2
+ majors,
3
+ description,
2
4
  cardinality,
3
5
  brokered,
4
6
  orgScoped,
@@ -6,19 +8,19 @@ const info = (cardinality, brokered = false, orgScoped = false, selection) => ({
6
8
  });
7
9
  /** The registered extension points. */
8
10
  export const EXTENSION_POINTS = {
9
- sandbox: info("single", true),
10
- "config-source": info("single"),
11
- "secret-store": info("single", true),
12
- "object-store": info("single", true),
13
- channel: info("multi", false, true),
14
- "model-provider": info("multi", true),
15
- "identity-source": info("multi"),
16
- directory: info("multi", false, true),
17
- counter: info("multi"),
18
- connector: info("multi", true),
19
- "file-preview": info("multi", false, true),
20
- "web-search": info("multi", true, true, "org-default"),
21
- "web-fetch": info("multi", true, true, "org-default"),
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"),
22
24
  };
23
25
  /** Cardinality of the registered extension points (derived from EXTENSION_POINTS). */
24
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
@@ -39,8 +39,12 @@ export interface HookScope {
39
39
  * `WebFetch` tool.
40
40
  */
41
41
  export type ModelCallPurpose = "step" | "compaction" | "title" | "web_fetch";
42
- /** What a reported model call was for (the same values as {@link ModelCallPurpose}). */
43
- export type UsagePurpose = ModelCallPurpose;
42
+ /**
43
+ * What a reported or requested model call was for: the {@link ModelCallPurpose} values, or
44
+ * `embedding` for an embedding call an extension made through `ctx.models` (`usage.report`, and
45
+ * `model.request` with {@link EmbeddingRequestHookPayload}; never `model.response`).
46
+ */
47
+ export type UsagePurpose = ModelCallPurpose | "embedding";
44
48
  /**
45
49
  * An image or document of a `model.request` payload. Hook payloads never carry media bytes or
46
50
  * attachment ids: each image or document block is replaced by this placeholder.
@@ -74,14 +78,15 @@ export type HookModelRequest = Omit<ModelRequest, "messages"> & {
74
78
  messages: HookModelMessage[];
75
79
  };
76
80
  /**
77
- * `model.request`: before a model call is sent. A policy hook may deny it. Fires once per attempt of a
78
- * step (`purpose: "step"`), once for each compaction call (`purpose: "compaction"`, `attempt` 1) and
81
+ * `model.request` of a chat model call (see {@link ModelRequestHookPayload}): once per attempt of a
82
+ * step (`purpose: "step"`), once for each compaction call (`purpose: "compaction"`, `attempt` 1),
79
83
  * once for each conversation title call (`purpose: "title"`, `runId` "", `step` 0, `attempt` 1; a
80
- * denial skips the title and is recorded only in the audit log).
84
+ * denial skips the title and is recorded only in the audit log) and once for each `WebFetch`
85
+ * extraction call (`purpose: "web_fetch"`).
81
86
  * `request` is the request as sent, except that images and documents are placeholders
82
87
  * ({@link HookMediaPlaceholder}): no media bytes, no attachment ids.
83
88
  */
84
- export interface ModelRequestHookPayload extends HookScope {
89
+ export interface ModelCallRequestHookPayload extends HookScope {
85
90
  purpose: ModelCallPurpose;
86
91
  step: number;
87
92
  attempt: number;
@@ -89,6 +94,45 @@ export interface ModelRequestHookPayload extends HookScope {
89
94
  model: string;
90
95
  request: Readonly<HookModelRequest>;
91
96
  }
97
+ /**
98
+ * `model.request` of an embedding call an extension made through `ctx.models.embed`
99
+ * (`purpose: "embedding"`): fired once per `embed` call, before the first batch is sent, with every
100
+ * text of the call (the kernel splits them into batches afterwards). A call with no input sends
101
+ * nothing and fires nothing. There is no conversation: the conversation fields of a chat model call
102
+ * (`accountId`, `agentId`, `agentVersion`, `conversationId`, `runId`, `parent`, `step`, `attempt`,
103
+ * `request`) are absent. A policy denial makes `embed` reject (`model.hook_denied`); nothing is sent
104
+ * to the provider and no usage is recorded.
105
+ */
106
+ export interface EmbeddingRequestHookPayload {
107
+ purpose: "embedding";
108
+ orgId: string;
109
+ /** The extension that called `ctx.models.embed`. */
110
+ extension: string;
111
+ /** The embedding purpose the extension called with (one it declared in `permissions.models`). */
112
+ embeddingPurpose: string;
113
+ provider: string;
114
+ model: string;
115
+ /** The texts of the call, in order (text only). */
116
+ input: readonly string[];
117
+ /** The dimensions the extension asked for, when it asked. */
118
+ dimensions?: number;
119
+ /** The input type the extension gave, when it gave one. */
120
+ inputType?: "query" | "document";
121
+ accountId?: undefined;
122
+ agentId?: undefined;
123
+ agentVersion?: undefined;
124
+ conversationId?: undefined;
125
+ runId?: undefined;
126
+ parent?: undefined;
127
+ }
128
+ /**
129
+ * `model.request`: before a model call is sent. A policy hook may deny it. Either a chat model call
130
+ * ({@link ModelCallRequestHookPayload}: steps, compaction, titles, `WebFetch`) or an extension's
131
+ * embedding call ({@link EmbeddingRequestHookPayload}, `purpose: "embedding"`); tell them apart by
132
+ * `purpose`. Handlers must accept the absent conversation fields of an embedding call (a policy
133
+ * hook that throws on one denies the call). Payloads carry no media bytes.
134
+ */
135
+ export type ModelRequestHookPayload = ModelCallRequestHookPayload | EmbeddingRequestHookPayload;
92
136
  /**
93
137
  * `model.response`: after a sent model call finished or failed, with its usage when known (step
94
138
  * attempts, compaction and title calls, see `purpose`). Not fired for a call the user aborted (the run was
@@ -138,12 +182,18 @@ export interface UsageReportPayload {
138
182
  */
139
183
  purpose: UsagePurpose;
140
184
  orgId: string;
141
- accountId: string;
142
- agentId: string;
143
- conversationId: string;
144
- /** A sub-agent's calls report `chat`; `parent` tells them apart. */
145
- conversationKind: "chat" | "eval";
146
- runId: string;
185
+ /** Absent for `embedding`. */
186
+ accountId?: string;
187
+ /** Absent for `embedding`. */
188
+ agentId?: string;
189
+ /** Absent for `embedding`. */
190
+ conversationId?: string;
191
+ /** A sub-agent's calls report `chat`; `parent` tells them apart. Absent for `embedding`. */
192
+ conversationKind?: "chat" | "eval";
193
+ /** Absent for `embedding`. */
194
+ runId?: string;
195
+ /** The calling extension; set for `embedding`. */
196
+ extension?: string;
147
197
  /** Set when the call belongs to a sub-agent: where it was dispatched from. */
148
198
  parent?: HookParentScope;
149
199
  provider: string;
@@ -169,9 +219,18 @@ export type HookVerdict = void | {
169
219
  };
170
220
  /**
171
221
  * A hook handler. The payload is a deep-frozen copy (mutating it throws). `signal` aborts when the
172
- * hook runs past `hooks.timeoutMs` or the call it observes is aborted.
222
+ * hook runs past `hooks.timeoutMs` or the call it observes is aborted. `call.config` is the
223
+ * extension's effective deployment config for the payload's org (the platform's config with the
224
+ * org's copy applied to the overridable fields, or `ctx.config` while the org follows it); the
225
+ * kernel always passes it, and never calls a hook for an org where that config is not valid
226
+ * (observe hooks are skipped, policy hooks deny).
173
227
  */
174
- export type HookHandler<K extends AvailableHook> = (payload: HookPayloads[K], signal: AbortSignal) => Promise<HookVerdict> | HookVerdict;
228
+ export type HookHandler<K extends AvailableHook> = (payload: HookPayloads[K], signal: AbortSignal, call?: HookCall) => Promise<HookVerdict> | HookVerdict;
229
+ /** The per-call context of a hook handler (see {@link HookHandler}). */
230
+ export interface HookCall {
231
+ /** The extension's effective deployment config for the payload's org. */
232
+ config: unknown;
233
+ }
175
234
  /**
176
235
  * - `policy` (only on {@link POLICY_HOOKS}): policy hooks run serially by plugin name (registration
177
236
  * order within a plugin) before the call; the first `{ deny }`, error or timeout blocks the call and
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "0.1.0-rc.2";
1
+ export declare const SDK_VERSION = "0.1.0-rc.4";
2
2
  export * from "./agent-template.js";
3
3
  export * from "./bootstrap.js";
4
4
  export * from "./cards.js";
@@ -16,6 +16,7 @@ export * from "./identity.js";
16
16
  export * from "./marketplace.js";
17
17
  export * from "./model.js";
18
18
  export * from "./model-params.js";
19
+ export * from "./org-access.js";
19
20
  export * from "./permissions.js";
20
21
  export * from "./plugin-manifest.js";
21
22
  export * from "./plugin-package.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export const SDK_VERSION = "0.1.0-rc.2";
1
+ export const SDK_VERSION = "0.1.0-rc.4";
2
2
  export * from "./agent-template.js";
3
3
  export * from "./bootstrap.js";
4
4
  export * from "./cards.js";
@@ -16,6 +16,7 @@ export * from "./identity.js";
16
16
  export * from "./marketplace.js";
17
17
  export * from "./model.js";
18
18
  export * from "./model-params.js";
19
+ export * from "./org-access.js";
19
20
  export * from "./permissions.js";
20
21
  export * from "./plugin-manifest.js";
21
22
  export * from "./plugin-package.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
+ }
package/dist/model.d.ts CHANGED
@@ -4,9 +4,32 @@
4
4
  * 见 `model-params.ts`);旧契约 `model-provider@1`(`ModelProviderV1` 等)由内核经适配层继续支持。
5
5
  */
6
6
  import type { Effort, ModelParameter, ParamValue } from "./model-params.js";
7
+ import type { LocalizedName } from "./plugin-package.js";
8
+ /** What a model does. Absent on a `ModelInfo` means `chat`. */
9
+ export type ModelKind = "chat" | "embedding" | "rerank" | "image_generation" | "video_generation" | "speech_to_text" | "text_to_speech";
10
+ /** Embedding facts of a model whose `kind` is `embedding`. */
11
+ export interface ModelEmbeddingInfo {
12
+ /** The output dimensions the model can produce (a single entry when not adjustable). */
13
+ dimensions: number[];
14
+ defaultDimensions: number;
15
+ /** Most tokens one input may hold. */
16
+ maxInputTokens: number;
17
+ /** Most inputs one `embed` call may carry. */
18
+ maxBatch: number;
19
+ }
7
20
  export interface ModelInfo {
8
21
  id: string;
9
22
  displayName: string;
23
+ /** Defaults to `chat`. Chat-only fields below stay required for every kind. */
24
+ kind?: ModelKind;
25
+ /** Who made the model (display only). */
26
+ publisher?: string;
27
+ /** Display only. */
28
+ description?: LocalizedName | string;
29
+ /** ISO date after which the model is retired (display only). */
30
+ retiresAt?: string;
31
+ /** Set when `kind` is `embedding`. */
32
+ embedding?: ModelEmbeddingInfo;
10
33
  contextWindow: number;
11
34
  maxOutputTokens: number;
12
35
  capabilities: {
@@ -206,14 +229,49 @@ export declare class ModelError extends Error {
206
229
  /** rate_limited / overloaded / server / timeout / network 为 true。 */
207
230
  get retryable(): boolean;
208
231
  }
232
+ /**
233
+ * Options of a model call made for an organization.
234
+ * `config` is the extension's effective deployment config for this org: the platform's deployment
235
+ * config with the org's copy applied to the overridable fields (`orgAccess(…, "override")`), or the
236
+ * platform's config (equal to `ctx.config`) while the org follows it. Use it, not `ctx.config`, for
237
+ * work done for this org. Optional in the type so older providers compile; the kernel always passes
238
+ * both.
239
+ */
240
+ export interface ModelCallOptions {
241
+ orgId?: string;
242
+ config?: unknown;
243
+ }
244
+ export interface EmbedRequest {
245
+ model: string;
246
+ input: string[];
247
+ /** One of the model's `embedding.dimensions`; the model's default when absent. */
248
+ dimensions?: number;
249
+ inputType?: "query" | "document";
250
+ }
251
+ export interface EmbedResult {
252
+ /** One vector per input, in input order. */
253
+ vectors: number[][];
254
+ usage: {
255
+ inputTokens: number;
256
+ };
257
+ }
258
+ /**
259
+ * Checks an embedding result against its request and the model's embedding facts. Returns null when
260
+ * valid, else the reason: wrong vector count, wrong dimension, or a non-finite number.
261
+ */
262
+ export declare function validateEmbedResult(req: EmbedRequest, info: ModelEmbeddingInfo, res: EmbedResult): string | null;
209
263
  /** `model-provider@2` 的实现。 */
210
264
  export interface ModelProvider {
211
265
  /** 每个模型的 `parameters` 须通过 `assertValidDeclarations`,否则内核不使用该供应商。 */
212
- listModels(): Promise<ModelInfo[]>;
266
+ listModels(opts?: ModelCallOptions): Promise<ModelInfo[]>;
213
267
  /** 事件序列必须以且仅以一个 message_complete 结束;出错抛 ModelError。 */
214
268
  stream(req: ModelRequest, opts: {
215
269
  signal: AbortSignal;
216
- }): AsyncIterable<ModelStreamEvent>;
270
+ } & ModelCallOptions): AsyncIterable<ModelStreamEvent>;
271
+ /** Present only on a provider that lists `embedding` models. */
272
+ embed?(req: EmbedRequest, opts: {
273
+ signal: AbortSignal;
274
+ } & ModelCallOptions): Promise<EmbedResult>;
217
275
  }
218
276
  /** `model-provider@1` 的实现(旧契约,内核经适配层接入:effort 由 `capabilities.effort` 推导)。 */
219
277
  export interface ModelProviderV1 {
package/dist/model.js CHANGED
@@ -148,5 +148,27 @@ export class ModelError extends Error {
148
148
  return isRetryableKind(this.kind);
149
149
  }
150
150
  }
151
+ /**
152
+ * Checks an embedding result against its request and the model's embedding facts. Returns null when
153
+ * valid, else the reason: wrong vector count, wrong dimension, or a non-finite number.
154
+ */
155
+ export function validateEmbedResult(req, info, res) {
156
+ if (res.vectors.length !== req.input.length) {
157
+ return `vector count ${res.vectors.length} does not match input count ${req.input.length}`;
158
+ }
159
+ const dims = req.dimensions ?? info.defaultDimensions;
160
+ for (let i = 0; i < res.vectors.length; i++) {
161
+ const v = res.vectors[i];
162
+ if (v.length !== dims) {
163
+ return `vector ${i} has dimension ${v.length}, expected ${dims}`;
164
+ }
165
+ for (const x of v) {
166
+ if (typeof x !== "number" || !Number.isFinite(x)) {
167
+ return `vector ${i} holds a non-finite number`;
168
+ }
169
+ }
170
+ }
171
+ return null;
172
+ }
151
173
  export const MODEL_PROVIDER_EXTENSION_POINT = "model-provider@1";
152
174
  export const MODEL_PROVIDER_EXTENSION_POINT_V2 = "model-provider@2";
@@ -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
+ }
@@ -22,7 +22,10 @@ export interface ExtensionPermissions {
22
22
  * on a replica, calls on these mount points fail closed there.
23
23
  */
24
24
  policyHooks?: PolicyHook[];
25
- /** 预留:模型用途,键须为 `<插件名>.<名称>`。 */
25
+ /**
26
+ * Model purposes (`ctx.models`): each key must be `<extension name>.<name>`; an org admin maps
27
+ * each to a model of its `kind`. `ctx.models.embed` / `describe` accept the `embedding` ones.
28
+ */
26
29
  models?: {
27
30
  purpose: string;
28
31
  kind: "chat" | "embedding";
@@ -85,6 +85,19 @@ export interface PluginExtensionSection {
85
85
  org?: JsonSchema;
86
86
  };
87
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
+ };
88
101
  /** `.capybara-plugin/plugin.json`. */
89
102
  export interface PluginManifest {
90
103
  $schema?: string;
@@ -94,8 +107,8 @@ export interface PluginManifest {
94
107
  /** Exact semver version. */
95
108
  version: string;
96
109
  publisher: string;
97
- /** ≤ 1024 characters. */
98
- description: string;
110
+ /** A string (English) or `{ en, zh? }`, each ≤ 1024 characters; see {@link pluginDescription}. */
111
+ description: PluginDescription;
99
112
  displayName?: LocalizedName;
100
113
  /** `./…` .svg / .png inside the plugin. */
101
114
  icon?: string;
@@ -48,6 +48,10 @@ export const PLUGIN_MANIFEST_LIMITS = {
48
48
  displayNameChars: 100,
49
49
  pathChars: 512,
50
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
+ }
51
55
  const L = PLUGIN_MANIFEST_LIMITS;
52
56
  const MAX_ERRORS = 50;
53
57
  const ICON_RE = /\.(?:svg|png)$/i;
@@ -116,7 +120,13 @@ const schema = z
116
120
  .max(256)
117
121
  .regex(SEMVER_VERSION_RE, { message: "version must be an exact semver version" }),
118
122
  publisher: text(L.publisherChars, "publisher"),
119
- 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
+ ]),
120
130
  displayName: localized.optional(),
121
131
  icon: dotPath("icon")
122
132
  .regex(ICON_RE, { message: "icon must be a .svg or .png file" })