@nylorun/runtime 0.36.1-beta → 0.38.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 (71) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +10 -0
  3. package/dist/api/a2a/card.d.ts +10 -4
  4. package/dist/api/a2a/card.js +18 -6
  5. package/dist/api/a2a/endpoint.d.ts +5 -2
  6. package/dist/api/a2a/endpoint.js +31 -9
  7. package/dist/api/a2a/extensions.d.ts +41 -0
  8. package/dist/api/a2a/extensions.js +92 -0
  9. package/dist/api/a2a/list.d.ts +60 -0
  10. package/dist/api/a2a/list.js +169 -0
  11. package/dist/api/a2a/protocol.d.ts +105 -9
  12. package/dist/api/a2a/protocol.js +169 -18
  13. package/dist/api/a2a/push.d.ts +76 -0
  14. package/dist/api/a2a/push.js +305 -0
  15. package/dist/api/a2a/routes.d.ts +12 -6
  16. package/dist/api/a2a/routes.js +409 -112
  17. package/dist/api/a2a/tasks.d.ts +72 -4
  18. package/dist/api/a2a/tasks.js +305 -27
  19. package/dist/api/components.d.ts +538 -0
  20. package/dist/api/components.js +3 -1
  21. package/dist/api/http/app.js +2 -0
  22. package/dist/api/http/routes/artifacts.js +19 -0
  23. package/dist/api/http/routes/turns.d.ts +18 -0
  24. package/dist/api/http/routes/turns.js +67 -0
  25. package/dist/api/inbound-files.d.ts +12 -1
  26. package/dist/api/inbound-files.js +44 -0
  27. package/dist/api/openapi.js +7 -2
  28. package/dist/harness/image-engine.d.ts +14 -0
  29. package/dist/harness/image-engine.js +16 -0
  30. package/dist/harness/pod.d.ts +2 -0
  31. package/dist/harness/pod.js +5 -0
  32. package/dist/harness-api/ws-server.js +7 -1
  33. package/dist/host/config-for.d.ts +2 -0
  34. package/dist/host/config-for.js +1 -0
  35. package/dist/host/main.js +1 -0
  36. package/dist/host/stack-config.d.ts +16 -0
  37. package/dist/host/stack-config.js +30 -0
  38. package/dist/management-openapi.json +37 -2
  39. package/dist/openapi.json +1485 -738
  40. package/dist/reads/types.d.ts +19 -0
  41. package/dist/sandbox/join.d.ts +7 -1
  42. package/dist/sandbox/join.js +5 -1
  43. package/dist/sandbox/pods/builtin.d.ts +18 -0
  44. package/dist/sandbox/pods/builtin.js +33 -0
  45. package/dist/sandbox/pods/client.d.ts +5 -4
  46. package/dist/sandbox/pods/spec.js +14 -1
  47. package/dist/store/postgres/drizzle/0039_a2a_push.sql +32 -0
  48. package/dist/store/postgres/drizzle/0040_turn_index.sql +23 -0
  49. package/dist/store/postgres/drizzle/0041_turn_index_backfill.sql +88 -0
  50. package/dist/store/postgres/drizzle/0042_worker_engine_image.sql +1 -0
  51. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  52. package/dist/store/postgres/reads.js +41 -1
  53. package/dist/store/postgres/schema.d.ts +1314 -533
  54. package/dist/store/postgres/schema.js +94 -0
  55. package/dist/store/postgres/store.js +135 -3
  56. package/dist/store/postgres/turns.d.ts +62 -0
  57. package/dist/store/postgres/turns.js +153 -0
  58. package/dist/store/types.d.ts +82 -3
  59. package/dist/tenant/runtime.js +7 -5
  60. package/dist/tenant/session-model.d.ts +11 -1
  61. package/dist/tenant/session-model.js +34 -5
  62. package/dist/tenant/sessions.d.ts +2 -1
  63. package/dist/tenant/sessions.js +8 -2
  64. package/dist/tenant/sweep.d.ts +3 -0
  65. package/dist/tenant/sweep.js +39 -11
  66. package/dist/tenant/types.d.ts +23 -0
  67. package/dist/tuning.d.ts +9 -0
  68. package/dist/tuning.js +11 -0
  69. package/dist/version.d.ts +1 -1
  70. package/dist/version.js +1 -1
  71. package/package.json +3 -3
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+ import { TurnChangesPage } from "../../components.js";
3
+ import { taskIdOf } from "../../a2a/tasks.js";
4
+ import { readAccess, readStoreOf } from "../../../reads/access.js";
5
+ import { tenantRoute } from "../define.js";
6
+ import { jsonResponse } from "../respond.js";
7
+ import { parseQuery } from "./reads.js";
8
+ /** A page's size when the query names none, and the most one holds. */
9
+ export const TURNS_PAGE_DEFAULT = 100;
10
+ export const TURNS_PAGE_MAX = 500;
11
+ const turnsQuery = z
12
+ .object({
13
+ changedAfter: z.string().optional().meta({
14
+ description: "The `cursor` a previous page returned: read the changes after it. Absent: from the first turn.",
15
+ }),
16
+ sessionId: z.string().optional().meta({ description: "Only this session's turns." }),
17
+ limit: z.coerce
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .optional()
22
+ .meta({
23
+ description: `At most this many turns, up to ${TURNS_PAGE_MAX}; a larger limit gets that many. Default ${TURNS_PAGE_DEFAULT}`,
24
+ }),
25
+ })
26
+ .strict();
27
+ /** A turn index row as the API shows it. */
28
+ export function turnSummary(row) {
29
+ const totals = row.totals;
30
+ return {
31
+ turnId: row.turnId,
32
+ sessionId: row.sessionId,
33
+ agentId: row.agentId,
34
+ status: row.status,
35
+ startedAt: row.startedAt,
36
+ endedAt: row.endedAt,
37
+ changedAt: row.changedAt,
38
+ totals: totals ? { steps: totals.steps, tokens: totals.tokens, costUsd: totals.costUsd } : null,
39
+ a2a: row.a2aContextId === null
40
+ ? null
41
+ : { contextId: row.a2aContextId, taskId: taskIdOf(row.a2aContextId, row.turnId) },
42
+ };
43
+ }
44
+ export function turnRoutes(api) {
45
+ tenantRoute(api, { credentials: ["application", "subject", "token"], scopes: ["sessions:own"] }, {
46
+ method: "get",
47
+ path: "/v1/turns",
48
+ tags: ["Sessions"],
49
+ summary: "Follow turns as they change",
50
+ description: "The turn index's change feed (Host feature `turns`): each turn's status, times, totals and A2A context and task, in the order the turns changed, a turn again each time it changes. Pass the page's `cursor` as `changedAfter` to read on; it is returned even when no turn changed. With `sessionId`, one session's turns, oldest first. An application key reads the whole Tenant; acting for a person, only theirs.",
51
+ request: { query: turnsQuery },
52
+ responses: {
53
+ 200: {
54
+ description: "The turns that changed after the cursor",
55
+ content: { "application/json": { schema: TurnChangesPage } },
56
+ },
57
+ },
58
+ }, async (c) => {
59
+ const scope = c.get("scope");
60
+ const q = parseQuery(turnsQuery, c.req.query());
61
+ const page = await readStoreOf(c.env.tenant).turnChanges(q.sessionId === undefined ? {} : { sessionId: q.sessionId }, {
62
+ limit: Math.min(q.limit ?? TURNS_PAGE_DEFAULT, TURNS_PAGE_MAX),
63
+ ...(q.changedAfter === undefined ? {} : { changedAfter: q.changedAfter }),
64
+ }, readAccess(scope));
65
+ return jsonResponse(200, { ...page, turns: page.turns.map(turnSummary) });
66
+ });
67
+ }
@@ -2,7 +2,8 @@
2
2
  * Files that arrive inside a message (multimodal messages, M7): the AG-UI and A2A endpoints take
