@nylorun/agents 0.13.0-beta → 0.16.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +220 -235
  3. package/dist/access.d.ts +6 -97
  4. package/dist/access.js +1 -103
  5. package/dist/ag-ui/handler.d.ts +3 -6
  6. package/dist/ag-ui/handler.js +2 -2
  7. package/dist/artifacts.d.ts +1 -1
  8. package/dist/files.d.ts +31 -0
  9. package/dist/files.js +60 -0
  10. package/dist/http.d.ts +6 -13
  11. package/dist/http.js +4 -16
  12. package/dist/index.d.ts +8 -16
  13. package/dist/index.js +4 -7
  14. package/dist/plugins/load.d.ts +3 -2
  15. package/dist/plugins/load.js +40 -100
  16. package/dist/plugins/plugin.d.ts +6 -1
  17. package/dist/plugins/plugin.js +20 -7
  18. package/dist/reads.d.ts +17 -0
  19. package/dist/reads.js +17 -0
  20. package/dist/sandboxes.d.ts +29 -0
  21. package/dist/sandboxes.js +10 -1
  22. package/dist/session-client.d.ts +94 -112
  23. package/dist/session-client.js +80 -107
  24. package/dist/skills/index.d.ts +1 -1
  25. package/dist/skills/load.d.ts +14 -3
  26. package/dist/skills/load.js +50 -32
  27. package/dist/skills/skills.d.ts +6 -4
  28. package/dist/skills/skills.js +17 -10
  29. package/dist/sse.js +1 -1
  30. package/dist/studio-embed.d.ts +1 -1
  31. package/dist/studio-embed.js +1 -1
  32. package/package.json +3 -7
  33. package/dist/action-handler.d.ts +0 -63
  34. package/dist/action-handler.js +0 -279
  35. package/dist/browser.d.ts +0 -104
  36. package/dist/browser.js +0 -281
  37. package/dist/delivery-token.d.ts +0 -62
  38. package/dist/delivery-token.js +0 -159
  39. package/dist/execute-action.d.ts +0 -16
  40. package/dist/execute-action.js +0 -288
  41. package/dist/plugins/launch.d.ts +0 -17
  42. package/dist/plugins/launch.js +0 -71
  43. package/dist/sandbox/client.d.ts +0 -63
  44. package/dist/sandbox/client.js +0 -29
  45. package/dist/sandbox/index.d.ts +0 -2
  46. package/dist/sandbox/index.js +0 -1
  47. package/dist/served-definitions.d.ts +0 -20
  48. package/dist/served-definitions.js +0 -100
  49. package/dist/token-endpoint.d.ts +0 -23
  50. package/dist/token-endpoint.js +0 -30
@@ -1,11 +1,13 @@
1
- import { SANDBOX_TOOL_NAMES, delegateOf, isBuiltWorkflow, isSandboxToolName, } from "@nylorun/core/define";
1
+ import { SANDBOX_TOOL_NAMES, codeToolRefusal, delegateOf, isBuiltWorkflow, isSandboxToolName, } from "@nylorun/core/define";
2
2
  import { z } from "zod";
3
3
  import { SCOPES_HEADER, SUBJECT_HEADER } from "@nylorun/core/compatibility";
4
- import { parseSessionEvent, parseSubjectHeaders, } from "@nylorun/core/contracts";
5
- import { AccessClient, TokensClient } from "./access.js";
4
+ import { SessionManifestViewSchema, SessionUsageTotalsSchema, ModelCallsPageSchema, parseSessionEvent, parseSubjectHeaders, } from "@nylorun/core/contracts";
5
+ import { SessionsReadClient } from "./reads.js";
6
+ import { AccessClient } from "./access.js";
6
7
  import { SandboxesClient } from "./sandboxes.js";
7
8
  import { ArtifactsClient } from "./artifacts.js";
8
- import { Transport, id, segment } from "./http.js";
9
+ import { FilesClient } from "./files.js";
10
+ import { Transport, env, id, segment } from "./http.js";
9
11
  import { observeSSE } from "./sse.js";
