@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.
- package/README.md +1 -1
- package/agent-docs/agent-templates.md +47 -0
- package/agent-docs/concepts.md +58 -0
- package/agent-docs/connector-templates.md +47 -0
- package/agent-docs/extension-points/channel.md +15 -0
- package/agent-docs/extension-points/config-source.md +15 -0
- package/agent-docs/extension-points/connector.md +15 -0
- package/agent-docs/extension-points/counter.md +15 -0
- package/agent-docs/extension-points/directory.md +15 -0
- package/agent-docs/extension-points/file-preview.md +15 -0
- package/agent-docs/extension-points/identity-source.md +15 -0
- package/agent-docs/extension-points/model-provider.md +15 -0
- package/agent-docs/extension-points/object-store.md +15 -0
- package/agent-docs/extension-points/sandbox.md +15 -0
- package/agent-docs/extension-points/secret-store.md +15 -0
- package/agent-docs/extension-points/web-fetch.md +15 -0
- package/agent-docs/extension-points/web-search.md +15 -0
- package/agent-docs/extensions.md +63 -0
- package/agent-docs/plugin-format.md +60 -0
- package/agent-docs/skills.md +44 -0
- package/agent-docs/tool-naming.md +21 -0
- package/dist/agent-template.d.ts +85 -0
- package/dist/agent-template.js +361 -2
- package/dist/config-ui.d.ts +131 -0
- package/dist/config-ui.js +602 -0
- package/dist/ctx.d.ts +3 -1
- package/dist/directory.d.ts +11 -2
- package/dist/extension-point.d.ts +9 -0
- package/dist/extension-point.js +17 -12
- package/dist/file-preview.d.ts +8 -0
- package/dist/hooks.d.ts +14 -4
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/marketplace.d.ts +29 -2
- package/dist/marketplace.js +30 -2
- package/dist/org-access.d.ts +27 -0
- package/dist/org-access.js +35 -0
- package/dist/plugin-manifest.d.ts +25 -2
- package/dist/plugin-manifest.js +20 -1
- package/dist/sandbox.d.ts +140 -60
- package/dist/sandbox.js +30 -6
- package/dist/secret.d.ts +4 -1
- package/dist/secret.js +5 -2
- package/dist/skill-invocation.d.ts +8 -5
- package/dist/skill-invocation.js +17 -8
- package/dist/skill.d.ts +20 -3
- package/dist/skill.js +28 -6
- package/dist/tool.d.ts +8 -0
- package/dist/ui.d.ts +8 -0
- package/dist/web.d.ts +105 -0
- package/dist/web.js +135 -0
- package/package.json +4 -2
- package/schemas/marketplace.v1.schema.json +36 -0
- 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
|
-
* `
|
|
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
|
*/
|
package/dist/directory.d.ts
CHANGED
|
@@ -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>;
|