@nylorun/agents 0.23.0-beta → 0.25.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.25.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - ed08ba8: `agentManifests.save({ agent, runtime, memory })` turns on a built agent's memory (`harness.memory`, `{ access?, instructions? }`), and a skill folder's `SKILL.md` may list the secret slots its scripts read in its frontmatter (`secrets: [GH_TOKEN]`): `.skills()`, `.plugin()` and `composeManifest` make them the skill's `secrets` in the harness section, which the Runtime checks against `runtime.secrets` at save. A `secrets` that is not a list of environment variable names skips the skill with a warning.
8
+ - eeb6294: Session usage as one ledger (protocol 18). The usage ledger keeps each model call's cost by token kind (uncached input, cache read, cache write, output) at the prices the adapter applied (migration 0037). For a protocol 18 client, `GET /v1/sessions/{id}/calls/model` returns each call's `startedAt`, `endedAt` and `usage.costSplit` (null for a call recorded before), and `GET /v1/sessions/{id}/usage` adds `costSplit` with `splitCalls`, and the calls' durations added up as `modelTimeMs` with `timedCalls`. A protocol 16 or 17 client reads usage as before. Studio's Usage tab is rebuilt on them: a Session card with one tokens bar and a ledger of tokens and cost by kind adding up to a total row, then the model calls grouped by turn with the same columns (cached, uncached, output, total, time, cost), and a row menu whose View event opens the call's event in the Events tab.
9
+
10
+ ### Patch Changes
11
+
12
+ - ed08ba8: `composeManifest(dir)`, and so `nylorun apply`, keep a `harness.manifest.json`'s `memory`: the composed harness section turned the Agent's memory on only when the manifest was saved as one document.
13
+ - ed08ba8: Public text names no internal roadmap codes: the OpenAPI documents' descriptions, the type comments of `@nylorun/core`, `@nylorun/agents` and `@nylorun/admin`, and the CLI's help, README and generated compose file. A Runtime test fails when either OpenAPI document names one.
14
+ - Pin core to the tested release.
15
+ - Updated dependencies [ed08ba8]
16
+ - Updated dependencies [ed08ba8]
17
+ - Updated dependencies [ed08ba8]
18
+ - Updated dependencies [eeb6294]
19
+ - Updated dependencies [ed08ba8]
20
+ - @nylorun/core@0.25.0-beta
21
+
22
+ ## 0.24.0-beta
23
+
24
+ ### Minor Changes
25
+
26
+ - cc8d034: Track T3 in the clients. The SDK has `client.agents.plan(id, input)` (what a move would do), `client.agents.restart(id, { retry? })` and `list({ outdated: true })`. The CLI has `nylorun agents pin <id> <n>`, `track <id>`, `restart <id> [--retry]`, `put --dry-run`, and `list --outdated` (`--behind` kept as its alias), and prints the version an Agent moves to. Studio's Agent page moves an Agent (Move to vN, Pin, Track), restarts it on a pending version or retries a failed one, and shows "vN at next start", a version waiting for secrets, or one that failed to start. The starter's dev loop no longer deletes and remakes its Agent: the tracking Agent moves on each save.
27
+ - 22c61ce: Protocol 16, Agents from one manifest (track T2), a fresh start. An agent manifest (v8) is one versioned document at `/v1/agent-manifests/{name}` with a `harness` section (what runs inside the loop, including the harness's id) and a `runtime` section (what an environment manifest said). The Runtime checks the two against each other when it is saved (`422 manifest_inconsistent` with `details.gaps`), in place of `requires` at session open. Agent definitions and environment manifests are gone.
28
+
29
+ Environments are Agents: `/v1/agents/{id}` and its sub-routes, made from a saved manifest only (`manifest: "name"` tracks it, `"name@n"` pins a version; either resolves once, into `resolvedVersion`), with a derived `state` (`working`, `sleeping`, `input_required`, `failing`, `paused`) and `attention`. Every session runs on one Agent (`PUT /v1/sessions/{id} { agent, ownerUserId }`) and pins its manifest version. No session is special (D71). Error codes, events (`agent.*`), scopes (`agents:write`, `agent-manifests:write`) and identity-file grants (`agents`) say Agent; model budget scope `agent` is `manifest`. Harness API 5: `turn.start.agent` replaces `turn.start.environment`, with `definition` and `loop`. Migration 0032 closes older sessions (`protocol_16`) and drops Environments with their disks and vaults.
30
+
31
+ The SDK has `client.agentManifests`, `client.agents` and `composeManifest(dir)` for three-file authoring; the CLI has `nylorun apply`, `nylorun agent-manifests`, `nylorun agents` and `nylorun migrate v8`; Studio has an Agents screen, an Agent page and an Agent manifests screen. See MIGRATION.md.
32
+
33
+ - 4fa160d: Agent memory, storage and API (track T5, D72). An agent manifest's `harness.memory` (`access`: `read_write` or `read_only`; `instructions`, at most 4,096 characters) turns on the Agent's memory: Markdown files under `/memories` it keeps across sessions, wakes and upgrades. The Runtime keeps them in Postgres (`memories`, `memory_versions`, migration 0036) with a version per change and who made it. `GET /v1/agents/{id}/memories` lists them (`pathPrefix`), `GET`, `PUT` (whole content, with an optional `contentSha256` precondition: `409 memory_conflict`) and `DELETE /v1/agents/{id}/memories/{path}` read, write and delete one (or a folder), and `GET /v1/agents/{id}/memory_versions` and `.../memory_versions/{versionId}` page the versions. A fork copies the memories (the fork plan adds `memories`); a delete removes them. `@nylorun/core` adds `memoryPathOf`, `memoryDescriptionOf` and the memory contracts; new Runtime limits `memoryBytes` (100 KiB) and `memoriesPerAgent` (10,000); new error code `memory_conflict`. The SDK adds `client.agents.memories`; the CLI adds `nylorun agents memories ls|cat|put|rm|versions`. The model's `memory` tool arrives in the next steps of T5.
34
+ - 9e60e40: Protocol 17, a queue per session (track T4, D76). A message sent while the session's turn runs or waits is queued (`{status: "queued", queuedMessageId, position}`, `message.queued`) and starts the next turn when this one ends, with every queued message, each its own user message (`queue.taken`); a cancel ends the turn, not the queue. `delivery: "reject"` keeps the refusal, now `409 session_busy`, and a protocol 16 client's messages default to it, so the Host serves protocol 16 and 17. The queue holds at most the Runtime limit `sessionQueuedMessages` (`429 queue_full`), and session reads count `queuedMessages`. Our engine takes `{kind: "messages", messages}` as a turn's input. The SDK's `input` and `inputParts` take `delivery`, and the CLI's `sessions send` takes `--reject-if-busy`. See MIGRATION.md.
35
+ - f85483d: Triggers (track T4, D74). An agent manifest's `runtime.triggers` declares `schedule` (cron with an IANA `timezone`) and `at` triggers, each with the session its runs go to (a session id, opened on the first run, or `new` for a session per run) and its prompt. They start active with a new Agent, fire on a durable timer per trigger with Postgres as the truth (one late fire after downtime, never a backfill, a fixed jitter per trigger), and each fire is a run (`started`, `queued`, `skipped` or `failed`, with a reason) at `GET /v1/agents/{id}/runs`. A schedule adds at most one queued message to a busy session. `GET /v1/agents/{id}/triggers` and `POST .../triggers/{triggerId}/pause`, `/resume` and `/run` (with an `Idempotency-Key`); Agent reads add `nextWakeAt`, and the Agent's stream adds `agent.run`, `agent.trigger_paused` and `agent.trigger_resumed`. New Runtime limits: `triggersPerAgent`, `triggerMinIntervalMs` and `triggerRunRetentionMs` (with floors), `triggerRunsPerHour`, `triggerJitterPercent`, `triggerJitterMaxMs`, `triggerPromptChars`; Host tuning `triggers.lateGraceMs` and `triggers.intervalCheckFires`. The SDK's `client.agents` adds `triggers`, `pauseTrigger`, `resumeTrigger`, `runTrigger` and `runs`; the CLI adds `nylorun agents triggers` and `nylorun agents runs`.
36
+ - ae54de7: Webhook triggers (track T4, D75). A `webhook: "standard-webhooks"` trigger has an endpoint, `POST /v1/webhooks/{endpointId}`, an anonymous, unversioned route that takes any body up to the new Runtime limit `webhookBodyBytes` and verifies Standard Webhooks (`webhook-id`, `webhook-timestamp` within the Host's `triggers.webhookToleranceMs`, `webhook-signature`) in the keys service, which alone opens the trigger's sealed secret; anything else is `401` and writes nothing. A delivery runs the trigger once per `webhook-id` (`202 {runId, status}`); a paused trigger answers a signed one `409 trigger_paused`. `POST /v1/agents/{id}/triggers/{triggerId}/secret` makes or replaces the `whsec_` secret, shown once (`agent.trigger_secret_rotated`); trigger reads show `webhookUrl` and `secretSet`. The payload reaches the model under the trigger's prompt in a labelled `<trigger-payload>` block. The SDK adds `agents.rotateWebhookSecret`, and the CLI `nylorun agents triggers new-secret`.
37
+
38
+ ### Patch Changes
39
+
40
+ - 1ded443: Track T3, Agents that evolve: an Agent moves between versions of its manifest in place, with its id, disk, vault and sessions. `PUT /v1/agents/{id}` with another version moves it (`?dryRun=true` answers the plan, `AgentMovePlan`), and saving a manifest moves every Agent that tracks it. A harness-only change applies at once; a runtime change a pod applies only when it starts (image, resources, network, setup, files) leaves the Agent outdated (`targetVersion`, `waitingFor: "start"`) until its pod's next start, on the same volume; a version that adds a required secret waits for its value (`waitingFor: "secrets"`, state `input_required`). Its manifest's name, kind, harness and storage are fixed: `409 agent_fixed_field` replaces `409 agent_upgrade_unavailable`. Only secrets that fill a slot of the running version are used; others stay in the vault, dormant. New events `agent.outdated` and `agent.version_changed`, `GET /v1/agents?outdated=true`, and the save plan's `moves` and `agents_move` warning. Migration 0033 adds the Agent's target and failed versions and its start check.
41
+ - Pin core to the tested release.
42
+ - Updated dependencies [218b078]
43
+ - Updated dependencies [22c61ce]
44
+ - Updated dependencies [1ded443]
45
+ - Updated dependencies [76dcd11]
46
+ - Updated dependencies [bf942ed]
47
+ - Updated dependencies [4fa160d]
48
+ - Updated dependencies [9fed948]
49
+ - Updated dependencies [ffeb3bb]
50
+ - Updated dependencies [9e60e40]
51
+ - Updated dependencies [f85483d]
52
+ - Updated dependencies [ae54de7]
53
+ - @nylorun/core@0.24.0-beta
54
+
3
55
  ## 0.23.0-beta
4
56
 
5
57
  ### Minor Changes
package/README.md CHANGED
@@ -7,7 +7,7 @@ packages. A developer application's production tree should contain only this
7
7
  package and `@nylorun/core` from Nylorun. Vocabulary:
8
8
  [runtime/server/src/CONTEXT.md](../../runtime/server/src/CONTEXT.md).
9
9
 
10
- ## Application entry: save your agents
10
+ ## Application entry: save your agents' manifests
11
11
 
12
12
  ```ts
13
13
  // src/main.ts
@@ -15,16 +15,24 @@ import { createClient } from "@nylorun/agents";
15
15
  import { agents } from "../agents/index.js";
16
16
 
17
17
  const client = await createClient();
18
- for (const agent of agents) await client.saveAgent(agent);
18
+ for (const agent of agents) await client.agentManifests.save({ agent, runtime: { kind: "virtual" } });
19
+ // An Agent per person, made from the manifest's latest version; sessions run on it.
20
+ await client.agents.put(`users/${user.id}/support`, { manifest: "support", owner: user.id });
21
+ const session = await client.createSession({
22
+ id: `support-${user.id}`, // ids are the app's: no session is special
23
+ agent: `users/${user.id}/support`,
24
+ ownerUserId: user.id,
25
+ });
19
26
  ```
20
27
 
21
- The Runtime runs each agent entirely from its manifest and the files saved with it
22
- (skills), so a session needs nothing from your process. Your services are reached as tools: [HTTP
23
- tools](#http-tools) and remote MCP servers. `saveAgent` refuses, before sending anything, an
24
- agent with a tool that would run your code (`tool({ run })`) or a flow agent with a tool
25
- stage: make it an `http()` tool or serve it from a remote MCP server. `tool({ run })` still
26
- runs in the local engine (`@nylorun/harness/run`). `implementationVersion` defaults to
27
- `NYLORUN_IMPLEMENTATION_VERSION`, else `dev`.
28
+ The Runtime runs each agent entirely from its saved **agent manifest** and the files saved with
29
+ it (skills), so a session needs nothing from your process. Your services are reached as tools:
30
+ [HTTP tools](#http-tools) and remote MCP servers. `agentManifests.save({ agent, runtime })` makes
31
+ the manifest's harness section from the built agent, names it after the agent's id (or `name`)
32
+ and puts the runtime section beside it ([Agent manifests and Agents](#agent-manifests-and-agents)).
33
+ It refuses, before sending anything, an agent with a tool that would run your code
34
+ (`tool({ run })`) or a flow agent with a tool stage: make it an `http()` tool or serve it from a
35
+ remote MCP server. `tool({ run })` still runs in the local engine (`@nylorun/harness/run`).
28
36
 
29
37
  `createClient()` finds the Runtime through the two `NYLORUN_*` variables or the Project link
30
38
  that `npx nylorun start` writes; with neither, it fails with `connection_missing` and names
@@ -47,7 +55,7 @@ const refundOrder = http({
47
55
  output: z.object({ refundId: z.string() }), // optional
48
56
  url: "https://billing.example.com/refunds",
49
57
  method: "POST", // default; or GET, PUT, PATCH, DELETE
50
- credential: "billing", // optional: the session's Environment holds a credential for this URL
58
+ credential: "billing", // optional: a runtime.secrets slot (bearer or headers) bound to this URL
51
59
  timeoutMs: 20_000, // default 60000, at most 300000
52
60
  });
53
61
 
@@ -122,10 +130,11 @@ rest) is never run twice by the Runtime: a crash fails it to the model as
122
130
  turn say, that is a new call with a new key: put a business key (an order or
123
131
  charge id) in the tool's input schema to refuse it too.
124
132
 
125
- `credential` works as for a remote MCP server: the vault of the session's
126
- Environment must hold a credential bound to the tool's exact URL (as written,
127
- `{field}` parameters included), else the call fails before it is sent
128
- (`http.credential`). A credential with `via` sends the call there instead, such
133
+ `credential` works as for a remote MCP server: it names a `bearer` or `headers`
134
+ slot of the manifest's runtime section bound to the tool's exact URL (as written,
135
+ `{field}` parameters included; the Runtime checks it when the manifest is saved),
136
+ and the vault of the session's Agent must hold a value for it, else the call fails
137
+ before it is sent (`http.credential`). A credential with `via` sends the call there instead, such
129
138
  as a gateway, with the session owner in its identity header (a gateway has one
130
139
  URL, so a tool with `{field}` parameters fails with `http.credential`), and a `401` is `credential_rejected` to the model, not retried.
131
140
  Whether a call waits for a person is not the tool's to say: it is the Tenant's
@@ -171,7 +180,7 @@ decides, and a call no rule matches runs:
171
180
  - `allow`: it runs.
172
181
 
173
182
  Sandbox tools never cross the gate: their network side is egress policy. A
174
- manifest no longer sets `approval` (the Runtime and `saveAgent` refuse it); a
183
+ manifest no longer sets `approval` (the Runtime and `agentManifests.save` refuse it); a
175
184
  session pinned to a manifest saved with `approval: "always"` keeps it, as an
176
185
  ask, until it ends.
177
186
 
@@ -215,16 +224,17 @@ const client = createClient({
215
224
  url: process.env.NYLORUN_RUNTIME_URL,
216
225
  key: process.env.NYLORUN_SERVER_KEY,
217
226
  });
218
- await client.saveAgent(assistant, { implementationVersion: "app-1" });
227
+ await client.agentManifests.save({ agent: assistant });
228
+ await client.agents.put("assistant", { manifest: "assistant" });
219
229
  const session = await client.createSession({
220
- agentId: assistant.id,
230
+ agent: "assistant",
221
231
  ownerUserId: authenticatedUser.id,
222
232
  });
223
233
  await session.input("Look up item 123", { idempotencyKey: requestId });
224
234
  const history = await session.history();
225
235
  ```
226
236
 
227
- Every request sets `Nylorun-Protocol` (14) and no `Nylorun-Tenant`: the Runtime
237
+ Every request sets `Nylorun-Protocol` (16) and no `Nylorun-Tenant`: the Runtime
228
238
  serves one Tenant. Before the first authenticated
229
239
  request, `Transport` fetches `/health` once, checks protocol compatibility, and
230
240
  throws `IncompatibleRuntimeError` / `incompatible_host` when the Host range or
@@ -242,7 +252,7 @@ const assistant = Agent({ id: "assistant", name: "Order assistant" })
242
252
  .skills("./assistant-skills");
243
253
  ```
244
254
 
245
- Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Every file of a skill's folder, binary included (not `.git/`, `node_modules/`, OS files like `.DS_Store`, or `.env` and `.env.*` files, which hold secrets), is part of the definition: the manifest names each one by path and SHA-256 (`skills.<name>.files`). A skill's files may hold together at most the Tenant's skill limit (`PUT /v1/tenant/skills`, default 2 GiB); no file and no count of files has a limit of its own, and an Environment's manifest may set a lower limit for its sessions (`limits.skills.totalBytes`; a session whose agent's skills do not fit is `409 environment_unsuitable`). `saveAgent` uploads the files the Runtime does not hold (`client.files`, `PUT /v1/files/sha256:<hex>`) before it puts the definition, which the Runtime refuses while it names a file it lacks (`400 definition_files_missing`) or a skill past the Tenant's limit (`413 limit_exceeded`). `read_skill_resource` reads files of up to 10 MiB into the model's context; a larger one is used in the sandbox.
255
+ Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Its `secrets` (`secrets: [GH_TOKEN]`, environment variable names) are the skill's `secrets` in the harness section: the `runtime.secrets` slots its scripts read, of type `environment_secret` or `environment_variable`; a skill whose `secrets` is not such a list is skipped with a warning. Every file of a skill's folder, binary included (not `.git/`, `node_modules/`, OS files like `.DS_Store`, or `.env` and `.env.*` files, which hold secrets), is part of the definition: the manifest names each one by path and SHA-256 (`skills.<name>.files`). A skill's files may hold together at most the Tenant's skill limit (`PUT /v1/tenant/skills`, default 2 GiB); no file and no count of files has a limit of its own, and a manifest's runtime section may set a lower limit for its Agents' sessions (`runtime.limits.skills.totalBytes`). Skills need a sandbox: the runtime section's kind is `virtual` or `pod`, which the Runtime checks when the manifest is saved. `agentManifests.save` uploads the files the Runtime does not hold (`client.files`, `PUT /v1/files/sha256:<hex>`) before it saves the manifest, which the Runtime refuses while it names a file it lacks (`400 definition_files_missing`) or a skill past the Tenant's limit (`413 limit_exceeded`). `read_skill_resource` reads files of up to 10 MiB into the model's context; a larger one is used in the sandbox.
246
256
 
247
257
  The Runtime serves the skills itself, with no call to your process. When our harness runs a turn, it adds the skill catalog to the instructions and two tools: `load_skill` returns a skill's `SKILL.md` body and the paths of its other files, and `read_skill_resource` returns a text file. A session with a sandbox also has each skill's files read-only under `/skills/<name>@<hash>/`, so the model can run a skill's scripts with `bash`.
248
258
 
@@ -262,72 +272,96 @@ Each key names a server; its `name` defaults to the key and, when given, must eq
262
272
 
263
273
  Set a server's tools one by one with `tools`, keyed by the server's own tool names, `"*"` for the rest: `{ "*": { enabled: false }, search_issues: { enabled: true }, create_issue: { enabled: true } }` is an allowlist (whether a call waits for a person is the [tool policy](#tool-policy)). When an agent's MCP tools would fill more than a tenth of the model's context window, the Runtime defers them: the model finds them with `tool_search` and runs them with `tool_call`. `deferred: true` or `false`, on a tool or on the server, decides instead.
264
274
 
265
- A manifest never carries a server's credential, and its `headers` hold no secret. You write a credential for the server's URL into an Environment (a token or a header map, or a gateway's key with `via` and an identity header for servers that need each person's sign-in, with `client.environments.secrets.put`), and sessions that name the Environment use it: [DEPLOYMENT.md](../../guides/DEPLOYMENT.md#mcp-servers-and-http-tools), which also shows `nylorun tenant mcp inspect` for listing a server's tools first. After `saveAgent`, `admin.coverage({ agentId, environmentId })` in `@nylorun/admin` says which credential each server and HTTP tool would get in that Environment ([Checking an agent's credentials](../../guides/DEPLOYMENT.md#checking-an-agents-credentials)). The model knows each tool as `server__tool`, with characters outside `[A-Za-z0-9_-]` replaced by `_`; a failed call reaches it with a code (`credential_rejected` on a `401`, `mcp.unreachable`, …), and a result past 32 KiB, or an image, becomes an artifact of the session that it reads with `read_artifact`.
275
+ A manifest never carries a server's credential, and its `headers` hold no secret. A server may name the runtime section's secret slot it authenticates with (`credential`: a `bearer` or `headers` slot bound to its URL, checked when the manifest is saved), and you write the credential into each Agent's vault (a token or a header map, or a gateway's key with `via` and an identity header for servers that need each person's sign-in, with `client.agents.secrets.put`); the sessions on that Agent use it: [DEPLOYMENT.md](../../guides/DEPLOYMENT.md#mcp-servers-and-http-tools), which also shows `nylorun tenant mcp inspect` for listing a server's tools first. `admin.coverage({ agentId })` in `@nylorun/admin` says which credential each server and HTTP tool of an Agent's manifest version would get from its vault ([Checking an agent's credentials](../../guides/DEPLOYMENT.md#checking-an-agents-credentials)). The model knows each tool as `server__tool`, with characters outside `[A-Za-z0-9_-]` replaced by `_`; a failed call reaches it with a code (`credential_rejected` on a `401`, `mcp.unreachable`, …), and a result past 32 KiB, or an image, becomes an artifact of the session that it reads with `read_artifact`.
276
+
277
+ ## Agent manifests and Agents
266
278
 
267
- A session runs in an **Environment**: one sandbox and one vault, with an id you choose, which outlives the sessions that name it (protocol 11). It is made from one version of an **environment manifest** or from inline fields, and never changes what it runs after that (protocol 14). The agent names no Environment, so the same agent runs in any Tenant; it may state what it needs (`.requires()`). Whether an Environment serves one session, one person or a project is your app's choice:
279
+ An **agent manifest** (v8, protocol 16) is one named, versioned document with two sections: `harness`, what runs inside the agent loop (the harness's `id`, `instructions`, `tools`, `skills`, `mcpServers`, `plugins`, `outputSchema`, the harness's own `loop` settings and the Agent's `memory`; or a flow, `kind: "workflow"`), and `runtime`, what the Runtime provides around it (`kind`, `image`, `network`, `setup`, `files`, `limits`, `lifecycle` and its secret slots, `secrets`). The Runtime checks the two against each other when the manifest is saved and refuses every gap at once (`422 manifest_inconsistent`, `details.gaps`): a custom harness off a pod, a `credential` or a skill's `secrets` that is no declared slot of the right type, skills on a runtime without a sandbox. A manifest without `runtime` makes Agents of kind `none`.
280
+
281
+ An **Agent** is the live instance of one version of a manifest: one environment of its runtime's kind, one vault, and the sessions that run on it, with an id you choose (`users/alice/triage`), which outlives them. It is made only from a saved manifest: `manifest: "triage"` tracks the manifest (its latest version when the Agent is made), `"triage@3"` pins version 3; either way it resolves the version once and keeps it. Every session runs on one Agent (`createSession({ agent })`), and no session is special: creating an Agent opens none, and the app picks session ids. Whether an Agent serves one person or a project is your app's choice:
268
282
 
269
283
  ```ts
270
- import { environmentManifest } from "@nylorun/agents";
271
-
272
- // A kind of Environment, saved like an agent: a named, versioned definition (or a JSON file with $schema).
273
- const analystBox = environmentManifest({
274
- name: "analyst-box",
275
- kind: "pod",
276
- image: "python:3.13-slim",
277
- network: { allowedHosts: ["pypi.org", "files.pythonhosted.org", "api.github.com"] },
278
- setup: [{ command: "pip install pandas" }],
279
- secrets: { GH_TOKEN: { type: "environment_secret", allowedHosts: ["api.github.com"], required: true } },
280
- });
281
- const plan = await client.environments.manifests.plan(analystBox); // changes, errors, warnings, Environments behind
282
- await client.environments.manifests.save(analystBox, { ifMatch: plan.from ?? 0 });
283
-
284
- // One per person or project, made from the manifest's latest version and pinned to it.
285
- await client.environments.put("users/alice/analyst", { manifest: "analyst-box", ownerUserId: "alice" });
286
- await client.environments.secrets.put("users/alice/analyst", "GH_TOKEN", { value: process.env.ALICE_GITHUB_TOKEN! });
287
-
288
- // Or from inline fields: get-or-create by id, write its secrets, open sessions on it.
289
- await client.environments.put("users/alice/web", {
290
- kind: "virtual", // the default; "pod" for a container with its own volume, "none" for a vault without compute
291
- network: { allowedHosts: ["pypi.org", "files.pythonhosted.org"] }, // no egress unless you list hosts
292
- ownerUserId: "alice", // the owner guard: only alice's sessions may name it
293
- labels: { team: "data" },
294
- lifecycle: { deleteAfter: "30d" }, // deleted once unused this long
295
- });
296
- await client.environments.secrets.put("users/alice/web", "GH_TOKEN", {
297
- type: "environment_secret",
298
- value: process.env.ALICE_GITHUB_TOKEN!,
299
- allowedHosts: ["api.github.com"],
300
- });
301
- const session = await client.createSession({ agentId: "analyst", ownerUserId: "alice", environment: "users/alice/web" });
284
+ // The manifest, from a built agent and the runtime section to run it in.
285
+ const manifest = {
286
+ agent: analyst, // named after its id, "analyst"
287
+ runtime: {
288
+ kind: "pod" as const,
289
+ image: "python:3.13-slim",
290
+ network: { allowedHosts: ["pypi.org", "files.pythonhosted.org", "api.github.com"] },
291
+ setup: [{ command: "pip install pandas" }],
292
+ secrets: { GH_TOKEN: { type: "environment_secret" as const, allowedHosts: ["api.github.com"], required: true } },
293
+ },
294
+ };
295
+ const plan = await client.agentManifests.plan(manifest); // from → to, changes, warnings, errors, Agents behind
296
+ await client.agentManifests.save(manifest, { ifMatch: plan.from ?? 0 }); // 412 manifest_changed if it moved
297
+
298
+ // One per person: get-or-create by id, write its secrets, open sessions on it.
299
+ await client.agents.put("users/alice/analyst", { manifest: "analyst", owner: "alice", labels: { team: "data" } });
300
+ await client.agents.secrets.put("users/alice/analyst", "GH_TOKEN", { value: process.env.ALICE_GITHUB_TOKEN! });
301
+ const session = await client.createSession({ id: "alice-q3", agent: "users/alice/analyst", ownerUserId: "alice" });
302
+ ```
302
303
 
303
- // One per session: created with the session (id `sessions/<session id>`), deleted by release().
304
- const { session: scratch, release } = await client.environments.forSession({
305
- session: { agentId: "analyst", ownerUserId: "alice" },
306
- recipe: { network: { allowedHosts: ["pypi.org"] } },
307
- });
308
- await release();
304
+ `agentManifests.save(input, { ifMatch, requestId })` and `plan(input, { ifMatch })` (a dry run that saves nothing) take a v8 document, what `composeManifest` read, or `{ agent, runtime?, harness?, loop?, memory?, name?, description?, metadata? }`: the SDK makes the harness section from the built agent or flow (`harnessSectionOf`, with its skills' `secrets`; `harness` is the harness's id; `memory`, `{ access?, instructions? }`, turns on an agent's memory, not a flow's) and names the manifest after the agent's id. First it uploads every file the manifest names by hash that the Runtime does not hold: the agent's skills, a flow's agents' skills, and the runtime's files (`runtimeFiles(path, contents)` hashes a `files` entry; pass its `bytes` as `files`). `If-Match` is the version you read (`0` for a new manifest). A save that changes nothing answers the latest version, `unchanged`. `get(name, version?)`, `list()`, `versions(name)` (with the Agents made from each) and `delete(name)` (refused while an Agent was made from it: `409 manifest_in_use`) read and remove them.
305
+
306
+ ### Three files: `composeManifest`
307
+
308
+ A manifest can also be authored as files, so a runtime is shared by many agents and skills stay folders. `composeManifest(dir)` (Node only) reads `<dir>/agent.manifest.json` and the files it names, each path relative to the file that names it, and returns `{ manifest, files, gaps, warnings }`; `agentManifests.save` takes it as it is and uploads `files`. `npx nylorun apply <dir>` does the same from the command line.
309
+
310
+ ```jsonc
311
+ // agents/triage/agent.manifest.json
312
+ { "manifestSchemaVersion": 8, "name": "triage", "harness": "./harness.manifest.json", "runtime": "../../shared/runtime.manifest.json" }
313
+
314
+ // agents/triage/harness.manifest.json
315
+ {
316
+ "instructions": ["Triage each new issue."],
317
+ "skills": "./skills", // each subfolder with a SKILL.md is a skill; its secrets, the frontmatter's
318
+ "mcpServers": "./mcp.json", // the Agent Plugins format
319
+ "plugins": ["../../plugins/github"], // folded in as .plugin() folds it
320
+ "credentials": { "linear": "linear" }, // MCP server or HTTP tool → runtime.secrets slot
321
+ "memory": { "access": "read_write" }, // turns on the Agent's memory
322
+ "requires": { "sandbox": "any", "secrets": ["GH_TOKEN"] } // checked here only
323
+ }
324
+
325
+ // shared/runtime.manifest.json
326
+ {
327
+ "kind": "pod",
328
+ "files": [{ "path": "/workspace/kit", "from": "./kit" }], // a folder, uploaded by hash
329
+ "secrets": {
330
+ "GH_TOKEN": { "type": "environment_secret", "allowedHosts": ["api.github.com"] },
331
+ "linear": { "type": "bearer", "url": "https://mcp.linear.app/mcp" }
332
+ }
333
+ }
309
334
  ```
310
335
 
311
- With a sandbox (kind `virtual` or `pod`), the model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a persistent `/workspace`. These tools run in the Runtime, not in your process. A `virtual` Environment is an emulated shell in the Runtime's harness, which is not a VM boundary and takes no `image`; a `pod` runs `image` with its own volume (`storage`) once the Tenant has sandbox pods (`nylorun local sandbox enable`). Kind `none` has a vault and no sandbox, for agents that use only remote MCP servers and HTTP tools. A session that names no Environment has no sandbox and no credentials.
336
+ ```ts
337
+ import { composeManifest, createClient } from "@nylorun/agents";
338
+
339
+ const composed = await composeManifest("agents/triage");
340
+ if (composed.gaps.length > 0) throw new Error(composed.gaps.join("\n"));
341
+ await client.agentManifests.save(composed);
342
+ ```
343
+
344
+ `gaps` names where the runtime misses the harness file's `requires` (`sandbox: "any"` needs kind `virtual` or `pod`, `"pod"` a pod; each of `secrets` a declared slot) and the gaps the Runtime would refuse it for (`sectionGaps`). `requires`, `credentials` and every `$schema` stay out of the document; a `credentials` name that is no server or HTTP tool, or a file that does not validate, throws `ComposeError`. JSON Schemas for editors: `@nylorun/core/schemas/agent-manifest.json`, `harness-manifest.json` and `runtime-manifest.json`.
345
+
346
+ ### Agents
312
347
 
313
- An Environment never changes what it runs (protocol 14, its view's `manifest`): a `put` on an existing one may change its `labels` and owner guard, fields that match are accepted (so get-or-create stays one call), and anything else is `409 environment_immutable`. A save of a new manifest version changes no Environment: make a new one, which can start from the old one's disk (`from`). The Runtime checks what an Environment runs against the Tenant's limits (`GET`/`PUT /v1/tenant/sandbox`: a network ceiling, a resource maximum, the idle timeout; `/v1/tenant/artifacts` and `/v1/tenant/skills` for its `limits`) and answers `400` with every problem it finds. `lifecycle.idle` stops a pod after that long without a turn (at most the Tenant's); `lifecycle.deleteAfter` deletes the Environment, its vault and its files once nothing has used it that long. Private networks, loopback, the host and cloud metadata endpoints are always blocked.
348
+ With a sandbox (kind `virtual` or `pod`), the model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a persistent `/workspace`. These tools run in the Runtime, not in your process. A `virtual` Agent's environment is an emulated shell in the Runtime's harness, which is not a VM boundary and takes no `image`; a `pod` runs `image` with its own volume (`storage`) once the Tenant has sandbox pods (`nylorun local sandbox enable`). Kind `none` has a vault and no sandbox, for agents that use only remote MCP servers and HTTP tools.
314
349
 
315
- Secrets are write-only: `client.environments.secrets.put(id, name, secret)` creates or replaces one, and `list`, `get` and `delete` never return a value. An `environment_secret` (`{ value, allowedHosts, inject? }`) shows its name to commands as the sentinel `nylorun_managed`, and egress-gate sets its header on HTTPS requests to `allowedHosts` (pods only); an `environment_variable` (`{ value }`) is a plain variable; `bearer` (`{ url, value, via?, identity? }`) and `headers` (`{ url, headers, via?, identity? }`) are credentials for the MCP servers and HTTP tools at `url`. A second secret for one host, or a second credential for one URL, is `409 credential_conflict` at the write ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#credentials)). When what an Environment runs declares a secret's slot (a manifest's `secrets`), send only `{ value }` (or `{ headers }`): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`. An agent built with `.requires({ sandbox: "pod", secrets: ["GH_TOKEN"] })` opens a session only on an Environment that has them (`409 environment_unsuitable`). `saveAgent(agent, { dryRun: true })` checks a definition and saves nothing.
350
+ An Agent runs one version of its manifest at a time and evolves in place, with its id, disk, vault and sessions: a `put` on an existing one may change its `labels` and `owner` guard and moves it to the version its `manifest` names (`"name"` tracks the latest, `"name@n"` pins), `plan(id, input)` answers what that move would do, and a save of a new version moves every Agent that tracks the manifest. A harness change applies at once (each session's next turn runs it); a pod's runtime change waits for its next start on the same volume (`targetVersion`, `waitingFor: "start"`; `restart(id)` applies it now), and a start that fails goes back to the version it ran (`failedVersion`; `restart(id, { retry: true })` tries again). A version that adds a required secret waits for its value (`waitingFor: "secrets"`). `list({ outdated: true })` lists the Agents still moving. Its manifest's name, kind, harness and storage are fixed (`409 agent_fixed_field`): `fork(id, { id: next, manifest? })` makes a new Agent (default the source's manifest, tracking), with the source's disk (`disk`: `latest`, a version, or `empty`) and the secrets the new manifest declares (`secrets: "none"` copies none), and `planFork` answers what a fork would do. The Runtime checks the runtime section against the Tenant's limits (`GET`/`PUT /v1/tenant/sandbox`: a network ceiling, a resource maximum, the idle timeout; `/v1/tenant/artifacts` and `/v1/tenant/skills` for its `limits`). `lifecycle.idle` stops a pod after that long without a turn (at most the Tenant's); `lifecycle.deleteAfter` deletes the Agent, its vault and its files once nothing has used it that long. Private networks, loopback, the host and cloud metadata endpoints are always blocked.
316
351
 
317
- Sessions on one Environment share its `/workspace` and run their turns at the same time (one turn per session); which sessions work on one disk, and when, is your app's call. Stop, reset, snapshot and restore wait until no turn runs (`409 environment_busy`). Deleting a session only detaches it. With `ownerUserId` set, a session of another owner is `403 environment_owner_mismatch` (a caller acting for a person gets the `404` of a missing Environment), checked when it opens and at every turn start. `client.environments.list({ labels })`, `page`, `get(id)`, `events(id, { types, since, until })` (its lifecycle, and `environment.egress` for every connection its sandbox opens, naming the turns that were running), `stop(id)`, `reset(id)` and `delete(id)` manage them, and `npx nylorun environments list | delete` does the same from the command line. Acting for a person (`app.as(...)`), changing an Environment or its secrets needs `environments:write`. A trusted issuer's token reaches only the Environments its issuer grants (the identity file's `environments` templates, exact ids or prefixes ending in `/*`), checked at every turn start. The Tenant holds at most `limits.environments` of them (`PUT /v1/tenant/sandbox`, default 100).
352
+ Secrets are write-only: `client.agents.secrets.put(id, name, secret)` creates or replaces one, and `list`, `get` and `delete` never return a value. When the manifest declares the secret's slot (`runtime.secrets`), send only `{ value }` (or `{ headers }`): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`. Typed, an `environment_secret` (`{ value, allowedHosts, inject? }`) shows its name to commands as the sentinel `nylorun_managed`, and egress-gate sets its header on HTTPS requests to `allowedHosts` (pods only); an `environment_variable` (`{ value }`) is a plain variable; `bearer` (`{ url, value, via?, identity? }`) and `headers` (`{ url, headers, via?, identity? }`) are credentials for the MCP servers and HTTP tools at `url`. A second secret for one host, or a second credential for one URL, is `409 credential_conflict` at the write ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#credentials)).
318
353
 
319
- A pod's disk can start prepared: `files` copies versions of Tenant-wide artifacts in, then `setup` runs its commands, once per new disk and before its first turn (a failed command fails the pod, and the next turn start runs the setup again). Between turns, `client.environments.snapshot(id)` keeps a version of the disk, `restore(id, version)` puts one back (a pod on a new volume seeded before its next turn), `put(newId, { from: { environment: id, version: "latest" } })` starts a new Environment from it, and `versions(id)` and `deleteVersion(id, n)` manage them ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#environments)):
354
+ Sessions on one Agent share its `/workspace` and run their turns at the same time (one turn per session); which sessions work on one disk, and when, is your app's call. Stop, reset, snapshot and restore wait until no turn runs (`409 agent_busy`). Deleting a session only detaches it. With `owner` set, a session of another owner is `403 agent_owner_mismatch` (a caller acting for a person gets the `404` of a missing Agent), checked when it opens and at every turn start. An Agent's view says what it runs (`manifest`, `resolvedVersion`, `tracking`), its `state` (`working`, `sleeping`, `input_required`, `failing` or `paused`, `AGENT_STATES`) with the sessions behind it (`attention`), its compute (`compute.state`) and its slots. `client.agents.list({ labels, state, manifest })`, `page`, `get(id)`, `events(id, { types, since, until })` (its lifecycle, and `agent.egress` for every connection its sandbox opens, naming the turns that were running), `stop(id)`, `reset(id)` and `delete(id)` manage them, and `npx nylorun agents list | put | rm` does the same from the command line. Acting for a person (`app.as(...)`), changing an Agent or its secrets needs `agents:write`, and saving a manifest `agent-manifests:write`. A trusted issuer's token reaches only the Agents its issuer grants (the identity file's `agents` templates, exact ids or prefixes ending in `/*`), checked at every turn start. The Tenant holds at most `limits.environments` of them (`PUT /v1/tenant/sandbox`, default 100).
355
+
356
+ A pod's disk can start prepared: the runtime's `files` are copied in (artifact versions, or files by hash), then `setup` runs its commands, once per new disk and before its first turn (a failed command fails the pod, and the next turn start runs the setup again). Between turns, `client.agents.snapshot(id)` keeps a version of the disk, `restore(id, version)` puts one back (a pod on a new volume seeded before its next turn), `put(newId, { manifest, from: { agent: id, version: "latest" } })` starts a new Agent from it, and `versions(id)` and `deleteVersion(id, n)` manage them ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#agents)):
320
357
 
321
358
  ```ts
322
- await client.environments.put("projects/acme", {
323
- kind: "pod",
324
- files: [{ artifactId: repoTarball.artifactId, version: 1, path: "/workspace/app" }],
325
- setup: [{ command: "npm ci", cwd: "/workspace/app", timeout: "15m" }],
326
- });
327
- const { version } = await client.environments.snapshot("projects/acme");
328
- await client.environments.put("projects/acme-try", { kind: "pod", from: { environment: "projects/acme", version } });
359
+ const { version } = await client.agents.snapshot("projects/acme");
360
+ await client.agents.put("projects/acme-try", { manifest: "acme", from: { agent: "projects/acme", version } });
329
361
  ```
330
362
 
363
+ ## Subagents
364
+
331
365
  Add an agent with `.subagents()` to let the model delegate to it:
332
366
 
333
367
  ```ts
@@ -360,12 +394,13 @@ Delegate when the parent should keep the answer. When a specialist should own th
360
394
  ## Flow agents
361
395
 
362
396
  An agent's body is either a ReAct loop (the model decides) or a flow (its manifest
363
- decides). A flow agent is built from stages and is registered, saved and opened as a
364
- session like any agent: put it in `export const agents`, `saveAgent(flowAgent)`, then
365
- `createSession({ agentId })` and `session.input(value)`. `input` sends string values as
397
+ decides). A flow agent is built from stages and is saved and run like any agent: put it in
398
+ `export const agents`, `agentManifests.save({ agent: flowAgent, runtime })`, make an Agent from
399
+ the manifest, then `createSession({ agent })` and `session.input(value)`. `input` sends string values as
366
400
  `content` and other JSON as `data`. On the wire a flow agent is a workflow manifest v3
367
401
  (`kind: "workflow"`, `workflowSchemaVersion: 3`) that embeds the agents it runs, so one
368
402
  document and one manifest hash cover the whole flow; its agents are not listed on their own.
403
+ In an agent manifest it is the harness section, `harness.kind: "workflow"`.
369
404
 
370
405
  A flow passes data between its stages itself: the first stage gets the flow's input, and
371
406
  every later stage gets the previous stage's output. So each agent returns, through its
@@ -435,7 +470,7 @@ object); a mismatch the build can see, such as an agent with no `.output()` befo
435
470
  is refused (`flow.input-mismatch`), and any other fails the stage when it runs
436
471
  (`tool.invalid-input`). The Runtime makes the request as for an agent's HTTP tool, with
437
472
  `Nylorun-Agent-Id` set to the flow agent's id and `credential` taken from the
438
- vault of the flow session's Environment; its answer is the next stage's input. A failure status, a timeout or
473
+ vault of the flow session's Agent; its answer is the next stage's input. A failure status, a timeout or
439
474
  an answer that does not match `output` fails the stage with its code (`http.status`,
440
475
  `http.timeout`, `tool.invalid-output`, …). A Loop body that starts with an HTTP stage
441
476
  is retried with the Loop's input, not the feedback.
@@ -479,13 +514,12 @@ nested flow agent's id in front of its own agents'. Control stages add nothing,
479
514
  wrapping a step in `.loop()` or moving it between cases keeps its session. An agent
480
515
  may appear once per flow; use it again under a new id with `writer.withId("final-writer")`
481
516
  (`flow.duplicate-leaf`). A code tool stage (`.pipe(tool({ run }))`) runs only in the local
482
- engine: the Runtime runs flows from their manifests alone, so `saveAgent` refuses a flow
483
- agent with one. Use an HTTP stage instead.
517
+ engine: the Runtime runs flows from their manifests alone, so `agentManifests.save` refuses a
518
+ flow agent with one. Use an HTTP stage instead.
484
519
 
485
- The agents in a flow share one Environment: open the flow's session on it,
486
- `createSession({ …, environment: id })`, and every linked session in the flow
487
- inherits it; turns within one flow do not block each other. Other sessions share
488
- it by naming the same id (see `client.environments`). Call the sandbox
520
+ A flow runs on its Agent: every linked session in the flow runs on the Agent the flow's
521
+ session runs on, with its sandbox and vault; turns within one flow do not block each other.
522
+ Other sessions on the same Agent share its sandbox. Call the sandbox
489
523
  built-ins via `session.sandbox` (application).
490
524
  Observe with `session.observe({ follow: true })` to merge linked agent streams;
491
525
  `pending()` lists waits across the tree. Studio renders the manifest tree and
@@ -498,9 +532,8 @@ workflow Loop): [runtime/harness/src/CONTEXT.md](../../runtime/harness/src/CONTE
498
532
  Use `session.observe({ cursor, signal })` for resumable canonical events, `session.inspect()` for waiting/uncertain state, and `approve`, `respond`, or `cancel` with an explicit stable idempotency key. Retry the same semantic command with the same key. `ownerUserId` must come from trusted server authentication. Application credentials are not browser credentials; browser applications need an authorized backend. Definition authoring is browser-bundleable.
499
533
 
500
534
  Connection defaults follow `resolveConnection` (options → environment → Project
501
- link). Implementation version defaults from `NYLORUN_IMPLEMENTATION_VERSION`,
502
- then `dev`. Agents do not hash the manifest; the Runtime pins the digest of the
503
- definition a session opens with. Model selection and model credentials belong to
535
+ link). Agents do not hash the manifest; a session pins the manifest version its Agent runs
536
+ when it opens, and the digest of the loop manifest made from it. Model selection and model credentials belong to
504
537
  the Tenant.
505
538
 
506
539
  The Runtime sends a lost MCP or HTTP tool call again with the same key, then fails it to the
@@ -514,8 +547,9 @@ local execution is available through `@nylorun/harness/run`.
514
547
 
515
548
  The Runtime serves the Tenant's agents to any [AG-UI](https://docs.ag-ui.com)
516
549
  client (`@ag-ui/client`'s `HttpAgent`, CopilotKit, a desktop renderer) at
517
- `/v1/ag-ui/agents/{agentId}` (optional feature `ag-ui-endpoint`). Each AG-UI
518
- thread is one session per person, agent and thread. There are two ways in:
550
+ `/v1/ag-ui/agents/{agentId}` (optional feature `ag-ui-endpoint`), where `agentId` is an
551
+ Agent's id (`client.agents`). Each AG-UI thread is one session per person, Agent and thread,
552
+ on that Agent. There are two ways in:
519
553
 
520
554
  - **From your own server**, with `@nylorun/agents/ag-ui` (below): your server
521
555
  signs people in and the handler forwards each request to the Runtime acting
@@ -535,7 +569,7 @@ import support from "./agents/support.js";
535
569
 
536
570
  const agui = createAgUiHandler({
537
571
  basePath: "/api/agui",
538
- agents: [support], // nothing else is reachable
572
+ agents: ["support"], // Agent ids: nothing else is reachable
539
573
  subject: async (request) => (await getSignedInUser(request))?.id, // undefined → 401
540
574
  });
541
575
 
@@ -552,9 +586,10 @@ createServer(toNodeListener(agui)).listen(3000);
552
586
 
553
587
  `run`, `history`, `reattach` and `cancel` are also on the handler for your own
554
588
  routing. The default `client` is `createClient()`; pass `client` to use another
555
- one, and `session(subject, agentId)` to add the person's `environment` (their
556
- Environment's id: its sandbox and its vault's credentials) or `info`. They apply when the thread's session is
557
- created, on its first run; later runs keep them. Whatever the browser sends in
589
+ one, and `session(subject, agentId)` to add the session's `info`. It applies when the thread's
590
+ session is created, on its first run; later runs keep it. The session runs on the Agent the path
591
+ names, with its sandbox and its vault's credentials: to give each person their own keys, serve
592
+ each person's Agent (`users/<id>/support`) and route to it. Whatever the browser sends in
558
593
  `forwardedProps.nylorun` is replaced.
559
594
 
560
595
  - Each AG-UI message id is the command's idempotency key: a retried run replays
@@ -588,7 +623,7 @@ never with a key.
588
623
 
589
624
  ## A2A
590
625
 
591
- `@nylorun/agents/a2a` serves the Tenant's agents to other agents over
626
+ `@nylorun/agents/a2a` serves the Tenant's Agents to other agents over
592
627
  [A2A](https://a2a-protocol.org) 1.0 (JSON-RPC) from your own server: the
593
628
  gateway. Your server authenticates each partner and names the subject its tasks
594
629
  belong to. The handler forwards each request to the Runtime, which speaks A2A,
@@ -597,11 +632,9 @@ so the handler loads no A2A package and parses no message.
597
632
  ```ts
598
633
  import { createServer } from "node:http";
599
634
  import { createA2aHandler, toNodeListener } from "@nylorun/agents/a2a";
600
- import support from "./agents/support.js";
601
-
602
635
  const a2a = createA2aHandler({
603
636
  basePath: "/a2a",
604
- agents: [support], // nothing else is reachable
637
+ agents: ["support"], // Agent ids: nothing else is reachable
605
638
  // undefined → 401; return { subject, agents } to give a partner fewer agents
606
639
  subject: (request) => partnerFor(request.headers.get("x-partner-key"))?.subject,
607
640
  publicUrl: "https://api.example.com/a2a", // for the card; default: the request's origin
@@ -628,7 +661,7 @@ What the Runtime answers (Host feature `a2a-endpoint`):
628
661
  for input, for at most 5 minutes (then it returns `WORKING`); with
629
662
  `configuration.returnImmediately` it returns at once and the partner polls
630
663
  `GetTask`. `CancelTask` works on a running task.
631
- - A `contextId` is one session per partner, agent and context; a task is one
664
+ - A `contextId` is one session per partner, Agent and context, on that Agent; a task is one
632
665
  turn. A message without `taskId` starts a task (one at a time per context).
633
666
  A question from the agent pauses the task as `TASK_STATE_INPUT_REQUIRED`
634
667
  with the question as the status message; the partner answers with a message
@@ -675,7 +708,7 @@ A session with a sandbox gives the agent `save_artifact`, which saves a file it
675
708
  made as an artifact of the session; each new artifact or version appears in the
676
709
  session's events as `artifact.created` or `artifact.version.created`. The Tenant's
677
710
  limits (`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default),
678
- and those an Environment's manifest sets for its sessions' artifacts (`limits.artifacts`,
711
+ and those a manifest's runtime section sets for its Agents' artifacts (`runtime.limits.artifacts`,
679
712
  at most the Tenant's), refuse a larger upload with `413 limit_exceeded`, and nothing
680
713
  is stored. Acting for
681
714
  a person (`as()`, or a trusted issuer's token), a client reaches only the artifacts of that
@@ -739,7 +772,7 @@ const app = await createClient(); // the application key, on the server only
739
772
 
740
773
  // Per request, after your own sign-in:
741
774
  const person = app.as(`app:${user.id}`, { scopes: ["sessions:own"] });
742
- await person.createSession({ agentId: "support", ownerUserId: `app:${user.id}` });
775
+ await person.createSession({ agent: `users/${user.id}/support`, ownerUserId: `app:${user.id}` });
743
776
  await person.listSessions(); // only this person's sessions
744
777
  ```
745
778
 
@@ -753,16 +786,15 @@ compatibility check, so calling `as()` per request is cheap.
753
786
  | Scope | Allows |
754
787
  | --- | --- |
755
788
  | `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel; and their artifacts |
756
- | `agents:read` | Listing the Tenant's agents |
757
- | `agents:write` | Saving agents; listing agents |
758
- | `environments:write` | Creating, changing, stopping, resetting and deleting Environments, and writing their secrets (reading them takes `sessions:own` or this) |
789
+ | `agents:read` | Reading the Tenant's agent manifests and Agents |
790
+ | `agents:write` | Creating, changing, stopping, resetting and deleting Agents, and writing their secrets (reading them takes `sessions:own` or this) |
791
+ | `agent-manifests:write` | Saving and deleting agent manifests |
759
792
 
760
793
  No scope reaches the sandbox tool routes; call them without `as()`. `tenant:settings` is retired (protocol 8): it grants
761
794
  nothing, and no subject reaches the Management API (`/v1/tenant/*`).
762
- `sandboxes:write` (protocol 10) is refused with a message naming
763
- `environments:write`. Credentials live in Environments: the server writes a
764
- person's own keys into their Environment (with `ownerUserId` set to them) and
765
- names it when it opens their sessions
795
+ `environments:write` is refused with a message naming `agents:write` (protocol 16).
796
+ Credentials live in Agents' vaults: the server writes a person's own keys into
797
+ their Agent (made with `owner` set to them) and opens their sessions on it
766
798
  ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#credentials)). A subject is 1–200 visible ASCII
767
799
  characters (spaces only inside) and `host` is reserved. Your server must drop
768
800
  any `Nylorun-*` header its own clients send, and only an application key can act
@@ -780,7 +812,7 @@ and revoking them is management: `admin.signingKeys.list()`, `.rotate()`
780
812
  A web page or an app calls the Runtime directly with the JWT your identity
781
813
  provider gave the person: list the provider in the Runtime's identity file
782
814
  ([Trusted issuers](../../guides/DEPLOYMENT.md#trusted-issuers)), and the Runtime takes the
783
- token's subject, scopes, agents and Environment grants from it. Nylorun mints no
815
+ token's subject, scopes and Agent grants from it. Nylorun mints no
784
816
  token and ships no browser client: use your provider's SDK for sign-in and send
785
817
  the token as `Authorization: Bearer <token>` with `Nylorun-Protocol`, through a
786
818
  reverse proxy that answers CORS (the Runtime sends no CORS headers). An expired
@@ -7,8 +7,7 @@
7
7
  * The Agent Card comes from the Runtime, with this endpoint's URL and the host's provider and
8
8
  * security schemes added.
9
9
  */
10
- import type { BuiltWorkflow } from "@nylorun/core/define";
11
- import { type AgentSource, type AgentsClient } from "../client.js";
10
+ import { type AgentsClient } from "../client.js";
12
11
  /** A partner as the host authenticated it: its subject, and optionally fewer agents. */
13
12
  export interface A2aCaller {
14
13
  /** Owner of the partner's tasks, e.g. `a2a:acme`. Keep it stable for a partner. */
@@ -30,8 +29,14 @@ export interface A2aCardOptions {
30
29
  securityRequirements?: readonly unknown[];
31
30
  }
32
31
  export interface A2aHandlerOptions {
33
- /** Agents this endpoint serves: built agents, workflows or their ids. Nothing else is reachable. */
34
- agents: readonly (AgentSource | BuiltWorkflow | string)[];
32
+ /**
33
+ * The Agents this endpoint serves, by id (`client.agents`, protocol 16), or as
34
+ * `client.agents.get` answers them: a partner's tasks run in sessions on them. Nothing else is
35
+ * reachable.
36
+ */
37
+ agents: readonly (string | {
38
+ readonly id: string;
39
+ })[];
35
40
  /**
36
41
  * The partner the request is from, authenticated by the host. `undefined` answers `401`;
37
42
  * there is no anonymous mode. Not called for the Agent Card, which is public.
@@ -1,3 +1,12 @@
1
+ /**
2
+ * An A2A endpoint over the Tenant's agents, as a web-standard `Request` → `Response` handler
3
+ * (gateway mode). The host authenticates each partner (`subject`); the handler forwards the
4
+ * partner's JSON-RPC request to the Runtime as that subject, and the Runtime speaks A2A (Host
5
+ * feature `a2a-endpoint`). It parses no A2A message and sends no header the partner chose
6
+ * except `A2A-Version` and `A2A-Extensions`, so a partner can never name a subject or scopes.
7
+ * The Agent Card comes from the Runtime, with this endpoint's URL and the host's provider and
8
+ * security schemes added.
9
+ */
1
10
  import { createClient } from "../client.js";
2
11
  import { IncompatibleRuntimeError, RuntimeError } from "../http.js";
3
12
  /** The Host feature the handler forwards to. */