@nylorun/agents 0.4.0-beta → 0.6.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,19 @@
1
+ import type { ErrorCode } from "@nylorun/core/compatibility";
2
+ export interface ResolvedConnection {
3
+ url: string;
4
+ tenant: string;
5
+ key: string;
6
+ role: "application" | "executor";
7
+ source: "options" | "environment" | "project-link";
8
+ }
9
+ export declare class ConnectionError extends Error {
10
+ readonly code: ErrorCode;
11
+ constructor(message: string);
12
+ }
13
+ /** Resolve Tenant API connection: options → environment → project link (D§7.1). */
14
+ export declare function resolveConnection(options?: {
15
+ url?: string;
16
+ tenant?: string;
17
+ key?: string;
18
+ cwd?: string;
19
+ }): Promise<ResolvedConnection>;
@@ -0,0 +1,98 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { ProjectCredentialsFileSchema, ProjectLinkFileSchema, } from "@nylorun/core/contracts";
4
+ import { env } from "./http.js";
5
+ export class ConnectionError extends Error {
6
+ code = "connection_missing";
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ConnectionError";
10
+ }
11
+ }
12
+ function missing(tried) {
13
+ throw new ConnectionError(`connection_missing: no Runtime connection found (tried ${tried.join(", ")})`);
14
+ }
15
+ function stripTrailingSlash(url) {
16
+ return url.replace(/\/$/, "");
17
+ }
18
+ async function readProjectLink(startDir) {
19
+ let dir = resolve(startDir);
20
+ for (;;) {
21
+ const nylorun = join(dir, ".nylorun");
22
+ const linkPath = join(nylorun, "link.json");
23
+ let linkRaw;
24
+ try {
25
+ linkRaw = await readFile(linkPath, "utf8");
26
+ }
27
+ catch (error) {
28
+ if (error.code === "ENOENT") {
29
+ const parent = dirname(dir);
30
+ if (parent === dir)
31
+ return undefined;
32
+ dir = parent;
33
+ continue;
34
+ }
35
+ throw error;
36
+ }
37
+ const link = ProjectLinkFileSchema.parse(JSON.parse(linkRaw));
38
+ const credentials = ProjectCredentialsFileSchema.parse(JSON.parse(await readFile(join(nylorun, "credentials.json"), "utf8")));
39
+ return {
40
+ url: stripTrailingSlash(link.hostUrl),
41
+ tenant: link.tenantId,
42
+ key: credentials.applicationKey,
43
+ role: "application",
44
+ source: "project-link",
45
+ };
46
+ }
47
+ }
48
+ /** Resolve Tenant API connection: options → environment → project link (D§7.1). */
49
+ export async function resolveConnection(options) {
50
+ const tried = [];
51
+ const optionUrl = options?.url;
52
+ const optionTenant = options?.tenant;
53
+ const optionKey = options?.key;
54
+ const anyOption = optionUrl !== undefined ||
55
+ optionTenant !== undefined ||
56
+ optionKey !== undefined;
57
+ tried.push("options");
58
+ if (anyOption) {
59
+ if (optionUrl && optionTenant && optionKey) {
60
+ return {
61
+ url: stripTrailingSlash(optionUrl),
62
+ tenant: optionTenant,
63
+ key: optionKey,
64
+ role: "application",
65
+ source: "options",
66
+ };
67
+ }
68
+ missing(tried.concat(["environment", "project-link"]));
69
+ }
70
+ const envUrl = env("NYLORUN_RUNTIME_URL");
71
+ const envTenant = env("NYLORUN_TENANT");
72
+ const envServerKey = env("NYLORUN_SERVER_KEY");
73
+ const envExecutorKey = env("NYLORUN_EXECUTOR_KEY");
74
+ const anyEnv = envUrl !== undefined ||
75
+ envTenant !== undefined ||
76
+ envServerKey !== undefined ||
77
+ envExecutorKey !== undefined;
78
+ tried.push("environment");
79
+ if (anyEnv) {
80
+ const key = envExecutorKey ?? envServerKey;
81
+ if (envUrl && envTenant && key) {
82
+ return {
83
+ url: stripTrailingSlash(envUrl),
84
+ tenant: envTenant,
85
+ key,
86
+ role: envExecutorKey !== undefined ? "executor" : "application",
87
+ source: "environment",
88
+ };
89
+ }
90
+ missing(tried.concat(["project-link"]));
91
+ }
92
+ tried.push("project-link");
93
+ const cwd = options?.cwd ?? (typeof process !== "undefined" ? process.cwd() : ".");
94
+ const fromLink = await readProjectLink(cwd);
95
+ if (fromLink)
96
+ return fromLink;
97
+ missing(tried);
98
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Deterministic executor token from an application key (D§7.3).
3
+ * Same key, Tenant and agent always produce the same token.
4
+ */
5
+ export declare function deriveExecutorToken(applicationKey: string, tenantId: string, agentId: string): string;
@@ -0,0 +1,17 @@
1
+ import { createHmac } from "node:crypto";
2
+ /**
3
+ * Deterministic executor token from an application key (D§7.3).
4
+ * Same key, Tenant and agent always produce the same token.
5
+ */
6
+ export function deriveExecutorToken(applicationKey, tenantId, agentId) {
7
+ const message = Buffer.concat([
8
+ Buffer.from("nylorun/executor/v1", "utf8"),
9
+ Buffer.from([0]),
10
+ Buffer.from(tenantId, "utf8"),
11
+ Buffer.from([0]),
12
+ Buffer.from(agentId, "utf8"),
13
+ ]);
14
+ return createHmac("sha256", Buffer.from(applicationKey, "utf8"))
15
+ .update(message)
16
+ .digest("hex");
17
+ }
@@ -1,4 +1,10 @@
1
- import { type BuiltAgent } from "@nylorun/core/define";
1
+ import { type BuiltAgent, type BuiltWorkflow } from "@nylorun/core/define";
2
2
  import type { Action, ActionOutcome } from "@nylorun/core/contracts";
3
+ import type { ActionSandbox } from "./sandbox/client.js";
4
+ export type ExecutableDefinition = BuiltAgent | BuiltWorkflow;
5
+ export type ExecuteActionOptions = {
6
+ /** Claim-scoped sandbox client; omitted when the session has no sandbox. */
7
+ readonly sandbox?: ActionSandbox;
8
+ };
3
9
  /** Executes code only after a host-issued claim. No engine dependency. */
4
- export declare function executeAction(action: Action, agent: BuiltAgent, signal: AbortSignal): Promise<ActionOutcome>;
10
+ export declare function executeAction(action: Action, root: ExecutableDefinition, signal: AbortSignal, options?: ExecuteActionOptions): Promise<ActionOutcome>;
@@ -1,4 +1,4 @@
1
- import { implementationsFor, isToolError, normalizeToolDefinition, normalizedSchemasFor, runHookPoint, } from "@nylorun/core/define";
1
+ import { delegateOf, implementationsFor, isBuiltWorkflow, isToolError, normalizeToolDefinition, normalizedSchemasFor, runHookPoint, } from "@nylorun/core/define";
2
2
  class Suspend {
3
3
  outcome;
4
4
  constructor(outcome) {
@@ -9,7 +9,21 @@ const object = (value) => value !== null && typeof value === "object" && !Array.
9
9
  ? value
10
10
  : {};
11
11
  /** Executes code only after a host-issued claim. No engine dependency. */
12
- export async function executeAction(action, agent, signal) {
12
+ export async function executeAction(action, root, signal, options = {}) {
13
+ const sandbox = options.sandbox;
14
+ if (action.kind === "fn" || action.kind === "verify") {
15
+ if (!isBuiltWorkflow(root))
16
+ throw new Error(`Action ${action.kind} requires a workflow definition`);
17
+ return executeWorkflowFn(action, root.getBinding(), signal, sandbox);
18
+ }
19
+ if (isBuiltWorkflow(root)) {
20
+ if (action.kind === "tool" && "key" in action && typeof action.key === "string")
21
+ return executeWorkflowTool(action, root.getBinding(), signal, sandbox);
22
+ throw new Error(`Unsupported action kind ${action.kind} on workflow`);
23
+ }
24
+ // Work for an agent used as a tool runs that agent's code, served from the root's binding.
25
+ const agent = action.agent ? delegatedAgent(root, action.agent.id) : root;
26
+ const ref = action.agent ?? { id: root.id, path: root.id };
13
27
  if (action.kind === "hook")
14
28
  // All capabilities at this hook point run concurrently in one action. Never throws.
15
29
  return {
@@ -17,6 +31,8 @@ export async function executeAction(action, agent, signal) {
17
31
  results: await runHookPoint(implementationsFor(agent), action.hook, action.input),
18
32
  },
19
33
  };
34
+ if (action.kind !== "tool" || !("capabilityId" in action))
35
+ throw new Error(`Unsupported action kind ${action.kind}`);
20
36
  const impl = implementationsFor(agent)[action.capabilityId];
21
37
  if (!impl)
22
38
  throw new Error(`No capability ${action.capabilityId}`);
@@ -24,7 +40,25 @@ export async function executeAction(action, agent, signal) {
24
40
  if (!raw)
25
41
  throw new Error("Missing tool implementation");
26
42
  const tool = normalizeToolDefinition(raw);
27
- const schemas = normalizedSchemasFor(tool);
43
+ return runTool(action, tool, ref, signal, sandbox);
44
+ }
45
+ async function executeWorkflowTool(action, binding, signal, sandbox) {
46
+ const impl = binding.nodes[action.key];
47
+ if (!impl || impl.kind !== "tool")
48
+ throw new Error(`No tool implementation for key ${action.key}`);
49
+ const path = "path" in action && typeof action.path === "string" ? action.path : action.key;
50
+ return runTool(action, impl.tool, { id: action.agentId, path }, signal, sandbox);
51
+ }
52
+ async function runTool(action, raw, ref, signal, sandbox) {
53
+ const tool = "execute" in raw && typeof raw.execute === "function" && "inputSchema" in raw
54
+ ? raw
55
+ : normalizeToolDefinition(raw);
56
+ const schemas = "inputSchema" in tool && tool.inputSchema && "validate" in tool.inputSchema
57
+ ? {
58
+ inputSchema: tool.inputSchema,
59
+ outputSchema: "outputSchema" in tool ? tool.outputSchema : undefined,
60
+ }
61
+ : normalizedSchemasFor(tool);
28
62
  const input = schemas.inputSchema.validate(action.input);
29
63
  if (!input.ok)
30
64
  return {
@@ -77,7 +111,9 @@ export async function executeAction(action, agent, signal) {
77
111
  signal,
78
112
  info: ctx.info,
79
113
  session: { id: action.sessionId },
114
+ agent: ref,
80
115
  ...(ctx.resume ? { resume: ctx.resume } : {}),
116
+ ...(sandbox ? { sandbox } : {}),
81
117
  state: {
82
118
  get: (key) => state[key],
83
119
  set: (key, value) => {
@@ -115,14 +151,17 @@ export async function executeAction(action, agent, signal) {
115
151
  };
116
152
  try {
117
153
  signal.throwIfAborted();
118
- if (tool.approval && !gateApproved) {
119
- const approval = await tool.approval(input.value);
120
- if (approval)
154
+ const approval = "approval" in tool ? tool.approval : undefined;
155
+ if (approval && !gateApproved) {
156
+ const result = await approval(input.value);
157
+ if (result)
121
158
  throw new Suspend({
122
159
  kind: "interaction-required",
123
160
  interaction: {
124
161
  kind: "approval",
125
- prompt: typeof approval === "string" ? approval : `Approve ${tool.name}?`,
162
+ prompt: typeof result === "string"
163
+ ? result
164
+ : `Approve ${"name" in tool ? tool.name : "tool"}?`,
126
165
  },
127
166
  token: durableToken({ approvalGate: true }),
128
167
  });
@@ -130,7 +169,12 @@ export async function executeAction(action, agent, signal) {
130
169
  }
131
170
  if (resume.kind === "approval" && resume.approved === false)
132
171
  return { value: { kind: "denied", reason: "Approval denied" } };
133
- const value = await tool.execute(input.value, context);
172
+ const execute = "execute" in tool && typeof tool.execute === "function"
173
+ ? tool.execute
174
+ : undefined;
175
+ if (!execute)
176
+ throw new Error("Missing tool implementation");
177
+ const value = await execute(input.value, context);
134
178
  const tagged = object(value);
135
179
  let outcome;
136
180
  if ([
@@ -170,6 +214,51 @@ export async function executeAction(action, agent, signal) {
170
214
  };
171
215
  }
172
216
  }
217
+ async function executeWorkflowFn(action, binding, signal, sandbox) {
218
+ const impl = binding.nodes[action.key];
219
+ if (!impl || impl.kind !== action.kind)
220
+ throw new Error(`No ${action.kind} implementation for key ${action.key}`);
221
+ signal.throwIfAborted();
222
+ try {
223
+ // Verify may receive a tool-like context later (L3/L4); tracer passes input only.
224
+ const value = action.kind === "verify"
225
+ ? await impl.fn(action.input, {
226
+ signal,
227
+ info: action.context.info,
228
+ session: { id: action.sessionId },
229
+ ...(sandbox ? { sandbox } : {}),
230
+ step: async (_name, fn) => fn(),
231
+ approve: async () => {
232
+ throw new Error("Approvals in verify are not available in the tracer");
233
+ },
234
+ ask: async () => {
235
+ throw new Error("ask in verify is not available in the tracer");
236
+ },
237
+ progress() { },
238
+ })
239
+ : await impl.fn(action.input);
240
+ return { value };
241
+ }
242
+ catch (error) {
243
+ if (signal.aborted)
244
+ throw signal.reason;
245
+ return {
246
+ value: {
247
+ kind: "failed",
248
+ code: action.kind === "verify" ? "loop.verify-failed" : "fn.failed",
249
+ message: message(error),
250
+ },
251
+ };
252
+ }
253
+ }
254
+ function delegatedAgent(root, id) {
255
+ for (const capability of Object.values(implementationsFor(root))) {
256
+ const child = delegateOf(capability.tools?.[id])?.agent;
257
+ if (child)
258
+ return child;
259
+ }
260
+ throw new Error(`Agent '${root.id}' does not use '${id}' as a tool`);
261
+ }
173
262
  function message(error) {
174
263
  return error instanceof Error ? error.message : String(error);
175
264
  }
@@ -1,7 +1,11 @@
1
- import { type AgentSource } from "./client.js";
1
+ import { type BuiltWorkflow } from "@nylorun/core/define";
2
+ import { AgentsClient, type AgentSource } from "./client.js";
3
+ import { deriveExecutorToken } from "./derived-credentials.js";
2
4
  import { type Destination } from "./http.js";
3
5
  export interface ConnectOptions {
4
- agents: readonly AgentSource[];
6
+ agents: readonly (AgentSource | BuiltWorkflow)[];
7
+ /** Application-mode client; when set, connectAgents uses application mode (D§7.2). */
8
+ application?: AgentsClient;
5
9
  runtime?: Destination;
6
10
  implementationVersion?: string;
7
11
  onError?: (error: unknown) => void;
@@ -13,3 +17,4 @@ export interface AgentConnection {
13
17
  close(): Promise<void>;
14
18
  }
15
19
  export declare function connectAgents(options: ConnectOptions): AgentConnection;
20
+ export { deriveExecutorToken };
package/dist/executor.js CHANGED
@@ -1,24 +1,53 @@
1
1
  import { AgentManifestSchema, ActionSchema, ActionClaimResponseSchema, } from "@nylorun/core/contracts";
2
- import { assertNoMiddlewareClosures } from "./client.js";
2
+ import { agentFrom, isBuiltWorkflow, } from "@nylorun/core/define";
3
+ import { AgentsClient, assertNoMiddlewareClosures, } from "./client.js";
4
+ import { resolveConnection } from "./connection.js";
5
+ import { deriveExecutorToken } from "./derived-credentials.js";
3
6
  import { Transport, RuntimeError, delay, env, id, segment, } from "./http.js";
4
7
  import { readSSE } from "./sse.js";
5
8
  import { executeAction } from "./execute-action.js";
6
- export function connectAgents(options) {
7
- const transport = new Transport(options.runtime, "executor");
8
- const version = options.implementationVersion ??
9
+ import { createActionSandbox, definitionDeclaresSandbox, } from "./sandbox/client.js";
10
+ function implementationVersionOf(options) {
11
+ return (options.implementationVersion ??
9
12
  env("NYLORUN_IMPLEMENTATION_VERSION") ??
10
- "dev";
13
+ "dev");
14
+ }
15
+ function buildAgents(options) {
11
16
  const agents = new Map();
12
- for (const source of options.agents) {
13
- const built = source.build?.() ?? source;
17
+ const addAgent = (built) => {
14
18
  assertNoMiddlewareClosures(built);
15
19
  AgentManifestSchema.parse(built.manifest);
16
20
  if (agents.has(built.id))
17
21
  throw new Error(`Duplicate connected agent ${built.id}`);
18
22
  agents.set(built.id, built);
23
+ };
24
+ for (const source of options.agents) {
25
+ if (isBuiltWorkflow(source)) {
26
+ if (agents.has(source.id))
27
+ throw new Error(`Duplicate connected agent ${source.id}`);
28
+ agents.set(source.id, source);
29
+ for (const binding of Object.values(source.getBinding().agents))
30
+ addAgent(agentFrom(binding.manifest, binding.implementations));
31
+ continue;
32
+ }
33
+ const built = source.build?.() ?? source;
34
+ if (isBuiltWorkflow(built)) {
35
+ if (agents.has(built.id))
36
+ throw new Error(`Duplicate connected agent ${built.id}`);
37
+ agents.set(built.id, built);
38
+ for (const binding of Object.values(built.getBinding().agents))
39
+ addAgent(agentFrom(binding.manifest, binding.implementations));
40
+ continue;
41
+ }
42
+ addAgent(built);
19
43
  }
20
44
  if (!agents.size)
21
45
  throw new Error("connectAgents requires at least one agent");
46
+ return agents;
47
+ }
48
+ function connectExecutorMode(options, runtime, agents) {
49
+ const transport = new Transport(runtime, "executor");
50
+ const version = implementationVersionOf(options);
22
51
  const controller = new AbortController();
23
52
  const signal = controller.signal;
24
53
  const active = new Map();
@@ -125,7 +154,17 @@ export function connectAgents(options) {
125
154
  report(error);
126
155
  }
127
156
  });
128
- const outcome = await executeAction(claim.action, agent, work.signal);
157
+ const outcome = await executeAction(claim.action, agent, work.signal, {
158
+ sandbox: definitionDeclaresSandbox(agent.manifest)
159
+ ? createActionSandbox({
160
+ transport,
161
+ actionId: action.actionId,
162
+ claimId: claim.claimId,
163
+ generation: claim.generation,
164
+ signal: work.signal,
165
+ })
166
+ : undefined,
167
+ });
129
168
  work.signal.throwIfAborted();
130
169
  const command = {
131
170
  type: "action_result",
@@ -214,3 +253,101 @@ export function connectAgents(options) {
214
253
  },
215
254
  };
216
255
  }
256
+ function connectApplicationMode(options, application, agents) {
257
+ const version = implementationVersionOf(options);
258
+ const children = [];
259
+ let resolveReady;
260
+ let rejectReady;
261
+ const ready = new Promise((resolve, reject) => {
262
+ resolveReady = resolve;
263
+ rejectReady = reject;
264
+ });
265
+ void ready.catch(() => { });
266
+ const boot = (async () => {
267
+ for (const agent of agents.values()) {
268
+ await application.saveAgent(agent, { implementationVersion: version });
269
+ }
270
+ const registrations = [...agents.values()].map((agent) => ({
271
+ agentId: agent.id,
272
+ token: deriveExecutorToken(application.transport.key, application.transport.tenant, agent.id),
273
+ implementationVersion: version,
274
+ }));
275
+ await application.transport.json("/v1/executors", "PUT", {
276
+ executors: registrations,
277
+ });
278
+ for (const agent of agents.values()) {
279
+ const token = deriveExecutorToken(application.transport.key, application.transport.tenant, agent.id);
280
+ const child = connectExecutorMode({
281
+ agents: [agent],
282
+ implementationVersion: version,
283
+ onError: options.onError,
284
+ }, {
285
+ url: application.transport.url,
286
+ tenant: application.transport.tenant,
287
+ key: token,
288
+ fetch: application.transport.fetcher,
289
+ }, new Map([[agent.id, agent]]));
290
+ children.push(child);
291
+ }
292
+ await Promise.all(children.map((child) => child.ready));
293
+ resolveReady();
294
+ })().catch((error) => {
295
+ rejectReady(error);
296
+ });
297
+ return {
298
+ ready,
299
+ async close() {
300
+ await boot.catch(() => { });
301
+ await Promise.allSettled(children.map((child) => child.close()));
302
+ },
303
+ };
304
+ }
305
+ export function connectAgents(options) {
306
+ const agents = buildAgents(options);
307
+ // D§7.2 mode table: runtime → executor; application → application;
308
+ // neither → resolveConnection() role.
309
+ if (options.runtime) {
310
+ return connectExecutorMode(options, options.runtime, agents);
311
+ }
312
+ if (options.application) {
313
+ return connectApplicationMode(options, options.application, agents);
314
+ }
315
+ let resolveReady;
316
+ let rejectReady;
317
+ const ready = new Promise((resolve, reject) => {
318
+ resolveReady = resolve;
319
+ rejectReady = reject;
320
+ });
321
+ void ready.catch(() => { });
322
+ let handle;
323
+ const boot = (async () => {
324
+ const connection = await resolveConnection();
325
+ if (connection.role === "executor") {
326
+ handle = connectExecutorMode(options, {
327
+ url: connection.url,
328
+ tenant: connection.tenant,
329
+ key: connection.key,
330
+ }, agents);
331
+ }
332
+ else {
333
+ const application = new AgentsClient({
334
+ url: connection.url,
335
+ tenant: connection.tenant,
336
+ key: connection.key,
337
+ });
338
+ handle = connectApplicationMode(options, application, agents);
339
+ }
340
+ await handle.ready;
341
+ resolveReady();
342
+ })().catch((error) => {
343
+ rejectReady(error);
344
+ });
345
+ return {
346
+ ready,
347
+ async close() {
348
+ await boot.catch(() => { });
349
+ await handle?.close();
350
+ },
351
+ };
352
+ }
353
+ export { deriveExecutorToken };
package/dist/http.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { type Compatibility, type ProtocolRange } from "@nylorun/core/compatibility";
1
2
  export interface Destination {
2
3
  url?: string;
3
4
  key?: string;
5
+ tenant?: string;
4
6
  fetch?: typeof fetch;
5
7
  }
6
8
  export declare function env(name: string): string | undefined;
@@ -9,12 +11,30 @@ export declare class RuntimeError extends Error {
9
11
  readonly body: unknown;
10
12
  constructor(status: number, body: unknown);
11
13
  }
14
+ export type IncompatibleReason = Extract<Compatibility, {
15
+ ok: false;
16
+ }>;
17
+ /** Host protocol is outside the client's supported range or missing required features. */
18
+ export declare class IncompatibleRuntimeError extends Error {
19
+ readonly compatibility: IncompatibleReason;
20
+ readonly remedy: string;
21
+ readonly host: ProtocolRange;
22
+ constructor(compatibility: IncompatibleReason);
23
+ }
12
24
  export declare class Transport {
13
25
  readonly url: string;
14
26
  readonly key: string;
27
+ readonly tenant: string;
15
28
  readonly fetcher: typeof fetch;
29
+ private compatible;
16
30
  constructor(options?: Destination, role?: "server" | "executor");
17
- request(path: string, init?: RequestInit): Promise<Response>;
31
+ /** Clears the cached Host compatibility result (used after a 426). */
32
+ clearCompatibilityCache(): void;
33
+ private ensureCompatible;
34
+ private authHeaders;
35
+ request(path: string, init?: RequestInit, options?: {
36
+ retried426?: boolean;
37
+ }): Promise<Response>;
18
38
  json<T>(path: string, method?: string, body?: unknown, signal?: AbortSignal): Promise<T>;
19
39
  }
20
40
  export declare const id: () => `${string}-${string}-${string}-${string}-${string}`;