@nylorun/agents 0.19.1-beta → 0.20.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,81 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.20.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 5165451: **A pod's disk can start prepared, and every Environment's disk has versions (R2d steps 2 and 3).** Before, a new pod volume was empty and a disk could only be kept or reset. Now:
8
+
9
+ - **Setup and input files** (pods, Host feature `environment-setup`): `PUT /v1/environments/{id}` takes `files` (versions of Tenant-wide artifacts, copied in: a file at its `path`, a folder under it) and `setup` (commands run in order with `/bin/sh -c`, `cwd` default `/workspace`, `timeout` default 10m and at most 30m, at most 16 of each). They run once per new disk, after the pod's engine joins and before its first turn, with the Environment's variables, secrets (as the sentinel) and egress. A failed command fails the pod (`observed: failed`, reason `setup: command N …`, an `environment.failed` event with the exit code and the end of the output) and the turn waiting for it; the next turn start runs the setup again. A virtual Environment refuses both with a `400`.
10
+ - **Disk versions** (Host feature `environment-disk-versions`): `POST /v1/environments/{id}/snapshot` keeps a version of the disk between turns (`/workspace`, and a pod's `/harness` but the engine's own files) as a version of a folder artifact the Environment owns, with modes, modification times, links and empty directories (folder manifest `nylorun.folder.v2`); each file's bytes are stored once, and an unchanged disk makes no version. `POST .../restore` puts a version back: a virtual Environment at once, a pod on a new volume seeded before its next turn (`disk.operation` in the view meanwhile; turns wait). `PUT` with `from: { environment, version }` creates an Environment from another's version. `GET .../versions` and `DELETE .../versions/{n}` list and delete them; their trees, files, diffs and zips read through `/v1/artifacts`. A lost pod volume with a version is recovered from the latest one instead of refusing turns until a reset. Snapshot and restore during a turn are `409 environment_busy`. `EnvironmentView.disk` gives the latest version and the copy in progress. Migration `0021_disk_versions`.
11
+ - `@nylorun/agents`: `client.environments.snapshot`, `restore`, `versions` and `deleteVersion`; `put` takes `setup`, `files` and `from`.
12
+
13
+ - 35e29b7: **Environments replace sandboxes and vaults (protocol 11). This is a fresh start: upgrading deletes your sandboxes, their disks and every vault except the model's, and closes every open session (its history stays readable).** List what you have before upgrading; see "Environments replace sandboxes and vaults" in MIGRATION.md.
14
+
15
+ An Environment is where sessions run: one sandbox (kind `virtual`, `pod` or `none`) with a recipe that can change, and one vault of secrets for its shell, MCP servers and HTTP tools. The app decides who uses which Environment; the Runtime keeps it by id and holds its lifecycle.
16
+
17
+ - `@nylorun/core`: protocol 11 (the Host accepts 11 only; older clients get `426`), required feature `environments`. `PutEnvironmentRequestSchema`, `EnvironmentViewSchema` (`recipe`, `version`, `appliedVersion`, `ownerUserId`, `lastUsedAt`), `PutEnvironmentSecretRequestSchema`, `EnvironmentSecretViewSchema`, `ENVIRONMENT_EVENT_TYPES`, `TenantEnvironmentsConfigSchema`, `environmentGranted`. `PutSessionRequest.environment` replaces `sandbox`, `vaultIds` and `credentialSelections`; `SessionView.environment` replaces the session's sandbox and vault fields. Error codes `environment_busy`, `environment_unavailable`, `environment_lost`, `environment_not_granted`, `environment_owner_mismatch` and `session_closed` (the `sandbox_*` codes and `sandbox_expired` are gone); session events `environment.state`, `environment.exec` and `environment.attached`; scope `environments:write`. Credential coverage takes `environmentId` and answers `covered` or `missing`; the MCP preview takes `environmentId`. The vault Management schemas are removed.
18
+ - `@nylorun/runtime`: `/v1/environments/{id}` replaces `/v1/sandboxes/{id}`: `PUT` creates, finds or changes one (`kind` and `storage` fixed; a recipe change bumps `version`; a pod gets a new image or resources at its next start from stopped, a new allowlist within seconds), with list by label, events (`nylorun.environment-event/1`), stop, reset and delete. Each Environment owns one vault: `PUT|GET|DELETE /v1/environments/{id}/secrets/{name}` on the Runtime API, write-only, with a second credential for a host or URL refused at the write (`409 credential_conflict`). An optional owner guard (`ownerUserId`) is checked when a session opens and at every turn start. `lifecycle.deleteAfter` deletes an Environment unused that long; TTL and `onExpiry` are gone. Kind `none` holds secrets for MCP-only agents. `/v1/tenant/vaults`, user and installation vaults, the Tenant's default sandbox, inline sandboxes and `{ session }` sharing are removed. The Tenant setting is `environments.config` (`limits.environments`). Identity files grant `environments:` (an old `sandboxes:` key, or `sandboxes:write`, stops the Runtime with a message naming the new key). Migration `0020_environments`.
19
+ - `@nylorun/harness`: the Harness API claims `environment.state` and `environment.exec`, and its workspace fields name `environmentId`.
20
+ - `@nylorun/agents`: `client.environments` (`put`, `get`, `list`, `page`, `stop`, `reset`, `delete`, `events`, `secrets.put|list|get|delete`, `forSession`) replaces `client.sandboxes`; `createSession({ environment })`; `sessions.page({ environmentId })` filters by Environment. The AG-UI handler's session options take `environment`.
21
+ - `@nylorun/admin`: `admin.coverage({ agentId, environmentId })` replaces `admin.vaults`; `admin.mcp.preview({ url, environmentId })`.
22
+ - `@nylorun/studio`: Environments replace Sandboxes, with a create form, stop, reset, delete and their secrets; the Credentials forms write an Environment's secrets; New session picks an Environment and shows its coverage.
23
+ - `nylorun`: `nylorun sandbox ls|rm` list and delete Environments; `nylorun mcp inspect --environment <id>` replaces `--vault`; `nylo reset --environments` replaces `--sandboxes`; `nylo doctor sandbox` reports the Environment limits.
24
+
25
+ - 3013501: **Tool calls are delivered at least once, and every one is on the record (R5 step 1).** A gateway restart used to park the sessions it interrupted as `uncertain` until someone cancelled them, a harness could call tools with no record, and nothing listed a session's tool calls. Now every tool call ends in the log with its outcome, a crash sends it again with the same key, and the gateway runs only calls the effect journal recorded.
26
+
27
+ - **Every outcome in the log.** Each tool call ends in one `tool.completed` with `status` (`completed`: the tool answered; `failed`: the Runtime ended it, `tool.disabled`, `tool.unavailable` or `tool.interrupted`; `cancelled`), `kind` (`mcp`, `http`, `sandbox`, `platform`), `server`, `attempts` and `durationMs`. Refused and cancelled calls are in the log for the first time.
28
+ - **`GET /v1/sessions/{id}/calls/tool`** pages a session's tool calls from the effect journal, in the order they were first sent: tool, input, status, sends, error and timing. Same credentials and paging as `/calls/model`; `@nylorun/agents` adds `session.toolCalls()`. Host feature `tool-calls`.
29
+ - **At least once, with the same key.** An MCP or HTTP tool call that a crash, a shutdown or a lost gateway hop interrupted is sent again with its effect id as the key, up to 3 sends; then it fails to the model as `tool.interrupted` ("may have run") and the session continues. A gateway that restarts mid call runs the call again under its key instead of answering `uncertain`. HTTP tools get the key as `Idempotency-Key`, as before; MCP servers now get it in `_meta["nylorun/idempotency-key"]` on `tools/call`. A tool that honors the key acts once; `attempts` above 1 marks where one may have acted twice.
30
+ - **Sandbox commands are never run twice.** A sandbox or core-served call a crash interrupted has no key to dedupe on, so it fails to the model as `tool.interrupted` at once, and the model decides whether to run it again.
31
+ - **No tool call parks a session.** The `uncertain` state for tool calls, its cancel-only exit and R2b's read-only/idempotent `mcp.lost` exception are gone: a lost MCP answer is `mcp.lost` to the model for every tool. Model calls keep their own rules.
32
+ - **The gateway runs only journaled calls.** Under a run token, `/nylorun/v1/tool-calls` and `/nylorun/v1/http-calls` need the call's effect id as `Idempotency-Key` (`400 key_required`), and the effect journal must hold it running, for the run's turn, the same tool and the same input (`409 effect_not_journaled`). A harness that skips the journal gets nothing run.
33
+ - `@nylorun/harness`: `HarnessExecutors.resends(effect, error, run)` says a gate hop lost a call's answer; the harness then asks core again with the same intent and sends the call again.
34
+ - **Approval** stays in the manifest and our harness still asks for it. Only the Runtime's own harness asks: one built from the Harness API can skip it until tool policy moves to the gateway (R5 step 2).
35
+
36
+ - 6061a3a: **Tool approvals are the Tenant's tool policy (R5 step 2).** An agent's manifest used to say which tool calls wait for a person, and only the Runtime's own harness asked; a harness built from the Harness API could skip it. Now the operator sets one tool policy for every agent and every harness, and the Runtime enforces it before anything is sent.
37
+
38
+ - **`GET`/`PUT /v1/tenant/tool-policy`** (the Management API; `admin.toolPolicy.get()`/`.put({ rules })`): rules `{ effect: "allow" | "ask" | "deny", tool?, server?, kind?, agent?, when?, reason? }`. A rule matches a call when every selector it sets matches (`tool` and `server` take `*` globs; `when` tests input fields by dotted path with `equals`, `in`, `gt`, `gte`, `lt`, `lte` or `matches`). The strictest matching rule decides; a call no rule matches runs.
39
+ - **Enforced for every harness.** Core decides an MCP or HTTP tool call as it journals it. `deny` fails it to the model as `tool.denied`, unsent. `ask` holds the call (`held` at `/calls/tool`) and pauses the turn with one `approval` interaction per held call (`turn.paused`, with the tool, its input and the reason). `approve` sends that exact call, or fails it as `tool.denied`. The Tool Gate runs only calls the journal holds running, so a held or refused call cannot run. Nothing holds a process while it waits: an approval survives a restart. A flow's HTTP stages are decided the same way.
40
+ - **`approval` leaves the manifest.** `PUT /v1/agents/:id` and `saveAgent()` refuse a definition that sets `approval` on an HTTP tool, an MCP server or an MCP tool's settings; `http({ approval })` and `.mcp({ … approval })` throw; a turn manifest can no longer add one. The engine never asks for an HTTP or MCP tool's approval itself. A session pinned to a manifest saved with `approval: "always"` keeps it, as an ask.
41
+ - `@nylorun/studio`: a paused session shows an approval card for each held call, with its tool, why it is held and its input, and **Approve** / **Deny**.
42
+ - Migration: move each `approval: "always"` to an `ask` rule (see MIGRATION.md).
43
+
44
+ - 7800358: **Each turn is one OpenTelemetry trace, projected from the session log (R7).** An operator could not see what a turn did or where its time went without grepping container logs. Now every turn is a trace with a span for each model call, tool call and egress tunnel, exported over OTLP to any collector, readable per turn from the Runtime, and joined to the trace of the app that started it. No protocol change.
45
+
46
+ - **Traces from the log.** A turn's end is its root span, `invoke_agent <agent>`; each model call is a `chat <model>` span, each tool call and flow HTTP stage an `execute_tool <tool>` span, each egress-gate tunnel a `CONNECT <host>` span, and each pause and answer a marker. A model or gate tool span is the gate's own start and end; usage and cost are the gate's (`nylorun.usage.mismatch` when the harness reported other numbers). Attribute names follow the OTel GenAI conventions at one pinned commit, recorded as `nylorun.semconv.genai` on the resource. Ids are derived from the log under a key the Tenant keeps (setting `trace.key`), so every export and read of a turn gives the same ids.
47
+ - **OTLP export, off until you set an endpoint.** With `OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) the Runtime posts OTLP/HTTP JSON from a replication slot of its own, `nylorun_trace_export`, in batches of up to 512 spans or every 2 s; `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` work as usual. No turn waits for it. A collector that is down costs only its spans: after three retries a batch is dropped and logged as `trace_export_dropped`, and the slot moves on, so Postgres keeps no WAL for it. `nylorun start` passes these variables to the runtime container.
48
+ - **References by default.** Spans point at log events (`nylorun.event.cursor`) and carry no content unless you opt in per kind with `NYLORUN_TRACE_CONTENT` (`user`, `outputs`, `tool-results`, `errors`), or all of them with `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`, each cut at 8 KiB. A span never carries a credential value, a prompt, a person's id, an identity header or an idempotency key.
49
+ - **`GET /v1/sessions/{id}/turns/{turnId}/trace`** answers a turn's spans as OTLP JSON, the same spans the exporter posts: look at a turn without a collector, or post it to one to backfill. Application keys, as the `/calls` reads. Host feature `traces`; `@nylorun/agents` adds `session.trace(turnId)`, and `@nylorun/core/contracts` `TurnTraceSchema`.
50
+ - **Trace context in and out.** A `traceparent` (and `tracestate`) on `POST /v1/sessions/{id}/commands`, an AG-UI run or an A2A message puts the turn in the caller's trace; `command.message` records it as `trace`. The turn hands its context on: to the harness in `TurnStart.trace`, to HTTP tools as `traceparent` beside `Idempotency-Key`, to MCP servers as `traceparent` on the call's requests, and to sandbox commands as `TRACEPARENT`.
51
+ - **Harness API** (additive): the `effect.intent` answer `execute` may carry the tool call's `traceparent`. `@nylorun/harness`: `HarnessExecutors.tool` takes it as a fourth argument (`ToolCallContext`).
52
+
53
+ ### Patch Changes
54
+
55
+ - b6214d9: Add opt-in artifact metadata pagination to `GET /v1/artifacts?limit=…`, shared
56
+ `ArtifactListItem`/`ArtifactPage` contracts and SDK `artifacts.page()` (default 50,
57
+ maximum 200). Hosts advertise `artifact-reads`. Pages order by creation time and
58
+ ID descending, filter by session, kind and labels, bind cursors to the Tenant and
59
+ filters, and reapply ownership and agent grants on each read. Legacy unpaged
60
+ responses remain unchanged. A Drizzle migration adds the creation-order index;
61
+ pages reuse the existing bounded read-only pool. Studio and every other Runtime
62
+ client can consume this public API without database access.
63
+ - a0cbca1: **Clearer wording when a definition runs your code.** The refusal for a code tool or a flow's tool stage, the `hooks were removed` error and `nylo endpoints` now say that the Runtime runs agents from their manifests alone, instead of that it runs no code of yours during a session. Only the message text changed; anything matching the old wording (for example `runs no code of yours during a session`) needs the new text.
64
+ - Pin core to the tested release.
65
+ - Updated dependencies [17a97e9]
66
+ - Updated dependencies [b6214d9]
67
+ - Updated dependencies [e917bd6]
68
+ - Updated dependencies [5165451]
69
+ - Updated dependencies [35e29b7]
70
+ - Updated dependencies [e917bd6]
71
+ - Updated dependencies [a0cbca1]
72
+ - Updated dependencies [3013501]
73
+ - Updated dependencies [6061a3a]
74
+ - Updated dependencies [7d496e4]
75
+ - Updated dependencies [81c783e]
76
+ - Updated dependencies [7800358]
77
+ - @nylorun/core@0.20.0-beta
78
+
3
79
  ## 0.19.1-beta
4
80
 
5
81
  ### Patch Changes
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @nylorun/agents
2
2
 
3
3
  Runtime API client package: definition authoring and a session client. The Tenant's
4
- settings, models, vaults, signing keys and application keys are the Management
4
+ settings, models, credential coverage, signing keys and application keys are the Management
5
5
  API's, through [`@nylorun/admin`](../admin/README.md). Depends only on `@nylorun/core` among Nylorun
6
6
  packages. A developer application's production tree should contain only this
7
7
  package and `@nylorun/core` from Nylorun. Vocabulary:
8
- [runtime/src/CONTEXT.md](../runtime/src/CONTEXT.md).
8
+ [runtime/server/src/CONTEXT.md](../../runtime/server/src/CONTEXT.md).
9
9
 
10
10
  ## Application entry: save your agents
11
11
 
@@ -14,12 +14,12 @@ package and `@nylorun/core` from Nylorun. Vocabulary:
14
14
  import { createClient } from "@nylorun/agents";
15
15
  import { agents } from "../agents/index.js";
16
16
 
17
- const client = createClient();
17
+ const client = await createClient();
18
18
  for (const agent of agents) await client.saveAgent(agent);
19
19
  ```