10
12
  function sessionSandboxOf(transport, sessionId) {
11
13
  const call = (tool, args) => transport.json(`/v1/sessions/${segment(sessionId)}/sandbox/${segment(tool)}`, "POST", args);
@@ -28,8 +30,8 @@ function linkedSessionId(event) {
28
30
  ? sessionId
29
31
  : undefined;
30
32
  }
31
- const middlewareClosure = "Hosted definitions support declarative capabilities and before/after hooks, not middleware closures";
32
- /** The agent's declarations, then those of each agent it uses as a tool, keyed `<child>/<capability>`. */
33
+ const middlewareClosure = "Hosted definitions support declarative capabilities, not middleware closures";
34
+ /** The agent's declarations, then those of each agent it uses as a tool. */
33
35
  function declarationsOf(agent) {
34
36
  if (isBuiltWorkflow(agent) || agent.manifest.kind === "workflow")
35
37
  return [];
@@ -39,30 +41,26 @@ function declarationsOf(agent) {
39
41
  const binding = built.getBinding?.();
40
42
  if (!binding || !("declarations" in binding))
41
43
  return [];
42
- const found = (binding.declarations ?? []).map((item) => ({ key: item.id, item }));
44
+ const found = [...(binding.declarations ?? [])];
43
45
  for (const tool of binding.tools ?? []) {
44
46
  const delegate = delegateOf(tool);
45
- const child = delegate?.agent;
46
- for (const item of child?.getBinding().declarations ?? [])
47
- found.push({ key: `${child.id}/${item.id}`, item });
48
- // A flow agent used as a tool: its agents' declarations, keyed `<flow>/<agent>/<capability>`.
49
- const flow = delegate?.workflow;
50
- for (const leaf of Object.values(flow?.getBinding().agents ?? {}))
51
- for (const item of leaf.declarations ?? [])
52
- found.push({ key: `${flow.id}/${leaf.manifest.id}/${item.id}`, item });
47
+ found.push(...(delegate?.agent?.getBinding().declarations ?? []));
48
+ // A flow agent used as a tool: its agents' declarations.
49
+ for (const leaf of Object.values(delegate?.workflow?.getBinding().agents ?? {}))
50
+ found.push(...(leaf.declarations ?? []));
53
51
  }
54
52
  return found;
55
53
  }
56
- function pluginRootsOf(agent) {
57
- const roots = {};
58
- for (const { key, item } of declarationsOf(agent))
59
- if (item.pluginRoot)
60
- roots[key] = item.pluginRoot;
61
- return Object.keys(roots).length === 0 ? undefined : roots;
54
+ /** The bytes of every skill file the agent's definition names, by `sha256:<hex>`. */
55
+ function skillFilesOf(agent, into = new Map()) {
56
+ for (const item of declarationsOf(agent))
57
+ for (const [sha256, source] of Object.entries(item.skillFiles ?? {}))
58
+ into.set(sha256, source);
59
+ return into;
62
60
  }
63
61
  /** Closures stay on the live binding. The published manifest cannot name them. */
64
62
  export function assertNoMiddlewareClosures(agent) {
65
- if (declarationsOf(agent).some(({ item }) => item.hasMiddleware))
63
+ if (declarationsOf(agent).some((item) => item.hasMiddleware))
66
64
  throw new Error(middlewareClosure);
67
65
  }
68
66
  export class AgentsClient {
@@ -95,14 +93,7 @@ export class AgentsClient {
95
93
  subject,
96
94
  });
97
95
  }
98
- /**
99
- * Mints subject tokens for signed-in people (Host feature `subject-tokens`), so their
100
- * browser or app calls the Runtime directly. Application key only.
101
- */
102
- get tokens() {
103
- return new TokensClient(this.transport);
104
- }
105
- /** The access policy, signing keys and revocations (Host feature `subject-tokens`). */
96
+ /** The public keys that verify the tokens the Tenant signs. */
106
97
  get access() {
107
98
  return new AccessClient(this.transport);
108
99
  }
@@ -117,10 +108,17 @@ export class AgentsClient {
117
108
  get artifacts() {
118
109
  return new ArtifactsClient(this.transport);
119
110
  }
111
+ /** Definition files: the skill files a definition names, by content hash. */
112
+ get files() {
113
+ return new FilesClient(this.transport);
114
+ }
120
115
  /** The Runtime's protocol features, including optional ones such as `transcript-events`. */
121
116
  hostFeatures(options = {}) {
122
117
  return this.transport.hostFeatures(options.signal);
123
118
  }
119
+ get sessions() {
120
+ return new SessionsReadClient(this.transport);
121
+ }
124
122
  listAgents(options = {}) {
125
123
  return this.transport.json("/v1/agents", "GET", undefined, options.signal);
126
124
  }
@@ -130,44 +128,35 @@ export class AgentsClient {
130
128
  : "";
131
129
  return this.transport.json(`/v1/sessions${query}`, "GET", undefined, options.signal);
132
130
  }
133
- async saveAgent(agent, options) {
134
- if (isBuiltWorkflow(agent) && agent.manifest.workflowSchemaVersion === 2) {
135
- // A v2 workflow embeds its agents: one document, carrying their plugin roots.
136
- const pluginRoots = {};
131
+ /**
132
+ * Registers the agent's definition (`PUT /v1/agents/{id}`), first uploading the skill files
133
+ * it names that the Runtime does not hold. A definition with a tool that runs your code
134
+ * (`tool({ run })`, a flow's tool stage) is refused before anything is sent: the Runtime runs
135
+ * no code of yours during a session. `implementationVersion` defaults to
136
+ * `NYLORUN_IMPLEMENTATION_VERSION`, else `dev`.
137
+ */
138
+ async saveAgent(agent, options = {}) {
139
+ const files = new Map();
140
+ if (isBuiltWorkflow(agent)) {
141
+ // A flow agent embeds its agents: one document, carrying their files.
137
142
  for (const binding of Object.values(agent.getBinding().agents)) {
138
143
  const leaf = { id: binding.manifest.id, manifest: binding.manifest, getBinding: () => binding };
139
144
  assertNoMiddlewareClosures(leaf);
140
- for (const [key, root] of Object.entries(pluginRootsOf(leaf) ?? {}))
141
- pluginRoots[`${leaf.id}/${key}`] = root;
145
+ skillFilesOf(leaf, files);
142
146
  }
143
- return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
144
- requestId: options.requestId ?? id(),
145
- manifest: agent.manifest,
146
- implementationVersion: options.implementationVersion,
147
- ...(Object.keys(pluginRoots).length === 0 ? {} : { pluginRoots }),
148
- });
149
147
  }
150
- if (isBuiltWorkflow(agent)) {
151
- for (const binding of Object.values(agent.getBinding().agents)) {
152
- await this.saveAgent({
153
- id: binding.manifest.id,
154
- manifest: binding.manifest,
155
- getBinding: () => binding,
156
- }, options);
157
- }
158
- return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
159
- requestId: options.requestId ?? id(),
160
- manifest: agent.manifest,
161
- implementationVersion: options.implementationVersion,
162
- });
148
+ else {
149
+ assertNoMiddlewareClosures(agent);
150
+ skillFilesOf(agent, files);
163
151
  }
164
- assertNoMiddlewareClosures(agent);
165
- const pluginRoots = pluginRootsOf(agent);
152
+ const refusal = codeToolRefusal(agent.manifest);
153
+ if (refusal)
154
+ throw new Error(refusal);
155
+ await this.files.ensure(files);
166
156
  return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
167
157
  requestId: options.requestId ?? id(),
168
158
  manifest: agent.manifest,
169
- implementationVersion: options.implementationVersion,
170
- ...(pluginRoots === undefined ? {} : { pluginRoots }),
159
+ implementationVersion: options.implementationVersion ?? env("NYLORUN_IMPLEMENTATION_VERSION") ?? "dev",
171
160
  });
172
161
  }
