@nylorun/agents 0.2.0-beta → 0.4.0-beta

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.
@@ -0,0 +1,23 @@
1
+ import type { McpServerManifest, SkillRecord } from "@nylorun/core/define";
2
+ export declare const PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
3
+ export declare const MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
4
+ export interface PluginDiagnostic {
5
+ readonly severity: "info" | "warning";
6
+ readonly code: string;
7
+ readonly message: string;
8
+ readonly path?: string;
9
+ }
10
+ export declare class PluginError extends Error {
11
+ readonly code: string;
12
+ readonly diagnostics: readonly PluginDiagnostic[];
13
+ constructor(code: string, message: string, diagnostics?: readonly PluginDiagnostic[]);
14
+ }
15
+ export interface LoadedPlugin {
16
+ readonly root: string;
17
+ readonly name: string;
18
+ readonly description?: string;
19
+ readonly skills: Readonly<Record<string, SkillRecord>>;
20
+ readonly mcpServers: Readonly<Record<string, McpServerManifest>>;
21
+ readonly diagnostics: readonly PluginDiagnostic[];
22
+ }
23
+ export declare function loadPlugin(directory: string): LoadedPlugin;
@@ -0,0 +1,369 @@
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { loadSkillsFromDirectory } from "../skills/load.js";
4
+ import { expandPluginPlaceholders } from "./launch.js";
5
+ export const PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
6
+ export const MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
7
+ const PLUGIN_FIELDS = new Set([
8
+ "$schema",
9
+ "name",
10
+ "version",
11
+ "description",
12
+ "author",
13
+ "homepage",
14
+ "repository",
15
+ "license",
16
+ "keywords",
17
+ "extensions",
18
+ ]);
19
+ const AUTHOR_FIELDS = new Set(["name", "email", "url"]);
20
+ const RESERVED_ENV = new Set(["PLUGIN_ROOT", "PLUGIN_DATA"]);
21
+ export class PluginError extends Error {
22
+ code;
23
+ diagnostics;
24
+ constructor(code, message, diagnostics = []) {
25
+ super(message);
26
+ this.name = "PluginError";
27
+ this.code = code;
28
+ this.diagnostics = diagnostics;
29
+ }
30
+ }
31
+ export function loadPlugin(directory) {
32
+ const root = resolveRoot(directory);
33
+ const manifestPath = join(root, "plugin.json");
34
+ const manifestReal = realInside(root, manifestPath);
35
+ if (!manifestReal || !existsSync(manifestReal) || !statSync(manifestReal).isFile())
36
+ throw new PluginError("plugin.manifest-missing", `plugin.json must be a file inside ${root}`, [{ severity: "warning", code: "plugin.manifest-missing", message: "plugin.json is missing or escapes the package root", path: manifestPath }]);
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(readFileSync(manifestReal, "utf8"));
40
+ }
41
+ catch {
42
+ throw new PluginError("plugin.manifest-invalid", "plugin.json is not valid JSON");
43
+ }
44
+ if (!isRecord(parsed))
45
+ throw new PluginError("plugin.manifest-invalid", "plugin.json must be a JSON object");
46
+ const diagnostics = [];
47
+ for (const key of Object.keys(parsed)) {
48
+ if (!PLUGIN_FIELDS.has(key))
49
+ diagnostics.push({
50
+ severity: "warning",
51
+ code: "plugin.unknown-field",
52
+ message: `Ignored unknown plugin.json field '${key}'`,
53
+ path: manifestPath,
54
+ });
55
+ }
56
+ if (parsed.$schema !== PLUGIN_SCHEMA)
57
+ throw new PluginError("plugin.schema-unsupported", "plugin.json $schema must be the Agent Plugins 1.0.0 plugin schema", diagnostics);
58
+ if (!isPluginName(parsed.name))
59
+ throw new PluginError("plugin.name-invalid", "plugin.json name must be 1-64 characters of lowercase letters, digits, hyphens, and periods", diagnostics);
60
+ validateMetadata(parsed, diagnostics);
61
+ const extensions = parsed.extensions;
62
+ if (extensions !== undefined && !isRecord(extensions))
63
+ diagnostics.push({
64
+ severity: "warning",
65
+ code: "plugin.extensions-ignored",
66
+ message: "Ignored non-object extensions field",
67
+ path: manifestPath,
68
+ });
69
+ else if (isRecord(extensions)) {
70
+ for (const namespace of Object.keys(extensions))
71
+ diagnostics.push({
72
+ severity: "info",
73
+ code: "plugin.extension-ignored",
74
+ message: `Ignored unimplemented extension namespace '${namespace}'`,
75
+ path: manifestPath,
76
+ });
77
+ }
78
+ const skills = loadSkills(root, diagnostics);
79
+ const mcpServers = loadMcp(root, parsed.name, diagnostics);
80
+ return {
81
+ root,
82
+ name: parsed.name,
83
+ ...(typeof parsed.description === "string" ? { description: parsed.description } : {}),
84
+ skills,
85
+ mcpServers,
86
+ diagnostics,
87
+ };
88
+ }
89
+ function resolveRoot(directory) {
90
+ const resolved = resolve(directory);
91
+ if (!existsSync(resolved))
92
+ throw new PluginError("plugin.missing", `Plugin directory does not exist: ${directory}`);
93
+ const real = realpathSync(resolved);
94
+ if (!statSync(real).isDirectory())
95
+ throw new PluginError("plugin.missing", `Plugin path is not a directory: ${directory}`);
96
+ return real;
97
+ }
98
+ function validateMetadata(manifest, diagnostics) {
99
+ for (const field of ["version", "description", "homepage", "repository", "license"]) {
100
+ if (manifest[field] !== undefined && typeof manifest[field] !== "string")
101
+ throw new PluginError("plugin.manifest-invalid", `plugin.json ${field} must be a string`, diagnostics);
102
+ }
103
+ if (manifest.keywords !== undefined) {
104
+ if (!Array.isArray(manifest.keywords) || manifest.keywords.some((item) => typeof item !== "string"))
105
+ throw new PluginError("plugin.manifest-invalid", "plugin.json keywords must be an array of strings", diagnostics);
106
+ }
107
+ if (manifest.author !== undefined) {
108
+ if (!isRecord(manifest.author))
109
+ throw new PluginError("plugin.manifest-invalid", "plugin.json author must be an object", diagnostics);
110
+ for (const key of Object.keys(manifest.author)) {
111
+ if (!AUTHOR_FIELDS.has(key) || typeof manifest.author[key] !== "string")
112
+ throw new PluginError("plugin.manifest-invalid", "plugin.json author contains an invalid field", diagnostics);
113
+ }
114
+ }
115
+ }
116
+ function loadSkills(root, diagnostics) {
117
+ const location = join(root, "skills");
118
+ if (!existsSync(location))
119
+ return {};
120
+ const real = realInside(root, location);
121
+ if (!real || !statSync(real).isDirectory()) {
122
+ diagnostics.push({
123
+ severity: "warning",
124
+ code: "plugin.skills-invalid",
125
+ message: "skills/ is not a directory inside the package",
126
+ path: location,
127
+ });
128
+ return {};
129
+ }
130
+ return loadSkillsFromDirectory(real, diagnostics, {
131
+ boundary: root,
132
+ codePrefix: "plugin",
133
+ });
134
+ }
135
+ function loadMcp(root, pluginName, diagnostics) {
136
+ const location = join(root, "mcp.json");
137
+ if (!existsSync(location))
138
+ return {};
139
+ const real = realInside(root, location);
140
+ if (!real || !statSync(real).isFile()) {
141
+ diagnostics.push({
142
+ severity: "warning",
143
+ code: "plugin.mcp-invalid",
144
+ message: "mcp.json is not a file inside the package",
145
+ path: location,
146
+ });
147
+ return {};
148
+ }
149
+ let parsed;
150
+ try {
151
+ parsed = JSON.parse(readFileSync(real, "utf8"));
152
+ }
153
+ catch {
154
+ diagnostics.push({
155
+ severity: "warning",
156
+ code: "plugin.mcp-invalid",
157
+ message: "mcp.json is not valid JSON",
158
+ path: location,
159
+ });
160
+ return {};
161
+ }
162
+ if (!isRecord(parsed) || Object.keys(parsed).some((key) => key !== "$schema" && key !== "mcpServers")) {
163
+ diagnostics.push({
164
+ severity: "warning",
165
+ code: "plugin.mcp-invalid",
166
+ message: "mcp.json must contain only $schema and mcpServers",
167
+ path: location,
168
+ });
169
+ return {};
170
+ }
171
+ if (parsed.$schema !== MCP_SCHEMA) {
172
+ diagnostics.push({
173
+ severity: "warning",
174
+ code: "plugin.mcp-invalid",
175
+ message: "mcp.json $schema does not match the plugin schema version",
176
+ path: location,
177
+ });
178
+ return {};
179
+ }
180
+ if (!isRecord(parsed.mcpServers)) {
181
+ diagnostics.push({
182
+ severity: "warning",
183
+ code: "plugin.mcp-invalid",
184
+ message: "mcp.json mcpServers must be an object",
185
+ path: location,
186
+ });
187
+ return {};
188
+ }
189
+ const pluginData = join(dirname(root), ".nylorun", "plugin-data", pluginName);
190
+ const servers = {};
191
+ for (const [name, value] of Object.entries(parsed.mcpServers)) {
192
+ const server = validateServer(name, value, root, pluginData);
193
+ if (!server) {
194
+ diagnostics.push({
195
+ severity: "warning",
196
+ code: "plugin.mcp-server-skipped",
197
+ message: `Skipped invalid MCP server '${name}'`,
198
+ path: location,
199
+ });
200
+ continue;
201
+ }
202
+ if (server.type === "stdio")
203
+ mkdirSync(pluginData, { recursive: true });
204
+ servers[name] = server;
205
+ }
206
+ return servers;
207
+ }
208
+ function validateServer(name, value, root, pluginData) {
209
+ if (!isRecord(value) || typeof value.type !== "string")
210
+ return undefined;
211
+ if (value.type === "stdio")
212
+ return validateStdio(name, value, root, pluginData);
213
+ if (value.type === "streamable-http" || value.type === "sse")
214
+ return validateRemote(name, value);
215
+ return undefined;
216
+ }
217
+ function validateStdio(name, value, root, pluginData) {
218
+ const allowed = new Set(["type", "command", "args", "env", "cwd"]);
219
+ if (Object.keys(value).some((key) => !allowed.has(key)))
220
+ return undefined;
221
+ if (typeof value.command !== "string" || !isCommandToken(value.command))
222
+ return undefined;
223
+ if (value.command.startsWith("./") && !commandStaysInside(root, value.command))
224
+ return undefined;
225
+ let args;
226
+ if (value.args !== undefined) {
227
+ if (!Array.isArray(value.args) || value.args.some((item) => typeof item !== "string"))
228
+ return undefined;
229
+ args = value.args;
230
+ }
231
+ let env;
232
+ if (value.env !== undefined) {
233
+ if (!isRecord(value.env))
234
+ return undefined;
235
+ env = {};
236
+ for (const [key, item] of Object.entries(value.env)) {
237
+ if (typeof item !== "string" || RESERVED_ENV.has(key))
238
+ return undefined;
239
+ env[key] = item;
240
+ }
241
+ }
242
+ let cwd;
243
+ if (value.cwd !== undefined) {
244
+ if (typeof value.cwd !== "string" || !isCwdForm(value.cwd))
245
+ return undefined;
246
+ const expanded = expandPluginPlaceholders(value.cwd, root, pluginData);
247
+ const absolute = expanded.startsWith("/") || /^[A-Za-z]:[\\/]/.test(expanded)
248
+ ? expanded
249
+ : resolve(root, expanded);
250
+ const boundary = value.cwd === "${PLUGIN_DATA}" || value.cwd.startsWith("${PLUGIN_DATA}/")
251
+ ? pluginData
252
+ : root;
253
+ if (!isInside(boundary, absolute))
254
+ return undefined;
255
+ cwd = value.cwd;
256
+ }
257
+ return {
258
+ name,
259
+ type: "stdio",
260
+ command: value.command,
261
+ ...(args === undefined ? {} : { args }),
262
+ ...(env === undefined ? {} : { env }),
263
+ ...(cwd === undefined ? {} : { cwd }),
264
+ };
265
+ }
266
+ function validateRemote(name, value) {
267
+ const allowed = new Set(["type", "url", "headers"]);
268
+ if (Object.keys(value).some((key) => !allowed.has(key)))
269
+ return undefined;
270
+ if (typeof value.url !== "string" || !isRemoteUrl(value.url))
271
+ return undefined;
272
+ let headers;
273
+ if (value.headers !== undefined) {
274
+ if (!isRecord(value.headers))
275
+ return undefined;
276
+ const seen = new Set();
277
+ headers = {};
278
+ for (const [key, item] of Object.entries(value.headers)) {
279
+ if (typeof item !== "string" || !isHeaderName(key) || /[\r\n\0]/.test(item))
280
+ return undefined;
281
+ const folded = key.toLowerCase();
282
+ if (seen.has(folded))
283
+ return undefined;
284
+ seen.add(folded);
285
+ headers[key] = item;
286
+ }
287
+ }
288
+ return {
289
+ name,
290
+ type: value.type,
291
+ url: value.url,
292
+ ...(headers === undefined ? {} : { headers }),
293
+ };
294
+ }
295
+ function commandStaysInside(root, command) {
296
+ const lexical = resolve(root, command);
297
+ if (!isInside(root, lexical))
298
+ return false;
299
+ return !existsSync(lexical) || realInside(root, lexical) !== undefined;
300
+ }
301
+ function isCommandToken(command) {
302
+ if (!command || /\s/.test(command))
303
+ return false;
304
+ if (command.startsWith("./"))
305
+ return !command.split("/").includes("..");
306
+ return !command.includes("/") && !command.includes("\\") && !command.includes("${");
307
+ }
308
+ function isCwdForm(cwd) {
309
+ return (cwd.startsWith("./") ||
310
+ cwd === "${PLUGIN_ROOT}" ||
311
+ cwd.startsWith("${PLUGIN_ROOT}/") ||
312
+ cwd === "${PLUGIN_DATA}" ||
313
+ cwd.startsWith("${PLUGIN_DATA}/"));
314
+ }
315
+ function isRemoteUrl(value) {
316
+ let url;
317
+ try {
318
+ url = new URL(value);
319
+ }
320
+ catch {
321
+ return false;
322
+ }
323
+ if (url.username || url.password || url.hash)
324
+ return false;
325
+ if (url.protocol === "https:")
326
+ return true;
327
+ if (url.protocol !== "http:")
328
+ return false;
329
+ return isLoopback(url.hostname);
330
+ }
331
+ function isLoopback(hostname) {
332
+ const host = hostname.replace(/^\[|\]$/g, "");
333
+ if (host === "localhost" || host === "::1")
334
+ return true;
335
+ const match = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
336
+ if (!match)
337
+ return false;
338
+ return match.slice(1).every((part) => Number(part) <= 255);
339
+ }
340
+ function isHeaderName(name) {
341
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
342
+ }
343
+ function isPluginName(value) {
344
+ if (typeof value !== "string" || value.length < 1 || value.length > 64)
345
+ return false;
346
+ if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(value) && value.length !== 1)
347
+ return false;
348
+ if (value.length === 1)
349
+ return /^[a-z0-9]$/.test(value);
350
+ return !value.includes("--") && !value.includes("..");
351
+ }
352
+ function realInside(root, candidate) {
353
+ if (!existsSync(candidate))
354
+ return undefined;
355
+ try {
356
+ const real = realpathSync(candidate);
357
+ return isInside(root, real) ? real : undefined;
358
+ }
359
+ catch {
360
+ return undefined;
361
+ }
362
+ }
363
+ function isInside(root, target) {
364
+ const rel = relative(root, target);
365
+ return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
366
+ }
367
+ function isRecord(value) {
368
+ return !!value && typeof value === "object" && !Array.isArray(value);
369
+ }
@@ -0,0 +1,8 @@
1
+ import type { CapabilityDeclaration } from "@nylorun/core/define";
2
+ import { type LoadedPlugin, type PluginDiagnostic } from "./load.js";
3
+ export interface PluginCapability extends CapabilityDeclaration {
4
+ readonly diagnostics: readonly PluginDiagnostic[];
5
+ }
6
+ /** Read an Agent Plugin package and return one capability for `Agent.use`. */
7
+ export declare function plugin(directory: string): PluginCapability;
8
+ export declare function capabilityFromPlugin(loaded: LoadedPlugin): PluginCapability;
@@ -0,0 +1,35 @@
1
+ import { formatSkillCatalog, SKILLS_USAGE } from "../skills/catalog.js";
2
+ import { loadPlugin, } from "./load.js";
3
+ /** Read an Agent Plugin package and return one capability for `Agent.use`. */
4
+ export function plugin(directory) {
5
+ const loaded = loadPlugin(directory);
6
+ return capabilityFromPlugin(loaded);
7
+ }
8
+ export function capabilityFromPlugin(loaded) {
9
+ const names = Object.values(loaded.skills);
10
+ const instructions = names.length === 0
11
+ ? undefined
12
+ : [
13
+ SKILLS_USAGE,
14
+ formatSkillCatalog(names.map((skill) => ({
15
+ name: skill.name,
16
+ description: skill.description,
17
+ }))),
18
+ ];
19
+ const skills = Object.fromEntries(names.map((skill) => [
20
+ skill.name,
21
+ { name: skill.name, description: skill.description },
22
+ ]));
23
+ return {
24
+ id: loaded.name,
25
+ type: "agent-plugin",
26
+ pluginRoot: loaded.root,
27
+ ...(loaded.description === undefined ? {} : { description: loaded.description }),
28
+ ...(instructions === undefined ? {} : { instructions }),
29
+ ...(names.length === 0 ? {} : { skills, skillRecords: loaded.skills }),
30
+ ...(Object.keys(loaded.mcpServers).length === 0
31
+ ? {}
32
+ : { mcpServers: loaded.mcpServers }),
33
+ diagnostics: loaded.diagnostics,
34
+ };
35
+ }
@@ -0,0 +1,2 @@
1
+ export { sandbox, SandboxError } from "./sandbox.js";
2
+ export type { SandboxCapability, SandboxCapabilityOptions, SandboxOptions, } from "./sandbox.js";
@@ -0,0 +1 @@
1
+ export { sandbox, SandboxError } from "./sandbox.js";
@@ -0,0 +1,28 @@
1
+ import type { CapabilityDeclaration, SandboxManifest, ToolDefinition } from "@nylorun/core/define";
2
+ /** What computer the agent needs. The Runtime decides where it runs. */
3
+ export type SandboxOptions = SandboxManifest;
4
+ export interface SandboxCapabilityOptions {
5
+ /** Capability id. Defaults to `"sandbox"`. */
6
+ readonly id?: string;
7
+ }
8
+ export interface SandboxCapability extends CapabilityDeclaration {
9
+ readonly sandbox: SandboxManifest;
10
+ readonly tools: readonly ToolDefinition[];
11
+ }
12
+ export declare class SandboxError extends Error {
13
+ readonly code: string;
14
+ constructor(code: string, message: string);
15
+ }
16
+ /**
17
+ * Give the agent an isolated Linux machine with a persistent `/workspace`.
18
+ *
19
+ * Adds the built-in `bash`, `read`, `write`, `edit`, `grep` and `glob` tools. They run in the
20
+ * Nylorun Runtime, which picks the backend (a microsandbox VM where available). Agent code
21
+ * declares requirements only.
22
+ *
23
+ * ```ts
24
+ * Agent({ id: "analyst", instructions: "..." }).use(sandbox())
25
+ * Agent({ ... }).use(sandbox({ image: "node:24", network: { allow: ["api.github.com"] } }))
26
+ * ```
27
+ */
28
+ export declare function sandbox(options?: SandboxOptions, capability?: SandboxCapabilityOptions): SandboxCapability;
@@ -0,0 +1,93 @@
1
+ import { SANDBOX_DEFERRED_FIELDS, SANDBOX_FIELDS, SANDBOX_INSTRUCTIONS, SANDBOX_NETWORK_PRESETS, createSandboxTools, isSandboxHostPattern, parseSandboxDuration, parseSandboxSize, } from "@nylorun/core/define";
2
+ export class SandboxError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.name = "SandboxError";
7
+ this.code = code;
8
+ }
9
+ }
10
+ /**
11
+ * Give the agent an isolated Linux machine with a persistent `/workspace`.
12
+ *
13
+ * Adds the built-in `bash`, `read`, `write`, `edit`, `grep` and `glob` tools. They run in the
14
+ * Nylorun Runtime, which picks the backend (a microsandbox VM where available). Agent code
15
+ * declares requirements only.
16
+ *
17
+ * ```ts
18
+ * Agent({ id: "analyst", instructions: "..." }).use(sandbox())
19
+ * Agent({ ... }).use(sandbox({ image: "node:24", network: { allow: ["api.github.com"] } }))
20
+ * ```
21
+ */
22
+ export function sandbox(options = {}, capability = {}) {
23
+ const id = capability.id ?? "sandbox";
24
+ if (!isRecord(options))
25
+ throw new SandboxError("sandbox.invalid", "sandbox() expects an options object");
26
+ for (const key of Object.keys(options)) {
27
+ if (SANDBOX_DEFERRED_FIELDS.includes(key))
28
+ throw new SandboxError("sandbox.unsupported", `sandbox({ ${key} }) is not supported in this version. ` +
29
+ `Supported options: ${SANDBOX_FIELDS.join(", ")}. See docs/design/sandboxes.md for what is planned.`);
30
+ if (!SANDBOX_FIELDS.includes(key))
31
+ throw new SandboxError("sandbox.unknown-option", `Unknown sandbox option '${key}'. Supported options: ${SANDBOX_FIELDS.join(", ")}`);
32
+ }
33
+ return Object.freeze({
34
+ id,
35
+ instructions: Object.freeze([SANDBOX_INSTRUCTIONS]),
36
+ tools: createSandboxTools(),
37
+ sandbox: freezeOptions(options),
38
+ });
39
+ }
40
+ function freezeOptions(options) {
41
+ const result = {};
42
+ if (options.image !== undefined) {
43
+ if (typeof options.image !== "string" || options.image.trim().length === 0)
44
+ throw new SandboxError("sandbox.invalid-image", "sandbox({ image }) must be a non-empty OCI image reference");
45
+ result.image = options.image;
46
+ }
47
+ if (options.network !== undefined) {
48
+ if (!isRecord(options.network))
49
+ throw new SandboxError("sandbox.invalid-network", "sandbox({ network }) must be an object");
50
+ for (const key of Object.keys(options.network))
51
+ if (key !== "preset" && key !== "allow")
52
+ throw new SandboxError("sandbox.invalid-network", `Unknown network option '${key}'. Use preset and allow.`);
53
+ const { preset, allow } = options.network;
54
+ if (preset !== undefined && !SANDBOX_NETWORK_PRESETS.includes(preset))
55
+ throw new SandboxError("sandbox.invalid-network", `network.preset must be one of ${SANDBOX_NETWORK_PRESETS.join(", ")}`);
56
+ if (allow !== undefined) {
57
+ if (!Array.isArray(allow))
58
+ throw new SandboxError("sandbox.invalid-network", "network.allow must be an array of host names");
59
+ for (const host of allow)
60
+ if (typeof host !== "string" || !isSandboxHostPattern(host))
61
+ throw new SandboxError("sandbox.invalid-network", `network.allow entry '${String(host)}' must be a host name such as api.github.com or *.example.com (no scheme, port or path)`);
62
+ }
63
+ result.network = Object.freeze({
64
+ ...(preset === undefined ? {} : { preset: preset }),
65
+ ...(allow === undefined ? {} : { allow: Object.freeze([...allow]) }),
66
+ });
67
+ }
68
+ if (options.resources !== undefined) {
69
+ if (!isRecord(options.resources))
70
+ throw new SandboxError("sandbox.invalid-resources", "sandbox({ resources }) must be an object");
71
+ for (const key of Object.keys(options.resources))
72
+ if (key !== "cpus" && key !== "memory")
73
+ throw new SandboxError("sandbox.invalid-resources", `resources.${key} is not supported in this version. Use cpus and memory.`);
74
+ const { cpus, memory } = options.resources;
75
+ if (cpus !== undefined && (!Number.isInteger(cpus) || cpus < 1 || cpus > 64))
76
+ throw new SandboxError("sandbox.invalid-resources", "resources.cpus must be an integer from 1 to 64");
77
+ if (memory !== undefined && (typeof memory !== "string" || parseSandboxSize(memory) === undefined))
78
+ throw new SandboxError("sandbox.invalid-resources", "resources.memory must be a size such as 512MiB or 2GiB");
79
+ result.resources = Object.freeze({
80
+ ...(cpus === undefined ? {} : { cpus }),
81
+ ...(memory === undefined ? {} : { memory }),
82
+ });
83
+ }
84
+ if (options.idle !== undefined) {
85
+ if (typeof options.idle !== "string" || parseSandboxDuration(options.idle) === undefined)
86
+ throw new SandboxError("sandbox.invalid-idle", "sandbox({ idle }) must be a duration such as 30s, 15m or 1h");
87
+ result.idle = options.idle;
88
+ }
89
+ return Object.freeze(result);
90
+ }
91
+ function isRecord(value) {
92
+ return !!value && typeof value === "object" && !Array.isArray(value);
93
+ }
@@ -0,0 +1,5 @@
1
+ export declare const SKILLS_USAGE = "The following skills provide specialized instructions. When a task matches a skill description, call load_skill with that name before proceeding.";
2
+ export declare function formatSkillCatalog(skills: readonly {
3
+ name: string;
4
+ description: string;
5
+ }[]): string;
@@ -0,0 +1,14 @@
1
+ export const SKILLS_USAGE = "The following skills provide specialized instructions. When a task matches a skill description, call load_skill with that name before proceeding.";
2
+ export function formatSkillCatalog(skills) {
3
+ const entries = skills
4
+ .map((skill) => ` <skill>\n <name>${escapeXml(skill.name)}</name>\n <description>${escapeXml(skill.description)}</description>\n </skill>`)
5
+ .join("\n");
6
+ return `<available_skills>\n${entries}\n</available_skills>`;
7
+ }
8
+ function escapeXml(value) {
9
+ return value
10
+ .replaceAll("&", "&amp;")
11
+ .replaceAll("<", "&lt;")
12
+ .replaceAll(">", "&gt;")
13
+ .replaceAll('"', "&quot;");
14
+ }
@@ -0,0 +1,5 @@
1
+ export { skills } from "./skills.js";
2
+ export type { SkillsCapability, SkillsOptions } from "./skills.js";
3
+ export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
4
+ export type { SkillDiagnostic } from "./load.js";
5
+ export { formatSkillCatalog, SKILLS_USAGE } from "./catalog.js";
@@ -0,0 +1,3 @@
1
+ export { skills } from "./skills.js";
2
+ export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
3
+ export { formatSkillCatalog, SKILLS_USAGE } from "./catalog.js";
@@ -0,0 +1,27 @@
1
+ import type { SkillRecord } from "@nylorun/core/define";
2
+ export interface SkillDiagnostic {
3
+ readonly severity: "info" | "warning";
4
+ readonly code: string;
5
+ readonly message: string;
6
+ readonly path?: string;
7
+ }
8
+ export declare class SkillsError extends Error {
9
+ readonly code: string;
10
+ readonly diagnostics: readonly SkillDiagnostic[];
11
+ constructor(code: string, message: string, diagnostics?: readonly SkillDiagnostic[]);
12
+ }
13
+ /** Resolve a skills catalog directory; throws when missing or not a directory. */
14
+ export declare function resolveSkillsRoot(directory: string): string;
15
+ /**
16
+ * Load Agent Skills (agentskills.io) from a catalog directory.
17
+ * Each immediate subdirectory with a valid SKILL.md becomes one skill.
18
+ */
19
+ export declare function loadSkillsFromDirectory(directory: string, diagnostics: SkillDiagnostic[], options?: {
20
+ readonly boundary?: string;
21
+ readonly codePrefix?: string;
22
+ }): Readonly<Record<string, SkillRecord>>;
23
+ export declare function parseSkill(text: string, directoryName: string): {
24
+ name: string;
25
+ description: string;
26
+ instructions: string;
27
+ } | undefined;