3
3
  * a file's bytes inline (base64), store them as an artifact of the message's session, and send
4
4
  * the message as `parts` through the session's normal command path, so every check a message
5
- * with file parts meets applies. A file by URL is refused: the Runtime fetches nothing.
5
+ * with file parts meets applies. AG-UI refuses a file by URL; A2A's `url` parts are fetched
6
+ * (`fetchUrlFile`, through `guardedFetch` within `inlineFileBytes`) and then stored the same way.
6
7
  *
7
8
  * A retried message (the same idempotency key) reuses the files the first one stored rather than
8
9
  * storing them again, so it replays the first turn instead of conflicting with it. A message the
@@ -11,6 +12,7 @@
11
12
  import type { MessagePart } from "@nylorun/core/contracts";
12
13
  import { type ArtifactAccess } from "../artifacts/service.js";
13
14
  import type { TenantContext } from "../tenant/context.js";
15
+ import { type OutboundPolicy } from "../tenant/outbound.js";
14
16
  /** A file sent inline: its name, declared media type and base64 bytes. */
15
17
  export interface InlineFile {
16
18
  readonly name: string;
@@ -30,6 +32,15 @@ export type InboundPart = {
30
32
  };
31
33
  /** The 400 for a file sent by URL. */
32
34
  export declare function refuseUrl(): never;
35
+ /**
36
+ * Fetches a file sent by URL (A2A `url` parts) under `policy`, within the Tenant's Runtime limit
37
+ * `inlineFileBytes`, as an inline file: its media type is the part's, else the answer's. A URL
38
+ * the policy refuses, or that does not answer 2xx, is `400`; a file past the limit `413`.
39
+ */
40
+ export declare function fetchUrlFile(ctx: TenantContext, url: string, named: {
41
+ mediaType?: string;
42
+ filename?: string;
43
+ }, policy: OutboundPolicy): Promise<InlineFile>;
33
44
  /** A name for a file its client did not name: its place in the message and its type's extension. */
34
45
  export declare function inlineFileName(mediaType: string, index: number): string;
35
46
  /**
@@ -3,6 +3,7 @@ import { extensionFor, normalizeContentType } from "@nylorun/core/contracts";
3
3
  import { deleteArtifact, uploadArtifact } from "../artifacts/service.js";
4
4
  import { fail } from "../tenant/http.js";
5
5
  import { readRuntimeLimits } from "../tenant/runtime-limits.js";
6
+ import { guardedFetch, OutboundFailed, OutboundRefused } from "../tenant/outbound.js";
6
7
  const BASE64 = /^[A-Za-z0-9+/_-]*={0,2}$/;
7
8
  /** The 400 for a file sent by URL. */
8
9
  export function refuseUrl() {
@@ -10,6 +11,49 @@ export function refuseUrl() {
10
11
  code: "invalid_request",
11
12
  });
12
13
  }