173
162
  async createSession(options) {
@@ -185,48 +174,6 @@ export class AgentsClient {
185
174
  });
186
175
  return this.session(sessionId);
187
176
  }
188
- createVault(options) {
189
- return this.transport.json("/v1/vaults", "POST", {
190
- requestId: options.requestId ?? id(),
191
- idempotencyKey: options.idempotencyKey,
192
- name: options.name,
193
- ownerUserId: options.ownerUserId,
194
- ...(options.metadata ? { metadata: options.metadata } : {}),
195
- });
196
- }
197
- listVaults(ownerUserId, signal) {
198
- return this.transport.json(`/v1/vaults?ownerUserId=${encodeURIComponent(ownerUserId)}`, "GET", undefined, signal);
199
- }
200
- getVault(vaultId, signal) {
201
- return this.transport.json(`/v1/vaults/${segment(vaultId)}`, "GET", undefined, signal);
202
- }
203
- deleteVault(vaultId) {
204
- return this.transport.json(`/v1/vaults/${segment(vaultId)}`, "DELETE");
205
- }
206
- createCredential(vaultId, options) {
207
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials`, "POST", {
208
- requestId: options.requestId ?? id(),
209
- idempotencyKey: options.idempotencyKey,
210
- name: options.name,
211
- auth: options.auth,
212
- });
213
- }
214
- listCredentials(vaultId, signal) {
215
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials`, "GET", undefined, signal);
216
- }
217
- getCredential(vaultId, credentialId, signal) {
218
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "GET", undefined, signal);
219
- }
220
- rotateCredential(vaultId, credentialId, options) {
221
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "POST", {
222
- requestId: options.requestId ?? id(),
223
- idempotencyKey: options.idempotencyKey,
224
- auth: options.auth,
225
- });
226
- }
227
- deleteCredential(vaultId, credentialId) {
228
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "DELETE");
229
- }
230
177
  session(sessionId) {
231
178
  return new SessionClient(this.transport, sessionId);
232
179
  }
