@capybara-hq/plugin-sdk 0.1.0-rc.0 → 0.1.0-rc.2
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 +2 -2
- package/dist/agent-template.js +1 -1
- package/dist/config-ui.d.ts +130 -0
- package/dist/config-ui.js +584 -0
- package/dist/extension-point.d.ts +5 -0
- package/dist/extension-point.js +4 -1
- package/dist/hooks.d.ts +6 -4
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/plugin-manifest.d.ts +10 -0
- package/dist/plugin-manifest.js +9 -0
- package/dist/secret.d.ts +4 -1
- package/dist/secret.js +5 -2
- package/dist/skill-invocation.d.ts +3 -3
- package/dist/skill-invocation.js +2 -2
- package/dist/tool-key.d.ts +28 -0
- package/dist/tool-key.js +40 -0
- package/dist/web.d.ts +80 -0
- package/dist/web.js +135 -0
- package/package.json +1 -1
- package/schemas/plugin.v1.schema.json +20 -0
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ npm packages; the host provides it at runtime.
|
|
|
14
14
|
npm install @capybara-hq/plugin-sdk
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
- Writing an extension: [.agents/skills/writing-extensions](https://github.com/capybara-hq/
|
|
18
|
-
and
|
|
17
|
+
- Writing an extension: [.agents/skills/writing-extensions](https://github.com/capybara-hq/plugins/tree/main/.agents/skills/writing-extensions)
|
|
18
|
+
in capybara-hq/plugins and its reference plugin [plugins/example-counter](https://github.com/capybara-hq/plugins/tree/main/plugins/example-counter).
|
|
19
19
|
- Writing a plugin (manifest, skills, connector templates, dependencies, marketplaces):
|
|
20
20
|
[.agents/skills/writing-plugins](https://github.com/capybara-hq/core/tree/main/.agents/skills/writing-plugins).
|
|
21
21
|
- The repository: [capybara-hq/core](https://github.com/capybara-hq/core).
|
package/dist/agent-template.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent templates (`agents/<name>.md` in a plugin; sub-agents spec §16.1): Markdown with YAML
|
|
3
3
|
* frontmatter, the body being the instructions. A template is a type an agent with the builtin
|
|
4
|
-
* `
|
|
4
|
+
* `Agent` tool can dispatch as a sub-agent (`<plugin>:<name>` in its roster). Used by the kernel,
|
|
5
5
|
* the packer and `capybara-plugin check`.
|
|
6
6
|
*/
|
|
7
7
|
import { readFrontmatter } from "./frontmatter.js";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { LocalizedName } from "./plugin-package.js";
|
|
3
|
+
/**
|
|
4
|
+
* Config UI hints (extension config forms spec §3): declarative presentation hints attached to an
|
|
5
|
+
* extension's config schema with {@link ui}, serialized as `x-ui` into the JSON Schema
|
|
6
|
+
* ({@link configJsonSchema}) and drawn by one generic renderer in the web app. Hints never change
|
|
7
|
+
* what validates: the zod schema stays the single source of truth for shape and validation.
|
|
8
|
+
*/
|
|
9
|
+
/** A JSON Schema object (as produced by {@link configJsonSchema}). */
|
|
10
|
+
export type JsonSchema = Record<string, unknown>;
|
|
11
|
+
/** The input widget of a field. */
|
|
12
|
+
export type ConfigWidget = "text" | "textarea" | "url" | "number" | "select" | "radio" | "switch" | "checkbox" | "tristate" | "tags" | "json";
|
|
13
|
+
/** A labelled value of an enum (or boolean) field. */
|
|
14
|
+
export interface ConfigUiOption {
|
|
15
|
+
value: string | number | boolean;
|
|
16
|
+
label: LocalizedName;
|
|
17
|
+
help?: LocalizedName;
|
|
18
|
+
}
|
|
19
|
+
/** A root-level section that fields join with `section`. */
|
|
20
|
+
export interface ConfigUiSection {
|
|
21
|
+
id: string;
|
|
22
|
+
label: LocalizedName;
|
|
23
|
+
help?: LocalizedName;
|
|
24
|
+
}
|
|
25
|
+
/** A prefilled item offered when adding an item to an array of objects. */
|
|
26
|
+
export interface ConfigUiPreset {
|
|
27
|
+
id: string;
|
|
28
|
+
label: LocalizedName;
|
|
29
|
+
help?: LocalizedName;
|
|
30
|
+
/** Prefilled item fields; never a secret. */
|
|
31
|
+
values: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
/** A secret drawn inside each item of an array, stored at a fixed record path keyed by an item field. */
|
|
34
|
+
export interface ConfigUiItemSecret {
|
|
35
|
+
/** Dot path, from the schema root, of a `secret(z.record(z.string(), z.string()))` field. */
|
|
36
|
+
path: string;
|
|
37
|
+
/** The item's string field whose value is the record key (e.g. `name`). */
|
|
38
|
+
keyBy: string;
|
|
39
|
+
label: LocalizedName;
|
|
40
|
+
help?: LocalizedName;
|
|
41
|
+
placeholder?: string | LocalizedName;
|
|
42
|
+
/** The item field the input is drawn after (default: last). */
|
|
43
|
+
after?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Presentation hints of one config schema node (zod meta `ui`, JSON Schema `x-ui`). */
|
|
46
|
+
export interface ConfigUiHints {
|
|
47
|
+
label?: LocalizedName;
|
|
48
|
+
help?: LocalizedName;
|
|
49
|
+
placeholder?: string | LocalizedName;
|
|
50
|
+
/** Shown after a number input ("tokens", "ms"). */
|
|
51
|
+
unit?: LocalizedName;
|
|
52
|
+
/** Sort key among siblings (default: declaration order). */
|
|
53
|
+
order?: number;
|
|
54
|
+
/** Root-level section id (`sections` on the root). */
|
|
55
|
+
section?: string;
|
|
56
|
+
/** Collapsed under "Advanced" in its object or item. */
|
|
57
|
+
advanced?: boolean;
|
|
58
|
+
widget?: ConfigWidget;
|
|
59
|
+
options?: ConfigUiOption[];
|
|
60
|
+
/** Objects (and array items): fields stacked (default) or one compact row. */
|
|
61
|
+
layout?: "stack" | "row";
|
|
62
|
+
/** Arrays: how each item is drawn and added. */
|
|
63
|
+
item?: {
|
|
64
|
+
/** `{field}` template from the item's own scalar fields, e.g. "{name}". */
|
|
65
|
+
title?: string;
|
|
66
|
+
subtitle?: string;
|
|
67
|
+
addLabel?: LocalizedName;
|
|
68
|
+
reorder?: boolean;
|
|
69
|
+
};
|
|
70
|
+
presets?: ConfigUiPreset[];
|
|
71
|
+
secrets?: ConfigUiItemSecret[];
|
|
72
|
+
/** Root only. */
|
|
73
|
+
sections?: ConfigUiSection[];
|
|
74
|
+
/** Root only: localized messages for custom issues (`params: { ui: key }`). */
|
|
75
|
+
messages?: Record<string, LocalizedName>;
|
|
76
|
+
}
|
|
77
|
+
/** Limits of config UI hints and of the JSON Schemas a plugin package carries. */
|
|
78
|
+
export declare const CONFIG_UI_LIMITS: {
|
|
79
|
+
/** Characters per language of a `label` (also `unit`, `item.addLabel`). */
|
|
80
|
+
readonly labelChars: 100;
|
|
81
|
+
/** Characters per language of a `help` (also `messages` values). */
|
|
82
|
+
readonly helpChars: 500;
|
|
83
|
+
/** Characters (per language) of a `placeholder`. */
|
|
84
|
+
readonly placeholderChars: 200;
|
|
85
|
+
/** UTF-8 bytes of all `x-ui` JSON of one schema. */
|
|
86
|
+
readonly hintsBytes: number;
|
|
87
|
+
/** Options per node. */
|
|
88
|
+
readonly options: 200;
|
|
89
|
+
/** Presets per node. */
|
|
90
|
+
readonly presets: 20;
|
|
91
|
+
/** Root sections. */
|
|
92
|
+
readonly sections: 20;
|
|
93
|
+
/**
|
|
94
|
+
* UTF-8 bytes of each JSON Schema in `plugin.json` `extension.configSchemas` (96 KiB: two of
|
|
95
|
+
* them must fit in the 256 KiB `plugin.json` text with the rest of the manifest).
|
|
96
|
+
*/
|
|
97
|
+
readonly configSchemaBytes: number;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Attaches config UI hints to a schema (zod meta `ui`), merging with existing meta (`secret`,
|
|
101
|
+
* `hot`, `description`). Returns the same schema type. Convention: put `ui()` outermost, after
|
|
102
|
+
* `.default()` / `.optional()`.
|
|
103
|
+
*/
|
|
104
|
+
export declare function ui<T extends z.ZodTypeAny>(schema: T, hints: ConfigUiHints): T;
|
|
105
|
+
/**
|
|
106
|
+
* An extension config schema as a JSON Schema for forms (the input side, defaults included, no
|
|
107
|
+
* `$schema`):
|
|
108
|
+
* - meta `secret: true` becomes `"x-secret": true`, `hot: true` becomes `"x-hot": true`, `ui`
|
|
109
|
+
* becomes `"x-ui"` (a JSON copy of the hints); `title` / `description` are filled from
|
|
110
|
+
* `x-ui.label.en` / `x-ui.help.en` when the node has none;
|
|
111
|
+
* - unrepresentable types become `{ "x-unsupported": true }` instead of throwing;
|
|
112
|
+
* - under a secret node no value is published: `default` / `examples` / `const` / `enum` and
|
|
113
|
+
* `x-ui.options` / `x-ui.presets` are removed from it and all its descendants.
|
|
114
|
+
*/
|
|
115
|
+
export declare function configJsonSchema(schema: z.ZodTypeAny): JsonSchema;
|
|
116
|
+
/** One problem of the hints of a config schema. */
|
|
117
|
+
export interface ConfigUiProblem {
|
|
118
|
+
/**
|
|
119
|
+
* Dot path of the node holding the hint: property names, with `items` / `additionalProperties`
|
|
120
|
+
* for array items and record values (`endpoints.items.models`); `""` is the root.
|
|
121
|
+
*/
|
|
122
|
+
path: string;
|
|
123
|
+
message: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Validates the `x-ui` hints of a JSON Schema produced by {@link configJsonSchema} (spec §3.3) at
|
|
127
|
+
* a config level. Pure; used by the packer, `capybara-plugin check` and the kernel. Returns every
|
|
128
|
+
* problem (empty = valid); never throws. Problem paths are dot paths of the node holding the hint.
|
|
129
|
+
*/
|
|
130
|
+
export declare function validateConfigUi(schema: JsonSchema, level: "deployment" | "org" | "connection"): ConfigUiProblem[];
|
|
@@ -0,0 +1,584 @@
|
|
|
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`, `ui`
|
|
52
|
+
* becomes `"x-ui"` (a JSON copy of the hints); `title` / `description` are filled from
|
|
53
|
+
* `x-ui.label.en` / `x-ui.help.en` when the node has none;
|
|
54
|
+
* - unrepresentable types become `{ "x-unsupported": true }` instead of throwing;
|
|
55
|
+
* - under a secret node no value is published: `default` / `examples` / `const` / `enum` and
|
|
56
|
+
* `x-ui.options` / `x-ui.presets` are removed from it and all its descendants.
|
|
57
|
+
*/
|
|
58
|
+
export function configJsonSchema(schema) {
|
|
59
|
+
const out = z.toJSONSchema(schema, {
|
|
60
|
+
io: "input",
|
|
61
|
+
unrepresentable: "any",
|
|
62
|
+
override: (ctx) => {
|
|
63
|
+
const js = ctx.jsonSchema;
|
|
64
|
+
const def = ctx.zodSchema._zod.def;
|
|
65
|
+
if (js.secret === true)
|
|
66
|
+
js["x-secret"] = true;
|
|
67
|
+
delete js.secret;
|
|
68
|
+
if (js.hot === true)
|
|
69
|
+
js["x-hot"] = true;
|
|
70
|
+
delete js.hot;
|
|
71
|
+
if ("ui" in js) {
|
|
72
|
+
// A JSON copy: hints are data, and the scrub must never mutate the schema's meta.
|
|
73
|
+
const hints = JSON.parse(JSON.stringify(js.ui ?? null));
|
|
74
|
+
delete js.ui;
|
|
75
|
+
if (isObj(hints)) {
|
|
76
|
+
js["x-ui"] = hints;
|
|
77
|
+
const label = isObj(hints.label) ? hints.label.en : undefined;
|
|
78
|
+
const help = isObj(hints.help) ? hints.help.en : undefined;
|
|
79
|
+
if (js.title === undefined && typeof label === "string")
|
|
80
|
+
js.title = label;
|
|
81
|
+
if (js.description === undefined && typeof help === "string")
|
|
82
|
+
js.description = help;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (UNSUPPORTED.has(def.type))
|
|
86
|
+
js["x-unsupported"] = true;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
delete out.$schema;
|
|
90
|
+
scrubSecrets(out);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/** Keywords that would reveal values: never emitted for a secret. */
|
|
94
|
+
const VALUE_KEYWORDS = ["default", "examples", "const", "enum"];
|
|
95
|
+
/** Hints that would reveal values: never emitted for a secret. */
|
|
96
|
+
const VALUE_HINTS = ["options", "presets"];
|
|
97
|
+
/** Keywords whose value maps names to sub-schemas: their keys are field names, not keywords. */
|
|
98
|
+
const SCHEMA_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
|
|
99
|
+
/**
|
|
100
|
+
* Removes value-revealing keywords from secret nodes and all their descendants (`inSecret` passed
|
|
101
|
+
* down). Only the keywords default / examples / const / enum are skipped as data; keys inside
|
|
102
|
+
* `properties` and other maps are field names, so fields named default / enum are traversed.
|
|
103
|
+
*/
|
|
104
|
+
function scrubSecrets(node, inSecret = false) {
|
|
105
|
+
if (Array.isArray(node)) {
|
|
106
|
+
for (const n of node)
|
|
107
|
+
scrubSecrets(n, inSecret);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!isObj(node))
|
|
111
|
+
return;
|
|
112
|
+
const secret = inSecret || node["x-secret"] === true;
|
|
113
|
+
if (secret) {
|
|
114
|
+
for (const k of VALUE_KEYWORDS)
|
|
115
|
+
delete node[k];
|
|
116
|
+
const hints = node["x-ui"];
|
|
117
|
+
if (isObj(hints))
|
|
118
|
+
for (const k of VALUE_HINTS)
|
|
119
|
+
delete hints[k];
|
|
120
|
+
}
|
|
121
|
+
for (const [k, v] of Object.entries(node)) {
|
|
122
|
+
if (k === "x-ui")
|
|
123
|
+
continue;
|
|
124
|
+
if (SCHEMA_MAPS.has(k) && isObj(v)) {
|
|
125
|
+
for (const child of Object.values(v))
|
|
126
|
+
scrubSecrets(child, secret);
|
|
127
|
+
}
|
|
128
|
+
else if (!VALUE_KEYWORDS.includes(k)) {
|
|
129
|
+
scrubSecrets(v, secret);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const WIDGETS = new Set([
|
|
134
|
+
"text",
|
|
135
|
+
"textarea",
|
|
136
|
+
"url",
|
|
137
|
+
"number",
|
|
138
|
+
"select",
|
|
139
|
+
"radio",
|
|
140
|
+
"switch",
|
|
141
|
+
"checkbox",
|
|
142
|
+
"tristate",
|
|
143
|
+
"tags",
|
|
144
|
+
"json",
|
|
145
|
+
]);
|
|
146
|
+
const HINT_KEYS = new Set([
|
|
147
|
+
"label",
|
|
148
|
+
"help",
|
|
149
|
+
"placeholder",
|
|
150
|
+
"unit",
|
|
151
|
+
"order",
|
|
152
|
+
"section",
|
|
153
|
+
"advanced",
|
|
154
|
+
"widget",
|
|
155
|
+
"options",
|
|
156
|
+
"layout",
|
|
157
|
+
"item",
|
|
158
|
+
"presets",
|
|
159
|
+
"secrets",
|
|
160
|
+
"sections",
|
|
161
|
+
"messages",
|
|
162
|
+
]);
|
|
163
|
+
/** The only hints a secret node may carry. */
|
|
164
|
+
const SECRET_HINT_KEYS = new Set(["label", "help", "placeholder", "order", "section", "advanced"]);
|
|
165
|
+
const ITEM_KEYS = new Set(["title", "subtitle", "addLabel", "reorder"]);
|
|
166
|
+
const utf8Bytes = (s) => new TextEncoder().encode(s).length;
|
|
167
|
+
/** The JSON types a node accepts (`type`, a type list, or the branches of a nullable `anyOf`). */
|
|
168
|
+
function typesOf(node) {
|
|
169
|
+
const t = node.type;
|
|
170
|
+
if (typeof t === "string")
|
|
171
|
+
return new Set([t]);
|
|
172
|
+
if (Array.isArray(t))
|
|
173
|
+
return new Set(t.filter((x) => typeof x === "string"));
|
|
174
|
+
const branches = (node.anyOf ?? node.oneOf);
|
|
175
|
+
if (Array.isArray(branches)) {
|
|
176
|
+
const out = new Set();
|
|
177
|
+
for (const b of branches)
|
|
178
|
+
if (isObj(b))
|
|
179
|
+
for (const x of typesOf(b))
|
|
180
|
+
out.add(x);
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
if ("const" in node)
|
|
184
|
+
return new Set([jsonType(node.const)]);
|
|
185
|
+
if (Array.isArray(node.enum))
|
|
186
|
+
return new Set(node.enum.map(jsonType));
|
|
187
|
+
return new Set();
|
|
188
|
+
}
|
|
189
|
+
/** The non-null shape of a node: itself, or the one non-null branch of a nullable `anyOf`. */
|
|
190
|
+
function shapeOf(node) {
|
|
191
|
+
const branches = (node.anyOf ?? node.oneOf);
|
|
192
|
+
if (node.type === undefined && Array.isArray(branches)) {
|
|
193
|
+
const rest = branches.filter((b) => isObj(b) && b.type !== "null");
|
|
194
|
+
if (rest.length === 1 && rest[0] !== undefined)
|
|
195
|
+
return rest[0];
|
|
196
|
+
}
|
|
197
|
+
return node;
|
|
198
|
+
}
|
|
199
|
+
function jsonType(v) {
|
|
200
|
+
if (v === null)
|
|
201
|
+
return "null";
|
|
202
|
+
if (Array.isArray(v))
|
|
203
|
+
return "array";
|
|
204
|
+
if (typeof v === "number")
|
|
205
|
+
return Number.isInteger(v) ? "integer" : "number";
|
|
206
|
+
return typeof v;
|
|
207
|
+
}
|
|
208
|
+
/** The enum values of a node (`enum`, `const`, or `anyOf` / `oneOf` branches of those), or null. */
|
|
209
|
+
function enumOf(node) {
|
|
210
|
+
if (Array.isArray(node.enum))
|
|
211
|
+
return node.enum;
|
|
212
|
+
if ("const" in node)
|
|
213
|
+
return [node.const];
|
|
214
|
+
const branches = (node.anyOf ?? node.oneOf);
|
|
215
|
+
if (Array.isArray(branches)) {
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const b of branches) {
|
|
218
|
+
if (!isObj(b))
|
|
219
|
+
return null;
|
|
220
|
+
if (b.type === "null")
|
|
221
|
+
continue;
|
|
222
|
+
const e = enumOf(b);
|
|
223
|
+
if (e === null)
|
|
224
|
+
return null;
|
|
225
|
+
out.push(...e);
|
|
226
|
+
}
|
|
227
|
+
return out.length > 0 ? out : null;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const isNumberType = (types) => types.has("number") || types.has("integer");
|
|
232
|
+
const only = (types, ...allowed) => types.size > 0 && [...types].every((t) => t === "null" || allowed.includes(t));
|
|
233
|
+
const isScalar = (node) => {
|
|
234
|
+
const types = typesOf(node);
|
|
235
|
+
return !types.has("object") && !types.has("array");
|
|
236
|
+
};
|
|
237
|
+
/** Does `value` fit the JSON type (and enum) of `node`? */
|
|
238
|
+
function fits(node, value) {
|
|
239
|
+
const types = typesOf(node);
|
|
240
|
+
const t = jsonType(value);
|
|
241
|
+
const typeOk = types.size === 0 || types.has(t) || (t === "integer" && types.has("number"));
|
|
242
|
+
if (!typeOk)
|
|
243
|
+
return false;
|
|
244
|
+
const e = enumOf(node);
|
|
245
|
+
return e === null || e.some((x) => x === value);
|
|
246
|
+
}
|
|
247
|
+
function properties(node) {
|
|
248
|
+
const p = shapeOf(node).properties;
|
|
249
|
+
return isObj(p) ? p : {};
|
|
250
|
+
}
|
|
251
|
+
function itemsOf(node) {
|
|
252
|
+
const i = shapeOf(node).items;
|
|
253
|
+
return isObj(i) ? i : null;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Validates the `x-ui` hints of a JSON Schema produced by {@link configJsonSchema} (spec §3.3) at
|
|
257
|
+
* a config level. Pure; used by the packer, `capybara-plugin check` and the kernel. Returns every
|
|
258
|
+
* problem (empty = valid); never throws. Problem paths are dot paths of the node holding the hint.
|
|
259
|
+
*/
|
|
260
|
+
export function validateConfigUi(schema, level) {
|
|
261
|
+
const problems = [];
|
|
262
|
+
const L = CONFIG_UI_LIMITS;
|
|
263
|
+
const rootHints = isObj(schema["x-ui"]) ? schema["x-ui"] : {};
|
|
264
|
+
const sectionIds = new Set(Array.isArray(rootHints.sections)
|
|
265
|
+
? rootHints.sections.flatMap((s) => (isObj(s) && typeof s.id === "string" ? [s.id] : []))
|
|
266
|
+
: []);
|
|
267
|
+
let hintsBytes = 0;
|
|
268
|
+
const resolvePath = (dotPath) => {
|
|
269
|
+
let node = schema;
|
|
270
|
+
for (const seg of dotPath.split(".")) {
|
|
271
|
+
if (node === null)
|
|
272
|
+
return null;
|
|
273
|
+
const props = properties(node);
|
|
274
|
+
node = Object.hasOwn(props, seg) ? (props[seg] ?? null) : null;
|
|
275
|
+
}
|
|
276
|
+
return node;
|
|
277
|
+
};
|
|
278
|
+
const visit = (node, path, isRoot) => {
|
|
279
|
+
const report = (message) => problems.push({ path, message });
|
|
280
|
+
const text = (value, what, max, plainOk = false) => {
|
|
281
|
+
if (value === undefined)
|
|
282
|
+
return;
|
|
283
|
+
if (plainOk && typeof value === "string") {
|
|
284
|
+
if (value.trim() === "")
|
|
285
|
+
report(`${what} must not be empty`);
|
|
286
|
+
else if (value.length > max)
|
|
287
|
+
report(`${what} must be at most ${max} characters`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!isObj(value)) {
|
|
291
|
+
report(`${what} must be { zh, en }${plainOk ? " or a string" : ""}`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
for (const lang of ["zh", "en"]) {
|
|
295
|
+
const v = value[lang];
|
|
296
|
+
if (typeof v !== "string" || v.trim() === "")
|
|
297
|
+
report(`${what}.${lang} must not be empty`);
|
|
298
|
+
else if (v.length > max)
|
|
299
|
+
report(`${what}.${lang} must be at most ${max} characters`);
|
|
300
|
+
}
|
|
301
|
+
for (const k of Object.keys(value)) {
|
|
302
|
+
if (k !== "zh" && k !== "en")
|
|
303
|
+
report(`${what} has an unknown key "${k}"`);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const raw = node["x-ui"];
|
|
307
|
+
if (raw !== undefined) {
|
|
308
|
+
hintsBytes += utf8Bytes(JSON.stringify(raw));
|
|
309
|
+
if (!isObj(raw))
|
|
310
|
+
report("x-ui must be an object");
|
|
311
|
+
else
|
|
312
|
+
checkHints(raw, node, isRoot, report, text);
|
|
313
|
+
}
|
|
314
|
+
const shape = shapeOf(node);
|
|
315
|
+
const props = shape.properties;
|
|
316
|
+
if (isObj(props)) {
|
|
317
|
+
for (const [k, child] of Object.entries(props)) {
|
|
318
|
+
if (isObj(child))
|
|
319
|
+
visit(child, path === "" ? k : `${path}.${k}`, false);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (isObj(shape.items))
|
|
323
|
+
visit(shape.items, path === "" ? "items" : `${path}.items`, false);
|
|
324
|
+
if (isObj(shape.additionalProperties)) {
|
|
325
|
+
visit(shape.additionalProperties, path === "" ? "additionalProperties" : `${path}.additionalProperties`, false);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const checkHints = (h, node, isRoot, report, text) => {
|
|
329
|
+
const shape = shapeOf(node);
|
|
330
|
+
const types = typesOf(node);
|
|
331
|
+
const isArray = types.has("array");
|
|
332
|
+
const items = itemsOf(node);
|
|
333
|
+
const itemProps = items !== null && typesOf(items).has("object") ? properties(items) : null;
|
|
334
|
+
for (const k of Object.keys(h)) {
|
|
335
|
+
if (!HINT_KEYS.has(k))
|
|
336
|
+
report(`x-ui has an unknown hint "${k}"`);
|
|
337
|
+
}
|
|
338
|
+
if (node["x-secret"] === true) {
|
|
339
|
+
const bad = Object.keys(h).filter((k) => HINT_KEYS.has(k) && !SECRET_HINT_KEYS.has(k));
|
|
340
|
+
if (bad.length > 0) {
|
|
341
|
+
report(`a secret field allows only label, help, placeholder, order, section and advanced hints, not ${bad.join(", ")}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
text(h.label, "label", L.labelChars);
|
|
345
|
+
text(h.help, "help", L.helpChars);
|
|
346
|
+
text(h.placeholder, "placeholder", L.placeholderChars, true);
|
|
347
|
+
text(h.unit, "unit", L.labelChars);
|
|
348
|
+
if (h.order !== undefined && (typeof h.order !== "number" || !Number.isFinite(h.order))) {
|
|
349
|
+
report("order must be a number");
|
|
350
|
+
}
|
|
351
|
+
if (h.advanced !== undefined && typeof h.advanced !== "boolean") {
|
|
352
|
+
report("advanced must be a boolean");
|
|
353
|
+
}
|
|
354
|
+
if (h.section !== undefined) {
|
|
355
|
+
if (typeof h.section !== "string" || !sectionIds.has(h.section)) {
|
|
356
|
+
report(`section "${String(h.section)}" is not an id of the root's sections`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!isRoot && h.sections !== undefined)
|
|
360
|
+
report("sections are allowed only on the root");
|
|
361
|
+
if (!isRoot && h.messages !== undefined)
|
|
362
|
+
report("messages are allowed only on the root");
|
|
363
|
+
if (isRoot && h.sections !== undefined) {
|
|
364
|
+
if (!Array.isArray(h.sections))
|
|
365
|
+
report("sections must be an array");
|
|
366
|
+
else {
|
|
367
|
+
if (h.sections.length > L.sections)
|
|
368
|
+
report(`at most ${L.sections} sections`);
|
|
369
|
+
const ids = new Set();
|
|
370
|
+
h.sections.forEach((s, i) => {
|
|
371
|
+
if (!isObj(s) || typeof s.id !== "string" || s.id === "") {
|
|
372
|
+
report(`sections[${i}].id must be a non-empty string`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (ids.has(s.id))
|
|
376
|
+
report(`duplicate section id "${s.id}"`);
|
|
377
|
+
ids.add(s.id);
|
|
378
|
+
text(s.label, `sections[${i}].label`, L.labelChars);
|
|
379
|
+
if (s.label === undefined)
|
|
380
|
+
report(`sections[${i}].label is required`);
|
|
381
|
+
text(s.help, `sections[${i}].help`, L.helpChars);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (isRoot && h.messages !== undefined) {
|
|
386
|
+
if (!isObj(h.messages))
|
|
387
|
+
report("messages must be an object");
|
|
388
|
+
else
|
|
389
|
+
for (const [k, v] of Object.entries(h.messages))
|
|
390
|
+
text(v, `messages.${k}`, L.helpChars);
|
|
391
|
+
}
|
|
392
|
+
// widget fits the type
|
|
393
|
+
if (h.widget !== undefined) {
|
|
394
|
+
const w = h.widget;
|
|
395
|
+
const enumValues = enumOf(shape);
|
|
396
|
+
const ok = typeof w === "string" &&
|
|
397
|
+
WIDGETS.has(w) &&
|
|
398
|
+
(w === "json" ||
|
|
399
|
+
(["switch", "checkbox", "tristate"].includes(w) && only(types, "boolean")) ||
|
|
400
|
+
(["select", "radio"].includes(w) && enumValues !== null) ||
|
|
401
|
+
(w === "tags" && isArray && items !== null && only(typesOf(items), "string")) ||
|
|
402
|
+
(["url", "textarea", "text"].includes(w) && only(types, "string")) ||
|
|
403
|
+
(w === "number" && isNumberType(types) && only(types, "number", "integer")));
|
|
404
|
+
if (!ok)
|
|
405
|
+
report(`widget "${String(w)}" does not fit this field's type`);
|
|
406
|
+
}
|
|
407
|
+
// options: members of the enum (or booleans), each once
|
|
408
|
+
if (h.options !== undefined) {
|
|
409
|
+
if (!Array.isArray(h.options))
|
|
410
|
+
report("options must be an array");
|
|
411
|
+
else {
|
|
412
|
+
if (h.options.length > L.options)
|
|
413
|
+
report(`at most ${L.options} options`);
|
|
414
|
+
const allowed = enumOf(shape) ??
|
|
415
|
+
(isArray && items !== null ? enumOf(items) : null) ??
|
|
416
|
+
(only(types, "boolean") ? [true, false] : null);
|
|
417
|
+
const seen = [];
|
|
418
|
+
h.options.forEach((o, i) => {
|
|
419
|
+
if (!isObj(o)) {
|
|
420
|
+
report(`options[${i}] must be an object`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (allowed === null)
|
|
424
|
+
report(`options[${i}]: this field has no enum to label`);
|
|
425
|
+
else if (!allowed.some((v) => v === o.value)) {
|
|
426
|
+
report(`options[${i}].value ${JSON.stringify(o.value)} is not a value of the field`);
|
|
427
|
+
}
|
|
428
|
+
if (seen.some((v) => v === o.value)) {
|
|
429
|
+
report(`options[${i}].value ${JSON.stringify(o.value)} is listed twice`);
|
|
430
|
+
}
|
|
431
|
+
seen.push(o.value);
|
|
432
|
+
text(o.label, `options[${i}].label`, L.labelChars);
|
|
433
|
+
if (o.label === undefined)
|
|
434
|
+
report(`options[${i}].label is required`);
|
|
435
|
+
text(o.help, `options[${i}].help`, L.helpChars);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// layout
|
|
440
|
+
if (h.layout !== undefined) {
|
|
441
|
+
if (h.layout !== "stack" && h.layout !== "row")
|
|
442
|
+
report('layout must be "stack" or "row"');
|
|
443
|
+
else if (h.layout === "row") {
|
|
444
|
+
const target = isArray && items !== null && typesOf(items).has("object")
|
|
445
|
+
? items
|
|
446
|
+
: types.has("object")
|
|
447
|
+
? node
|
|
448
|
+
: null;
|
|
449
|
+
if (target === null)
|
|
450
|
+
report('layout "row" applies only to objects and arrays of objects');
|
|
451
|
+
else {
|
|
452
|
+
const nonScalar = Object.entries(properties(target))
|
|
453
|
+
.filter(([, p]) => !isScalar(p))
|
|
454
|
+
.map(([k]) => k);
|
|
455
|
+
if (nonScalar.length > 0) {
|
|
456
|
+
report(`layout "row" needs scalar fields; not scalar: ${nonScalar.join(", ")}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// item / presets / secrets: arrays only; presets and secrets: arrays of objects
|
|
462
|
+
for (const k of ["item", "presets", "secrets"]) {
|
|
463
|
+
if (h[k] === undefined)
|
|
464
|
+
continue;
|
|
465
|
+
if (!isArray)
|
|
466
|
+
report(`${k} applies only to arrays`);
|
|
467
|
+
else if (k !== "item" && itemProps === null) {
|
|
468
|
+
report(`${k} applies only to arrays of objects`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (isObj(h.item) && isArray) {
|
|
472
|
+
for (const k of Object.keys(h.item)) {
|
|
473
|
+
if (!ITEM_KEYS.has(k))
|
|
474
|
+
report(`item has an unknown key "${k}"`);
|
|
475
|
+
}
|
|
476
|
+
for (const k of ["title", "subtitle"]) {
|
|
477
|
+
const tpl = h.item[k];
|
|
478
|
+
if (tpl === undefined)
|
|
479
|
+
continue;
|
|
480
|
+
if (typeof tpl !== "string") {
|
|
481
|
+
report(`item.${k} must be a string`);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
for (const m of tpl.matchAll(/\{([^{}]*)\}/g)) {
|
|
485
|
+
const field = m[1] ?? "";
|
|
486
|
+
const p = itemProps !== null && Object.hasOwn(itemProps, field) ? itemProps[field] : null;
|
|
487
|
+
if (p === null || p === undefined || !isScalar(p)) {
|
|
488
|
+
report(`item.${k} names "${field}", which is not a scalar field of the item`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
text(h.item.addLabel, "item.addLabel", L.labelChars);
|
|
493
|
+
if (h.item.reorder !== undefined && typeof h.item.reorder !== "boolean") {
|
|
494
|
+
report("item.reorder must be a boolean");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else if (h.item !== undefined && isArray) {
|
|
498
|
+
report("item must be an object");
|
|
499
|
+
}
|
|
500
|
+
if (h.presets !== undefined && itemProps !== null) {
|
|
501
|
+
if (!Array.isArray(h.presets))
|
|
502
|
+
report("presets must be an array");
|
|
503
|
+
else {
|
|
504
|
+
if (h.presets.length > L.presets)
|
|
505
|
+
report(`at most ${L.presets} presets`);
|
|
506
|
+
const ids = new Set();
|
|
507
|
+
h.presets.forEach((p, i) => {
|
|
508
|
+
if (!isObj(p) || typeof p.id !== "string" || p.id === "") {
|
|
509
|
+
report(`presets[${i}].id must be a non-empty string`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (ids.has(p.id))
|
|
513
|
+
report(`duplicate preset id "${p.id}"`);
|
|
514
|
+
ids.add(p.id);
|
|
515
|
+
text(p.label, `presets[${i}].label`, L.labelChars);
|
|
516
|
+
if (p.label === undefined)
|
|
517
|
+
report(`presets[${i}].label is required`);
|
|
518
|
+
text(p.help, `presets[${i}].help`, L.helpChars);
|
|
519
|
+
if (!isObj(p.values)) {
|
|
520
|
+
report(`presets[${i}].values must be an object`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
for (const [k, v] of Object.entries(p.values)) {
|
|
524
|
+
const prop = Object.hasOwn(itemProps, k) ? itemProps[k] : undefined;
|
|
525
|
+
if (prop === undefined)
|
|
526
|
+
report(`presets[${i}].values.${k} is not a field of the item`);
|
|
527
|
+
else if (prop["x-secret"] === true) {
|
|
528
|
+
report(`presets[${i}].values.${k} is a secret; presets never carry secrets`);
|
|
529
|
+
}
|
|
530
|
+
else if (!fits(prop, v)) {
|
|
531
|
+
report(`presets[${i}].values.${k} does not match the field's type or values`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (h.secrets !== undefined && itemProps !== null) {
|
|
538
|
+
if (level !== "deployment") {
|
|
539
|
+
report("secrets drawn per item are allowed only in the deployment config");
|
|
540
|
+
}
|
|
541
|
+
if (!Array.isArray(h.secrets))
|
|
542
|
+
report("secrets must be an array");
|
|
543
|
+
else
|
|
544
|
+
h.secrets.forEach((s, i) => {
|
|
545
|
+
if (!isObj(s)) {
|
|
546
|
+
report(`secrets[${i}] must be an object`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const target = typeof s.path === "string" ? resolvePath(s.path) : null;
|
|
550
|
+
const ap = target === null ? null : shapeOf(target).additionalProperties;
|
|
551
|
+
if (target === null ||
|
|
552
|
+
target["x-secret"] !== true ||
|
|
553
|
+
!typesOf(target).has("object") ||
|
|
554
|
+
!isObj(ap) ||
|
|
555
|
+
ap.type !== "string") {
|
|
556
|
+
report(`secrets[${i}].path "${String(s.path)}" is not a secret record of strings (secret(z.record(z.string(), z.string())))`);
|
|
557
|
+
}
|
|
558
|
+
const keyProp = typeof s.keyBy === "string" && Object.hasOwn(itemProps, s.keyBy)
|
|
559
|
+
? itemProps[s.keyBy]
|
|
560
|
+
: undefined;
|
|
561
|
+
if (keyProp === undefined || !only(typesOf(keyProp), "string")) {
|
|
562
|
+
report(`secrets[${i}].keyBy "${String(s.keyBy)}" is not a string field of the item`);
|
|
563
|
+
}
|
|
564
|
+
if (s.after !== undefined &&
|
|
565
|
+
(typeof s.after !== "string" || !Object.hasOwn(itemProps, s.after))) {
|
|
566
|
+
report(`secrets[${i}].after "${String(s.after)}" is not a field of the item`);
|
|
567
|
+
}
|
|
568
|
+
text(s.label, `secrets[${i}].label`, L.labelChars);
|
|
569
|
+
if (s.label === undefined)
|
|
570
|
+
report(`secrets[${i}].label is required`);
|
|
571
|
+
text(s.help, `secrets[${i}].help`, L.helpChars);
|
|
572
|
+
text(s.placeholder, `secrets[${i}].placeholder`, L.placeholderChars, true);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
visit(schema, "", true);
|
|
577
|
+
if (hintsBytes > L.hintsBytes) {
|
|
578
|
+
problems.push({
|
|
579
|
+
path: "",
|
|
580
|
+
message: `x-ui hints total ${hintsBytes} bytes; at most ${L.hintsBytes} (64 KiB) per schema`,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
return problems;
|
|
584
|
+
}
|
|
@@ -9,6 +9,11 @@ export interface ExtensionPointInfo {
|
|
|
9
9
|
brokered: boolean;
|
|
10
10
|
/** Enabled per org (consumers may pass `orgId` to check that the org enabled the provider). */
|
|
11
11
|
orgScoped: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* How the kernel picks one provider among several: `org-default` means each org chooses its
|
|
14
|
+
* default provider (absent: no selection, every enabled provider is used).
|
|
15
|
+
*/
|
|
16
|
+
selection?: "org-default";
|
|
12
17
|
}
|
|
13
18
|
/** The registered extension points. */
|
|
14
19
|
export declare const EXTENSION_POINTS: Record<string, ExtensionPointInfo>;
|
package/dist/extension-point.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const info = (cardinality, brokered = false, orgScoped = false) => ({
|
|
1
|
+
const info = (cardinality, brokered = false, orgScoped = false, selection) => ({
|
|
2
2
|
cardinality,
|
|
3
3
|
brokered,
|
|
4
4
|
orgScoped,
|
|
5
|
+
...(selection ? { selection } : {}),
|
|
5
6
|
});
|
|
6
7
|
/** The registered extension points. */
|
|
7
8
|
export const EXTENSION_POINTS = {
|
|
@@ -16,6 +17,8 @@ export const EXTENSION_POINTS = {
|
|
|
16
17
|
counter: info("multi"),
|
|
17
18
|
connector: info("multi", true),
|
|
18
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"),
|
|
19
22
|
};
|
|
20
23
|
/** Cardinality of the registered extension points (derived from EXTENSION_POINTS). */
|
|
21
24
|
export const EXTENSION_POINT_CARDINALITY = Object.fromEntries(Object.entries(EXTENSION_POINTS).map(([k, v]) => [k, v.cardinality]));
|
package/dist/hooks.d.ts
CHANGED
|
@@ -7,12 +7,12 @@ import type { ContentBlock, ModelRequest, ModelUsage } from "./model.js";
|
|
|
7
7
|
export declare const HOOKS_AVAILABLE: readonly ["model.request", "model.response", "tool.preExecute", "tool.postExecute", "usage.report"];
|
|
8
8
|
/** A mount point that `ctx.hooks.on` accepts. */
|
|
9
9
|
export type AvailableHook = (typeof HOOKS_AVAILABLE)[number];
|
|
10
|
-
/** Where a sub-agent was dispatched from: the parent conversation, its run and the `
|
|
10
|
+
/** Where a sub-agent was dispatched from: the parent conversation, its run and the `Agent` call. */
|
|
11
11
|
export interface HookParentScope {
|
|
12
12
|
conversationId: string;
|
|
13
13
|
runId: string;
|
|
14
14
|
/**
|
|
15
|
-
* The `
|
|
15
|
+
* The `Agent` call's id; `skill-<seq>` for a person's forked skill invocation, which has no
|
|
16
16
|
* tool call in the parent's log.
|
|
17
17
|
*/
|
|
18
18
|
callId: string;
|
|
@@ -35,9 +35,10 @@ export interface HookScope {
|
|
|
35
35
|
* What a model call is for: `step` is a run step's call (the agent's turn), `compaction` is the call
|
|
36
36
|
* that summarizes earlier history before a step when the context grows too large, `title` is the call
|
|
37
37
|
* that writes a conversation's title after its first completed run (no run: `runId` is "", `step` 0,
|
|
38
|
-
* `attempt` 1)
|
|
38
|
+
* `attempt` 1), `web_fetch` is the call that extracts an answer from a fetched web page for the
|
|
39
|
+
* `WebFetch` tool.
|
|
39
40
|
*/
|
|
40
|
-
export type ModelCallPurpose = "step" | "compaction" | "title";
|
|
41
|
+
export type ModelCallPurpose = "step" | "compaction" | "title" | "web_fetch";
|
|
41
42
|
/** What a reported model call was for (the same values as {@link ModelCallPurpose}). */
|
|
42
43
|
export type UsagePurpose = ModelCallPurpose;
|
|
43
44
|
/**
|
|
@@ -111,6 +112,7 @@ export interface ModelResponseHookPayload extends HookScope {
|
|
|
111
112
|
export interface ToolPreHookPayload extends HookScope {
|
|
112
113
|
step: number;
|
|
113
114
|
callId: string;
|
|
115
|
+
/** The model-visible key: a builtin name, `mcp__<slug>__<tool>` or `plugin__<plugin>__<tool>`; split it with `parseToolKey`. */
|
|
114
116
|
toolKey: string;
|
|
115
117
|
specHash: string;
|
|
116
118
|
input: unknown;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.1.0-rc.
|
|
1
|
+
export declare const SDK_VERSION = "0.1.0-rc.2";
|
|
2
2
|
export * from "./agent-template.js";
|
|
3
3
|
export * from "./bootstrap.js";
|
|
4
4
|
export * from "./cards.js";
|
|
5
5
|
export * from "./channel.js";
|
|
6
|
+
export * from "./config-ui.js";
|
|
6
7
|
export * from "./connector.js";
|
|
7
8
|
export * from "./connector-auth.js";
|
|
8
9
|
export * from "./ctx.js";
|
|
@@ -24,4 +25,6 @@ export * from "./skill.js";
|
|
|
24
25
|
export * from "./skill-invocation.js";
|
|
25
26
|
export * from "./storage.js";
|
|
26
27
|
export * from "./tool.js";
|
|
28
|
+
export * from "./tool-key.js";
|
|
27
29
|
export * from "./ui.js";
|
|
30
|
+
export * from "./web.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.1.0-rc.
|
|
1
|
+
export const SDK_VERSION = "0.1.0-rc.2";
|
|
2
2
|
export * from "./agent-template.js";
|
|
3
3
|
export * from "./bootstrap.js";
|
|
4
4
|
export * from "./cards.js";
|
|
5
5
|
export * from "./channel.js";
|
|
6
|
+
export * from "./config-ui.js";
|
|
6
7
|
export * from "./connector.js";
|
|
7
8
|
export * from "./connector-auth.js";
|
|
8
9
|
export * from "./ctx.js";
|
|
@@ -24,4 +25,6 @@ export * from "./skill.js";
|
|
|
24
25
|
export * from "./skill-invocation.js";
|
|
25
26
|
export * from "./storage.js";
|
|
26
27
|
export * from "./tool.js";
|
|
28
|
+
export * from "./tool-key.js";
|
|
27
29
|
export * from "./ui.js";
|
|
30
|
+
export * from "./web.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { type JsonSchema } from "./config-ui.js";
|
|
2
3
|
import { type ConnectorTemplateSpec, type LocalizedName, type StaticExtensionManifest } from "./plugin-package.js";
|
|
3
4
|
/**
|
|
4
5
|
* The plugin manifest `.capybara-plugin/plugin.json` (package ecosystem spec §4.1). A plugin is a
|
|
@@ -74,6 +75,15 @@ export interface PluginExtensionSection {
|
|
|
74
75
|
migrations?: string;
|
|
75
76
|
/** `./ui`. */
|
|
76
77
|
ui?: string;
|
|
78
|
+
/**
|
|
79
|
+
* The config JSON Schemas (`configJsonSchema`, with `x-ui` hints), written by the packer so an
|
|
80
|
+
* extension not loaded on a replica still gets its form; each ≤
|
|
81
|
+
* {@link CONFIG_UI_LIMITS}`.configSchemaBytes`. Not part of the `manifest_mismatch` comparison.
|
|
82
|
+
*/
|
|
83
|
+
configSchemas?: {
|
|
84
|
+
deployment?: JsonSchema;
|
|
85
|
+
org?: JsonSchema;
|
|
86
|
+
};
|
|
77
87
|
}
|
|
78
88
|
/** `.capybara-plugin/plugin.json`. */
|
|
79
89
|
export interface PluginManifest {
|
package/dist/plugin-manifest.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import semver from "semver";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { AGENT_TEMPLATE_LIMITS } from "./agent-template.js";
|
|
4
|
+
import { CONFIG_UI_LIMITS } from "./config-ui.js";
|
|
4
5
|
import { defineExtension, EXTENSION_NAME_RE } from "./define-extension.js";
|
|
5
6
|
import { CATALOG_ITEM_NAME_RE, CONNECTOR_TEMPLATE_LIMITS, isSafePackagePath, SEMVER_VERSION_RE, validateConnectorTemplate, } from "./plugin-package.js";
|
|
6
7
|
import { hasUpperBound } from "./semver-range.js";
|
|
@@ -87,6 +88,11 @@ const staticExtensionManifest = z.strictObject({
|
|
|
87
88
|
ui: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
88
89
|
config: z.strictObject({ deployment: configFields, org: configFields }),
|
|
89
90
|
});
|
|
91
|
+
const configSchemaJson = z
|
|
92
|
+
.record(z.string(), z.unknown())
|
|
93
|
+
.refine((v) => new TextEncoder().encode(JSON.stringify(v)).length <= CONFIG_UI_LIMITS.configSchemaBytes, {
|
|
94
|
+
message: `must be at most ${CONFIG_UI_LIMITS.configSchemaBytes} bytes of JSON (${CONFIG_UI_LIMITS.configSchemaBytes / 1024} KiB)`,
|
|
95
|
+
});
|
|
90
96
|
const extensionSection = z.strictObject({
|
|
91
97
|
entry: dotPath("entry").refine((p) => p.endsWith(".mjs"), {
|
|
92
98
|
message: "entry must be a .mjs file",
|
|
@@ -94,6 +100,9 @@ const extensionSection = z.strictObject({
|
|
|
94
100
|
manifest: staticExtensionManifest,
|
|
95
101
|
migrations: dotPath("migrations").optional(),
|
|
96
102
|
ui: dotPath("ui").optional(),
|
|
103
|
+
configSchemas: z
|
|
104
|
+
.strictObject({ deployment: configSchemaJson.optional(), org: configSchemaJson.optional() })
|
|
105
|
+
.optional(),
|
|
97
106
|
});
|
|
98
107
|
const schema = z
|
|
99
108
|
.strictObject({
|
package/dist/secret.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Marks a schema as a secret field (Zod 4 meta `{ secret: true }`), merging with existing meta
|
|
4
|
+
* (`ui` hints, `hot`), so `secret(ui(x, …))` and `ui(secret(x), …)` are the same.
|
|
5
|
+
*/
|
|
3
6
|
export declare function secret<T extends z.ZodTypeAny>(schema: T): T;
|
|
4
7
|
/** 读取 schema 的 meta,判断是否敏感;内核 `meta({secret:true})` 同样可识别。 */
|
|
5
8
|
export declare function isSecret(schema: z.ZodTypeAny): boolean;
|
package/dist/secret.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Marks a schema as a secret field (Zod 4 meta `{ secret: true }`), merging with existing meta
|
|
3
|
+
* (`ui` hints, `hot`), so `secret(ui(x, …))` and `ui(secret(x), …)` are the same.
|
|
4
|
+
*/
|
|
2
5
|
export function secret(schema) {
|
|
3
|
-
return schema.meta({ secret: true });
|
|
6
|
+
return schema.meta({ ...(schema.meta() ?? {}), secret: true });
|
|
4
7
|
}
|
|
5
8
|
/** 读取 schema 的 meta,判断是否敏感;内核 `meta({secret:true})` 同样可识别。 */
|
|
6
9
|
export function isSecret(schema) {
|
|
@@ -37,7 +37,7 @@ export declare function renderSkillBody(body: string, invocation: SkillInvocatio
|
|
|
37
37
|
export interface RenderSkillInvocationInput {
|
|
38
38
|
/**
|
|
39
39
|
* The name the person typed after `/` (`fix-issue`, or `<plugin>:<name>`): used in the lead, the
|
|
40
|
-
* `name="…"` attribute and the `
|
|
40
|
+
* `name="…"` attribute and the `Skill` argument (where the qualified form resolves).
|
|
41
41
|
*/
|
|
42
42
|
name: string;
|
|
43
43
|
/**
|
|
@@ -53,9 +53,9 @@ export interface RenderSkillInvocationInput {
|
|
|
53
53
|
args: string;
|
|
54
54
|
/** Decided by the caller from the agent's tools and the version's manifest. */
|
|
55
55
|
lines: {
|
|
56
|
-
/** The version has files besides SKILL.md: `load` when the agent has `
|
|
56
|
+
/** The version has files besides SKILL.md: `load` when the agent has `Skill`. */
|
|
57
57
|
files?: "load" | "unavailable";
|
|
58
|
-
/** The body has shell injections and the agent has `
|
|
58
|
+
/** The body has shell injections and the agent has `Bash`. */
|
|
59
59
|
shellHint?: boolean;
|
|
60
60
|
};
|
|
61
61
|
/** false: leave out the lead sentence (a forked skill's task for the sub-agent). Default true. */
|
package/dist/skill-invocation.js
CHANGED
|
@@ -180,13 +180,13 @@ export function renderSkillInvocation(i) {
|
|
|
180
180
|
}
|
|
181
181
|
parts.push(`<skill_invocation name="${escapeAttr(i.name)}" version="${i.version}">\n${body}\n</skill_invocation>`);
|
|
182
182
|
if (i.lines.files === "load") {
|
|
183
|
-
parts.push(`Its files are not in the sandbox yet: call
|
|
183
|
+
parts.push(`Its files are not in the sandbox yet: call Skill with "${i.name}" to copy them to ${SKILLS_SANDBOX_ROOT}/${i.skillName}/ before running its scripts.`);
|
|
184
184
|
}
|
|
185
185
|
else if (i.lines.files === "unavailable") {
|
|
186
186
|
parts.push("Its other files are not available in this conversation.");
|
|
187
187
|
}
|
|
188
188
|
if (i.lines.shellHint) {
|
|
189
|
-
parts.push("Lines written as !`command` were meant to run before you read this; run those commands with
|
|
189
|
+
parts.push("Lines written as !`command` were meant to run before you read this; run those commands with Bash if you need their output.");
|
|
190
190
|
}
|
|
191
191
|
return parts.join("\n\n");
|
|
192
192
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-visible tool key grammar (aligned with Claude Code):
|
|
3
|
+
*
|
|
4
|
+
* - builtin: `^[A-Z][A-Za-z]{1,31}$` (PascalCase), e.g. `Edit`
|
|
5
|
+
* - connector: `mcp__<slug>__<tool>`, e.g. `mcp__crm__search_accounts`
|
|
6
|
+
* - extension: `plugin__<plugin>__<tool>`, e.g. `plugin__todo__write`
|
|
7
|
+
*
|
|
8
|
+
* Pure functions only; extensions that match keys (hooks) use `parseToolKey` instead of splitting.
|
|
9
|
+
*/
|
|
10
|
+
export type ParsedToolKey = {
|
|
11
|
+
kind: "builtin";
|
|
12
|
+
name: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "connector";
|
|
15
|
+
slug: string;
|
|
16
|
+
tool: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "extension";
|
|
19
|
+
plugin: string;
|
|
20
|
+
tool: string;
|
|
21
|
+
};
|
|
22
|
+
export declare const BUILTIN_TOOL_NAME_RE: RegExp;
|
|
23
|
+
/** Parse a tool key; `null` when it fits none of the three forms. */
|
|
24
|
+
export declare function parseToolKey(key: string): ParsedToolKey | null;
|
|
25
|
+
/** `mcp__<slug>__<tool>` */
|
|
26
|
+
export declare function connectorToolKey(slug: string, tool: string): string;
|
|
27
|
+
/** `plugin__<plugin>__<tool>` */
|
|
28
|
+
export declare function extensionToolKey(plugin: string, tool: string): string;
|
package/dist/tool-key.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-visible tool key grammar (aligned with Claude Code):
|
|
3
|
+
*
|
|
4
|
+
* - builtin: `^[A-Z][A-Za-z]{1,31}$` (PascalCase), e.g. `Edit`
|
|
5
|
+
* - connector: `mcp__<slug>__<tool>`, e.g. `mcp__crm__search_accounts`
|
|
6
|
+
* - extension: `plugin__<plugin>__<tool>`, e.g. `plugin__todo__write`
|
|
7
|
+
*
|
|
8
|
+
* Pure functions only; extensions that match keys (hooks) use `parseToolKey` instead of splitting.
|
|
9
|
+
*/
|
|
10
|
+
export const BUILTIN_TOOL_NAME_RE = /^[A-Z][A-Za-z]{1,31}$/;
|
|
11
|
+
const SEP = "__";
|
|
12
|
+
/** Parse a tool key; `null` when it fits none of the three forms. */
|
|
13
|
+
export function parseToolKey(key) {
|
|
14
|
+
if (!key.includes(SEP)) {
|
|
15
|
+
return BUILTIN_TOOL_NAME_RE.test(key) ? { kind: "builtin", name: key } : null;
|
|
16
|
+
}
|
|
17
|
+
const first = key.indexOf(SEP);
|
|
18
|
+
const family = key.slice(0, first);
|
|
19
|
+
if (family !== "mcp" && family !== "plugin")
|
|
20
|
+
return null;
|
|
21
|
+
const rest = key.slice(first + SEP.length);
|
|
22
|
+
const second = rest.indexOf(SEP);
|
|
23
|
+
if (second <= 0)
|
|
24
|
+
return null;
|
|
25
|
+
const owner = rest.slice(0, second);
|
|
26
|
+
const tool = rest.slice(second + SEP.length);
|
|
27
|
+
if (tool === "")
|
|
28
|
+
return null;
|
|
29
|
+
return family === "mcp"
|
|
30
|
+
? { kind: "connector", slug: owner, tool }
|
|
31
|
+
: { kind: "extension", plugin: owner, tool };
|
|
32
|
+
}
|
|
33
|
+
/** `mcp__<slug>__<tool>` */
|
|
34
|
+
export function connectorToolKey(slug, tool) {
|
|
35
|
+
return `mcp${SEP}${slug}${SEP}${tool}`;
|
|
36
|
+
}
|
|
37
|
+
/** `plugin__<plugin>__<tool>` */
|
|
38
|
+
export function extensionToolKey(plugin, tool) {
|
|
39
|
+
return `plugin${SEP}${plugin}${SEP}${tool}`;
|
|
40
|
+
}
|
package/dist/web.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Web search extension point ref (`web-search@1`). */
|
|
2
|
+
export declare const WEB_SEARCH_EXTENSION_POINT = "web-search@1";
|
|
3
|
+
/** Web fetch extension point ref (`web-fetch@1`). */
|
|
4
|
+
export declare const WEB_FETCH_EXTENSION_POINT = "web-fetch@1";
|
|
5
|
+
/** One search hit. */
|
|
6
|
+
export interface WebSearchResult {
|
|
7
|
+
title: string;
|
|
8
|
+
/** Absolute http / https URL. */
|
|
9
|
+
url: string;
|
|
10
|
+
/** Plain text or markdown, trimmed by the provider. */
|
|
11
|
+
snippet?: string;
|
|
12
|
+
/** ISO 8601 date or datetime, when known. */
|
|
13
|
+
publishedAt?: string;
|
|
14
|
+
}
|
|
15
|
+
/** A web search provider (`web-search@1`). The kernel passes the org's decrypted `orgConfig`. */
|
|
16
|
+
export interface WebSearchProvider {
|
|
17
|
+
/** False when this org's config lacks what the provider needs. */
|
|
18
|
+
configured?(orgConfig: unknown): boolean;
|
|
19
|
+
search(args: {
|
|
20
|
+
query: string;
|
|
21
|
+
/** Hints only: the kernel filters the results again. */
|
|
22
|
+
allowedDomains?: string[];
|
|
23
|
+
blockedDomains?: string[];
|
|
24
|
+
maxResults: number;
|
|
25
|
+
orgConfig: unknown;
|
|
26
|
+
signal: AbortSignal;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
results: WebSearchResult[];
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
/** A fetched page, converted to markdown. */
|
|
32
|
+
export interface WebFetchPage {
|
|
33
|
+
/** After redirects; equal to the requested URL when there were none. */
|
|
34
|
+
finalUrl: string;
|
|
35
|
+
title?: string;
|
|
36
|
+
markdown: string;
|
|
37
|
+
contentType?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A web fetch provider (`web-fetch@1`).
|
|
41
|
+
*
|
|
42
|
+
* The kernel checks the URL it passes and the returned `finalUrl` without DNS: it refuses IP
|
|
43
|
+
* literals in private, loopback, link-local or reserved ranges and single-label or `localhost`
|
|
44
|
+
* names. A provider that fetches from inside the deployment (rather than through a remote
|
|
45
|
+
* service) must also refuse non-public addresses after DNS resolution, for the URL and for every
|
|
46
|
+
* redirect it follows. It may reuse the kernel's rules: {@link isPublicHostname} on
|
|
47
|
+
* `new URL(url).hostname` (never a hand-parsed host), and on each resolved address it connects
|
|
48
|
+
* to (an IPv6 address in brackets, as in a URL).
|
|
49
|
+
*/
|
|
50
|
+
export interface WebFetchProvider {
|
|
51
|
+
/** False when this org's config lacks what the provider needs. */
|
|
52
|
+
configured?(orgConfig: unknown): boolean;
|
|
53
|
+
fetch(args: {
|
|
54
|
+
/** Already validated and upgraded by the kernel. */
|
|
55
|
+
url: string;
|
|
56
|
+
orgConfig: unknown;
|
|
57
|
+
signal: AbortSignal;
|
|
58
|
+
}): Promise<WebFetchPage>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Why a web provider call failed: `not_found` (404 / no content), `blocked` (the site refused the
|
|
62
|
+
* provider), `auth` (bad credentials), `rate_limited`, `unavailable` (provider down, 5xx, timeout),
|
|
63
|
+
* `invalid` (the provider rejected the query or URL).
|
|
64
|
+
*/
|
|
65
|
+
export type WebProviderErrorKind = "not_found" | "blocked" | "auth" | "rate_limited" | "unavailable" | "invalid";
|
|
66
|
+
/** Thrown by web providers; the kernel maps `kind` to fixed, model-visible error text. */
|
|
67
|
+
export declare class WebProviderError extends Error {
|
|
68
|
+
readonly kind: WebProviderErrorKind;
|
|
69
|
+
constructor(message: string, opts: {
|
|
70
|
+
kind: WebProviderErrorKind;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether a URL hostname can name a public internet host. Pure, no DNS: false for `localhost`,
|
|
75
|
+
* single-label names, `*.localhost` / `*.local` / `*.internal`, and IP literals (IPv6 in brackets,
|
|
76
|
+
* as in a URL) in private, loopback, link-local, CGNAT, multicast, unspecified, documentation and
|
|
77
|
+
* reserved ranges, including IPv4-mapped IPv6. A name that resolves to a private address is not
|
|
78
|
+
* detected here.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isPublicHostname(host: string): boolean;
|
package/dist/web.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Web search extension point ref (`web-search@1`). */
|
|
2
|
+
export const WEB_SEARCH_EXTENSION_POINT = "web-search@1";
|
|
3
|
+
/** Web fetch extension point ref (`web-fetch@1`). */
|
|
4
|
+
export const WEB_FETCH_EXTENSION_POINT = "web-fetch@1";
|
|
5
|
+
/** Thrown by web providers; the kernel maps `kind` to fixed, model-visible error text. */
|
|
6
|
+
export class WebProviderError extends Error {
|
|
7
|
+
kind;
|
|
8
|
+
constructor(message, opts) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "WebProviderError";
|
|
11
|
+
this.kind = opts.kind;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function ipv4Octets(host) {
|
|
15
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
16
|
+
if (!m)
|
|
17
|
+
return undefined;
|
|
18
|
+
const o = m.slice(1).map(Number);
|
|
19
|
+
return o.every((n) => n <= 255) ? o : undefined;
|
|
20
|
+
}
|
|
21
|
+
function isPublicIpv4(o) {
|
|
22
|
+
const [a, b, c] = o;
|
|
23
|
+
if (a === 0 || a === 10 || a === 127)
|
|
24
|
+
return false;
|
|
25
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
26
|
+
return false; // CGNAT
|
|
27
|
+
if (a === 169 && b === 254)
|
|
28
|
+
return false;
|
|
29
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
30
|
+
return false;
|
|
31
|
+
if (a === 192 && b === 168)
|
|
32
|
+
return false;
|
|
33
|
+
if (a === 192 && b === 0 && (c === 0 || c === 2))
|
|
34
|
+
return false; // IETF protocol, TEST-NET-1
|
|
35
|
+
if (a === 192 && b === 88 && c === 99)
|
|
36
|
+
return false; // 6to4 relay
|
|
37
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
38
|
+
return false; // benchmarking
|
|
39
|
+
if (a === 198 && b === 51 && c === 100)
|
|
40
|
+
return false; // TEST-NET-2
|
|
41
|
+
if (a === 203 && b === 0 && c === 113)
|
|
42
|
+
return false; // TEST-NET-3
|
|
43
|
+
if (a >= 224)
|
|
44
|
+
return false; // multicast, reserved, broadcast
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/** Parses an IPv6 literal (no brackets) into eight 16-bit groups, or undefined. */
|
|
48
|
+
function ipv6Groups(host) {
|
|
49
|
+
let s = host;
|
|
50
|
+
const zone = s.indexOf("%");
|
|
51
|
+
if (zone >= 0)
|
|
52
|
+
s = s.slice(0, zone);
|
|
53
|
+
const tail = /(\d+\.\d+\.\d+\.\d+)$/.exec(s);
|
|
54
|
+
if (tail) {
|
|
55
|
+
const o = ipv4Octets(tail[1]);
|
|
56
|
+
if (!o)
|
|
57
|
+
return undefined;
|
|
58
|
+
s = `${s.slice(0, tail.index)}${((o[0] << 8) | o[1]).toString(16)}:${((o[2] << 8) | o[3]).toString(16)}`;
|
|
59
|
+
}
|
|
60
|
+
if (!/^[0-9a-f:]+$/i.test(s) || s.split("::").length > 2)
|
|
61
|
+
return undefined;
|
|
62
|
+
const [head = "", rest] = s.split("::");
|
|
63
|
+
const h = head === "" ? [] : head.split(":");
|
|
64
|
+
const t = rest === undefined || rest === "" ? [] : rest.split(":");
|
|
65
|
+
const parts = [...h, ...t];
|
|
66
|
+
if (!parts.every((p) => /^[0-9a-f]{1,4}$/i.test(p)))
|
|
67
|
+
return undefined;
|
|
68
|
+
let groups;
|
|
69
|
+
if (rest === undefined) {
|
|
70
|
+
if (h.length !== 8)
|
|
71
|
+
return undefined;
|
|
72
|
+
groups = h;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (h.length + t.length > 7)
|
|
76
|
+
return undefined;
|
|
77
|
+
groups = [...h, ...new Array(8 - h.length - t.length).fill("0"), ...t];
|
|
78
|
+
}
|
|
79
|
+
return groups.map((g) => Number.parseInt(g, 16));
|
|
80
|
+
}
|
|
81
|
+
function isPublicIpv6(g) {
|
|
82
|
+
const [g0, g1, g2, g3, g4, g5, g6, g7] = g;
|
|
83
|
+
const zeros5 = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0;
|
|
84
|
+
if (zeros5 && g5 === 0 && g6 === 0 && (g7 === 0 || g7 === 1))
|
|
85
|
+
return false; // :: and ::1
|
|
86
|
+
// IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible (::a.b.c.d): judge the embedded address.
|
|
87
|
+
if (zeros5 && (g5 === 0xffff || g5 === 0)) {
|
|
88
|
+
return isPublicIpv4([g6 >> 8, g6 & 255, g7 >> 8, g7 & 255]);
|
|
89
|
+
}
|
|
90
|
+
if ((g0 & 0xfe00) === 0xfc00)
|
|
91
|
+
return false; // unique local fc00::/7
|
|
92
|
+
if ((g0 & 0xffc0) === 0xfe80)
|
|
93
|
+
return false; // link-local fe80::/10
|
|
94
|
+
if ((g0 & 0xffc0) === 0xfec0)
|
|
95
|
+
return false; // site-local (deprecated)
|
|
96
|
+
if ((g0 & 0xff00) === 0xff00)
|
|
97
|
+
return false; // multicast
|
|
98
|
+
if (g0 === 0x2001 && g1 === 0x0db8)
|
|
99
|
+
return false; // documentation
|
|
100
|
+
if (g0 === 0x0064 && g1 === 0xff9b)
|
|
101
|
+
return isPublicIpv4([g6 >> 8, g6 & 255, g7 >> 8, g7 & 255]); // NAT64
|
|
102
|
+
if (g0 === 0x2002)
|
|
103
|
+
return isPublicIpv4([g1 >> 8, g1 & 255, g2 >> 8, g2 & 255]); // 6to4
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether a URL hostname can name a public internet host. Pure, no DNS: false for `localhost`,
|
|
108
|
+
* single-label names, `*.localhost` / `*.local` / `*.internal`, and IP literals (IPv6 in brackets,
|
|
109
|
+
* as in a URL) in private, loopback, link-local, CGNAT, multicast, unspecified, documentation and
|
|
110
|
+
* reserved ranges, including IPv4-mapped IPv6. A name that resolves to a private address is not
|
|
111
|
+
* detected here.
|
|
112
|
+
*/
|
|
113
|
+
export function isPublicHostname(host) {
|
|
114
|
+
let h = host.trim().toLowerCase();
|
|
115
|
+
if (h.endsWith("."))
|
|
116
|
+
h = h.slice(0, -1);
|
|
117
|
+
if (h === "")
|
|
118
|
+
return false;
|
|
119
|
+
if (h.startsWith("[")) {
|
|
120
|
+
if (!h.endsWith("]"))
|
|
121
|
+
return false;
|
|
122
|
+
const g = ipv6Groups(h.slice(1, -1));
|
|
123
|
+
return g !== undefined && isPublicIpv6(g);
|
|
124
|
+
}
|
|
125
|
+
const o = ipv4Octets(h);
|
|
126
|
+
if (o)
|
|
127
|
+
return isPublicIpv4(o);
|
|
128
|
+
if (/^[\d.]+$/.test(h))
|
|
129
|
+
return false; // malformed or shorthand numeric host
|
|
130
|
+
if (!h.includes("."))
|
|
131
|
+
return false;
|
|
132
|
+
if (h === "localhost" || /\.(localhost|local|internal)$/.test(h))
|
|
133
|
+
return false;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -310,6 +310,26 @@
|
|
|
310
310
|
"minLength": 3,
|
|
311
311
|
"maxLength": 512,
|
|
312
312
|
"pattern": "^\\.\\/"
|
|
313
|
+
},
|
|
314
|
+
"configSchemas": {
|
|
315
|
+
"type": "object",
|
|
316
|
+
"properties": {
|
|
317
|
+
"deployment": {
|
|
318
|
+
"type": "object",
|
|
319
|
+
"propertyNames": {
|
|
320
|
+
"type": "string"
|
|
321
|
+
},
|
|
322
|
+
"additionalProperties": {}
|
|
323
|
+
},
|
|
324
|
+
"org": {
|
|
325
|
+
"type": "object",
|
|
326
|
+
"propertyNames": {
|
|
327
|
+
"type": "string"
|
|
328
|
+
},
|
|
329
|
+
"additionalProperties": {}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
"additionalProperties": false
|
|
313
333
|
}
|
|
314
334
|
},
|
|
315
335
|
"required": [
|