14
+ /** How long fetching one file `url` part may take. */
15
+ const URL_FETCH_TIMEOUT_MS = 60_000;
16
+ /**
17
+ * Fetches a file sent by URL (A2A `url` parts) under `policy`, within the Tenant's Runtime limit
18
+ * `inlineFileBytes`, as an inline file: its media type is the part's, else the answer's. A URL
19
+ * the policy refuses, or that does not answer 2xx, is `400`; a file past the limit `413`.
20
+ */
21
+ export async function fetchUrlFile(ctx, url, named, policy) {
22
+ const { inlineFileBytes } = await ctx.store.tx((t) => readRuntimeLimits(t));
23
+ let response;
24
+ try {
25
+ response = await guardedFetch(policy, { maxResponseBytes: inlineFileBytes, timeoutMs: URL_FETCH_TIMEOUT_MS })(url);
26
+ }
27
+ catch (error) {
28
+ if (error instanceof OutboundFailed && error.code === "TOO_LARGE")
29
+ return fail(413, `A file sent by URL may hold at most ${inlineFileBytes} bytes (the Runtime limit inlineFileBytes)`, {
30
+ code: "limit_exceeded",
31
+ details: { limitBytes: inlineFileBytes },
32
+ });
33
+ const reason = error instanceof OutboundRefused || error instanceof OutboundFailed ? error.message : "it did not answer";
34
+ return fail(400, `The file at ${new URL(url).host} could not be fetched: ${reason}`, { code: "invalid_request" });
35
+ }
36
+ if (!response.ok)
37
+ return fail(400, `The file at ${new URL(url).host} could not be fetched: it answered ${response.status}`, {
38
+ code: "invalid_request",
39
+ });
40
+ const bytes = Buffer.from(await response.arrayBuffer());
41
+ const answered = response.headers.get("content-type")?.split(";")[0]?.trim();
42
+ const segment = new URL(url).pathname.split("/").filter(Boolean).at(-1) ?? "";
43
+ let last = segment;
44
+ try {
45
+ last = decodeURIComponent(segment);
46
+ }
47
+ catch {
48
+ // Not percent-encoding: the segment as it is.
49
+ }
50
+ const mediaType = named.mediaType ?? (answered || "application/octet-stream");
51
+ return {
52
+ name: named.filename ?? (last !== "" ? last : inlineFileName(mediaType, 0)),
53
+ mediaType,
54
+ data: bytes.toString("base64"),
55
+ };
56
+ }
13
57
  /** A name for a file its client did not name: its place in the message and its type's extension. */
