@nylorun/agents 0.12.0-beta → 0.14.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,77 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.14.0-beta
4
+
5
+ ### Major Changes
6
+
7
+ - bd478ee: **Subject tokens, browser keys and derived principals leave open source (F9 I3, protocol 7).** Upgrade these with the Runtime: they speak protocol 7, which this Runtime serves beside 4, 5 and 6; protocol 6 clients keep working on every route that remains. Browsers and apps present a trusted issuer's token (F9 I2), servers an operator key (F9 I1), and a person's own credentials come from the operator's credential resolver (F9 C1). See MIGRATION.md.
8
+
9
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`): subject tokens are gone.** `POST /v1/tokens`, `POST /v1/access/revocations`, subject-token verification, revocation epochs and the `subject.revoked` stream signal are removed; the routes answer `404`, and a JWT no trusted issuer signed is the opaque `404`. A stream opened with a token now ends only at its expiry (`event: nylorun.closed`, reason `token_expired`; `StreamClosedFrame` loses `revoked`). Migration `0010_oss_auth_removals` drops the `subject_epochs`, `subject_usage` and `publishable_keys` tables. The `token` caller is a trusted issuer's token only: it loses `role`, `limits` and `epoch`, and `issuer` is always set; `GET /v1/me` no longer reports `via: token`. Core drops `SUBJECT_TOKEN_TYPE`, `SUBJECT_TOKEN_AUDIENCE`, `subjectTokenIssuer` (the Runtime's own tokens use `tenantTokenIssuer`, same value), `CreateTokenRequest`/`Response`, `RevokeSubject*`, `SubjectTokenClaims`, `TOKEN_TTL_MIN_SECONDS` and `TOKEN_TTL_DEFAULT_SECONDS`; `TOKEN_SCOPES` is `agents:read`, `sessions:own`, `sandboxes:write`.
10
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`): the access policy and role limits are gone.** `GET`/`PUT /v1/access/policy` answer `404`, a turn is never refused with `429 limit_exceeded` for a role (limit requests at your proxy), and an existing `access.policy` setting is ignored. Core drops `AccessPolicy*`, `AccessRole*`, `RoleLimits*`, `DEFAULT_ACCESS_POLICY`, `PutAccessPolicyRequest` and `ROLE_NAME_PATTERN`.
11
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`): publishable keys and the Runtime's CORS are gone.** `/v1/access/publishable-keys*` answer `404`, `Nylorun-Key` is ignored, `NYLORUN_BROWSER_ACCESS` and `browserAccess` (host.json, `createHost`, `startEphemeralRuntime`) are removed, and the Runtime sends no CORS headers. A request with `Origin` reaches the Tenant routes of the public listener with no toggle: a trusted issuer's token is served, an application key or delivery token is `403 origin_rejected`; admin routes, `/health`, `/ready` and the operator listener still refuse `Origin`. Any `OPTIONS` request is `204` with `Allow` and no CORS header: the operator's proxy answers preflights. Route declarations lose `browser` (`x-nylorun-browser` leaves the OpenAPI document) and the `publishable` credential; the token security scheme is `issuerToken`. Core drops `PUBLISHABLE_KEY_HEADER`, `PUBLISHABLE_KEY_PATTERN`, `PUBLISHABLE_KEY_ID_PATTERN`, `tenantOfPublishableKey`, `newPublishableKey`, `newPublishableKeyId`, `PublishableKey*`, `OriginEntrySchema`, `originAllowed` and `LOOPBACK_ORIGIN_WILDCARDS`.
12
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`, `@nylorun/agents`): vault routes are application-only, and `vaults:own` is retired.** Every `/v1/vaults` route takes an application key acting for no one (operator keys, Studio); a request acting for a subject or a trusted issuer's token gets `403 scope_required`. `vaults:own` leaves `SUBJECT_SCOPES`, `TOKEN_SCOPES` and the issuer scopes; `Nylorun-Scopes` may still name it and it grants nothing. Existing user vaults stay attachable to their own owner's sessions. `client.createVault` takes `{ scope: "installation" }` or `ownerUserId`, and `client.listVaults()` takes an optional owner.
13
+ - **Breaking (`@nylorun/runtime`, `@nylorun/core`, `@nylorun/admin`, `nylorun`): derived principals are gone.** Only Studio's key is derived from the admin key. `NYLORUN_DERIVED_PRINCIPALS` is ignored and no longer written, `hostPrincipals` takes no `derived`, `startEphemeralRuntime` no `derivedPrincipals`, and `@nylorun/admin` drops `deriveTenantKey`, `PROJECT_PRINCIPAL_ID` and `admin.deriveTenantKey`. Keys an earlier Host derived stay in the database and keep working as ordinary keys; replace them with operator keys. Core renames `DERIVED_PRINCIPAL_ID_PATTERN` to `APPLICATION_KEY_ID_PATTERN`.
14
+ - **Breaking (`@nylorun/agents`):** `createTokenEndpoint`, `createBrowserClient` and the `@nylorun/agents/browser` entry, `client.tokens`, `TokensClient`, `PublishableKeysClient`, `client.access.getPolicy`/`putPolicy`/`revokeSubject`/`publishableKeys` and `Destination.publishableKey` are removed. `client.access.signingKeys` and `client.access.jwks()` stay, and `Destination.token` takes a trusted issuer's tokens.
15
+ - **Breaking (`@nylorun/cli`):** `nylo access policy|keys|revoke|token` are removed and name what replaces them; `nylo access signing-keys list|rotate|revoke` stays.
16
+ - **Breaking (`@nylorun/core`, `@nylorun/agents`, `@nylorun/studio`): the Studio embed message `open.babai` is now `open.session`** (`{ sessionId }`): it asks the embedding app to open that session in its own UI. Studio sends no such message itself.
17
+ - **Breaking (`nylorun`): Studio embedding is opt-in.** No origin may frame Studio by default; list your app's origins with `nylorun start --studio-embed-origin <origin>`. A Tenant's `.env` from an earlier nylorun keeps the origins it had until `--studio-embed-origin-reset`.
18
+ - `PROTOCOL_VERSION` is 7 and `HOST_PROTOCOL` 4–7; the Host features `subject-tokens`, `browser-access` and `derived-principals` are removed. `@nylorun/studio` and `@nylorun/create-agent` speak protocol 7.
19
+
20
+ ### Patch Changes
21
+
22
+ - Pin core to the tested release.
23
+ - Updated dependencies [8773586]
24
+ - Updated dependencies [e1bfb4b]
25
+ - Updated dependencies [5cfaed9]
26
+ - Updated dependencies [bd478ee]
27
+ - Updated dependencies [c0b604e]
28
+ - @nylorun/core@0.13.0-beta
29
+
30
+ ## 0.13.0-beta
31
+
32
+ ### Major Changes
33
+
34
+ - b352feb: **File artifacts, message parts and capability links (protocol 6).** Upgrade these with the Runtime: they speak protocol 6, which this Runtime serves beside 4 and 5. See MIGRATION.md.
35
+
36
+ - **`@nylorun/runtime`: file artifacts.** A file is an artifact: an `af_` id, a name and numbered immutable versions, with rows in the new `artifacts` and `artifact_versions` tables (migration `0004_artifacts`) and bytes in the Object store (`BlobStore`). `POST /v1/artifacts?name=&sessionId=` uploads a file in one streamed request and `POST /v1/artifacts/{id}/versions` adds a version, within the Tenant's limits (`GET`/`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default); a body past either is `413 limit_exceeded`, refused mid-stream with nothing stored. `GET /v1/artifacts` lists them (by session), `GET /v1/artifacts/{id}` reads one with its versions, `GET /v1/artifacts/{id}/versions/{n|latest}/content` downloads through the Runtime with HTTP Range (`206`, `Content-Range`, `416`), and `DELETE /v1/artifacts/{id}` deletes it with its bytes. A session's artifacts go with it on a sessions reset, and appear in its history as `artifact.created`, `artifact.version.created` and `artifact.deleted` (new in the event catalog). A subject reaches only the artifacts of their own sessions.
37
+ - **`@nylorun/runtime`: capability links.** `POST /v1/artifacts/{id}/links` mints a short-lived path, `/v1/artifact-links/<token>`, that downloads one version (with Range) with no credential and no `Nylorun-Protocol`: an ES256 JWT (`typ: nylorun-artifact+jwt`) signed with the Tenant's signing key, at most 15 minutes, and dead once the artifact is deleted. The Host logs its path without the token.
38
+ - **Breaking (`@nylorun/core`, `@nylorun/runtime`): message `parts`.** A user message may carry `parts`: `text`, and `file` by `artifactId` (and `version`, else the latest, pinned when the message is accepted). Model-gate reads the file from the Object store: an image goes to the model as image input, a text file as text, and any other file fails the call (`invalid_request`). The record and the transcript hold only the reference. The gateway builds its `BlobStore` from `NYLORUN_OBJECT_STORE_*` (the Tenant's `fs` store without it). `PROTOCOL_VERSION` is 6; `HOST_PROTOCOL` is 4–6 and protocol 6 clients require the feature `artifacts`.
39
+ - **`@nylorun/runtime`: `save_artifact`.** A session with a sandbox gets the `nylorun.artifacts` capability, whose `save_artifact` tool saves a sandbox file (`path`) or text (`content`) as an artifact of the session, carrying the turn and tool call on its event.
40
+ - **Breaking (`@nylorun/runtime`): `MediaStore` is removed.** `MediaStore` and `localMedia` leave `@nylorun/runtime/node`, and `piModel` takes `files` (a resolver from an artifact reference to its bytes) instead of `media`; images are file artifacts. The image checks (`decodeImageBase64`, `validateImageBytes`, `IMAGE_MEDIA_TYPES`, `MAX_IMAGE_BYTES`) stay.
41
+ - **`@nylorun/agents`:** `client.artifacts` (`upload`, `uploadVersion`, `list`, `get`, `download` with a range, `link`, `delete`), also on `BrowserClient`, and `session.inputParts(parts)`. A request with a body of its own type keeps it.
42
+ - `@nylorun/admin`, `@nylorun/cli`, `nylorun`, `@nylorun/studio`: speak protocol 6.
43
+
44
+ ### Minor Changes
45
+
46
+ - 6077272: **Folder artifacts and the turn-end outputs export (F8.2).** Part of protocol 6, with file artifacts.
47
+
48
+ - **`@nylorun/runtime`: the turn-end export.** When an agent's turn completes, the Runtime reads `/workspace/outputs` of the session's sandbox and keeps it as a version of the session's folder artifact `outputs`: the first export creates it, and each later turn whose outputs changed adds a version (`artifact.created` / `artifact.version.created` with `kind: "folder"`, `source: "export"`, `fileCount` and `claimed: true`, since the listing and bytes are what the sandbox supplied). Nothing is exported without a sandbox or without outputs. An export past 10,000 files, 1 GiB, the per-file limit or the Tenant total stores nothing and records `artifact.export.skipped` with its reason; a failure records `artifact.export.failed`. Neither fails the turn. Core reads the workspace through one seam, `WorkspaceReader`, which the Harness API and pod sandboxes will implement later.
49
+ - **`@nylorun/runtime`: folder artifacts.** A folder version is a manifest of paths to content-addressed files: each file's bytes are stored once at `blobs/sha256/<hex>`, so an unchanged file is never stored again, and the Tenant total (`GET /v1/tenant/artifacts` `usedBytes`) counts it once. New routes: `GET /v1/artifacts/{id}/versions/{n|latest}/tree` (the manifest), `…/files/{path}` (one file by its percent-encoded path, with Range), `…/diff?from=n` (files added, removed and changed) and `…/zip` (a streamed zip). `POST /v1/artifacts/{id}/links` takes `file` to link one file of a folder; a folder's link without it opens the zip. A folder's `/content`, a new version uploaded to a folder, and a message part naming a folder are `400`. Deleting a folder removes the files no other version names. Migration `0006_folder_artifacts` allows the `folder` kind and adds the `artifact_content` table.
50
+ - **`@nylorun/core`:** `ArtifactKindSchema` (`file`, `folder`), the `export` source, `FolderEntrySchema`, `FolderManifestSchema`, `ArtifactTreeSchema`, `ArtifactDiffSchema`, the `artifact.export.skipped` and `artifact.export.failed` events, and `fileCount` and `claimed` on `artifact.created` / `artifact.version.created`.
51
+ - **`@nylorun/agents`:** `client.artifacts.tree()`, `file()`, `diff()` and `zip()`, and `link(id, { file })`.
52
+
53
+ - 926711b: **Sandboxes are a resource (F7.1, blueprint D39; Host feature `sandboxes`).** A sandbox has its own id, a kind, a spec and labels, and outlives the sessions attached to it. Additive: protocol 5 is unchanged.
54
+
55
+ - `PUT /v1/sandboxes/{id}` creates a sandbox or finds the one with that id (get-or-create in one call); `GET /v1/sandboxes/{id}`, `GET /v1/sandboxes?label=key=value` (repeatable), `GET /v1/sandboxes/{id}/events` and `DELETE /v1/sandboxes/{id}`. Ids are `/`-separated segments (`team-a/proj-42`), sent percent-encoded as one path segment. Only kind `virtual` runs; `pod` is refused with `sandbox_unavailable`. The spec is resolved against the Tenant's limits and fixed once the sandbox exists; labels can change.
56
+ - A session attaches with `sandbox: { id }` and shares the sandbox's `/workspace` with every other session attached to it. Turns are serial per sandbox: a second session's turn is refused with `409 sandbox_busy` while another runs. Deleting a session (a sessions reset) only detaches it. Deleting a sandbox is refused while a turn runs in it; afterwards an attached session's next turn is refused with `sandbox_unavailable` until a sandbox with that id exists again.
57
+ - Subject tokens carry an `sbx` claim: `POST /v1/tokens` takes `sandboxes`, exact ids or prefixes ending in `/*` (at most 16). A token reaches only the sandboxes they match, checked when a session attaches and at every turn start (`403 sandbox_not_granted`); any other sandbox is the 404 of a missing one. The new scope `sandboxes:write` lets a role create and delete the sandboxes its grants reach. Application keys reach every sandbox.
58
+ - The Tenant holds at most `limits.sandboxes` sandboxes (`PUT /v1/tenant/sandbox`, default 100); one more is `409 limit_exceeded`.
59
+ - Lifecycle events (`sandbox.created`, `sandbox.attached`, `sandbox.detached`, `sandbox.deleted`) go to the sandbox's own stream in the record, through the record module; the session's log records `sandbox.attached`. The sandbox stream is not relayed to S2.
60
+ - New error codes `sandbox_not_granted`, `sandbox_busy` and `sandbox_unavailable`; the session view gains `sandboxId` and `sandboxSource: "sandbox"`. Migration `0004_sandbox_resources` adds `sandbox_resources`, `nylorun_streams.sandbox_events` and the sessions' `sandbox_id` column.
61
+ - `@nylorun/agents`: `client.sandboxes` with `ensure(id, spec)`, `get`, `list({ labels })`, `delete`, `events`, and `forSession({ session, spec })`, which creates a sandbox for one session, opens the session on it, and deletes it with `release()`. It replaces sharing through another session (`sandbox: { session }` and the view's `sandboxOwnerId`, now deprecated). `client.tokens.create` takes `sandboxes`.
62
+ - `nylorun sandbox ls [--label key=value]... [--json]` and `nylorun sandbox rm <id>` list and delete the running local Tenant's sandboxes.
63
+
64
+ ### Patch Changes
65
+
66
+ - Pin core to the tested release.
67
+ - Updated dependencies [b352feb]
68
+ - Updated dependencies [6077272]
69
+ - Updated dependencies [b6bf1f5]
70
+ - Updated dependencies [8ed4ea6]
71
+ - Updated dependencies [678e085]
72
+ - Updated dependencies [926711b]
73
+ - @nylorun/core@0.12.0-beta
74
+
3
75
  ## 0.12.0-beta
4
76
 
5
77
  ### Minor Changes
package/README.md CHANGED
@@ -107,7 +107,7 @@ eval "$(npx @nylorun/cli env)"
107
107
  # → NYLORUN_RUNTIME_URL, NYLORUN_SERVER_KEY
108
108
  ```
109
109
 
110
- Every request sets `Nylorun-Protocol` (5) and no `Nylorun-Tenant`: the Runtime
110
+ Every request sets `Nylorun-Protocol` (6) and no `Nylorun-Tenant`: the Runtime
111
111
  serves one Tenant. Before the first authenticated
112
112
  request, `Transport` fetches `/health` once, checks protocol compatibility, and
113
113
  throws `IncompatibleRuntimeError` / `incompatible_host` when the Host range or
@@ -156,7 +156,22 @@ const session = await client.createSession({
156
156
 
157
157
  The model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a persistent `/workspace`. These tools run in the Runtime, not in your process, so sandbox-only agents need no Action endpoint. 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`.
158
158
 
159
- `sandbox` takes `false` for none, `{ session }` to share another session's sandbox, or an inline sandbox as above; omit it for the Tenant's default. 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.
159
+ `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.
160
+
161
+ 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:
162
+
163
+ ```ts
164
+ // One per session: created with the session, deleted by release().
165
+ const { session, release } = await client.sandboxes.forSession({
166
+ session: { agentId: "analyst", ownerUserId },
167
+ spec: { network: { allow: ["pypi.org"] } },
168
+ });
169
+ // One per person or project: get-or-create by id, then attach sessions to it.
170
+ await client.sandboxes.ensure("team-a/proj-42", { labels: { project: "acme" } });
171
+ await client.createSession({ agentId: "analyst", ownerUserId, sandbox: { id: "team-a/proj-42" } });
172
+ ```
173
+
174
+ 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).
160
175
 
161
176
  Add an agent with `.subagents()` to let the model delegate to it:
162
177
 
@@ -263,7 +278,8 @@ directly and run as before; a flow agent can't be a child of them.
263
278
 
264
279
  The agents in a flow share one sandbox: open the flow's session with it,
265
280
  `createSession({ …, sandbox: { … } })`, and every agent, tool step and `verify` in the
266
- flow uses it. Share it with another session with `sandbox: { session }`, and call
281
+ flow uses it. Share it with other sessions by opening them all on one sandbox resource
282
+ (`sandbox: { id }`, see `client.sandboxes`), and call
267
283
  built-ins via `session.sandbox` (application) or `ctx.sandbox` (a tool).
268
284
  Observe with `session.observe({ follow: true })` to merge linked agent streams;
269
285
  `pending()` lists waits across the tree. Studio renders the manifest tree and
@@ -297,9 +313,10 @@ thread is one session per person, agent and thread. There are two ways in:
297
313
  - **From your own server**, with `@nylorun/agents/ag-ui` (below): your server
298
314
  signs people in and the handler forwards each request to the Runtime acting
299
315
  for that person. The browser never sees the Runtime.
300
- - **From the page itself**, with `@nylorun/agents/browser`'s `agUi()` and a
301
- subject token ([In the browser](#in-the-browser)). No chat traffic passes
302
- through your server.
316
+ - **From the page itself**, with `@ag-ui/client`'s `HttpAgent` and a `fetch`
317
+ that sends your identity provider's token
318
+ ([In the browser](#in-the-browser)). No chat traffic passes through your
319
+ server.
303
320
 
304
321
  Both reach the same threads: one started through the handler continues from
305
322
  the page.
@@ -346,8 +363,7 @@ created, on its first run; later runs keep them. Whatever the browser sends in
346
363
  - A busy or paused thread ends the run with `RUN_ERROR` code `session_busy`.
347
364
  - The handler calls the Runtime as each person (`client.as(subject, { scopes })`,
348
365
  below), so the Runtime itself keeps one person out of another's threads.
349
- `scopes` defaults to `["sessions:own"]`; add `"vaults:own"` when `session()`
350
- attaches the person's vaults. A `subject` the Runtime cannot name (see below)
366
+ `scopes` defaults to `["sessions:own"]`. A `subject` the Runtime cannot name (see below)
351
367
  answers `500` (`subject_invalid`).
352
368
  - The handler needs a Runtime with the optional feature `ag-ui-endpoint` and
353
369
  answers `502` (`runtime_feature_missing`) without it. It loads no AG-UI
@@ -360,8 +376,8 @@ Limitations: assistant text arrives once per model step (no token streaming);
360
376
  no reasoning, state, activity or subagent events, and an agent used as a tool
361
377
  shows only its result; frontend tools in `RunAgentInput.tools` are rejected
362
378
  (`400`); one text part per user message; earlier messages cannot be edited or
363
- regenerated. A browser reaches the Runtime only with a subject token and a
364
- publishable key, never with a Tenant key.
379
+ regenerated. A browser reaches the Runtime only with a trusted issuer's token,
380
+ never with a Tenant key.
365
381
 
366
382
  ## A2A
367
383
 
@@ -426,6 +442,56 @@ What the Runtime answers (Host feature `a2a-endpoint`):
426
442
  no header the partner chose except `A2A-Version` and `A2A-Extensions`. Without
427
443
  `a2a-endpoint` on the Runtime it answers `502` (`runtime_feature_missing`).
428
444
 
445
+ ## Files: artifacts and message parts
446
+
447
+ Files users upload or agents make are **artifacts** (protocol 6): an id, a name and
448
+ numbered versions, kept by the Runtime. `client.artifacts` uploads a file in one
449
+ streamed request, lists, downloads with Range, mints capability links and deletes;
450
+ a message names files in its `parts`, and the model reads an image as an image and
451
+ a text file as text:
452
+
453
+ ```ts
454
+ const { artifact } = await client.artifacts.upload(file, { name: "room.png", sessionId });
455
+ await session.inputParts(
456
+ [{ type: "text", text: "Redesign this room" }, { type: "file", artifactId: artifact.artifactId }],
457
+ { idempotencyKey: crypto.randomUUID() },
458
+ );
459
+
460
+ const files = await client.artifacts.list({ sessionId }); // what the user and the agent saved
461
+ const part = await client.artifacts.download(files[0].artifactId, { range: { start: 0, end: 1023 } });
462
+ const { url } = await client.artifacts.link(files[0].artifactId); // no credential needed, 5 minutes
463
+ ```
464
+
465
+ A session with a sandbox gives the agent `save_artifact`, which saves a file it
466
+ made as an artifact of the session; each new artifact or version appears in the
467
+ session's events as `artifact.created` or `artifact.version.created`. The Tenant's
468
+ limits (`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default)
469
+ refuse a larger upload with `413 limit_exceeded`, and nothing is stored. Acting for
470
+ a person (`as()`, or a trusted issuer's token), a client reaches only the artifacts of that
471
+ person's sessions, and an upload names one of them.
472
+
473
+ ### Outputs: folder artifacts
474
+
475
+ At the end of each turn, the Runtime exports the files the agent wrote into
476
+ `/workspace/outputs` of its sandbox as a version of the session's **folder**
477
+ artifact `outputs` (kind `folder`): the first export creates it, and each later
478
+ turn whose outputs changed adds a version, announced by `artifact.created` or
479
+ `artifact.version.created` with `source: "export"`. A file that did not change is
480
+ stored once. Read a folder as a tree, one file by path, a diff or a zip:
481
+
482
+ ```ts
483
+ const outputs = (await client.artifacts.list({ sessionId })).find((a) => a.kind === "folder");
484
+ const { entries } = await client.artifacts.tree(outputs.artifactId); // [{ path, size, sha256, contentType }]
485
+ const page = await client.artifacts.file(outputs.artifactId, "app/index.html", { range: { start: 0 } });
486
+ const { added, removed, changed } = await client.artifacts.diff(outputs.artifactId, { from: 1 });
487
+ const zip = await client.artifacts.zip(outputs.artifactId); // a streamed Response
488
+ const { url } = await client.artifacts.link(outputs.artifactId, { file: "app/index.html" }); // or the zip without `file`
489
+ ```
490
+
491
+ An export past a limit (10,000 files, 1 GiB, the per-file limit or the Tenant
492
+ total) stores nothing and records `artifact.export.skipped`; the turn still
493
+ completes.
494
+
429
495
  ## Acting for a person (app servers)
430
496
 
431
497
  A server that signs people in and calls the Runtime for them (an "app server")
@@ -437,147 +503,58 @@ import { createClient } from "@nylorun/agents";
437
503
  const app = createClient(); // the Tenant key, on the server only
438
504
 
439
505
  // Per request, after your own sign-in:
440
- const person = app.as(`app:${user.id}`, { scopes: ["sessions:own", "vaults:own"] });
506
+ const person = app.as(`app:${user.id}`, { scopes: ["sessions:own"] });
441
507
  await person.createSession({ agentId: "support", ownerUserId: `app:${user.id}` });
442
508
  await person.listSessions(); // only this person's sessions
443
509
  ```
444
510
 
445
511
  `as()` sends `Nylorun-Subject` and `Nylorun-Scopes` on every call, event
446
512
  streams included, and the Runtime (optional feature `subject-headers`)
447
- enforces both: another person's sessions and vaults answer the same `404` as
448
- missing ones, and a route outside the scopes answers `403` (`scope_required`).
513
+ enforces both: another person's sessions answer the same `404` as missing
514
+ ones, and a route outside the scopes answers `403` (`scope_required`).
449
515
  Nothing is minted, cached or refreshed, and the copy shares the client's
450
516
  compatibility check, so calling `as()` per request is cheap.
451
517
 
452
518
  | Scope | Allows |
453
519
  | --- | --- |
454
- | `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel |
455
- | `vaults:own` | The person's own vaults and credentials |
520
+ | `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel; and their artifacts |
456
521
  | `agents:read` | Listing the Tenant's agents |
457
522
  | `agents:write` | Saving agents; listing agents, models and providers |
458
523
  | `tenant:settings` | The Tenant's status, model provider and sandbox settings |
459
524
 
460
- No scope reaches Tenant reset, config seed, Action endpoints, actions or the sandbox
461
- tool routes; call those without `as()`. A subject is 1–200 visible ASCII
525
+ No scope reaches Tenant reset, config seed, Action endpoints, actions, vaults or
526
+ the sandbox tool routes; call those without `as()`. Vaults are the
527
+ installation's (`createVault({ scope: "installation", … })`); a person's own
528
+ credentials come from your credential resolver
529
+ ([DEPLOYMENT.md](../DEPLOYMENT.md#credentials)). A subject is 1–200 visible ASCII
462
530
  characters (spaces only inside) and `host` is reserved. Your server must drop
463
531
  any `Nylorun-*` header its own clients send, and only an application key can act
464
532
  for a subject.
465
533
 
466
- ## Minting subject tokens (app servers)
467
-
468
- To let a person's browser or app call the Runtime directly, your server mints a
469
- short-lived **subject token** for them instead of carrying their requests
470
- (optional feature `subject-tokens`). What a token may do comes from the Tenant's
471
- access policy, not from the caller:
472
-
473
- ```ts
474
- import { createClient } from "@nylorun/agents";
475
-
476
- const app = createClient(); // the Tenant key, on the server only
477
-
478
- // Once: roles, their agents and limits (or `nylo access policy init`).
479
- await app.access.putPolicy({
480
- version: 1,
481
- roles: {
482
- user: {
483
- scopes: ["sessions:own", "agents:read"],
484
- agents: ["support"],
485
- limits: { turnsPerHour: 60, concurrentTurns: 2 },
486
- },
487
- },
488
- anon: { scopes: [], agents: [] },
489
- tokens: { maxTtlSeconds: 600 },
490
- });
491
-
492
- // Per signed-in person, from a same-origin POST route:
493
- const { token, expiresAt } = await app.tokens.create({
494
- subject: `app:${user.id}`,
495
- role: "user",
496
- });
497
- ```
498
-
499
- The client sends it as `Authorization: Bearer <token>`.
500
- Tokens carry only `sessions:own`, `vaults:own` and `agents:read`, live at most
501
- 15 minutes, and see only `{ agentId, name, description }` of their role's
502
- agents. A token that expired or was revoked answers `401` with
503
- `code: "token_expired"`: fetch a new one and retry. Starting more turns than the
504
- role allows answers `429` with `code: "limit_exceeded"` and `Retry-After`.
505
-
506
- | Call | Does |
507
- | --- | --- |
508
- | `app.access.revokeSubject(subject)` | Ends every token minted so far for the person and their open event streams |
509
- | `app.access.signingKeys.rotate()` | Rotates the Tenant's signing keys without signing anyone out (`{ force: true }` for incidents) |
510
- | `app.access.signingKeys.list()`, `.revoke(kid)`, `app.access.jwks()` | Inspect and retire keys; the public keys |
511
-
512
- Keep tokens in memory on the client, never in `localStorage`, and serve the
513
- minting route without CORS. `nylo access …` does the same from the terminal.
514
-
515
- `createTokenEndpoint` is that route, ready to mount (Next.js, Hono or any
516
- `Request` → `Response` handler):
517
-
518
- ```ts
519
- import { createTokenEndpoint } from "@nylorun/agents";
520
-
521
- export const POST = createTokenEndpoint({
522
- client: app,
523
- role: "user",
524
- subject: (request) => userFromCookie(request)?.id,
525
- });
526
- ```
534
+ The Tenant's signing keys sign delivery tokens and capability links:
535
+ `app.access.signingKeys.list()`, `.rotate()` (`{ force: true }` for incidents),
536
+ `.revoke(kid)` and `app.access.jwks()`, or `nylo access signing-keys …` from the
537
+ terminal.
527
538
 
528
539
  ## In the browser
529
540
 
530
- `@nylorun/agents/browser` calls the Runtime from a web page or an app with a
531
- publishable key and the token route above (optional feature `browser-access`).
532
- It loads no Node module.
533
-
534
- ```ts
535
- import { createBrowserClient } from "@nylorun/agents/browser";
536
-
537
- const nylo = createBrowserClient({
538
- url: "https://runtime.example.com",
539
- publishableKey: "nr_pub_tn_…", // app.access.publishableKeys.create({ name, origins })
540
- token: () => fetch("/api/nylorun/token", { method: "POST" }).then((r) => r.json()),
541
- });
542
-
543
- const session = await nylo.createSession({ agentId: "support" }); // owned by the token's subject
544
- await session.input("Where is my order?", { idempotencyKey: crypto.randomUUID() });
545
- for await (const event of session.observe()) console.log(event.type);
546
- ```
547
-
548
- For a chat UI, `agUi()` gives `@ag-ui/client`'s `HttpAgent` a `fetch` with the
549
- key and a current token:
550
-
551
- ```ts
552
- import { HttpAgent } from "@ag-ui/client";
553
-
554
- const { url, fetch } = nylo.agUi("support");
555
- const agent = new HttpAgent({
556
- url,
557
- fetch,
558
- threadId,
559
- initialMessages: await nylo.agUiHistory("support", threadId),
560
- });
561
- ```
562
-
563
- When the Runtime ends a run's stream because the token expired or was revoked,
564
- that `fetch` reattaches from the last event with a new token, so the agent sees
565
- one run. A person's token may attach their own vaults to a new thread
566
- (`forwardedProps: { nylorun: { session: { vaultIds } } }`) but not set `info`.
567
-
568
- The client keeps the token in memory, fetches a new one a minute before it
569
- expires or when the Runtime answers `401 token_expired`, and never asks for two
570
- at once. Event streams that the Runtime ends at token expiry or revocation
571
- reconnect from their last event with a new token. Create a publishable key per
572
- app with the origins that serve it (`http://localhost:*` for development);
573
- requests from other origins get the opaque `404`.
541
+ A web page or an app calls the Runtime directly with the JWT your identity
542
+ provider gave the person: list the provider in the Runtime's identity file
543
+ ([Trusted issuers](../DEPLOYMENT.md#trusted-issuers)), and the Runtime takes the
544
+ token's subject, scopes, agents and sandbox grants from it. Nylorun mints no
545
+ token and ships no browser client: use your provider's SDK for sign-in and send
546
+ the token as `Authorization: Bearer <token>` with `Nylorun-Protocol`, through a
547
+ reverse proxy that answers CORS (the Runtime sends no CORS headers). An expired
548
+ token answers `401` with `code: "token_expired"`; `GET /v1/me` shows what a token
549
+ renders to. For a chat UI, give `@ag-ui/client`'s `HttpAgent` a `fetch` that adds
550
+ those headers.
574
551
 
575
- 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`, `/a2a` or `/browser` for focused imports, or the root for convenience. Studio uses `/client`. See [the adopted host contract](../harness/HOST_CONTRACT.md).
552
+ 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).
576
553
 
577
554
  ## Embedding Studio (desktop apps)
578
555
 
579
556
  `@nylorun/agents/studio-embed` is the contract between Studio and an app that
580
- shows it in an iframe, such as Babai. It exports the message schema
557
+ shows it in an iframe. It exports the message schema
581
558
  (`StudioEmbedMessageSchema`, envelope `{ type: "nylorun.studio", protocol, kind }`),
582
559
  the login-token and session schemas, `STUDIO_EMBED_PROTOCOLS` and
583
560
  `parseFrameAncestors`.
@@ -596,4 +573,6 @@ window.addEventListener("message", (event) => {
596
573
  The embedder's backend mints the login token with the admin key
597
574
  (`mintStudioLoginToken` in `@nylorun/admin`) and the page passes it to Studio in
598
575
  `init`. No key reaches the page. Studio's origin allowlist is
599
- `NYLORUN_STUDIO_FRAME_ANCESTORS`.
576
+ `NYLORUN_STUDIO_FRAME_ANCESTORS`, empty by default: list your app's origins with
577
+ `nylorun start --studio-embed-origin <origin>`. The message `open.session`
578
+ (`{ sessionId }`) asks the embedding app to open that session in its own UI.
package/dist/access.d.ts CHANGED
@@ -1,30 +1,10 @@
1
1
  /**
2
- * Subject tokens and access management for app servers (Host feature `subject-tokens`), on the
3
- * application key. `client.tokens.create` mints a short-lived token for one signed-in person;
4
- * `client.access` manages the policy that decides what each role may do, the signing keys and
5
- * revocations. Each call checks the Host feature before sending anything.
2
+ * The Tenant's signing keys and public keys, for app servers on the application key. The
3
+ * Runtime signs delivery tokens (Action endpoints) and capability links with them; an Action
4
+ * endpoint verifies delivery tokens with the public keys (`jwks`).
6
5
  */
7
- import type { AccessPolicy, CreateTokenResponse, Jwks, PublishableKey, SigningKeyView, TokenScope } from "@nylorun/core/contracts";
6
+ import type { Jwks, SigningKeyView } from "@nylorun/core/contracts";
8
7
  import { type Transport } from "./http.js";
9
- export interface CreateTokenOptions {
10
- /** The person the token is for, e.g. `app:42`. */
11
- subject: string;
12
- /** A role of the Tenant's access policy. */
13
- role: string;
14
- /** Narrow the role's scopes. Default: all of the role's scopes. */
15
- scopes?: readonly TokenScope[];
16
- /** Narrow the role's agents. Default: all of the role's agents. */
17
- agents?: readonly string[];
18
- /** 60 to 900 seconds, at most the policy's `maxTtlSeconds`. Default: that maximum. */
19
- ttlSeconds?: number;
20
- signal?: AbortSignal;
21
- }
22
- export declare class TokensClient {
23
- private readonly transport;
24
- constructor(transport: Transport);
25
- /** Mints a subject token. Only the application key may; never acting for a subject. */
26
- create(options: CreateTokenOptions): Promise<CreateTokenResponse>;
27
- }
28
8
  export declare class SigningKeysClient {
29
9
  private readonly transport;
30
10
  constructor(transport: Transport);
@@ -33,7 +13,7 @@ export declare class SigningKeysClient {
33
13
  }): Promise<SigningKeyView[]>;
34
14
  /**
35
15
  * standby → current → previous → revoked, and a new standby. Refused while the previous key
36
- * may still verify live tokens; `force` skips that and signs those tokens out.
16
+ * may still verify live tokens; `force` skips that and ends those tokens.
37
17
  */
38
18
  rotate(options?: {
39
19
  force?: boolean;
@@ -44,57 +24,11 @@ export declare class SigningKeysClient {
44
24
  signal?: AbortSignal;
45
25
  }): Promise<SigningKeyView>;
46
26
  }
47
- /**
48
- * Publishable keys (Host feature `browser-access`): shipped in a web page or an app, they name
49
- * the Tenant and the app, and list the origins a browser may use them from.
50
- */
51
- export declare class PublishableKeysClient {
52
- private readonly transport;
53
- constructor(transport: Transport);
54
- list(options?: {
55
- signal?: AbortSignal;
56
- }): Promise<PublishableKey[]>;
57
- /**
58
- * A new key. `origins` are exact origins (`https://app.example.com`) or
59
- * `http://localhost:*`; `[]` allows native apps only.
60
- */
61
- create(options: {
62
- name: string;
63
- origins: readonly string[];
64
- signal?: AbortSignal;
65
- }): Promise<PublishableKey>;
66
- /** Replaces the key's origins. */
67
- update(keyId: string, options: {
68
- origins: readonly string[];
69
- signal?: AbortSignal;
70
- }): Promise<PublishableKey>;
71
- /** Revokes the key: every request that sends it gets the opaque 404. */
72
- revoke(keyId: string, options?: {
73
- signal?: AbortSignal;
74
- }): Promise<PublishableKey>;
75
- }
76
27
  export declare class AccessClient {
77
28
  private readonly transport;
78
29
  readonly signingKeys: SigningKeysClient;
79
- readonly publishableKeys: PublishableKeysClient;
80
30
  constructor(transport: Transport);
81
- getPolicy(options?: {
82
- signal?: AbortSignal;
83
- }): Promise<AccessPolicy>;
84
- putPolicy(policy: AccessPolicy, options?: {
85
- signal?: AbortSignal;
86
- }): Promise<AccessPolicy>;
87
- /**
88
- * Ends every token minted so far for `subject` and its open streams. Running turns continue;
89
- * cancel them separately if they must stop.
90
- */
91
- revokeSubject(subject: string, options?: {
92
- signal?: AbortSignal;
93
- }): Promise<{
94
- subject: string;
95
- epoch: number;
96
- }>;
97
- /** The public keys that verify the Tenant's subject tokens. */
31
+ /** The public keys that verify the Tenant's delivery tokens. */
98
32
  jwks(options?: {
99
33
  signal?: AbortSignal;
100
34
  }): Promise<Jwks>;
package/dist/access.js CHANGED
@@ -1,111 +1,35 @@
1
1
  import { id, segment } from "./http.js";
2
- const FEATURE = "subject-tokens";
3
- export class TokensClient {
4
- transport;
5
- constructor(transport) {
6
- this.transport = transport;
7
- }
8
- /** Mints a subject token. Only the application key may; never acting for a subject. */
9
- async create(options) {
10
- await this.transport.requireFeature(FEATURE, options.signal);
11
- return this.transport.json("/v1/tokens", "POST", {
12
- requestId: id(),
13
- subject: options.subject,
14
- role: options.role,
15
- ...(options.scopes ? { scopes: [...options.scopes] } : {}),
16
- ...(options.agents ? { agents: [...options.agents] } : {}),
17
- ...(options.ttlSeconds === undefined ? {} : { ttlSeconds: options.ttlSeconds }),
18
- }, options.signal);
19
- }
20
- }
21
2
  export class SigningKeysClient {
22
3
  transport;
23
4
  constructor(transport) {
24
5
  this.transport = transport;
25
6
  }
26
7
  async list(options = {}) {
27
- await this.transport.requireFeature(FEATURE, options.signal);
28
8
  const reply = await this.transport.json("/v1/access/signing-keys", "GET", undefined, options.signal);
29
9
  return reply.keys;
30
10
  }
31
11
  /**
32
12
  * standby → current → previous → revoked, and a new standby. Refused while the previous key
33
- * may still verify live tokens; `force` skips that and signs those tokens out.
13
+ * may still verify live tokens; `force` skips that and ends those tokens.
34
14
  */
35
15
  async rotate(options = {}) {
36
- await this.transport.requireFeature(FEATURE, options.signal);
37
16
  const reply = await this.transport.json("/v1/access/signing-keys/rotate", "POST", { requestId: id(), ...(options.force ? { force: true } : {}) }, options.signal);
38
17
  return reply.keys;
39
18
  }
40
19
  /** Revokes a previous or standby key; the current key must be rotated first. */
41
20
  async revoke(keyId, options = {}) {
42
- await this.transport.requireFeature(FEATURE, options.signal);
43
21
  return this.transport.json(`/v1/access/signing-keys/${segment(keyId)}/revoke`, "POST", { requestId: id() }, options.signal);
44
22
  }
45
23
  }
46
- /**
47
- * Publishable keys (Host feature `browser-access`): shipped in a web page or an app, they name
48
- * the Tenant and the app, and list the origins a browser may use them from.
49
- */
50
- export class PublishableKeysClient {
51
- transport;
52
- constructor(transport) {
53
- this.transport = transport;
54
- }
55
- async list(options = {}) {
56
- await this.transport.requireFeature("browser-access", options.signal);
57
- const reply = await this.transport.json("/v1/access/publishable-keys", "GET", undefined, options.signal);
58
- return reply.keys;
59
- }
60
- /**
61
- * A new key. `origins` are exact origins (`https://app.example.com`) or
62
- * `http://localhost:*`; `[]` allows native apps only.
63
- */
64
- async create(options) {
65
- await this.transport.requireFeature("browser-access", options.signal);
66
- return this.transport.json("/v1/access/publishable-keys", "POST", { requestId: id(), name: options.name, origins: [...options.origins] }, options.signal);
67
- }
68
- /** Replaces the key's origins. */
69
- async update(keyId, options) {
70
- await this.transport.requireFeature("browser-access", options.signal);
71
- return this.transport.json(`/v1/access/publishable-keys/${segment(keyId)}`, "PUT", { requestId: id(), origins: [...options.origins] }, options.signal);
72
- }
73
- /** Revokes the key: every request that sends it gets the opaque 404. */
74
- async revoke(keyId, options = {}) {
75
- await this.transport.requireFeature("browser-access", options.signal);
76
- return this.transport.json(`/v1/access/publishable-keys/${segment(keyId)}`, "DELETE", undefined, options.signal);
77
- }
78
- }
79
24
  export class AccessClient {
80
25
  transport;
81
26
  signingKeys;
82
- publishableKeys;
83
27
  constructor(transport) {
84
28
  this.transport = transport;
85
29
  this.signingKeys = new SigningKeysClient(transport);
86
- this.publishableKeys = new PublishableKeysClient(transport);
87
- }
88
- async getPolicy(options = {}) {
89
- await this.transport.requireFeature(FEATURE, options.signal);
90
- const reply = await this.transport.json("/v1/access/policy", "GET", undefined, options.signal);
91
- return reply.policy;
92
- }
93
- async putPolicy(policy, options = {}) {
94
- await this.transport.requireFeature(FEATURE, options.signal);
95
- const reply = await this.transport.json("/v1/access/policy", "PUT", { requestId: id(), policy }, options.signal);
96
- return reply.policy;
97
- }
98
- /**
99
- * Ends every token minted so far for `subject` and its open streams. Running turns continue;
100
- * cancel them separately if they must stop.
101
- */
102
- async revokeSubject(subject, options = {}) {
103
- await this.transport.requireFeature(FEATURE, options.signal);
104
- return this.transport.json("/v1/access/revocations", "POST", { requestId: id(), subject }, options.signal);
105
30
  }
106
- /** The public keys that verify the Tenant's subject tokens. */
31
+ /** The public keys that verify the Tenant's delivery tokens. */
107
32
  async jwks(options = {}) {
108
- await this.transport.requireFeature(FEATURE, options.signal);
109
33
  return this.transport.json("/v1/access/jwks", "GET", undefined, options.signal);
110
34
  }
111
35
  }