@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
@@ -0,0 +1,602 @@
1
+ import { z } from "zod";
2
+ /** Limits of config UI hints and of the JSON Schemas a plugin package carries. */
3
+ export const CONFIG_UI_LIMITS = {
4
+ /** Characters per language of a `label` (also `unit`, `item.addLabel`). */
5
+ labelChars: 100,
6
+ /** Characters per language of a `help` (also `messages` values). */
7
+ helpChars: 500,
8
+ /** Characters (per language) of a `placeholder`. */
9
+ placeholderChars: 200,
10
+ /** UTF-8 bytes of all `x-ui` JSON of one schema. */
11
+ hintsBytes: 64 * 1024,
12
+ /** Options per node. */
13
+ options: 200,
14
+ /** Presets per node. */
15
+ presets: 20,
16
+ /** Root sections. */
17
+ sections: 20,
18
+ /**
19
+ * UTF-8 bytes of each JSON Schema in `plugin.json` `extension.configSchemas` (96 KiB: two of
20
+ * them must fit in the 256 KiB `plugin.json` text with the rest of the manifest).
21
+ */
22
+ configSchemaBytes: 96 * 1024,
23
+ };
24
+ /**
25
+ * Attaches config UI hints to a schema (zod meta `ui`), merging with existing meta (`secret`,
26
+ * `hot`, `description`). Returns the same schema type. Convention: put `ui()` outermost, after
27
+ * `.default()` / `.optional()`.
28
+ */
29
+ export function ui(schema, hints) {
30
+ return schema.meta({ ...(schema.meta() ?? {}), ui: hints });
31
+ }
32
+ /** zod types JSON Schema cannot express: emitted as `{}` with `x-unsupported`. */
33
+ const UNSUPPORTED = new Set([
34
+ "bigint",
35
+ "date",
36
+ "symbol",
37
+ "undefined",
38
+ "void",
39
+ "nan",
40
+ "map",
41
+ "set",
42
+ "function",
43
+ "promise",
44
+ "custom",
45
+ "file",
46
+ ]);
47
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
48
+ /**
49
+ * An extension config schema as a JSON Schema for forms (the input side, defaults included, no
50
+ * `$schema`):
51
+ * - meta `secret: true` becomes `"x-secret": true`, `hot: true` becomes `"x-hot": true`, `org`
52
+ * (`orgAccess`) becomes `"x-org"`, `ui`
53
+ * becomes `"x-ui"` (a JSON copy of the hints); `title` / `description` are filled from
54
+ * `x-ui.label.en` / `x-ui.help.en` when the node has none;
55
+ * - unrepresentable types become `{ "x-unsupported": true }` instead of throwing;
56
+ * - under a secret node no value is published: `default` / `examples` / `const` / `enum` and
57
+ * `x-ui.options` / `x-ui.presets` are removed from it and all its descendants.
58
+ */
59
+ export function configJsonSchema(schema) {
60
+ const out = z.toJSONSchema(schema, {
61
+ io: "input",
62
+ unrepresentable: "any",
63
+ override: (ctx) => {
64
+ const js = ctx.jsonSchema;
65
+ const def = ctx.zodSchema._zod.def;
66
+ if (js.secret === true)
67
+ js["x-secret"] = true;
68
+ delete js.secret;
69
+ if (js.hot === true)
70
+ js["x-hot"] = true;
71
+ delete js.hot;
72
+ if (js.org !== undefined)
73
+ js["x-org"] = js.org;
74
+ delete js.org;
75
+ if ("ui" in js) {
76
+ // A JSON copy: hints are data, and the scrub must never mutate the schema's meta.
77
+ const hints = JSON.parse(JSON.stringify(js.ui ?? null));
78
+ delete js.ui;
79
+ if (isObj(hints)) {
80
+ js["x-ui"] = hints;
81
+ const label = isObj(hints.label) ? hints.label.en : undefined;
82
+ const help = isObj(hints.help) ? hints.help.en : undefined;
83
+ if (js.title === undefined && typeof label === "string")
84
+ js.title = label;
85
+ if (js.description === undefined && typeof help === "string")
86
+ js.description = help;
87
+ }
88
+ }
89
+ if (UNSUPPORTED.has(def.type))
90
+ js["x-unsupported"] = true;
91
+ },
92
+ });
93
+ delete out.$schema;
94
+ scrubSecrets(out);
95
+ return out;
96
+ }
97
+ /** Keywords that would reveal values: never emitted for a secret. */
98
+ const VALUE_KEYWORDS = ["default", "examples", "const", "enum"];
99
+ /** Hints that would reveal values: never emitted for a secret. */
100
+ const VALUE_HINTS = ["options", "presets"];
101
+ /** Keywords whose value maps names to sub-schemas: their keys are field names, not keywords. */
102
+ const SCHEMA_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
103
+ /**
104
+ * Removes value-revealing keywords from secret nodes and all their descendants (`inSecret` passed
105
+ * down). Only the keywords default / examples / const / enum are skipped as data; keys inside
106
+ * `properties` and other maps are field names, so fields named default / enum are traversed.
107
+ */
108
+ function scrubSecrets(node, inSecret = false) {
109
+ if (Array.isArray(node)) {
110
+ for (const n of node)
111
+ scrubSecrets(n, inSecret);
112
+ return;
113
+ }
114
+ if (!isObj(node))
115
+ return;
116
+ const secret = inSecret || node["x-secret"] === true;
117
+ if (secret) {
118
+ for (const k of VALUE_KEYWORDS)
119
+ delete node[k];
120
+ const hints = node["x-ui"];
121
+ if (isObj(hints))
122
+ for (const k of VALUE_HINTS)
123
+ delete hints[k];
124
+ }
125
+ for (const [k, v] of Object.entries(node)) {
126
+ if (k === "x-ui")
127
+ continue;
128
+ if (SCHEMA_MAPS.has(k) && isObj(v)) {
129
+ for (const child of Object.values(v))
130
+ scrubSecrets(child, secret);
131
+ }
132
+ else if (!VALUE_KEYWORDS.includes(k)) {
133
+ scrubSecrets(v, secret);
134
+ }
135
+ }
136
+ }
137
+ const WIDGETS = new Set([
138
+ "text",
139
+ "textarea",
140
+ "url",
141
+ "number",
142
+ "select",
143
+ "radio",
144
+ "switch",
145
+ "checkbox",
146
+ "tristate",
147
+ "tags",
148
+ "json",
149
+ ]);
150
+ const HINT_KEYS = new Set([
151
+ "label",
152
+ "help",
153
+ "placeholder",
154
+ "unit",
155
+ "order",
156
+ "section",
157
+ "advanced",
158
+ "widget",
159
+ "options",
160
+ "layout",
161
+ "item",
162
+ "presets",
163
+ "secrets",
164
+ "sections",
165
+ "messages",
166
+ ]);
167
+ /** The only hints a secret node may carry. */
168
+ const SECRET_HINT_KEYS = new Set(["label", "help", "placeholder", "order", "section", "advanced"]);
169
+ const ITEM_KEYS = new Set(["title", "subtitle", "addLabel", "reorder"]);
170
+ const utf8Bytes = (s) => new TextEncoder().encode(s).length;
171
+ /** The JSON types a node accepts (`type`, a type list, or the branches of a nullable `anyOf`). */
172
+ function typesOf(node) {
173
+ const t = node.type;
174
+ if (typeof t === "string")
175
+ return new Set([t]);
176
+ if (Array.isArray(t))
177
+ return new Set(t.filter((x) => typeof x === "string"));
178
+ const branches = (node.anyOf ?? node.oneOf);
179
+ if (Array.isArray(branches)) {
180
+ const out = new Set();
181
+ for (const b of branches)
182
+ if (isObj(b))
183
+ for (const x of typesOf(b))
184
+ out.add(x);
185
+ return out;
186
+ }
187
+ if ("const" in node)
188
+ return new Set([jsonType(node.const)]);
189
+ if (Array.isArray(node.enum))
190
+ return new Set(node.enum.map(jsonType));
191
+ return new Set();
192
+ }
193
+ /** The non-null shape of a node: itself, or the one non-null branch of a nullable `anyOf`. */
194
+ function shapeOf(node) {
195
+ const branches = (node.anyOf ?? node.oneOf);
196
+ if (node.type === undefined && Array.isArray(branches)) {
197
+ const rest = branches.filter((b) => isObj(b) && b.type !== "null");
198
+ if (rest.length === 1 && rest[0] !== undefined)
199
+ return rest[0];
200
+ }
201
+ return node;
202
+ }
203
+ function jsonType(v) {
204
+ if (v === null)
205
+ return "null";
206
+ if (Array.isArray(v))
207
+ return "array";
208
+ if (typeof v === "number")
209
+ return Number.isInteger(v) ? "integer" : "number";
210
+ return typeof v;
211
+ }
212
+ /** The enum values of a node (`enum`, `const`, or `anyOf` / `oneOf` branches of those), or null. */
213
+ function enumOf(node) {
214
+ if (Array.isArray(node.enum))
215
+ return node.enum;
216
+ if ("const" in node)
217
+ return [node.const];
218
+ const branches = (node.anyOf ?? node.oneOf);
219
+ if (Array.isArray(branches)) {
220
+ const out = [];
221
+ for (const b of branches) {
222
+ if (!isObj(b))
223
+ return null;
224
+ if (b.type === "null")
225
+ continue;
226
+ const e = enumOf(b);
227
+ if (e === null)
228
+ return null;
229
+ out.push(...e);
230
+ }
231
+ return out.length > 0 ? out : null;
232
+ }
233
+ return null;
234
+ }
235
+ const isNumberType = (types) => types.has("number") || types.has("integer");
236
+ const only = (types, ...allowed) => types.size > 0 && [...types].every((t) => t === "null" || allowed.includes(t));
237
+ const isScalar = (node) => {
238
+ const types = typesOf(node);
239
+ return !types.has("object") && !types.has("array");
240
+ };
241
+ /** Does `value` fit the JSON type (and enum) of `node`? */
242
+ function fits(node, value) {
243
+ const types = typesOf(node);
244
+ const t = jsonType(value);
245
+ const typeOk = types.size === 0 || types.has(t) || (t === "integer" && types.has("number"));
246
+ if (!typeOk)
247
+ return false;
248
+ const e = enumOf(node);
249
+ return e === null || e.some((x) => x === value);
250
+ }
251
+ function properties(node) {
252
+ const p = shapeOf(node).properties;
253
+ return isObj(p) ? p : {};
254
+ }
255
+ function itemsOf(node) {
256
+ const i = shapeOf(node).items;
257
+ return isObj(i) ? i : null;
258
+ }
259
+ /**
260
+ * Validates the `x-ui` hints of a JSON Schema produced by {@link configJsonSchema} (spec §3.3) at
261
+ * a config level. Pure; used by the packer, `capybara-plugin check` and the kernel. Returns every
262
+ * problem (empty = valid); never throws. Problem paths are dot paths of the node holding the hint.
263
+ */
264
+ export function validateConfigUi(schema, level) {
265
+ const problems = [];
266
+ const L = CONFIG_UI_LIMITS;
267
+ const rootHints = isObj(schema["x-ui"]) ? schema["x-ui"] : {};
268
+ const sectionIds = new Set(Array.isArray(rootHints.sections)
269
+ ? rootHints.sections.flatMap((s) => (isObj(s) && typeof s.id === "string" ? [s.id] : []))
270
+ : []);
271
+ let hintsBytes = 0;
272
+ const resolvePath = (dotPath) => {
273
+ let node = schema;
274
+ for (const seg of dotPath.split(".")) {
275
+ if (node === null)
276
+ return null;
277
+ const props = properties(node);
278
+ node = Object.hasOwn(props, seg) ? (props[seg] ?? null) : null;
279
+ }
280
+ return node;
281
+ };
282
+ const visit = (node, path, isRoot, depth) => {
283
+ const report = (message) => problems.push({ path, message });
284
+ if (node["x-org"] !== undefined) {
285
+ const org = node["x-org"];
286
+ if (level !== "deployment")
287
+ report(`x-org is only allowed at the deployment level, not ${level}`);
288
+ else if (depth !== 1)
289
+ report("x-org is only allowed on a top-level field of the config");
290
+ else if (org !== "readonly" && org !== "override") {
291
+ report(`x-org must be "readonly" or "override", not ${JSON.stringify(org)}`);
292
+ }
293
+ else if (org === "readonly" && node["x-secret"] === true) {
294
+ report('x-org "readonly" on a secret field: an org never sees a secret, so there is nothing to show');
295
+ }
296
+ }
297
+ const text = (value, what, max, plainOk = false) => {
298
+ if (value === undefined)
299
+ return;
300
+ if (plainOk && typeof value === "string") {
301
+ if (value.trim() === "")
302
+ report(`${what} must not be empty`);
303
+ else if (value.length > max)
304
+ report(`${what} must be at most ${max} characters`);
305
+ return;
306
+ }
307
+ if (!isObj(value)) {
308
+ report(`${what} must be { zh, en }${plainOk ? " or a string" : ""}`);
309
+ return;
310
+ }
311
+ for (const lang of ["zh", "en"]) {
312
+ const v = value[lang];
313
+ if (typeof v !== "string" || v.trim() === "")
314
+ report(`${what}.${lang} must not be empty`);
315
+ else if (v.length > max)
316
+ report(`${what}.${lang} must be at most ${max} characters`);
317
+ }
318
+ for (const k of Object.keys(value)) {
319
+ if (k !== "zh" && k !== "en")
320
+ report(`${what} has an unknown key "${k}"`);
321
+ }
322
+ };
323
+ const raw = node["x-ui"];
324
+ if (raw !== undefined) {
325
+ hintsBytes += utf8Bytes(JSON.stringify(raw));
326
+ if (!isObj(raw))
327
+ report("x-ui must be an object");
328
+ else
329
+ checkHints(raw, node, isRoot, report, text);
330
+ }
331
+ const shape = shapeOf(node);
332
+ const props = shape.properties;
333
+ if (isObj(props)) {
334
+ for (const [k, child] of Object.entries(props)) {
335
+ if (isObj(child))
336
+ visit(child, path === "" ? k : `${path}.${k}`, false, depth + 1);
337
+ }
338
+ }
339
+ if (isObj(shape.items)) {
340
+ visit(shape.items, path === "" ? "items" : `${path}.items`, false, depth + 1);
341
+ }
342
+ if (isObj(shape.additionalProperties)) {
343
+ visit(shape.additionalProperties, path === "" ? "additionalProperties" : `${path}.additionalProperties`, false, depth + 1);
344
+ }
345
+ };
346
+ const checkHints = (h, node, isRoot, report, text) => {
347
+ const shape = shapeOf(node);
348
+ const types = typesOf(node);
349
+ const isArray = types.has("array");
350
+ const items = itemsOf(node);
351
+ const itemProps = items !== null && typesOf(items).has("object") ? properties(items) : null;
352
+ for (const k of Object.keys(h)) {
353
+ if (!HINT_KEYS.has(k))
354
+ report(`x-ui has an unknown hint "${k}"`);
355
+ }
356
+ if (node["x-secret"] === true) {
357
+ const bad = Object.keys(h).filter((k) => HINT_KEYS.has(k) && !SECRET_HINT_KEYS.has(k));
358
+ if (bad.length > 0) {
359
+ report(`a secret field allows only label, help, placeholder, order, section and advanced hints, not ${bad.join(", ")}`);
360
+ }
361
+ }
362
+ text(h.label, "label", L.labelChars);
363
+ text(h.help, "help", L.helpChars);
364
+ text(h.placeholder, "placeholder", L.placeholderChars, true);
365
+ text(h.unit, "unit", L.labelChars);
366
+ if (h.order !== undefined && (typeof h.order !== "number" || !Number.isFinite(h.order))) {
367
+ report("order must be a number");
368
+ }
369
+ if (h.advanced !== undefined && typeof h.advanced !== "boolean") {
370
+ report("advanced must be a boolean");
371
+ }
372
+ if (h.section !== undefined) {
373
+ if (typeof h.section !== "string" || !sectionIds.has(h.section)) {
374
+ report(`section "${String(h.section)}" is not an id of the root's sections`);
375
+ }
376
+ }
377
+ if (!isRoot && h.sections !== undefined)
378
+ report("sections are allowed only on the root");
379
+ if (!isRoot && h.messages !== undefined)
380
+ report("messages are allowed only on the root");
381
+ if (isRoot && h.sections !== undefined) {
382
+ if (!Array.isArray(h.sections))
383
+ report("sections must be an array");
384
+ else {
385
+ if (h.sections.length > L.sections)
386
+ report(`at most ${L.sections} sections`);
387
+ const ids = new Set();
388
+ h.sections.forEach((s, i) => {
389
+ if (!isObj(s) || typeof s.id !== "string" || s.id === "") {
390
+ report(`sections[${i}].id must be a non-empty string`);
391
+ return;
392
+ }
393
+ if (ids.has(s.id))
394
+ report(`duplicate section id "${s.id}"`);
395
+ ids.add(s.id);
396
+ text(s.label, `sections[${i}].label`, L.labelChars);
397
+ if (s.label === undefined)
398
+ report(`sections[${i}].label is required`);
399
+ text(s.help, `sections[${i}].help`, L.helpChars);
400
+ });
401
+ }
402
+ }
403
+ if (isRoot && h.messages !== undefined) {
404
+ if (!isObj(h.messages))
405
+ report("messages must be an object");
406
+ else
407
+ for (const [k, v] of Object.entries(h.messages))
408
+ text(v, `messages.${k}`, L.helpChars);
409
+ }
410
+ // widget fits the type
411
+ if (h.widget !== undefined) {
412
+ const w = h.widget;
413
+ const enumValues = enumOf(shape);
414
+ const ok = typeof w === "string" &&
415
+ WIDGETS.has(w) &&
416
+ (w === "json" ||
417
+ (["switch", "checkbox", "tristate"].includes(w) && only(types, "boolean")) ||
418
+ (["select", "radio"].includes(w) && enumValues !== null) ||
419
+ (w === "tags" && isArray && items !== null && only(typesOf(items), "string")) ||
420
+ (["url", "textarea", "text"].includes(w) && only(types, "string")) ||
421
+ (w === "number" && isNumberType(types) && only(types, "number", "integer")));
422
+ if (!ok)
423
+ report(`widget "${String(w)}" does not fit this field's type`);
424
+ }
425
+ // options: members of the enum (or booleans), each once
426
+ if (h.options !== undefined) {
427
+ if (!Array.isArray(h.options))
428
+ report("options must be an array");
429
+ else {
430
+ if (h.options.length > L.options)
431
+ report(`at most ${L.options} options`);
432
+ const allowed = enumOf(shape) ??
433
+ (isArray && items !== null ? enumOf(items) : null) ??
434
+ (only(types, "boolean") ? [true, false] : null);
435
+ const seen = [];
436
+ h.options.forEach((o, i) => {
437
+ if (!isObj(o)) {
438
+ report(`options[${i}] must be an object`);
439
+ return;
440
+ }
441
+ if (allowed === null)
442
+ report(`options[${i}]: this field has no enum to label`);
443
+ else if (!allowed.some((v) => v === o.value)) {
444
+ report(`options[${i}].value ${JSON.stringify(o.value)} is not a value of the field`);
445
+ }
446
+ if (seen.some((v) => v === o.value)) {
447
+ report(`options[${i}].value ${JSON.stringify(o.value)} is listed twice`);
448
+ }
449
+ seen.push(o.value);
450
+ text(o.label, `options[${i}].label`, L.labelChars);
451
+ if (o.label === undefined)
452
+ report(`options[${i}].label is required`);
453
+ text(o.help, `options[${i}].help`, L.helpChars);
454
+ });
455
+ }
456
+ }
457
+ // layout
458
+ if (h.layout !== undefined) {
459
+ if (h.layout !== "stack" && h.layout !== "row")
460
+ report('layout must be "stack" or "row"');
461
+ else if (h.layout === "row") {
462
+ const target = isArray && items !== null && typesOf(items).has("object")
463
+ ? items
464
+ : types.has("object")
465
+ ? node
466
+ : null;
467
+ if (target === null)
468
+ report('layout "row" applies only to objects and arrays of objects');
469
+ else {
470
+ const nonScalar = Object.entries(properties(target))
471
+ .filter(([, p]) => !isScalar(p))
472
+ .map(([k]) => k);
473
+ if (nonScalar.length > 0) {
474
+ report(`layout "row" needs scalar fields; not scalar: ${nonScalar.join(", ")}`);
475
+ }
476
+ }
477
+ }
478
+ }
479
+ // item / presets / secrets: arrays only; presets and secrets: arrays of objects
480
+ for (const k of ["item", "presets", "secrets"]) {
481
+ if (h[k] === undefined)
482
+ continue;
483
+ if (!isArray)
484
+ report(`${k} applies only to arrays`);
485
+ else if (k !== "item" && itemProps === null) {
486
+ report(`${k} applies only to arrays of objects`);
487
+ }
488
+ }
489
+ if (isObj(h.item) && isArray) {
490
+ for (const k of Object.keys(h.item)) {
491
+ if (!ITEM_KEYS.has(k))
492
+ report(`item has an unknown key "${k}"`);
493
+ }
494
+ for (const k of ["title", "subtitle"]) {
495
+ const tpl = h.item[k];
496
+ if (tpl === undefined)
497
+ continue;
498
+ if (typeof tpl !== "string") {
499
+ report(`item.${k} must be a string`);
500
+ continue;
501
+ }
502
+ for (const m of tpl.matchAll(/\{([^{}]*)\}/g)) {
503
+ const field = m[1] ?? "";
504
+ const p = itemProps !== null && Object.hasOwn(itemProps, field) ? itemProps[field] : null;
505
+ if (p === null || p === undefined || !isScalar(p)) {
506
+ report(`item.${k} names "${field}", which is not a scalar field of the item`);
507
+ }
508
+ }
509
+ }
510
+ text(h.item.addLabel, "item.addLabel", L.labelChars);
511
+ if (h.item.reorder !== undefined && typeof h.item.reorder !== "boolean") {
512
+ report("item.reorder must be a boolean");
513
+ }
514
+ }
515
+ else if (h.item !== undefined && isArray) {
516
+ report("item must be an object");
517
+ }
518
+ if (h.presets !== undefined && itemProps !== null) {
519
+ if (!Array.isArray(h.presets))
520
+ report("presets must be an array");
521
+ else {
522
+ if (h.presets.length > L.presets)
523
+ report(`at most ${L.presets} presets`);
524
+ const ids = new Set();
525
+ h.presets.forEach((p, i) => {
526
+ if (!isObj(p) || typeof p.id !== "string" || p.id === "") {
527
+ report(`presets[${i}].id must be a non-empty string`);
528
+ return;
529
+ }
530
+ if (ids.has(p.id))
531
+ report(`duplicate preset id "${p.id}"`);
532
+ ids.add(p.id);
533
+ text(p.label, `presets[${i}].label`, L.labelChars);
534
+ if (p.label === undefined)
535
+ report(`presets[${i}].label is required`);
536
+ text(p.help, `presets[${i}].help`, L.helpChars);
537
+ if (!isObj(p.values)) {
538
+ report(`presets[${i}].values must be an object`);
539
+ return;
540
+ }
541
+ for (const [k, v] of Object.entries(p.values)) {
542
+ const prop = Object.hasOwn(itemProps, k) ? itemProps[k] : undefined;
543
+ if (prop === undefined)
544
+ report(`presets[${i}].values.${k} is not a field of the item`);
545
+ else if (prop["x-secret"] === true) {
546
+ report(`presets[${i}].values.${k} is a secret; presets never carry secrets`);
547
+ }
548
+ else if (!fits(prop, v)) {
549
+ report(`presets[${i}].values.${k} does not match the field's type or values`);
550
+ }
551
+ }
552
+ });
553
+ }
554
+ }
555
+ if (h.secrets !== undefined && itemProps !== null) {
556
+ if (level !== "deployment") {
557
+ report("secrets drawn per item are allowed only in the deployment config");
558
+ }
559
+ if (!Array.isArray(h.secrets))
560
+ report("secrets must be an array");
561
+ else
562
+ h.secrets.forEach((s, i) => {
563
+ if (!isObj(s)) {
564
+ report(`secrets[${i}] must be an object`);
565
+ return;
566
+ }
567
+ const target = typeof s.path === "string" ? resolvePath(s.path) : null;
568
+ const ap = target === null ? null : shapeOf(target).additionalProperties;
569
+ if (target === null ||
570
+ target["x-secret"] !== true ||
571
+ !typesOf(target).has("object") ||
572
+ !isObj(ap) ||
573
+ ap.type !== "string") {
574
+ report(`secrets[${i}].path "${String(s.path)}" is not a secret record of strings (secret(z.record(z.string(), z.string())))`);
575
+ }
576
+ const keyProp = typeof s.keyBy === "string" && Object.hasOwn(itemProps, s.keyBy)
577
+ ? itemProps[s.keyBy]
578
+ : undefined;
579
+ if (keyProp === undefined || !only(typesOf(keyProp), "string")) {
580
+ report(`secrets[${i}].keyBy "${String(s.keyBy)}" is not a string field of the item`);
581
+ }
582
+ if (s.after !== undefined &&
583
+ (typeof s.after !== "string" || !Object.hasOwn(itemProps, s.after))) {
584
+ report(`secrets[${i}].after "${String(s.after)}" is not a field of the item`);
585
+ }
586
+ text(s.label, `secrets[${i}].label`, L.labelChars);
587
+ if (s.label === undefined)
588
+ report(`secrets[${i}].label is required`);
589
+ text(s.help, `secrets[${i}].help`, L.helpChars);
590
+ text(s.placeholder, `secrets[${i}].placeholder`, L.placeholderChars, true);
591
+ });
592
+ }
593
+ };
594
+ visit(schema, "", true, 0);
595
+ if (hintsBytes > L.hintsBytes) {
596
+ problems.push({
597
+ path: "",
598
+ message: `x-ui hints total ${hintsBytes} bytes; at most ${L.hintsBytes} (64 KiB) per schema`,
599
+ });
600
+ }
601
+ return problems;
602
+ }
package/dist/ctx.d.ts CHANGED
@@ -109,7 +109,9 @@ export interface ExtensionContext {
109
109
  * manifest's `requires` / `optional`. Without `orgId` it is the deployment-level view and returns
110
110
  * synchronously; with `orgId` it returns a Promise, and orgScoped extension points (such as
111
111
  * `channel`, `directory`) are filtered by that org's enablement (queried every time, not cached);
112
- * `get` rejects with `ExtensionPointUnavailableError` when the provider is not enabled in that org.
112
+ * with `orgId`, a provider whose effective config is not valid in that org is excluded too;
113
+ * `get` rejects with `ExtensionPointUnavailableError` when the provider is not enabled in that
114
+ * org or its config is not valid there.
113
115
  * When there is no provider, `get` throws `ExtensionPointUnavailableError` and `list` returns an
114
116
  * empty list.
115
117
  */
@@ -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,20 @@ 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[];
16
+ /**
17
+ * How the kernel picks one provider among several: `org-default` means each org chooses its
18
+ * default provider (absent: no selection, every enabled provider is used).
19
+ */
20
+ selection?: "org-default";
12
21
  }
13
22
  /** The registered extension points. */
14
23
  export declare const EXTENSION_POINTS: Record<string, ExtensionPointInfo>;