@@ -307,19 +254,45 @@ export class SessionClient {
307
254
  ...(options.reason ? { reason: options.reason } : {}),
308
255
  }, options.signal);
309
256
  }
310
- /** `agent` narrows to one agent used as a tool: its delegationId or its path. */
257
+ async manifest(options = {}) {
258
+ await this.transport.requireFeature("session-reads", options.signal);
259
+ return SessionManifestViewSchema.parse(await this.transport.json(`${this.path}/manifest`, "GET", undefined, options.signal));
260
+ }
261
+ async usage(options = {}) {
262
+ await this.transport.requireFeature("session-reads", options.signal);
263
+ const query = new URLSearchParams();
264
+ if (options.turnId !== undefined)
265
+ query.set("turnId", options.turnId);
266
+ return SessionUsageTotalsSchema.parse(await this.transport.json(`${this.path}/usage${query.size ? `?${query}` : ""}`, "GET", undefined, options.signal));
267
+ }
268
+ async modelCalls(options = {}) {
269
+ await this.transport.requireFeature("session-reads", options.signal);
270
+ const query = new URLSearchParams({ limit: String(options.limit ?? 50) });
271
+ if (options.turnId !== undefined)
272
+ query.set("turnId", options.turnId);
273
+ if (options.cursor !== undefined)
274
+ query.set("cursor", options.cursor);
275
+ return ModelCallsPageSchema.parse(await this.transport.json(`${this.path}/calls/model?${query}`, "GET", undefined, options.signal));
276
+ }
311
277
  async history(options = {}) {
312
278
  const query = new URLSearchParams();
313
279
  if (options.cursor)
314
280
  query.set("cursor", options.cursor);
315
281
  if (options.agent)
316
282
  query.set("agent", options.agent);
283
+ if (options.limit !== undefined) {
284
+ await this.transport.requireFeature("session-reads", options.signal);
285
+ query.set("limit", String(options.limit));
286
+ }
317
287
  const search = query.toString();
318
- const page = z
319
- .object({ items: z.array(z.unknown()), cursor: z.string().nullable() })
320
- .parse(await this.transport.json(`${this.path}/items${search ? `?${search}` : ""}`, "GET", undefined, options.signal));
288
+ const schema = z.object({ items: z.array(z.unknown()), cursor: z.string().nullable() });
289
+ const page = (options.limit === undefined ? schema : schema.extend({ tail: z.boolean() })).parse(await this.transport.json(`${this.path}/items${search ? `?${search}` : ""}`, "GET", undefined, options.signal));
321
290
  // Typed when the catalog knows the type; a newer Runtime's type stays a bare envelope.
322
- return { items: page.items.map(parseSessionEvent), cursor: page.cursor };
291
+ return {
292
+ items: page.items.map(parseSessionEvent),
293
+ cursor: page.cursor,
294
+ ...("tail" in page ? { tail: page.tail } : {}),
295
+ };
323
296
  }