20
20
 
21
- The Runtime runs each agent from its manifest and the files saved with it (skills), and
22
- never calls your code during a session. Your services are reached as tools: [HTTP
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
23
  tools](#http-tools) and remote MCP servers. `saveAgent` refuses, before sending anything, an
24
24
  agent with a tool that would run your code (`tool({ run })`) or a flow agent with a tool
25
25
  stage: make it an `http()` tool or serve it from a remote MCP server. `tool({ run })` still
@@ -29,7 +29,7 @@ runs in the local engine (`@nylorun/harness/run`). `implementationVersion` defau
29
29
  `createClient()` finds the Runtime through the two `NYLORUN_*` variables or the Project link
30
30
  that `npx nylorun start` writes; with neither, it fails with `connection_missing` and names
31
31
  those steps. Action endpoints (`createActionHandler`), and before them `connectAgents` and
32
- executors, were removed; see [MIGRATION.md](../MIGRATION.md#action-endpoints-are-removed).
32
+ executors, were removed; see [MIGRATION.md](../../guides/MIGRATION.md#action-endpoints-are-removed).
33
33
 
34
34
  ## HTTP tools
35
35
 
@@ -47,15 +47,14 @@ const refundOrder = http({
47
47
  output: z.object({ refundId: z.string() }), // optional
48
48
  url: "https://billing.example.com/refunds",
49
49
  method: "POST", // default; or PUT, PATCH
50
- credential: "billing", // optional: a vault credential bound to this URL
50
+ credential: "billing", // optional: the session's Environment holds a credential for this URL
51
51
  timeoutMs: 20_000, // default 60000, at most 300000
52
- approval: "always", // optional: each call waits for session.approve()
53
52
  });
54
53
 
55
54
  export const support = Agent({ id: "support" }).tools(refundOrder);
56
55
  ```
57
56
 
58
- The tool goes into the manifest as `http` (and `approval`), and the Runtime's
57
+ The tool goes into the manifest as `http`, and the Runtime's
59
58
  Tool Gate sends the model's input as the JSON body, under the Host's address
60
59
  policy (`NYLORUN_ENDPOINT_*`), with `content-type: application/json`,
61
60
  `Nylorun-Session-Id`, `Nylorun-Turn-Id`, `Nylorun-Agent-Id` and an
@@ -63,21 +62,73 @@ policy (`NYLORUN_ENDPOINT_*`), with `content-type: application/json`,
63
62
  restart, so your service can drop duplicates. A `2xx` JSON answer is the output
64
63
  (text when it is not JSON and the tool has no `output`), checked against
65
64
  `output`. Any other status (with the start of its body), a timeout, a refused
66
- address or a mismatched output is a tool error the model sees. A call whose
67
- answer was lost with the gateway is `uncertain` and never sent again.
68
-
69
- `credential` works as for a remote MCP server: the session's attached vaults
70
- must hold a credential bound to the tool's exact URL (a `credentialSelections`
71
- entry whose `serverName` is the `credential` picks one when several are), else
72
- the call fails before it is sent (`http.credential`). A credential with `via`
65
+ address or a mismatched output is a tool error the model sees.
66
+
67
+ **How tool calls are delivered.** Each MCP or HTTP tool call the model decides
68
+ is sent at least once, and every send carries the same key: `Idempotency-Key`
69
+ for an HTTP tool, `_meta["nylorun/idempotency-key"]` on an MCP `tools/call`. A
70
+ crash, a restart or a lost connection to the gateway sends the call again with
71
+ that key, up to 3 sends; after that the model gets `tool.interrupted` ("may
72
+ have run") and decides. A tool that honors the key runs once; one that ignores
73
+ it may act twice, and `attempts` above 1 in `tool.completed` and in
74
+ `session.toolCalls()` marks where. A sandbox command (`bash`, `write` and the
75
+ rest) is never run twice by the Runtime: a crash fails it to the model as
76
+ `tool.interrupted`. When the model decides the same call again, in a later
77
+ turn say, that is a new call with a new key: put a business key (an order or
78
+ charge id) in the tool's input schema to refuse it too.
79
+
80
+ `credential` works as for a remote MCP server: the vault of the session's
81
+ Environment must hold a credential bound to the tool's exact URL, else the call
82
+ fails before it is sent (`http.credential`). A credential with `via`
73
83
  sends the call there instead, such as a gateway, with the session owner in its
74
- identity header, and a `401` is `credential_rejected` to the model, not retried. `approval: "always"` pauses the turn for `session.approve()`; a
75
- denied call never runs and the model sees the denial. A remote MCP server takes
76
- `approval: "always"` too, for every one of its tools:
77
- `.mcp({ shop: { type: "streamable-http", url, approval: "always" } })`.
78
- An HTTP tool is also a flow stage (see [Flow agents](#flow-agents)), and runs
84
+ identity header, and a `401` is `credential_rejected` to the model, not retried.
85
+ Whether a call waits for a person is not the tool's to say: it is the Tenant's
86
+ [tool policy](#tool-policy). An HTTP tool is also a flow stage (see [Flow agents](#flow-agents)), and runs
79
87
  only on the Runtime (a local `run()` reports `http.runtime-only`).
80
88
 
89
+ ## Tool policy
90
+
91
+ Which MCP and HTTP tool calls run, wait for a person, or are refused is the
92
+ Tenant's tool policy, set by the operator on the Management API with a
93
+ management key (`PUT /v1/tenant/tool-policy`, `admin.toolPolicy.put()` in
94
+ `@nylorun/admin`). The Runtime applies it to every call of every harness as it
95
+ journals the call, before anything is sent, and its Tool Gate runs only a call
96
+ the journal holds running, so a harness cannot skip it.
97
+
98
+ ```ts
99
+ import { createAdmin } from "@nylorun/admin";
100
+
101
+ await createAdmin().toolPolicy.put({
102
+ rules: [
103
+ { effect: "ask", tool: "refund_order", reason: "Refunds need a person" },
104
+ { effect: "deny", tool: "refund_order", when: { amount: { gt: 10_000 } } },
105
+ { effect: "ask", server: "github", tool: "github__delete_*" },
106
+ ],
107
+ });
108
+ ```
109
+
110
+ A rule matches a call when every selector it sets matches: `tool` (the name the
111
+ model calls it by; `*` matches any run of characters), `server` (an MCP
112
+ server's name), `kind` (`mcp` or `http`), `agent` (the agent that calls it) and
113
+ `when` (fields of the call's input by dotted path, each tested with `equals`,
114
+ `in`, `gt`, `gte`, `lt`, `lte` or `matches`). The strictest matching rule
115
+ decides, and a call no rule matches runs:
116
+
117
+ - `deny`: the call is never sent; the model gets `tool.denied` with the rule's
118
+ `reason`.
119
+ - `ask`: the turn pauses (`turn.paused`, with the tool, its input and the
120
+ reason). `session.approve(interactionId, true)` sends that exact call;
121
+ `false` fails it to the model as `tool.denied`, unsent. Nothing holds a
122
+ process while it waits, so an approval can wait a day, or a Runtime restart.
123
+ Studio shows an approval card; AG-UI ends the run with a `tool_approval`
124
+ interrupt.
125
+ - `allow`: it runs.
126
+
127
+ Sandbox tools never cross the gate: their network side is egress policy. A
128
+ manifest no longer sets `approval` (the Runtime and `saveAgent` refuse it); a
129
+ session pinned to a manifest saved with `approval: "always"` keeps it, as an
130
+ ask, until it ends.
131
+
81
132
  ## Connection resolution
82
133
 
83
134
  `resolveConnection` / `createClient()`:
@@ -128,11 +179,11 @@ const history = await session.history();
128
179
  ```
129
180
 
130
181
  ```sh
131
- eval "$(npx @nylorun/cli env)"
182
+ eval "$(npx -p nylorun nylo env)"
132
183
  # → NYLORUN_RUNTIME_URL, NYLORUN_SERVER_KEY
133
184
  ```
134
185
 
135
- Every request sets `Nylorun-Protocol` (8) and no `Nylorun-Tenant`: the Runtime
186
+ Every request sets `Nylorun-Protocol` (11) and no `Nylorun-Tenant`: the Runtime
136
187
  serves one Tenant. Before the first authenticated
137
188
  request, `Transport` fetches `/health` once, checks protocol compatibility, and
138
189
  throws `IncompatibleRuntimeError` / `incompatible_host` when the Host range or
@@ -168,42 +219,56 @@ const assistant = Agent({ id: "assistant", name: "Assistant" })
168
219
 
169
220
  Each key names a server; its `name` defaults to the key and, when given, must equal it. Repeated `.mcp()` calls add servers to the same capability. Nylorun accepts remote servers only: `streamable-http` and `sse` ([Agent Plugins MCP servers](https://agent-plugins.org/plugin-authors/mcp-servers)); a `stdio` server is refused, so run it behind an HTTP transport and declare its URL. Attach an Agent Plugin package with `.plugin(path)`: its skills and remote MCP servers join the agent, and a stdio server in its `mcp.json` throws.
170
221
 
171
- 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, approval: "always" } }` is an allowlist with one approval. 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. Either setting makes the manifest v6 (see MIGRATION.md).
222
+ 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. Either setting makes the manifest v6 (see MIGRATION.md).
172
223
 
173
- A manifest never carries a server's credential, and its `headers` hold no secret. The operator gives the server's URL a vault credential (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), and the session's attached vaults supply it: [DEPLOYMENT.md](../DEPLOYMENT.md#mcp-servers-and-http-tools), which also shows `nylorun mcp inspect` for listing a server's tools first. 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`.
224
+ 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 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`.
174
225
 
175
- Give a session a sandbox when you open it. The agent declares nothing, so the same agent runs with or without one, in any Tenant:
226
+ 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). The agent declares nothing, so the same agent runs with or without one, in any Tenant. Whether an Environment serves one session, one person or a project is your app's choice:
176
227
 
177
228
  ```ts
178
- const session = await client.createSession({
179
- agentId: "analyst",
180
- ownerUserId,
181
- sandbox: {
182
- network: { allow: ["pypi.org", "files.pythonhosted.org"] }, // no egress unless you list hosts
183
- resources: { cpus: 2, memory: "2GiB" },
184
- },
229
+ // One per person or project: get-or-create by id (or change its recipe), write its secrets, open sessions on it.
230
+ await client.environments.put("users/alice/web", {
231
+ kind: "virtual", // the default; "pod" for a container with its own volume, "none" for a vault without compute
232
+ network: { allowedHosts: ["pypi.org", "files.pythonhosted.org"] }, // no egress unless you list hosts
233
+ ownerUserId: "alice", // the owner guard: only alice's sessions may name it
234
+ labels: { team: "data" },
235
+ lifecycle: { deleteAfter: "30d" }, // deleted once unused this long
185
236
  });
237
+ await client.environments.secrets.put("users/alice/web", "GH_TOKEN", {
238
+ type: "environment_secret",
239
+ value: process.env.ALICE_GITHUB_TOKEN!,
240
+ allowedHosts: ["api.github.com"],
241
+ });
242
+ const session = await client.createSession({ agentId: "analyst", ownerUserId: "alice", environment: "users/alice/web" });
243
+
244
+ // One per session: created with the session (id `sessions/<session id>`), deleted by release().
245
+ const { session: scratch, release } = await client.environments.forSession({
246
+ session: { agentId: "analyst", ownerUserId: "alice" },
247
+ recipe: { network: { allowedHosts: ["pypi.org"] } },
248
+ });
249
+ await release();
186
250
  ```
187
251
 
188
- The model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a persistent `/workspace`. These tools run in the Runtime, not in your process. The Runtime decides where the sandbox runs: today an emulated shell in the Runtime process, which is not a VM boundary and takes no `image`.
252
+ 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 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.
253
+
254
+ `kind` and `storage` are fixed once the Environment exists (a different one is a `409`); `labels` replace, and any other change bumps its `version`: network changes apply within seconds, a pod gets a new `image` or `resources` when it next starts from stopped, and a virtual one's change reaches the sessions opened after it. The Runtime checks a recipe against the Tenant's limits (`GET`/`PUT /v1/tenant/sandbox`: a network ceiling, a resource maximum, the idle timeout) 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.
255
+
256
+ 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)).
189
257
 
190
- `sandbox` takes `false` for none, `{ id }` to attach a sandbox resource (below), or an inline sandbox as above; omit it for the Tenant's default. `{ session }`, sharing another session's sandbox, still works and is deprecated. The Runtime checks it against the Tenant's limits (`GET`/`PUT /v1/tenant/sandbox`: a network ceiling, a resource maximum, the idle timeout) and answers `400` with every problem it finds. A caller acting for a user (`app.as(...)`) can't define one inline; it gets the Tenant's default or `false`. Private networks, loopback, the host and cloud metadata endpoints are always blocked.
258
+ Sessions on one Environment share its `/workspace`, and one turn runs in it at a time: a second session's turn is refused with `409 environment_busy` until the first ends. 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)`, `stop(id)`, `reset(id)` and `delete(id)` manage them, and `npx nylorun sandbox ls | rm` does the same on a local Tenant. 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).
191
259
 
192
- A sandbox can also be a resource with its own id, which outlives the sessions attached to it (Host feature `sandboxes`). Whether it serves one session, one person or a project is your choice:
260
+ 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)):
193
261
 
194
262
  ```ts
195
- // One per session: created with the session, deleted by release().
196
- const { session, release } = await client.sandboxes.forSession({
197
- session: { agentId: "analyst", ownerUserId },
198
- spec: { network: { allow: ["pypi.org"] } },
263
+ await client.environments.put("projects/acme", {
264
+ kind: "pod",
265
+ files: [{ artifactId: repoTarball.artifactId, version: 1, path: "/workspace/app" }],
266
+ setup: [{ command: "npm ci", cwd: "/workspace/app", timeout: "15m" }],
199
267
  });
200
- // One per person or project: get-or-create by id, then attach sessions to it.
201
- await client.sandboxes.ensure("team-a/proj-42", { labels: { project: "acme" } });
202
- await client.createSession({ agentId: "analyst", ownerUserId, sandbox: { id: "team-a/proj-42" } });
268
+ const { version } = await client.environments.snapshot("projects/acme");
269
+ await client.environments.put("projects/acme-try", { kind: "pod", from: { environment: "projects/acme", version } });
203
270
  ```
204
271
 
205
- Sessions attached to one sandbox share its `/workspace`, and one turn runs in it at a time: a second session's turn is refused with `409 sandbox_busy` until the first ends. Deleting a session only detaches it. A spec is fixed once the sandbox exists; `labels` can change. `client.sandboxes.list({ labels })`, `get(id)` and `delete(id)` manage them, and `npx nylorun sandbox ls | rm` does the same on a local Tenant. A trusted issuer's token reaches only the sandboxes its issuer grants (the identity file's `sandboxes` templates, exact ids or prefixes ending in `/*`), checked at every turn start; creating and deleting through one needs `sandboxes:write`. The Tenant holds at most `limits.sandboxes` of them (`PUT /v1/tenant/sandbox`, default 100).
206
-
207
272
  Add an agent with `.subagents()` to let the model delegate to it:
208
273
 
209
274
  ```ts
@@ -243,9 +308,9 @@ session like any agent: put it in `export const agents`, `saveAgent(flowAgent)`,
243
308
  (`kind: "workflow"`, `workflowSchemaVersion: 3`) that embeds the agents it runs, so one
244
309
  document and one manifest hash cover the whole flow; its agents are not listed on their own.
245
310
 
246
- A flow runs no code of yours: the first stage gets the flow's input, and every later stage
247
- gets the previous stage's output. So each agent returns, through its `.output()` schema,
248
- what the next stage needs.
311
+ A flow passes data between its stages itself: the first stage gets the flow's input, and
312
+ every later stage gets the previous stage's output. So each agent returns, through its
313
+ `.output()` schema, what the next stage needs.
249
314
 
250
315
  ```ts
251
316
  import { Agent, VerdictSchema, http } from "@nylorun/agents";
@@ -284,28 +349,9 @@ export const shipFeature = Agent({ id: "ship-feature" })
284
349
  export const agents = [shipFeature];
285
350
  ```
286
351
 
287
- A flow stage cannot take `approval` yet (`flow.approval-unsupported`). To have a person
288
- approve opening the PR, give the HTTP tool to an agent as the last stage instead: that
289
- agent's turn pauses until the call is approved, and the flow session's `pending()` lists
290
- the wait:
291
-
292
- ```ts
293
- const prOpener = Agent({ id: "pr-opener" })
294
- .instructions("Open one pull request with open_pr from the summaries you are given.")
295
- .tools(
296
- http({
297
- name: "open_pr",
298
- description: "Open a pull request with the given summaries.",
299
- input: z.object({ summaries: z.array(z.string()) }),
300
- url: "https://ci.example.com/pull-requests",
301
- credential: "github",
302
- approval: "always",
303
- }),
304
- );
305
-
306
- // …
307
- .pipe(prWriter, prOpener);
308
- ```
352
+ The Tenant's [tool policy](#tool-policy) decides a flow's HTTP stages as it decides an
353
+ agent's HTTP tool calls: an `ask` rule on `open_pr` pauses the flow's turn until a person
354
+ approves the call, and the flow session's `pending()` lists the wait.
309
355
 
310
356
  | Stage | Role |
311
357
  | --- | --- |
@@ -329,12 +375,11 @@ body. Its input is the previous output, which must match the tool's `input` (an
329
375
  object); a mismatch the build can see, such as an agent with no `.output()` before it,
330
376
  is refused (`flow.input-mismatch`), and any other fails the stage when it runs
331
377
  (`tool.invalid-input`). The Runtime makes the request as for an agent's HTTP tool, with
332
- `Nylorun-Agent-Id` set to the flow agent's id and `credential` taken from the flow
333
- session's vaults; its answer is the next stage's input. A failure status, a timeout or
378
+ `Nylorun-Agent-Id` set to the flow agent's id and `credential` taken from the
379
+ vault of the flow session's Environment; its answer is the next stage's input. A failure status, a timeout or
334
380
  an answer that does not match `output` fails the stage with its code (`http.status`,
335
381
  `http.timeout`, `tool.invalid-output`, …). A Loop body that starts with an HTTP stage
336
- is retried with the Loop's input, not the feedback. `approval: "always"` is not
337
- supported on a flow stage yet (`flow.approval-unsupported`).
382
+ is retried with the Loop's input, not the feedback.
338
383
 
339
384
  A Loop may be judged by your service instead of an agent: `http({ url, method?,
340
385
  credential?, timeoutMs? })`, with no name or input, is an HTTP verifier. The Runtime
@@ -374,21 +419,21 @@ nested flow agent's id in front of its own agents'. Control stages add nothing,
374
419
  wrapping a step in `.loop()` or moving it between cases keeps its session. An agent
375
420
  may appear once per flow; use it again under a new id with `writer.withId("final-writer")`
376
421
  (`flow.duplicate-leaf`). A code tool stage (`.pipe(tool({ run }))`) runs only in the local
377
- engine: the Runtime runs no code of yours, so `saveAgent` refuses a flow agent with one. Use
378
- an HTTP stage instead.
422
+ engine: the Runtime runs flows from their manifests alone, so `saveAgent` refuses a flow
423
+ agent with one. Use an HTTP stage instead.
379
424
 
380
- The agents in a flow share one sandbox: open the flow's session with it,
381
- `createSession({ …, sandbox: { … } })`, and every agent in the flow uses it. Share it
382
- with other sessions by opening them all on one sandbox resource
383
- (`sandbox: { id }`, see `client.sandboxes`), and call
425
+ The agents in a flow share one Environment: open the flow's session on it,
426
+ `createSession({ …, environment: id })`, and every linked session in the flow
427
+ inherits it; turns within one flow do not block each other. Other sessions share
428
+ it by naming the same id (see `client.environments`). Call the sandbox
384
429
  built-ins via `session.sandbox` (application).
385
430
  Observe with `session.observe({ follow: true })` to merge linked agent streams;
386
431
  `pending()` lists waits across the tree. Studio renders the manifest tree and
387
432
  live node status. Source examples:
388
433
  `examples/agents/{chain,switch,parallel,map,loop,ship-feature}/` (outside the
389
434
  default release registry). Host contract:
390
- [HOST_CONTRACT.md](../harness/HOST_CONTRACT.md). Vocabulary (turn loop vs
391
- workflow Loop): [harness/src/CONTEXT.md](../harness/src/CONTEXT.md).
435
+ [HOST_CONTRACT.md](../../runtime/harness/HOST_CONTRACT.md). Vocabulary (turn loop vs
436
+ workflow Loop): [runtime/harness/src/CONTEXT.md](../../runtime/harness/src/CONTEXT.md).
392
437
 
393
438
  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.
394
439
 
@@ -446,8 +491,8 @@ createServer(toNodeListener(agui)).listen(3000);
446
491
 
447
492
  `run`, `history`, `reattach` and `cancel` are also on the handler for your own
448
493
  routing. The default `client` is `createClient()`; pass `client` to use another
449
- one, and `session(subject, agentId)` to add the person's `vaultIds`,
450
- `credentialSelections` or `info`. They apply when the thread's session is
494
+ one, and `session(subject, agentId)` to add the person's `environment` (their
495
+ Environment's id: its sandbox and its vault's credentials) or `info`. They apply when the thread's session is
451
496
  created, on its first run; later runs keep them. Whatever the browser sends in
452
497
  `forwardedProps.nylorun` is replaced.
453
498
 
@@ -471,7 +516,7 @@ created, on its first run; later runs keep them. Whatever the browser sends in
471
516
  package itself: the Runtime does the protocol work.
472
517
 
473
518
  A complete web backend with a test is in
474
- [examples/src/ag-ui](../examples/README.md#an-agent-in-your-web-app-ag-ui).
519
+ [examples/src/ag-ui](../../examples/README.md#an-agent-in-your-web-app-ag-ui).
475
520
 
476
521
  Limitations: assistant text arrives once per model step (no token streaming);
477
522
  no reasoning, state, activity or subagent events, and an agent used as a tool
@@ -537,7 +582,7 @@ What the Runtime answers (Host feature `a2a-endpoint`):
537
582
  (`UnsupportedOperationError`; the card says `streaming: false`), push
538
583
  notifications, the extended card, and approvals. An agent that pauses for an
539
584
  approval shows `INPUT_REQUIRED`, refuses a reply, and can be canceled, so do
540
- not publish agents with approval-gated tools yet.
585
+ not publish agents whose tools the Tenant's tool policy asks about yet.
541
586
  - The handler calls the Runtime as the partner's subject with the scope
542
587
  `sessions:own` only, so one partner never reaches another's tasks. It sends
543
588
  no header the partner chose except `A2A-Version` and `A2A-Extensions`. Without
@@ -571,6 +616,29 @@ refuse a larger upload with `413 limit_exceeded`, and nothing is stored. Acting
571
616
  a person (`as()`, or a trusted issuer's token), a client reaches only the artifacts of that
572
617
  person's sessions, and an upload names one of them.
573
618
 
619
+ Hosts advertising `artifact-reads` also support bounded metadata pages, newest
620
+ first. `artifacts.page()` defaults to 50 rows (1–200), checks feature discovery,
621
+ and returns `{ artifacts, nextCursor }`. Pass the same filters on every page:
622
+
623
+ ```ts
624
+ const filters = { kind: "file" as const, labels: { team: "design" } };
625
+ let cursor: string | undefined;
626
+ do {
627
+ const page = await client.artifacts.page({ ...filters, cursor });
628
+ for (const artifact of page.artifacts) console.log(artifact.name, artifact.latestVersion);
629
+ cursor = page.nextCursor ?? undefined;
630
+ } while (cursor);
631
+ ```
632
+
633
+ Omit `sessionId` for all reachable artifacts, including Tenant-owned artifacts
634
+ when using an application key as itself. With `sessionId`, only that session's
635
+ artifacts are included; acting for a person also applies ownership and agent
636
+ grants on every page. Every label must match exactly. Pages omit version histories
637
+ and bytes: use `get()` and the existing version/download methods for those.
638
+ Cursors bind to the Tenant and filters. Pages are independent reads; refresh from
639
+ the beginning for newer artifacts. The existing unbounded `list()` keeps its
640
+ oldest-first behavior and does not require `artifact-reads`.
641
+
574
642
  ### Outputs: folder artifacts
575
643
 
576
644
  At the end of each turn, the Runtime exports the files the agent wrote into
@@ -601,7 +669,7 @@ keeps its application key to itself and names the person on each call:
601
669
  ```ts
602
670
  import { createClient } from "@nylorun/agents";
603
671
 
604
- const app = createClient(); // the application key, on the server only
672
+ const app = await createClient(); // the application key, on the server only
605
673
 
606
674
  // Per request, after your own sign-in:
607
675
  const person = app.as(`app:${user.id}`, { scopes: ["sessions:own"] });
@@ -621,14 +689,15 @@ compatibility check, so calling `as()` per request is cheap.
621
689
  | `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel; and their artifacts |
622
690
  | `agents:read` | Listing the Tenant's agents |
623
691
  | `agents:write` | Saving agents; listing agents |
692
+ | `environments:write` | Creating, changing, stopping, resetting and deleting Environments, and writing their secrets (reading them takes `sessions:own` or this) |
624
693
 
625
694
  No scope reaches the sandbox tool routes; call them without `as()`. `tenant:settings` is retired (protocol 8): it grants
626
- nothing, and no subject reaches the Management API (`/v1/tenant/*`). Vaults are
627
- the installation's, created through the Management API with a management key
628
- (`admin.vaults.create({ scope: "installation", … })` in
629
- [`@nylorun/admin`](../admin/README.md)); a session attaches them with
630
- `vaultIds`, and a person's own keys go in their user vault
631
- ([DEPLOYMENT.md](../DEPLOYMENT.md#credentials)). A subject is 1–200 visible ASCII
695
+ nothing, and no subject reaches the Management API (`/v1/tenant/*`).
696
+ `sandboxes:write` (protocol 10) is refused with a message naming
697
+ `environments:write`. Credentials live in Environments: the server writes a
698
+ person's own keys into their Environment (with `ownerUserId` set to them) and
699
+ names it when it opens their sessions
700
+ ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#credentials)). A subject is 1–200 visible ASCII
632
701
  characters (spaces only inside) and `host` is reserved. Your server must drop
633
702
  any `Nylorun-*` header its own clients send, and only an application key can act
634
703
  for a subject.
@@ -644,8 +713,8 @@ and revoking them is management: `admin.signingKeys.list()`, `.rotate()`
644
713
 
645
714
  A web page or an app calls the Runtime directly with the JWT your identity
646
715
  provider gave the person: list the provider in the Runtime's identity file
647
- ([Trusted issuers](../DEPLOYMENT.md#trusted-issuers)), and the Runtime takes the
648
- token's subject, scopes, agents and sandbox grants from it. Nylorun mints no
716
+ ([Trusted issuers](../../guides/DEPLOYMENT.md#trusted-issuers)), and the Runtime takes the
717
+ token's subject, scopes, agents and Environment grants from it. Nylorun mints no
649
718
  token and ships no browser client: use your provider's SDK for sign-in and send
650
719
  the token as `Authorization: Bearer <token>` with `Nylorun-Protocol`, through a
651
720
  reverse proxy that answers CORS (the Runtime sends no CORS headers). An expired
@@ -653,7 +722,7 @@ token answers `401` with `code: "token_expired"`; `GET /v1/me` shows what a toke
653
722
  renders to. For a chat UI, give `@ag-ui/client`'s `HttpAgent` a `fetch` that adds
654
723
  those headers.
655
724
 
656
- The SDK depends only on core within the Nylorun packages; installing it does not install harness or any AG-UI or A2A package. Use `/define`, `/client`, `/ag-ui` or `/a2a` for focused imports, or the root for convenience. Studio uses `/client`. See [the adopted host contract](../harness/HOST_CONTRACT.md).
725
+ The SDK depends only on core within the Nylorun packages; installing it does not install harness or any AG-UI or A2A package. Use `/define`, `/client`, `/ag-ui` or `/a2a` for focused imports, or the root for convenience. Studio uses `/client`. See [the adopted host contract](../../runtime/harness/HOST_CONTRACT.md).
657
726
 
658
727
  ## Embedding Studio (desktop apps)
659
728
 
@@ -11,12 +11,12 @@
11
11
  import { type SubjectScope } from "@nylorun/core/contracts";
12
12
  import type { BuiltWorkflow } from "@nylorun/core/define";
13
13
  import { type AgentSource, type AgentsClient } from "../client.js";
14
- /** Per-session parameters the host may add, such as the person's vault for MCP servers. */
14
+ /** Per-session parameters the host may add, such as the person's Environment. */
15
15
  export interface AgUiSessionOptions {
16
16
  /** Set when the thread's session is created, on its first run; later runs keep them. */
17
17
  info?: Record<string, unknown>;
18
- vaultIds?: readonly string[];
19
- credentialSelections?: Parameters<AgentsClient["createSession"]>[0]["credentialSelections"];
18
+ /** The Environment the thread's session runs in: its sandbox and its vault's credentials. */
19
+ environment?: string;
20
20
  }
21
21
  export interface AgUiHandlerOptions {
22
22
  /** Agents this endpoint may run: built agents, workflows or their ids. Nothing else is reachable. */
@@ -29,7 +29,7 @@ export interface AgUiHandlerOptions {
29
29
  client?: AgentsClient | Promise<AgentsClient>;
30
30
  /** What each subject may do through this endpoint. Default `["sessions:own"]`. */
31
31
  scopes?: readonly SubjectScope[];
32
- /** Optional per-session parameters, e.g. the person's vault for connected accounts. */
32
+ /** Optional per-session parameters, e.g. the person's Environment with their keys. */
33
33
  session?(subject: string, agentId: string): AgUiSessionOptions | Promise<AgUiSessionOptions>;
34
34
  }
35
35
  export interface AgUiHandler {
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Acting for a person (`client.as`, or a trusted issuer's token), only the artifacts of their own sessions.
13
13
  */
14
- import type { ArtifactDiff, ArtifactLink, ArtifactTree, ArtifactView, DeleteArtifactResponse, UploadArtifactResponse } from "@nylorun/core/contracts";
14
+ import { type ArtifactKind, type ArtifactPage, type ArtifactDiff, type ArtifactLink, type ArtifactTree, type ArtifactView, type DeleteArtifactResponse, type UploadArtifactResponse } from "@nylorun/core/contracts";
15
15
  import { type Transport } from "./http.js";
16
16
  /** What an upload sends: bytes, a `Blob` or `File`, text, or a stream of bytes. */
17
17
  export type ArtifactBody = Uint8Array | ArrayBuffer | Blob | string | ReadableStream<Uint8Array>;
@@ -25,6 +25,17 @@ export interface UploadArtifactOptions {
25
25
  readonly labels?: Readonly<Record<string, string>>;
26
26
  readonly signal?: AbortSignal;
27
27
  }
28
+ export interface ArtifactPageOptions {
29
+ /** Only this session's artifacts; excludes Tenant-owned artifacts. */
30
+ readonly sessionId?: string;
31
+ readonly kind?: ArtifactKind;
32
+ /** Every label must match exactly. */
33
+ readonly labels?: Readonly<Record<string, string>>;
34
+ /** Default 50, from 1 to 200. */
35
+ readonly limit?: number;
36
+ readonly cursor?: string;
37
+ readonly signal?: AbortSignal;
38
+ }
28
39
  /** A capability link, with the absolute URL to open on this Runtime. */
29
40
  export type ArtifactLinkWithUrl = ArtifactLink & {
30
41
  readonly url: string;
@@ -48,6 +59,8 @@ export declare class ArtifactsClient {
48
59
  sessionId?: string;
49
60
  signal?: AbortSignal;
50
61
  }): Promise<ArtifactView[]>;
62
+ /** Newest first, with metadata only (Host feature `artifact-reads`). */
63
+ page(options?: ArtifactPageOptions): Promise<ArtifactPage>;
51
64
  /** The artifact, with every version. */
52
65
  get(artifactId: string, options?: {
53
66
  signal?: AbortSignal;