14
58
  export function inlineFileName(mediaType, index) {
15
59
  const extension = extensionFor(mediaType);
@@ -202,6 +202,7 @@ const RUNTIME_TAGS = [
202
202
  "GET /v1/sessions/{sessionId}/calls/tool",
203
203
  "GET /v1/sessions/{sessionId}/turns/{turnId}/trace",
204
204
  "POST /v1/sessions/{sessionId}/sandbox/{tool}",
205
+ "GET /v1/turns",
205
206
  ],
206
207
  },
207
208
  {
@@ -218,8 +219,12 @@ const RUNTIME_TAGS = [
218
219
  {
219
220
  name: "A2A",
220
221
  group: "Sessions",
221
- description: "The same sessions through Agent2Agent: a context is one session per subject, Agent and `contextId`, and a task is one turn; every caller's session runs on the Agent's disk and vault, so an app that serves several audiences makes an Agent for each. The card is how an A2A client finds the Agent.",
222
- operations: ["GET /v1/a2a/agents/{agentId}/card", "POST /v1/a2a/agents/{agentId}"],
222
+ description: "The same sessions through Agent2Agent (A2A 1.0, JSON-RPC) under the Nylorun profile v2: a context is one session per subject, Agent and `contextId`, and a task is one turn; every caller's session runs on the Agent's disk and vault, so an app that serves several audiences makes an Agent for each. The card is how an A2A client finds the Agent; push notifications carry a task's updates to the caller, and the files a task writes are downloaded from `/v1/a2a/files/…` by the same subject.",
223
+ operations: [
224
+ "GET /v1/a2a/agents/{agentId}/card",
225
+ "POST /v1/a2a/agents/{agentId}",
226
+ "GET /v1/a2a/files/{artifactId}/versions/{version}",
227
+ ],
223
228
  },
224
229
  {
225
230
  name: "Artifacts",
@@ -0,0 +1,14 @@
1
+ /**
2
+ * An engine the workload image carries (ADR-99): the Runtime image's tini, node and bundled engine
3
+ * under `/opt/nylorun`, run by the sandboxes service with `entrypoint: "builtin"`. Such an engine is
4
+ * the Runtime version the image was built from, so it says which at `host/join`; the Runtime decides
5
+ * from it whether the next pod runs it or copies its own in (`sandbox/pods/builtin.ts`).
6
+ */
7
+ /** Where an image keeps the engine it carries. */
8
+ export declare const BUILTIN_ENGINE_ROOT = "/opt/nylorun";
9
+ /** Sent at `host/join` only by an engine running from `BUILTIN_ENGINE_ROOT`: its Runtime version. */
10
+ export declare const IMAGE_ENGINE_HEADER = "nylorun-image-engine";
11
+ /** A Runtime version, as the header carries it. */
12
+ export declare const ENGINE_VERSION_PATTERN: RegExp;
13
+ /** Whether code at `moduleUrl` runs from an image's engine (the engine passes its own URL). */
14
+ export declare function isBuiltinEngine(moduleUrl: string): boolean;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * An engine the workload image carries (ADR-99): the Runtime image's tini, node and bundled engine
3
+ * under `/opt/nylorun`, run by the sandboxes service with `entrypoint: "builtin"`. Such an engine is
4
+ * the Runtime version the image was built from, so it says which at `host/join`; the Runtime decides
5
+ * from it whether the next pod runs it or copies its own in (`sandbox/pods/builtin.ts`).
6
+ */
7
+ /** Where an image keeps the engine it carries. */
8
+ export const BUILTIN_ENGINE_ROOT = "/opt/nylorun";
9
+ /** Sent at `host/join` only by an engine running from `BUILTIN_ENGINE_ROOT`: its Runtime version. */
10
+ export const IMAGE_ENGINE_HEADER = "nylorun-image-engine";
11
+ /** A Runtime version, as the header carries it. */
12
+ export const ENGINE_VERSION_PATTERN = /^[0-9A-Za-z.+-]{1,64}$/;
13
+ /** Whether code at `moduleUrl` runs from an image's engine (the engine passes its own URL). */
14
+ export function isBuiltinEngine(moduleUrl) {
15
+ return moduleUrl.startsWith(`file://${BUILTIN_ENGINE_ROOT}/`);
16
+ }
@@ -59,4 +59,6 @@ export declare function podHost(config: PodHostConfig, logger: {
59
59
  volumeFile?: string;
60
60
  /** Where the engine writes the trust bundle: the public roots and the egress CA (R2c). */
61
61
  caBundleFile?: string;
62
+ /** Whether this engine runs from the workload image (default: from where its code is). */
63
+ imageEngine?: boolean;
62
64
  }): PodHost;
@@ -16,6 +16,8 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
16
16
  import { rootCertificates } from "node:tls";
17
17
  import { dirname } from "node:path";
18
18
  import { hostTuning } from "../tuning.js";
19
+ import { RUNTIME_VERSION } from "../version.js";
20
+ import { IMAGE_ENGINE_HEADER, isBuiltinEngine } from "./image-engine.js";
19
21
  /** The variables CLIs read their trust bundle from: OpenSSL, Node, Python, curl, git, AWS. */
20
22
  const TRUST_VARIABLES = [
21
23
  "SSL_CERT_FILE",
@@ -29,6 +31,7 @@ class Refused extends Error {
29
31
  }
30
32
  export function podHost(config, logger, options = {}) {
31
33
  const call = options.fetch ?? fetch;
34
+ const imageEngine = options.imageEngine ?? isBuiltinEngine(import.meta.url);
32
35
  let held;
33
36
  let setup;
34
37
  let seeded = false;
@@ -41,6 +44,8 @@ export function podHost(config, logger, options = {}) {
41
44
  headers: {
42
45
  ...(init.body === undefined ? {} : { "content-type": "application/json" }),
43
46
  ...(init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}),
47
+ // An engine the workload image carries says which Runtime it came from (ADR-99).
48
+ ...(path === "join" && imageEngine ? { [IMAGE_ENGINE_HEADER]: RUNTIME_VERSION } : {}),
44
49
  },
45
50
  ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
46
51
  signal: AbortSignal.timeout(hostTuning().harness.podJoinTimeoutMs),
@@ -31,6 +31,7 @@ import { HOST_TOKEN_TYP } from "../tenant/host-token.js";
31
31
  import { HostAuthError } from "../sandbox/join.js";
32
32
  import { parseSetupReport } from "../sandbox/setup.js";
33
33
  import { hostTuning } from "../tuning.js";
34
+ import { ENGINE_VERSION_PATTERN, IMAGE_ENGINE_HEADER } from "../harness/image-engine.js";
34
35
  export { HOST_JOIN_PATH, HOST_RENEW_PATH };
35
36
  /** Where a pod's engine reads its setup and reports it (R2d step 2, `sandbox/setup.ts`). */
36
37
  export const HOST_SETUP_PATH = `${HARNESS_API_PATH}/host/setup`;
@@ -45,6 +46,11 @@ function refuse(socket, status, text, headers = {}) {
45
46
  ];
46
47
  socket.end(`${lines.join("\r\n")}\r\n\r\n`);
47
48
  }
49
+ /** The Runtime version an engine built into the image reports at join (ADR-99), when well formed. */
50
+ function imageEngineOf(request) {
51
+ const value = request.headers[IMAGE_ENGINE_HEADER];
52
+ return typeof value === "string" && ENGINE_VERSION_PATTERN.test(value) ? value : undefined;
53
+ }
48
54
  function bearerOf(header) {
49
55
  return /^Bearer\s+(\S+)$/i.exec(header?.trim() ?? "")?.[1];
50
56
  }
@@ -110,7 +116,7 @@ export async function startHarnessListener(options) {
110
116
  return reply(response, 503, { error: { code: "unavailable", message: "Try again" } });
111
117
  try {
112
118
  const answer = path === HOST_JOIN_PATH
113
- ? await authority.join(parseJoin(await readBody(request)))
119
+ ? await authority.join(parseJoin(await readBody(request)), { imageEngine: imageEngineOf(request) })
114
120
  : await authority.renew(bearerOf(request.headers.authorization) ?? "");
115
121
  return reply(response, 200, answer);
116
122
  }
@@ -16,4 +16,6 @@ export declare function configForFactory(options: {
16
16
  issuers?: TenantConfig["issuers"];
17
17
  /** The URL clients reach the Runtime API at (`NYLORUN_PUBLIC_URL`). */
18
18
  publicUrl?: string;
19
+ /** The A2A server's settings (`StackConfig.a2a`). */
20
+ a2a?: TenantConfig["a2a"];
19
21
  }): (id: string) => TenantConfig;
@@ -17,6 +17,7 @@ export function configForFactory(options) {
17
17
  ...(options.delivery ? { delivery: options.delivery } : {}),
18
18
  ...(options.issuers ? { issuers: options.issuers } : {}),
19
19
  ...(options.publicUrl ? { publicUrl: options.publicUrl } : {}),
20
+ ...(options.a2a ? { a2a: options.a2a } : {}),
20
21
  logger: {
21
22
  info: (message, fields) => options.logger.info(message, { tenantId: id, ...fields }),
22
23
  warn: (message, fields) => options.logger.warn(message, { tenantId: id, ...fields }),
package/dist/host/main.js CHANGED
@@ -293,6 +293,7 @@ export async function main() {
293
293
  ...(stack.delivery ? { delivery: stack.delivery } : {}),
294
294
  ...(issuers ? { issuers } : {}),
295
295
  ...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
296
+ ...(stack.a2a ? { a2a: stack.a2a } : {}),
296
297
  });
297
298
  // The process's Durable Session Execution: Restate when its endpoints are set, else the
298
299
  // in-process memory execution. An invocation that arrives while the Tenant opens waits for it.
@@ -264,6 +264,17 @@ export interface StackConfig {
264
264
  privateAddresses?: "allow" | "refuse";
265
265
  allowHttp?: boolean;
266
266
  };
267
+ /**
268
+ * The A2A server's push notifications (Runtime config `a2a`): `NYLORUN_A2A_PUSH_SECRET`
269
+ * (`a2a.push.secret`, `whsec_<base64>`) signs them, and `NYLORUN_A2A_PUSH_ALLOW_HOSTS`
270
+ * (`a2a.push.allowHosts`, comma-separated `host:port`s) names the private hosts they may reach.
271
+ */
272
+ a2a?: {
273
+ push?: {
274
+ secret?: string;
275
+ allowHosts?: string[];
276
+ };
277
+ };
267
278
  /** Present when the process runs core or loop: who its Tenant is on first start. */
268
279
  tenant?: TenantSettings;
269
280
  /**
@@ -348,6 +359,11 @@ export declare function normalizeAllowedHost(name: string, entry: string): strin
348
359
  export declare function isLoopbackHost(host: string): boolean;
349
360
  /** Parse the stack configuration; throws `StackConfigError` naming the variable. */
350
361
  export declare function parseStackConfig(env: EnvSnapshot, argv: readonly string[]): StackConfig;
362
+ /**
363
+ * `host:port` entries, comma-separated; a host may be a name, an IPv4 or a bracketed IPv6 address,
364
+ * and the port `*` for any port (a test receiver on a port it picks).
365
+ */
366
+ export declare function parseAllowHosts(value: string): string[];
351
367
  /** Endpoint summary for logs: which endpoints are set, never their values. */
352
368
  export declare function describeEndpoints(endpoints: StackEndpoints): string[];
353
369
  export {};
@@ -328,6 +328,7 @@ export function parseStackConfig(env, argv) {
328
328
  if (harnessListener && listen?.port === harnessListener.listen.port)
329
329
  throw new StackConfigError("NYLORUN_HARNESS_LISTEN_PORT must differ from NYLORUN_LISTEN_PORT");
330
330
  const delivery = parseDelivery(env);
331
+ const a2a = parseA2a(env);
331
332
  const tenant = servesApi ? parseTenant(env) : undefined;
332
333
  const workloadToken = parseWorkloadToken(env);
333
334
  const modelHostIdentity = parseModelHostIdentity(env);
@@ -353,6 +354,7 @@ export function parseStackConfig(env, argv) {
353
354
  ...(harnessListener ? { harnessListener } : {}),
354
355
  ...(sandboxes ? { sandboxes } : {}),
355
356
  ...(delivery ? { delivery } : {}),
357
+ ...(a2a ? { a2a } : {}),
356
358
  ...(tenant ? { tenant } : {}),
357
359
  ...(workloadToken ? { workloadToken } : {}),
358
360
  ...(modelHostIdentity ? { modelHostIdentity } : {}),
@@ -714,6 +716,34 @@ function parseDelivery(env) {
714
716
  ...(http ? { allowHttp: http === "allow" } : {}),
715
717
  };
716
718
  }
719
+ /** `StackConfig.a2a` from `NYLORUN_A2A_PUSH_*`, or `undefined` when none is set. */
720
+ function parseA2a(env) {
721
+ const secret = read(env, "NYLORUN_A2A_PUSH_SECRET");
722
+ if (secret !== undefined) {
723
+ const key = secret.startsWith("whsec_") ? Buffer.from(secret.slice(6), "base64") : undefined;
724
+ if (!key || key.length < 24 || !/^[A-Za-z0-9+/]+={0,2}$/.test(secret.slice(6)))
725
+ throw new StackConfigError("NYLORUN_A2A_PUSH_SECRET must be a Standard Webhooks secret: whsec_ and the base64 of at least 24 bytes");
726
+ }
727
+ const hosts = read(env, "NYLORUN_A2A_PUSH_ALLOW_HOSTS");
728
+ const allowHosts = hosts === undefined ? undefined : parseAllowHosts(hosts);
729
+ if (secret === undefined && allowHosts === undefined)
730
+ return undefined;
731
+ return { push: { ...(secret !== undefined ? { secret } : {}), ...(allowHosts ? { allowHosts } : {}) } };
732
+ }
733
+ /**
734
+ * `host:port` entries, comma-separated; a host may be a name, an IPv4 or a bracketed IPv6 address,
735
+ * and the port `*` for any port (a test receiver on a port it picks).
736
+ */
737
+ export function parseAllowHosts(value) {
738
+ const entries = value
739
+ .split(",")
740
+ .map((entry) => entry.trim().toLowerCase())
741
+ .filter((entry) => entry !== "");
742
+ for (const entry of entries)
743
+ if (!/^(\[[0-9a-f:.]+\]|[a-z0-9.-]+):(\d{1,5}|\*)$/.test(entry) || Number(entry.slice(entry.lastIndexOf(":") + 1)) > 65535)
744
+ throw new StackConfigError(`NYLORUN_A2A_PUSH_ALLOW_HOSTS entries must be host:port, not ${entry}`);
745
+ return entries;
746
+ }
717
747
  /** Endpoint summary for logs: which endpoints are set, never their values. */
718
748
  export function describeEndpoints(endpoints) {
719
749
  return Object.entries(endpoints)
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.2.0",
3
3
  "info": {
4
4
  "title": "Nylorun Management API",
5
- "version": "0.36.1-beta",
5
+ "version": "0.38.0-beta",
6
6
  "x-nylorun-protocol": {
7
7
  "min": 16,
8
8
  "max": 18,
@@ -22,6 +22,7 @@
22
22
  "subject-headers",
23
23
  "ag-ui-endpoint",
24
24
  "a2a-endpoint",
25
+ "a2a-profile-v2",
25
26
  "sandbox-pods",
26
27
  "trusted-issuers",
27
28
  "session-reads",
@@ -37,7 +38,9 @@
37
38
  "agent-folders",
38
39
  "session-model",
39
40
  "model-host-identity",
40
- "credential-provider"
41
+ "credential-provider",
42
+ "turns",
43
+ "agent-model"
41
44
  ]
42
45
  },
43
46
  "description": "For operators: the Tenant's models, vaults, signing keys, settings and application keys, at `/v1/tenant/*` on the Tenant's URL, through `@nylorun/admin`. Every operation takes a management key, acting as itself: never for a subject and never from a browser. Management keys are issued only on the Tenant's machine (`nylorun tenant keys put <id> --management`) or from `NYLORUN_MANAGEMENT_KEY_FILE`."
@@ -928,6 +931,22 @@
928
931
  },
929
932
  "additionalProperties": false
930
933
  },
934
+ "engine": {
935
+ "type": "object",
936
+ "properties": {
937
+ "images": {
938
+ "type": "array",
939
+ "items": {
940
+ "type": "string",
941
+ "minLength": 1,
942
+ "maxLength": 256,
943
+ "pattern": "^[a-z0-9]([a-z0-9._:/-]*[a-z0-9])?$"
944
+ },
945
+ "maxItems": 32
946
+ }
947
+ },
948
+ "additionalProperties": false
949
+ },
931
950
  "placement": {
932
951
  "type": "object",
933
952
  "additionalProperties": {
@@ -2318,6 +2337,22 @@
2318
2337
  },
2319
2338
  "additionalProperties": false
2320
2339
  },
2340
+ "engine": {
2341
+ "type": "object",
2342
+ "properties": {
2343
+ "images": {
2344
+ "type": "array",
2345
+ "items": {
2346
+ "type": "string",
2347
+ "minLength": 1,
2348
+ "maxLength": 256,
2349
+ "pattern": "^[a-z0-9]([a-z0-9._:/-]*[a-z0-9])?$"
2350
+ },
2351
+ "maxItems": 32
2352
+ }
2353
+ },
2354
+ "additionalProperties": false
2355
+ },
2321
2356
  "placement": {
2322
2357
  "type": "object",
2323
2358
  "additionalProperties": {