324
297
  async *observe(options = {}) {
325
298
  if (!options.follow) {
@@ -398,5 +371,5 @@ export class SessionClient {
398
371
  }
399
372
  }
400
373
  export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
401
- export { AccessClient, PublishableKeysClient, SigningKeysClient, TokensClient, } from "./access.js";
374
+ export { AccessClient } from "./access.js";
402
375
  export { SandboxesClient } from "./sandboxes.js";
@@ -1,5 +1,5 @@
1
1
  export { skills } from "./skills.js";
2
2
  export type { SkillsCapability, SkillsOptions } from "./skills.js";
3
3
  export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
4
- export type { SkillDiagnostic } from "./load.js";
4
+ export type { LoadedSkill, SkillDiagnostic } from "./load.js";
5
5
  export { formatSkillCatalog, SKILLS_USAGE } from "./catalog.js";
@@ -1,4 +1,4 @@
1
- import type { SkillRecord } from "@nylorun/core/define";
1
+ import { type SkillFileSource } from "@nylorun/core/define";
2
2
  export interface SkillDiagnostic {
3
3
  readonly severity: "info" | "warning";
4
4
  readonly code: string;
@@ -10,16 +10,27 @@ export declare class SkillsError extends Error {
10
10
  readonly diagnostics: readonly SkillDiagnostic[];
11
11
  constructor(code: string, message: string, diagnostics?: readonly SkillDiagnostic[]);
12
12
  }
13
+ /** One skill read from its folder: what the manifest names, and where each file's bytes are. */
14
+ export interface LoadedSkill {
15
+ readonly name: string;
16
+ readonly description: string;
17
+ /** Every file of the folder by its `/`-separated path, as `sha256:<hex>`. */
18
+ readonly files: Readonly<Record<string, string>>;
19
+ /** Each file's bytes, by `sha256:<hex>`, for the client to upload. */
20
+ readonly sources: Readonly<Record<string, SkillFileSource>>;
21
+ }
13
22
  /** Resolve a skills catalog directory; throws when missing or not a directory. */
14
23
  export declare function resolveSkillsRoot(directory: string): string;
15
24
  /**
16
25
  * Load Agent Skills (agentskills.io) from a catalog directory.
17
- * Each immediate subdirectory with a valid SKILL.md becomes one skill.
26
+ * Each immediate subdirectory with a valid SKILL.md becomes one skill, with every file of its
27
+ * folder (text or binary) hashed. Throws `SkillsError` for a skill the Runtime cannot hold: a
28
+ * file over 10 MiB, more than 500 files, or a path a manifest cannot name.
18
29
  */
19
30
  export declare function loadSkillsFromDirectory(directory: string, diagnostics: SkillDiagnostic[], options?: {
20
31
  readonly boundary?: string;
21
32
  readonly codePrefix?: string;
22
- }): Readonly<Record<string, SkillRecord>>;
33
+ }): Readonly<Record<string, LoadedSkill>>;
23
34
  export declare function parseSkill(text: string, directoryName: string): {
24
35
  name: string;
25
36
  description: string;
@@ -1,7 +1,15 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync, readdirSync, readFileSync, realpathSync, statSync, } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
2
4
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { DEFINITION_FILE_MAX_BYTES, SKILL_ENTRY, SKILL_FILES_MAX, skillFilePathIssue, } from "@nylorun/core/define";
3
6
  import matter from "gray-matter";
4
7
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ /** Version control and operating system files, never part of a skill. */
9
+ const SKIPPED_DIRECTORIES = new Set([".git", ".hg", ".svn", "node_modules"]);
10
+ const SKIPPED_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
11
+ /** Environment files hold secrets: never uploaded (`.env`, `.env.local`, …). */
12
+ const isEnvFile = (name) => name === ".env" || name.startsWith(".env.");
5
13
  export class SkillsError extends Error {
6
14
  code;
7
15
  diagnostics;
@@ -24,7 +32,9 @@ export function resolveSkillsRoot(directory) {
24
32
  }
25
33
  /**
26
34
  * Load Agent Skills (agentskills.io) from a catalog directory.
27
- * Each immediate subdirectory with a valid SKILL.md becomes one skill.
35
+ * Each immediate subdirectory with a valid SKILL.md becomes one skill, with every file of its
36
+ * folder (text or binary) hashed. Throws `SkillsError` for a skill the Runtime cannot hold: a
37
+ * file over 10 MiB, more than 500 files, or a path a manifest cannot name.
28
38
  */
29
39
  export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
30
40
  const root = resolveSkillsRoot(directory);
@@ -34,7 +44,7 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
34
44
  for (const entry of readdirSync(root, { withFileTypes: true })) {
35
45
  if (!entry.isDirectory() || entry.name.startsWith("."))
36
46
  continue;
37
- if (entry.name === "node_modules" || entry.name === ".git")
47
+ if (SKIPPED_DIRECTORIES.has(entry.name))
38
48
  continue;
39
49
  const skillDir = join(root, entry.name);
40
50
  const directoryReal = realInside(boundary, skillDir);
@@ -47,7 +57,7 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
47
57
  });
48
58
  continue;
49
59
  }
50
- const skillFile = join(directoryReal, "SKILL.md");
60
+ const skillFile = join(directoryReal, SKILL_ENTRY);
51
61
  if (!existsSync(skillFile))
52
62
  continue;
53
63
  const skillReal = realInside(boundary, skillFile);
@@ -79,12 +89,10 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
79
89
  });
80
90
  continue;
81
91
  }
82
- const resources = readResources(boundary, directoryReal, diagnostics, prefix);
83
92
  skills[parsed.name] = {
84
93
  name: parsed.name,
85
94
  description: parsed.description,
86
- instructions: parsed.instructions,
87
- ...(Object.keys(resources).length === 0 ? {} : { resources }),
95
+ ...readFiles(parsed.name, boundary, directoryReal, diagnostics, prefix),
88
96
  };
89
97
  }
90
98
  return skills;
@@ -112,47 +120,57 @@ export function parseSkill(text, directoryName) {
112
120
  function asTrimmedString(value) {
113
121
  return typeof value === "string" ? value.trim() : "";
114
122
  }
115
- function readResources(boundary, directory, diagnostics, prefix) {
116
- const resources = {};
117
- const walk = (current, depth) => {
118
- if (depth > 6)
123
+ /** Every file of the skill's folder, hashed, and where to read each one's bytes. */
124
+ function readFiles(name, boundary, directory, diagnostics, prefix) {
125
+ const files = {};
126
+ const sources = {};
127
+ const visited = new Set();
128
+ // `at` is the folder's path in the skill: symbolic links inside the package are followed.
129
+ const walk = (current, at) => {
130
+ if (visited.has(current))
119
131
  return;
132
+ visited.add(current);
120
133
  for (const entry of readdirSync(current, { withFileTypes: true })) {
121
- if (entry.name.startsWith(".") || entry.name === "node_modules")
122
- continue;
123
134
  const full = join(current, entry.name);
124
- if (entry.isDirectory()) {
125
- const real = realInside(boundary, full);
126
- if (!real)
127
- continue;
128
- walk(real, depth + 1);
129
- continue;
130
- }
131
- if (!entry.isFile() && !entry.isSymbolicLink())
132
- continue;
133
- if (depth === 0 && entry.name === "SKILL.md")
134
- continue;
135
135
  const real = realInside(boundary, full);
136
- if (!real || !statSync(real).isFile()) {
136
+ if (!real) {
137
137
  diagnostics.push({
138
138
  severity: "warning",
139
139
  code: `${prefix}.resource-denied`,
140
- message: `Denied resource outside the package: ${entry.name}`,
140
+ message: `Denied a file of skill '${name}' outside the package: ${entry.name}`,
141
141
  path: full,
142
142
  });
143
143
  continue;
144
144
  }
145
- const bytes = readFileSync(real);
146
- if (bytes.includes(0))
145
+ const stats = statSync(real);
146
+ const path = at === "" ? entry.name : `${at}/${entry.name}`;
147
+ if (stats.isDirectory()) {
148
+ if (!SKIPPED_DIRECTORIES.has(entry.name))
149
+ walk(real, path);
147
150
  continue;
148
- const rel = relative(directory, real).split(sep).join("/");
149
- if (!rel || rel.startsWith(".."))
151
+ }
152
+ if (!stats.isFile() || SKIPPED_FILES.has(entry.name) || isEnvFile(entry.name))
150
153
  continue;
151
- resources[rel] = bytes.toString("utf8");
154
+ const issue = skillFilePathIssue(path);
155
+ if (issue)
156
+ throw new SkillsError(`${prefix}.invalid-path`, `Skill '${name}' has a file whose path ${issue}: ${path}`);
157
+ if (stats.size > DEFINITION_FILE_MAX_BYTES)
158
+ throw new SkillsError(`${prefix}.file-too-large`, `Skill '${name}' file ${path} is ${stats.size} bytes; a skill file may be at most ${DEFINITION_FILE_MAX_BYTES} (10 MiB)`);
159
+ const hash = `sha256:${createHash("sha256").update(readFileSync(real)).digest("hex")}`;
160
+ files[path] = hash;
161
+ sources[hash] = {
162
+ size: stats.size,
163
+ read: async () => new Uint8Array(await readFile(real)),
164
+ };
165
+ if (Object.keys(files).length > SKILL_FILES_MAX)
166
+ throw new SkillsError(`${prefix}.too-many-files`, `Skill '${name}' has more than ${SKILL_FILES_MAX} files; a skill may have at most ${SKILL_FILES_MAX}`);
152
167
  }
153
168
  };
154
- walk(directory, 0);
155
- return resources;
169
+ walk(directory, "");
170
+ return { files: sorted(files), sources };
171
+ }
172
+ function sorted(files) {
173
+ return Object.fromEntries(Object.entries(files).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
156
174
  }
157
175
  function realInside(root, candidate) {
158
176
  if (!existsSync(candidate))
@@ -1,5 +1,5 @@
1
1
  import type { CapabilityDeclaration } from "@nylorun/core/define";
2
- import { type SkillDiagnostic } from "./load.js";
2
+ import { type LoadedSkill, type SkillDiagnostic } from "./load.js";
3
3
  export interface SkillsOptions {
4
4
  /** Capability id. Defaults to the catalog directory basename. */
5
5
  readonly id?: string;
@@ -21,8 +21,10 @@ export interface SkillsCapability extends CapabilityDeclaration {
21
21
  * references/policy.md
22
22
  * ```
23
23
  *
24
- * Sets both `skills` (manifest names/descriptions) and `skillRecords` (bodies +
25
- * resources) so callers do not duplicate content. The harness injects
26
- * `load_skill` / `read_skill_resource` from the records.
24
+ * Sets `skills`: each skill's name, description and files (every file of its folder, binary
25
+ * included, by content hash). The client uploads the files the Runtime lacks when it registers
26
+ * the agent, and the Runtime serves `load_skill` / `read_skill_resource` from them.
27
27
  */
28
28
  export declare function skills(directory: string, options?: SkillsOptions): SkillsCapability;
29
+ /** The manifest's skills and the bytes behind their files, for a capability declaration. */
30
+ export declare function skillsDeclaration(listed: readonly LoadedSkill[]): Required<Pick<CapabilityDeclaration, "skills" | "skillFiles">>;
@@ -13,23 +13,20 @@ import { loadSkillsFromDirectory, resolveSkillsRoot, } from "./load.js";
13
13
  * references/policy.md
14
14
  * ```
15
15
  *
16
- * Sets both `skills` (manifest names/descriptions) and `skillRecords` (bodies +
17
- * resources) so callers do not duplicate content. The harness injects
18
- * `load_skill` / `read_skill_resource` from the records.
16
+ * Sets `skills`: each skill's name, description and files (every file of its folder, binary
17
+ * included, by content hash). The client uploads the files the Runtime lacks when it registers
18
+ * the agent, and the Runtime serves `load_skill` / `read_skill_resource` from them.
19
19
  */
20
20
  export function skills(directory, options = {}) {
21
21
  const root = resolveSkillsRoot(directory);
22
22
  const diagnostics = [];
23
- const records = loadSkillsFromDirectory(root, diagnostics);
24
- const listed = Object.values(records);
23
+ const loaded = loadSkillsFromDirectory(root, diagnostics);
24
+ const listed = Object.values(loaded);
25
25
  const id = options.id ?? basename(root);
26
26
  if (listed.length === 0) {
27
27
  return { id, root, diagnostics };
28
28
  }
29
- const skillManifest = Object.fromEntries(listed.map((skill) => [
30
- skill.name,
31
- { name: skill.name, description: skill.description },
32
- ]));
29
+ const { skills: skillManifest, skillFiles } = skillsDeclaration(listed);
33
30
  return {
34
31
  id,
35
32
  root,
@@ -41,7 +38,17 @@ export function skills(directory, options = {}) {
41
38
  }))),
42
39
  ],
43
40
  skills: skillManifest,
44
- skillRecords: records,
41
+ skillFiles,
45
42
  diagnostics,
46
43
  };
47
44
  }
45
+ /** The manifest's skills and the bytes behind their files, for a capability declaration. */
46
+ export function skillsDeclaration(listed) {
47
+ return {
48
+ skills: Object.fromEntries(listed.map((skill) => [
49
+ skill.name,
50
+ { name: skill.name, description: skill.description, files: skill.files },
51
+ ])),
52
+ skillFiles: Object.assign({}, ...listed.map((skill) => skill.sources)),
53
+ };
54
+ }
package/dist/sse.js CHANGED
@@ -65,7 +65,7 @@ export async function* observeSSE(transport, path, options = {}) {
65
65
  const signal = options.signal ?? controller.signal;
66
66
  let cursor = options.cursor;
67
67
  let retry = 250;
68
- // The Runtime ended the stream because its subject token expired or was revoked: reconnect
68
+ // The Runtime ended the stream because its token expired: reconnect
69
69
  // at once (the transport fetches a new token), but only once in a row.
70
70
  let closedByRuntime = 0;
71
71
  while (!signal.aborted) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `@nylorun/agents/studio-embed`: the contract between Studio and an app that embeds it in an
3
- * iframe (Studio design §8). Studio's web app and embedders such as Babai import it from here,
3
+ * iframe (Studio design §8). Studio's web app and the apps that embed it import it from here,
4
4
  * because Studio may not depend on `@nylorun/core` directly.
5
5
  *
6
6
  * No Node-only module is imported here or below.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `@nylorun/agents/studio-embed`: the contract between Studio and an app that embeds it in an
3
- * iframe (Studio design §8). Studio's web app and embedders such as Babai import it from here,
3
+ * iframe (Studio design §8). Studio's web app and the apps that embed it import it from here,
4
4
  * because Studio may not depend on `@nylorun/core` directly.
5
5
  *
6
6
  * No Node-only module is imported here or below.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nylorun/agents",
3
- "version": "0.13.0-beta",
4
- "description": "Nylorun agent definitions, session client and Action endpoint",
3
+ "version": "0.16.0-beta",
4
+ "description": "Nylorun agent definitions and session client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "files": [
@@ -28,10 +28,6 @@
28
28
  "types": "./dist/ag-ui/index.d.ts",
29
29
  "import": "./dist/ag-ui/index.js"
30
30
  },
31
- "./browser": {
32
- "types": "./dist/browser.d.ts",
33
- "import": "./dist/browser.js"
34
- },
35
31
  "./a2a": {
36
32
  "types": "./dist/a2a/index.d.ts",
37
33
  "import": "./dist/a2a/index.js"
@@ -47,7 +43,7 @@
47
43
  "test": "vitest run --passWithNoTests"
48
44
  },
49
45
  "dependencies": {
50
- "@nylorun/core": "0.12.0-beta",
46
+ "@nylorun/core": "0.15.0-beta",
51
47
  "gray-matter": "^4.0.3"
52
48
  },
53
49
  "peerDependencies": {