@nylorun/agents 0.11.0-beta → 0.13.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,69 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.13.0-beta
4
+
5
+ ### Major Changes
6
+
7
+ - 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.
8
+
9
+ - **`@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.
10
+ - **`@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.
11
+ - **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`.
12
+ - **`@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.
13
+ - **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.
14
+ - **`@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.
15
+ - `@nylorun/admin`, `@nylorun/cli`, `nylorun`, `@nylorun/studio`: speak protocol 6.
16
+
17
+ ### Minor Changes
18
+
19
+ - 6077272: **Folder artifacts and the turn-end outputs export (F8.2).** Part of protocol 6, with file artifacts.
20
+
21
+ - **`@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.
22
+ - **`@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.
23
+ - **`@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`.
24
+ - **`@nylorun/agents`:** `client.artifacts.tree()`, `file()`, `diff()` and `zip()`, and `link(id, { file })`.
25
+
26
+ - 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.
27
+
28
+ - `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.
29
+ - 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.
30
+ - 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.
31
+ - The Tenant holds at most `limits.sandboxes` sandboxes (`PUT /v1/tenant/sandbox`, default 100); one more is `409 limit_exceeded`.
32
+ - 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.
33
+ - 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.
34
+ - `@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`.
35
+ - `nylorun sandbox ls [--label key=value]... [--json]` and `nylorun sandbox rm <id>` list and delete the running local Tenant's sandboxes.
36
+
37
+ ### Patch Changes
38
+
39
+ - Pin core to the tested release.
40
+ - Updated dependencies [b352feb]
41
+ - Updated dependencies [6077272]
42
+ - Updated dependencies [b6bf1f5]
43
+ - Updated dependencies [8ed4ea6]
44
+ - Updated dependencies [678e085]
45
+ - Updated dependencies [926711b]
46
+ - @nylorun/core@0.12.0-beta
47
+
48
+ ## 0.12.0-beta
49
+
50
+ ### Minor Changes
51
+
52
+ - 4b9906f: **Breaking: "Tenant" replaces "stack", and `nylorun start` works anywhere.** Each local installation holds one Tenant, and its name is the Tenant's name, so help, output, errors and docs say Tenant. See MIGRATION.md.
53
+
54
+ - **Breaking (`nylorun`): selection.** Every command acts on the Tenant `--tenant <name>` names (replaces `--name`, which is removed), else `NYLORUN_TENANT` (replaces `NYLORUN_STACK`), else the Project link's `tenant`; `start` in a project names a new one after the project directory. Outside a project, or with `start --no-link`, commands act on the Tenant `default`. In a project without a link, commands other than `start` exit 2 and list the machine's Tenants. A name starting with `tn_` (a Tenant id) is refused.
55
+ - **Breaking (`nylorun`): files.** Host roots are under `~/.nylorun/tenants/<name>/` (was `~/.nylorun/stacks/<name>/`) with `tenant.json` (was `stack.json`); the `.env` key is `NYLORUN_TENANT_NAME` (was `NYLORUN_STACK_NAME`); `NYLORUN_COMPOSE_PROJECT` replaces `NYLORUN_STACK_PROJECT`. Compose projects stay `nylorun-<name>`. Every command first moves 0.4 Host roots from `~/.nylorun/stacks/` to `~/.nylorun/tenants/` (renaming `stack.json` and the `.env` key), so they keep their volumes and keys. `nylorun ls` lists only directories with `tenant.json`.
56
+ - **Breaking (`nylorun`): removed.** `nylorun legacy` and all handling of the single stack of releases before 0.4; the hidden `nylorun stack <cmd>` alias; `nylorun doctor stack|runtime`. Output says Tenant: `ls` prints `TENANT` and JSON `{ "tenants": [...] }`, `status` prints the Tenant id on its own line, and `doctor`'s row is `tenant`.
57
+ - **Breaking: Project link format 3.** `.nylorun/link.json` is `{ "format": 3, "tenant", "tenantId", "hostUrl", "hostId" }`; `tenant` replaces `stack`. `@nylorun/agents`, `@nylorun/cli` and `@nylorun/admin` refuse an older link and name `npx nylorun start`, which rewrites it. `@nylorun/core`'s `ProjectLinkFileSchema` parses formats 0–3 with `tenant`.
58
+ - **Breaking (`@nylorun/admin`):** `createAdmin({ tenant })` replaces `{ stack }`, `tenantHostRoot(name)` replaces `stackHostRoot(name)`, and local resolution reads `NYLORUN_TENANT` and the link's `tenant`.
59
+ - `@nylorun/cli`: `status` and `endpoints` drop the `stack` line and JSON key; messages say Tenant. `@nylorun/agents`: a Runtime too old for the client says "update the Runtime (npx nylorun@latest start)". `@nylorun/studio`: setup hints say Tenant. `@nylorun/create-agent`: the next steps describe `npx nylorun@beta start` as this project's Tenant and its link.
60
+
61
+ ### Patch Changes
62
+
63
+ - Pin core to the tested release.
64
+ - Updated dependencies [4b9906f]
65
+ - @nylorun/core@0.11.0-beta
66
+
3
67
  ## 0.11.0-beta
4
68
 
5
69
  ### Major Changes
package/README.md CHANGED
@@ -30,7 +30,7 @@ reads the Tenant's public keys. Mark a long tool
30
30
  posts the result. `npx @nylorun/cli endpoints` shows each endpoint and how its
31
31
  deliveries are doing.
32
32
 
33
- The URL must be one the Runtime can reach: `localhost` on the local stack
33
+ The URL must be one the Runtime can reach: `localhost` on a local Tenant
34
34
  (its Runtime runs in Docker and maps `localhost` to this machine), a public URL
35
35
  in production, or a tunnel (ngrok, Cloudflare Tunnel) for a remote Runtime.
36
36
 
@@ -49,13 +49,14 @@ for upgrading from `nylorun serve`.
49
49
  2. Environment — if `NYLORUN_RUNTIME_URL` or `NYLORUN_SERVER_KEY` is set, both
50
50
  must be present
51
51
  3. Project link — `.nylorun/link.json` + `credentials.json` (the application
52
- key), written by `npx nylorun start`. A link from before one Tenant per
53
- installation (format 0 or 1) fails with `connection_missing`: run
54
- `npx nylorun start` in the project again.
52
+ key), written by `npx nylorun start`. A link from an older nylorun
53
+ (format 0 to 2) fails with `connection_missing`: run `npx nylorun start` in
54
+ the project again.
55
55
 
56
56
  Sources never mix. Partial environment fails with `connection_missing`. A
57
- Runtime serves one Tenant, so nothing names it: the `tenant` option and
58
- `NYLORUN_TENANT` are gone (protocol 5).
57
+ Runtime serves one Tenant, so nothing names it: the `tenant` option is gone
58
+ (protocol 5), and `NYLORUN_TENANT`, which picks a local Tenant for `nylorun`,
59
+ is ignored.
59
60
 
60
61
  ```ts
61
62
  import { Agent, createActionHandler, createClient, tool } from "@nylorun/agents";
@@ -106,7 +107,7 @@ eval "$(npx @nylorun/cli env)"
106
107
  # → NYLORUN_RUNTIME_URL, NYLORUN_SERVER_KEY
107
108
  ```
108
109
 
109
- 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
110
111
  serves one Tenant. Before the first authenticated
111
112
  request, `Transport` fetches `/health` once, checks protocol compatibility, and
112
113
  throws `IncompatibleRuntimeError` / `incompatible_host` when the Host range or
@@ -155,7 +156,22 @@ const session = await client.createSession({
155
156
 
156
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`.
157
158
 
158
- `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 subject token reaches only the sandboxes it is minted for, checked at every turn start: `client.tokens.create({ subject, role, sandboxes: ["team-a/*", "user-42"] })` takes exact ids or prefixes ending in `/*`. Creating and deleting through a subject token needs a role with `sandboxes:write`. The Tenant holds at most `limits.sandboxes` of them (`PUT /v1/tenant/sandbox`, default 100).
159
175
 
160
176
  Add an agent with `.subagents()` to let the model delegate to it:
161
177
 
@@ -262,7 +278,8 @@ directly and run as before; a flow agent can't be a child of them.
262
278
 
263
279
  The agents in a flow share one sandbox: open the flow's session with it,
264
280
  `createSession({ …, sandbox: { … } })`, and every agent, tool step and `verify` in the
265
- 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
266
283
  built-ins via `session.sandbox` (application) or `ctx.sandbox` (a tool).
267
284
  Observe with `session.observe({ follow: true })` to merge linked agent streams;
268
285
  `pending()` lists waits across the tree. Studio renders the manifest tree and
@@ -425,6 +442,56 @@ What the Runtime answers (Host feature `a2a-endpoint`):
425
442
  no header the partner chose except `A2A-Version` and `A2A-Extensions`. Without
426
443
  `a2a-endpoint` on the Runtime it answers `502` (`runtime_feature_missing`).
427
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()`, a subject 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
+
428
495
  ## Acting for a person (app servers)
429
496
 
430
497
  A server that signs people in and calls the Runtime for them (an "app server")
@@ -450,7 +517,7 @@ compatibility check, so calling `as()` per request is cheap.
450
517
 
451
518
  | Scope | Allows |
452
519
  | --- | --- |
453
- | `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel |
520
+ | `sessions:own` | The person's own sessions: create, list, read, stream, message, approve, respond, cancel; and their artifacts |
454
521
  | `vaults:own` | The person's own vaults and credentials |
455
522
  | `agents:read` | Listing the Tenant's agents |
456
523
  | `agents:write` | Saving agents; listing agents, models and providers |
@@ -45,7 +45,7 @@ export function createA2aHandler(options) {
45
45
  const client = await (options.client ?? createClient());
46
46
  const features = await client.hostFeatures();
47
47
  if (!features.includes(REQUIRED_FEATURE))
48
- throw new Problem(502, `The Runtime does not support ${REQUIRED_FEATURE}; upgrade the stack (nylorun up).`, "runtime_feature_missing");
48
+ throw new Problem(502, `The Runtime does not support ${REQUIRED_FEATURE}; update the Runtime (npx nylorun@latest start).`, "runtime_feature_missing");
49
49
  return client;
50
50
  })();
51
51
  ready.catch(() => {
package/dist/access.d.ts CHANGED
@@ -15,6 +15,11 @@ export interface CreateTokenOptions {
15
15
  scopes?: readonly TokenScope[];
16
16
  /** Narrow the role's agents. Default: all of the role's agents. */
17
17
  agents?: readonly string[];
18
+ /**
19
+ * The sandboxes the token reaches (its `sbx` claim): exact ids, or prefixes ending in `/*`
20
+ * (`team-a/*`). Default: none (Host feature `sandboxes`).
21
+ */
22
+ sandboxes?: readonly string[];
18
23
  /** 60 to 900 seconds, at most the policy's `maxTtlSeconds`. Default: that maximum. */
19
24
  ttlSeconds?: number;
20
25
  signal?: AbortSignal;
package/dist/access.js CHANGED
@@ -14,6 +14,7 @@ export class TokensClient {
14
14
  role: options.role,
15
15
  ...(options.scopes ? { scopes: [...options.scopes] } : {}),
16
16
  ...(options.agents ? { agents: [...options.agents] } : {}),
17
+ ...(options.sandboxes ? { sandboxes: [...options.sandboxes] } : {}),
17
18
  ...(options.ttlSeconds === undefined ? {} : { ttlSeconds: options.ttlSeconds }),
18
19
  }, options.signal);
19
20
  }
@@ -65,7 +65,7 @@ export function createAgUiHandler(options) {
65
65
  const features = await client.hostFeatures();
66
66
  const missing = REQUIRED_FEATURES.filter((f) => !features.includes(f));
67
67
  if (missing.length > 0)
68
- throw new Problem(502, `The Runtime does not support ${missing.join(", ")}; upgrade the stack (nylorun up).`, "runtime_feature_missing");
68
+ throw new Problem(502, `The Runtime does not support ${missing.join(", ")}; update the Runtime (npx nylorun@latest start).`, "runtime_feature_missing");
69
69
  return client;
70
70
  })();
71
71
  ready.catch(() => {
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Artifacts (protocol 6): `client.artifacts`. A file is uploaded in one streamed request and
3
+ * kept as an artifact with numbered versions; a session's artifacts appear in its history as
4
+ * `artifact.*` events. Download through the Runtime (with Range), or mint a capability link: a
5
+ * short-lived URL that needs no credential, for `<img>` tags and sharing. A message names an
6
+ * artifact in a file part (`session.inputParts`), and the model reads it.
7
+ *
8
+ * Folders (F8.2): at the end of each turn the Runtime exports `/workspace/outputs` of the
9
+ * session's sandbox as a version of its `outputs` folder. Read one as a tree, one file by path,
10
+ * a diff between versions or a zip.
11
+ *
12
+ * Acting for a person (`client.as`, a subject token), only the artifacts of their own sessions.
13
+ */
14
+ import type { ArtifactDiff, ArtifactLink, ArtifactTree, ArtifactView, DeleteArtifactResponse, UploadArtifactResponse } from "@nylorun/core/contracts";
15
+ import { type Transport } from "./http.js";
16
+ /** What an upload sends: bytes, a `Blob` or `File`, text, or a stream of bytes. */
17
+ export type ArtifactBody = Uint8Array | ArrayBuffer | Blob | string | ReadableStream<Uint8Array>;
18
+ export interface UploadArtifactOptions {
19
+ /** The file's name, e.g. `brief.pdf`. */
20
+ readonly name: string;
21
+ /** The session it belongs to; required when acting for a person. */
22
+ readonly sessionId?: string;
23
+ /** The media type. Default: a `Blob`'s type, else what the name implies. */
24
+ readonly contentType?: string;
25
+ readonly labels?: Readonly<Record<string, string>>;
26
+ readonly signal?: AbortSignal;
27
+ }
28
+ /** A capability link, with the absolute URL to open on this Runtime. */
29
+ export type ArtifactLinkWithUrl = ArtifactLink & {
30
+ readonly url: string;
31
+ };
32
+ export declare class ArtifactsClient {
33
+ private readonly transport;
34
+ constructor(transport: Transport);
35
+ private send;
36
+ /**
37
+ * Uploads a file as a new artifact (`POST /v1/artifacts`), in one streamed request. A file past
38
+ * the Tenant's per-file limit, or past its total, is refused with `413 limit_exceeded`.
39
+ */
40
+ upload(body: ArtifactBody, options: UploadArtifactOptions): Promise<UploadArtifactResponse>;
41
+ /** Uploads the artifact's next version. */
42
+ uploadVersion(artifactId: string, body: ArtifactBody, options?: {
43
+ contentType?: string;
44
+ signal?: AbortSignal;
45
+ }): Promise<UploadArtifactResponse>;
46
+ /** The artifacts of one session, or of every session this client reaches; oldest first. */
47
+ list(options?: {
48
+ sessionId?: string;
49
+ signal?: AbortSignal;
50
+ }): Promise<ArtifactView[]>;
51
+ /** The artifact, with every version. */
52
+ get(artifactId: string, options?: {
53
+ signal?: AbortSignal;
54
+ }): Promise<ArtifactView>;
55
+ /**
56
+ * A version's bytes as the Runtime's response, to stream (`response.body`) or read. With
57
+ * `range`, only those bytes (`206`, `Content-Range`). Default: the latest version.
58
+ */
59
+ download(artifactId: string, options?: {
60
+ version?: number;
61
+ /** Bytes `start` to `end`, both inclusive; without `end`, to the end. */
62
+ range?: {
63
+ start: number;
64
+ end?: number;
65
+ };
66
+ signal?: AbortSignal;
67
+ }): Promise<Response>;
68
+ /**
69
+ * A capability link to one version (default: the latest): anyone holding the URL may download
70
+ * it, with no credential, until it expires (default 5 minutes, at most 15) or the artifact is
71
+ * deleted. A folder's link opens its zip, or with `file` that one file.
72
+ */
73
+ link(artifactId: string, options?: {
74
+ version?: number;
75
+ expiresIn?: number;
76
+ file?: string;
77
+ signal?: AbortSignal;
78
+ }): Promise<ArtifactLinkWithUrl>;
79
+ /** A folder version's files (default: the latest), sorted by path. */
80
+ tree(artifactId: string, options?: {
81
+ version?: number;
82
+ signal?: AbortSignal;
83
+ }): Promise<ArtifactTree>;
84
+ /**
85
+ * One file of a folder version (default: the latest) by its path in the folder, as the
86
+ * Runtime's response; with `range`, only those bytes (`206`).
87
+ */
88
+ file(artifactId: string, path: string, options?: {
89
+ version?: number;
90
+ /** Bytes `start` to `end`, both inclusive; without `end`, to the end. */
91
+ range?: {
92
+ start: number;
93
+ end?: number;
94
+ };
95
+ signal?: AbortSignal;
96
+ }): Promise<Response>;
97
+ /**
98
+ * What changed in a folder from version `from` (default: the one before) to `version`
99
+ * (default: the latest): files added, removed and changed.
100
+ */
101
+ diff(artifactId: string, options?: {
102
+ version?: number;
103
+ from?: number;
104
+ signal?: AbortSignal;
105
+ }): Promise<ArtifactDiff>;
106
+ /** A folder version (default: the latest) as a zip archive, streamed: the Runtime's response. */
107
+ zip(artifactId: string, options?: {
108
+ version?: number;
109
+ signal?: AbortSignal;
110
+ }): Promise<Response>;
111
+ private versionPath;
112
+ /** Deletes the artifact, every version and its bytes; its links stop working. */
113
+ delete(artifactId: string, options?: {
114
+ signal?: AbortSignal;
115
+ }): Promise<DeleteArtifactResponse>;
116
+ }
@@ -0,0 +1,116 @@
1
+ import { segment } from "./http.js";
2
+ function contentTypeOf(body, declared) {
3
+ if (declared)
4
+ return declared;
5
+ if (typeof Blob !== "undefined" && body instanceof Blob && body.type)
6
+ return body.type;
7
+ return typeof body === "string" ? "text/plain; charset=utf-8" : "application/octet-stream";
8
+ }
9
+ export class ArtifactsClient {
10
+ transport;
11
+ constructor(transport) {
12
+ this.transport = transport;
13
+ }
14
+ async send(path, body, contentType, signal) {
15
+ const response = await this.transport.request(path, {
16
+ method: "POST",
17
+ headers: { "content-type": contentType },
18
+ body: body,
19
+ // A stream is sent as it is read (Node's fetch needs this said).
20
+ ...(typeof ReadableStream !== "undefined" && body instanceof ReadableStream
21
+ ? { duplex: "half" }
22
+ : {}),
23
+ ...(signal ? { signal } : {}),
24
+ });
25
+ return (await response.json());
26
+ }
27
+ /**
28
+ * Uploads a file as a new artifact (`POST /v1/artifacts`), in one streamed request. A file past
29
+ * the Tenant's per-file limit, or past its total, is refused with `413 limit_exceeded`.
30
+ */
31
+ upload(body, options) {
32
+ const query = new URLSearchParams({ name: options.name });
33
+ if (options.sessionId !== undefined)
34
+ query.set("sessionId", options.sessionId);
35
+ for (const [key, value] of Object.entries(options.labels ?? {}))
36
+ query.append("label", `${key}=${value}`);
37
+ return this.send(`/v1/artifacts?${query}`, body, contentTypeOf(body, options.contentType), options.signal);
38
+ }
39
+ /** Uploads the artifact's next version. */
40
+ uploadVersion(artifactId, body, options = {}) {
41
+ return this.send(`/v1/artifacts/${segment(artifactId)}/versions`, body, contentTypeOf(body, options.contentType), options.signal);
42
+ }
43
+ /** The artifacts of one session, or of every session this client reaches; oldest first. */
44
+ async list(options = {}) {
45
+ const query = options.sessionId === undefined ? "" : `?sessionId=${segment(options.sessionId)}`;
46
+ const reply = await this.transport.json(`/v1/artifacts${query}`, "GET", undefined, options.signal);
47
+ return reply.artifacts;
48
+ }
49
+ /** The artifact, with every version. */
50
+ get(artifactId, options = {}) {
51
+ return this.transport.json(`/v1/artifacts/${segment(artifactId)}`, "GET", undefined, options.signal);
52
+ }
53
+ /**
54
+ * A version's bytes as the Runtime's response, to stream (`response.body`) or read. With
55
+ * `range`, only those bytes (`206`, `Content-Range`). Default: the latest version.
56
+ */
57
+ download(artifactId, options = {}) {
58
+ const range = options.range;
59
+ return this.transport.request(`/v1/artifacts/${segment(artifactId)}/versions/${options.version ?? "latest"}/content`, {
60
+ method: "GET",
61
+ ...(range ? { headers: { range: `bytes=${range.start}-${range.end ?? ""}` } } : {}),
62
+ ...(options.signal ? { signal: options.signal } : {}),
63
+ });
64
+ }
65
+ /**
66
+ * A capability link to one version (default: the latest): anyone holding the URL may download
67
+ * it, with no credential, until it expires (default 5 minutes, at most 15) or the artifact is
68
+ * deleted. A folder's link opens its zip, or with `file` that one file.
69
+ */
70
+ async link(artifactId, options = {}) {
71
+ const link = await this.transport.json(`/v1/artifacts/${segment(artifactId)}/links`, "POST", {
72
+ ...(options.version === undefined ? {} : { version: options.version }),
73
+ ...(options.expiresIn === undefined ? {} : { expiresIn: options.expiresIn }),
74
+ ...(options.file === undefined ? {} : { file: options.file }),
75
+ }, options.signal);
76
+ return { ...link, url: `${this.transport.url}${link.path}` };
77
+ }
78
+ /** A folder version's files (default: the latest), sorted by path. */
79
+ tree(artifactId, options = {}) {
80
+ return this.transport.json(`${this.versionPath(artifactId, options.version)}/tree`, "GET", undefined, options.signal);
81
+ }
82
+ /**
83
+ * One file of a folder version (default: the latest) by its path in the folder, as the
84
+ * Runtime's response; with `range`, only those bytes (`206`).
85
+ */
86
+ file(artifactId, path, options = {}) {
87
+ const range = options.range;
88
+ return this.transport.request(`${this.versionPath(artifactId, options.version)}/files/${encodeURIComponent(path)}`, {
89
+ method: "GET",
90
+ ...(range ? { headers: { range: `bytes=${range.start}-${range.end ?? ""}` } } : {}),
91
+ ...(options.signal ? { signal: options.signal } : {}),
92
+ });
93
+ }
94
+ /**
95
+ * What changed in a folder from version `from` (default: the one before) to `version`
96
+ * (default: the latest): files added, removed and changed.
97
+ */
98
+ diff(artifactId, options = {}) {
99
+ const query = options.from === undefined ? "" : `?from=${options.from}`;
100
+ return this.transport.json(`${this.versionPath(artifactId, options.version)}/diff${query}`, "GET", undefined, options.signal);
101
+ }
102
+ /** A folder version (default: the latest) as a zip archive, streamed: the Runtime's response. */
103
+ zip(artifactId, options = {}) {
104
+ return this.transport.request(`${this.versionPath(artifactId, options.version)}/zip`, {
105
+ method: "GET",
106
+ ...(options.signal ? { signal: options.signal } : {}),
107
+ });
108
+ }
109
+ versionPath(artifactId, version) {
110
+ return `/v1/artifacts/${segment(artifactId)}/versions/${version ?? "latest"}`;
111
+ }
112
+ /** Deletes the artifact, every version and its bytes; its links stop working. */
113
+ delete(artifactId, options = {}) {
114
+ return this.transport.json(`/v1/artifacts/${segment(artifactId)}`, "DELETE", undefined, options.signal);
115
+ }
116
+ }
package/dist/browser.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CredentialSelection, VaultInfo } from "@nylorun/core/contracts";
2
2
  import type { TokenSource } from "./http.js";
3
3
  import { AgentsClient, type SessionClient } from "./session-client.js";
4
+ import type { ArtifactsClient } from "./artifacts.js";
4
5
  /** What the token callback returns: the token, or the app server's `{ token, expiresAt }`. */
5
6
  export type TokenReply = string | {
6
7
  token: string;
@@ -58,12 +59,18 @@ export declare class BrowserClient {
58
59
  sessions: import("./session-client.js").SessionView[];
59
60
  }>;
60
61
  session(sessionId: string): SessionClient;
62
+ /** The person's file artifacts, in their own sessions (protocol 6). */
63
+ get artifacts(): ArtifactsClient;
61
64
  /** A session of the person's own; the owner is always the token's subject. */
62
65
  createSession(options: {
63
66
  id?: string;
64
67
  agentId: string;
65
68
  vaultIds?: readonly string[];
66
69
  credentialSelections?: readonly CredentialSelection[];
70
+ /** A sandbox the token's `sandboxes` grants reach (Host feature `sandboxes`). */
71
+ sandbox?: {
72
+ id: string;
73
+ };
67
74
  }): Promise<SessionClient>;
68
75
  createVault(options: {
69
76
  name: string;
package/dist/browser.js CHANGED
@@ -153,6 +153,10 @@ export class BrowserClient {
153
153
  session(sessionId) {
154
154
  return this.client.session(sessionId);
155
155
  }
156
+ /** The person's file artifacts, in their own sessions (protocol 6). */
157
+ get artifacts() {
158
+ return this.client.artifacts;
159
+ }
156
160
  /** A session of the person's own; the owner is always the token's subject. */
157
161
  async createSession(options) {
158
162
  return this.client.createSession({
@@ -11,7 +11,7 @@ export class ConnectionError extends Error {
11
11
  }
12
12
  function missing(tried) {
13
13
  throw new ConnectionError(`connection_missing: no Runtime connection found (tried ${tried.join(", ")}). ` +
14
- `Run "npx nylorun start" in this project to start its stack and link it, ` +
14
+ `Run "npx nylorun start" in this project to start its Tenant and link it, ` +
15
15
  `or set NYLORUN_RUNTIME_URL and NYLORUN_SERVER_KEY.`);
16
16
  }
17
17
  function stripTrailingSlash(url) {
@@ -37,11 +37,10 @@ async function readProjectLink(startDir) {
37
37
  throw error;
38
38
  }
39
39
  const link = ProjectLinkFileSchema.parse(JSON.parse(linkRaw));
40
- // Formats 0 and 1 named a Tenant on a multi-Tenant Host of an older Runtime.
41
- if (link.format < 2)
42
- throw new ConnectionError(`connection_missing: the Project link at ${linkPath} is for a stack of an older ` +
43
- `Runtime, before one Tenant per installation. Run "npx nylorun start" in this project ` +
44
- `to start its own stack and link it again.`);
40
+ // Formats 0 to 2 are from older releases.
41
+ if (link.format < 3)
42
+ throw new ConnectionError(`connection_missing: the Project link at ${linkPath} is from an older nylorun. ` +
43
+ `Run "npx nylorun start" in this project to link it again.`);
45
44
  const credentials = ProjectCredentialsFileSchema.parse(JSON.parse(await readFile(join(nylorun, "credentials.json"), "utf8")));
46
45
  return {
47
46
  url: stripTrailingSlash(link.hostUrl),
package/dist/http.js CHANGED
@@ -168,7 +168,8 @@ export class Transport {
168
168
  if (this.publishableKey)
169
169
  headers.set(PUBLISHABLE_KEY_HEADER, this.publishableKey);
170
170
  headers.set(PROTOCOL_HEADER, String(PROTOCOL_VERSION));
171
- if (init.body)
171
+ // JSON unless the caller says otherwise (an artifact upload sends the file's type).
172
+ if (init.body && !headers.has("content-type"))
172
173
  headers.set("Content-Type", "application/json");
173
174
  return headers;
174
175
  }
package/dist/index.d.ts CHANGED
@@ -4,10 +4,13 @@ export { Agent, AgentBuilder } from "./builder.js";
4
4
  export { AgentBuildError, AgentLifecycleError, Loop, Chain, Switch, Parallel, Map, VerdictSchema, isVerdict, withInstructions, withoutTools, WorkflowBuildError, isBuiltWorkflow, capability, CapabilityBuilder, flow, tool, defineSchema, ToolError, } from "@nylorun/core/define";
5
5
  export type { FlowAgentBuilder, Flow, Named, StageArgs, NestedStageArgs, CapabilityIdentity, } from "@nylorun/core/define";
6
6
  export type { AgentOptions, AgentManifest, BuiltAgent, BuiltWorkflow, LoopOptions, LoopDecideArgs, LoopVerifyArgs, LoopDecision, ChainOptions, SwitchOptions, ParallelOptions, MapOptions, Verdict, WorkflowBinding, WorkflowManifest, ToolDefinition, ToolExecutionContext, ToolOutcome, Patch, Decision, TurnDecision, HookScope, BeforeHook, AfterHook, JsonValue, JsonObject, } from "@nylorun/core/define";
7
- export type { LiveEvent, SessionEvent, SessionEventOf, EventType, SessionCommand, Action, ActionOutcome, CredentialInfo, CredentialSelection, VaultInfo, SubjectScope, } from "@nylorun/core/contracts";
7
+ export type { LiveEvent, SessionEvent, SessionEventOf, EventType, SessionCommand, Action, ActionOutcome, CredentialInfo, CredentialSelection, VaultInfo, SubjectScope, SandboxView, SandboxEvent, SandboxKind, MessagePart, ArtifactView, ArtifactVersionView, ArtifactLink, UploadArtifactResponse, ArtifactTree, ArtifactDiff, FolderEntry, } from "@nylorun/core/contracts";
8
8
  export { SUBJECT_SCOPES } from "@nylorun/core/contracts";
9
9
  export { AgentsClient, SessionClient, createClient } from "./client.js";
10
- export type { ActAsOptions, AgentSource, SessionView, CommandOptions, SessionSandbox, } from "./client.js";
10
+ export { ArtifactsClient } from "./artifacts.js";
11
+ export type { ArtifactBody, ArtifactLinkWithUrl, UploadArtifactOptions, } from "./artifacts.js";
12
+ export type { ActAsOptions, AgentSource, SessionView, CommandOptions, CreateSessionOptions, SessionSandbox, ForSessionOptions, SandboxSpec, SessionSandboxHandle, } from "./client.js";
13
+ export { SandboxesClient } from "./client.js";
11
14
  export { AccessClient, PublishableKeysClient, SigningKeysClient, TokensClient, } from "./access.js";
12
15
  export type { CreateTokenOptions } from "./access.js";
13
16
  export { createTokenEndpoint } from "./token-endpoint.js";
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ export { Agent, AgentBuilder } from "./builder.js";
4
4
  export { AgentBuildError, AgentLifecycleError, Loop, Chain, Switch, Parallel, Map, VerdictSchema, isVerdict, withInstructions, withoutTools, WorkflowBuildError, isBuiltWorkflow, capability, CapabilityBuilder, flow, tool, defineSchema, ToolError, } from "@nylorun/core/define";
5
5
  export { SUBJECT_SCOPES } from "@nylorun/core/contracts";
6
6
  export { AgentsClient, SessionClient, createClient } from "./client.js";
7
+ export { ArtifactsClient } from "./artifacts.js";
8
+ export { SandboxesClient } from "./client.js";
7
9
  export { AccessClient, PublishableKeysClient, SigningKeysClient, TokensClient, } from "./access.js";
8
10
  export { createTokenEndpoint } from "./token-endpoint.js";
9
11
  export { createActionHandler } from "./action-handler.js";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Sandboxes as a resource (Host feature `sandboxes`): `client.sandboxes`. A sandbox has its own
3
+ * id and outlives the sessions attached to it; whether it serves one session, one person or a
4
+ * project is the app's choice, made here:
5
+ *
6
+ * - `sandboxes.forSession(...)`: one sandbox for one session. It creates the sandbox, opens the
7
+ * session attached to it, and `release()` deletes the sandbox when the session is done. It
8
+ * replaces sharing through another session (`sandbox: { session }`, `sandboxOwnerId`).
9
+ * - `sandboxes.ensure(id, spec)`: one sandbox per person or per project, by a deterministic id
10
+ * (`user-42`, `team-a/proj-42`); get-or-create in one call. Attach sessions to it with
11
+ * `createSession({ sandbox: { id } })`.
12
+ *
13
+ * Ids may hold `/`; they are sent percent-encoded as one path segment.
14
+ */
15
+ import { type DeleteSandboxResponse, type PutSandboxRequest, type SandboxEvent, type SandboxView } from "@nylorun/core/contracts";
16
+ import { type Transport } from "./http.js";
17
+ import type { CreateSessionOptions, SessionClient } from "./session-client.js";
18
+ /** What a sandbox is made of: fixed once it exists, except its labels. */
19
+ export type SandboxSpec = Omit<PutSandboxRequest, "requestId">;
20
+ /** A session opened on a sandbox of its own (`sandboxes.forSession`). */
21
+ export interface SessionSandboxHandle {
22
+ readonly session: SessionClient;
23
+ readonly sandbox: SandboxView;
24
+ /** Deletes the sandbox and its files; call it when the session is done. */
25
+ release(options?: {
26
+ signal?: AbortSignal;
27
+ }): Promise<DeleteSandboxResponse>;
28
+ }
29
+ export interface ForSessionOptions {
30
+ /** The session to open, as `createSession` takes it, without `sandbox`. */
31
+ readonly session: Omit<CreateSessionOptions, "sandbox">;
32
+ /** The sandbox's spec and labels. Default: a virtual sandbox within the Tenant's defaults. */
33
+ readonly spec?: SandboxSpec;
34
+ /** The sandbox's id. Default: `sessions/<session id>`. */
35
+ readonly sandboxId?: string;
36
+ readonly signal?: AbortSignal;
37
+ }
38
+ export declare class SandboxesClient {
39
+ private readonly transport;
40
+ /** `AgentsClient.createSession`, bound. */
41
+ private readonly createSession;
42
+ constructor(transport: Transport,
43
+ /** `AgentsClient.createSession`, bound. */
44
+ createSession: (options: CreateSessionOptions) => Promise<SessionClient>);
45
+ private path;
46
+ /**
47
+ * Creates the sandbox, or finds the one with this id (`PUT /v1/sandboxes/{id}`). Its spec is
48
+ * fixed once it exists: a different one is a 409. Labels, when given, replace its labels.
49
+ */
50
+ ensure(sandboxId: string, spec?: SandboxSpec, options?: {
51
+ signal?: AbortSignal;
52
+ }): Promise<SandboxView>;
53
+ get(sandboxId: string, options?: {
54
+ signal?: AbortSignal;
55
+ }): Promise<SandboxView>;
56
+ /** The sandboxes with every label in `labels` that this client reaches. */
57
+ list(options?: {
58
+ labels?: Readonly<Record<string, string>>;
59
+ signal?: AbortSignal;
60
+ }): Promise<SandboxView[]>;
61
+ /** Deletes the sandbox and its files; `deleted` is false when there was none. */
62
+ delete(sandboxId: string, options?: {
63
+ signal?: AbortSignal;
64
+ }): Promise<DeleteSandboxResponse>;
65
+ /** The sandbox's lifecycle events, from `from` on. */
66
+ events(sandboxId: string, options?: {
67
+ from?: number;
68
+ signal?: AbortSignal;
69
+ }): Promise<SandboxEvent[]>;
70
+ /**
71
+ * One sandbox for one session: creates the sandbox (`sessions/<session id>` unless
72
+ * `sandboxId` names another), opens the session attached to it, and returns both with
73
+ * `release()`, which deletes the sandbox. If the session cannot be opened, the sandbox is
74
+ * deleted again.
75
+ */
76
+ forSession(options: ForSessionOptions): Promise<SessionSandboxHandle>;
77
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Sandboxes as a resource (Host feature `sandboxes`): `client.sandboxes`. A sandbox has its own
3
+ * id and outlives the sessions attached to it; whether it serves one session, one person or a
4
+ * project is the app's choice, made here:
5
+ *
6
+ * - `sandboxes.forSession(...)`: one sandbox for one session. It creates the sandbox, opens the
7
+ * session attached to it, and `release()` deletes the sandbox when the session is done. It
8
+ * replaces sharing through another session (`sandbox: { session }`, `sandboxOwnerId`).
9
+ * - `sandboxes.ensure(id, spec)`: one sandbox per person or per project, by a deterministic id
10
+ * (`user-42`, `team-a/proj-42`); get-or-create in one call. Attach sessions to it with
11
+ * `createSession({ sandbox: { id } })`.
12
+ *
13
+ * Ids may hold `/`; they are sent percent-encoded as one path segment.
14
+ */
15
+ import { isSandboxId, } from "@nylorun/core/contracts";
16
+ import { id, segment } from "./http.js";
17
+ const FEATURE = "sandboxes";
18
+ export class SandboxesClient {
19
+ transport;
20
+ createSession;
21
+ constructor(transport,
22
+ /** `AgentsClient.createSession`, bound. */
23
+ createSession) {
24
+ this.transport = transport;
25
+ this.createSession = createSession;
26
+ }
27
+ path(sandboxId) {
28
+ if (!isSandboxId(sandboxId))
29
+ throw new TypeError(`${sandboxId} is not a sandbox id: up to 200 characters, /-separated segments of letters, digits, '.', '_' and '-'`);
30
+ return `/v1/sandboxes/${segment(sandboxId)}`;
31
+ }
32
+ /**
33
+ * Creates the sandbox, or finds the one with this id (`PUT /v1/sandboxes/{id}`). Its spec is
34
+ * fixed once it exists: a different one is a 409. Labels, when given, replace its labels.
35
+ */
36
+ async ensure(sandboxId, spec = {}, options = {}) {
37
+ const path = this.path(sandboxId);
38
+ await this.transport.requireFeature(FEATURE, options.signal);
39
+ return this.transport.json(path, "PUT", { requestId: id(), ...spec }, options.signal);
40
+ }
41
+ async get(sandboxId, options = {}) {
42
+ const path = this.path(sandboxId);
43
+ await this.transport.requireFeature(FEATURE, options.signal);
44
+ return this.transport.json(path, "GET", undefined, options.signal);
45
+ }
46
+ /** The sandboxes with every label in `labels` that this client reaches. */
47
+ async list(options = {}) {
48
+ await this.transport.requireFeature(FEATURE, options.signal);
49
+ const query = new URLSearchParams();
50
+ for (const [key, value] of Object.entries(options.labels ?? {}))
51
+ query.append("label", `${key}=${value}`);
52
+ const search = query.toString();
53
+ const reply = await this.transport.json(`/v1/sandboxes${search ? `?${search}` : ""}`, "GET", undefined, options.signal);
54
+ return reply.sandboxes;
55
+ }
56
+ /** Deletes the sandbox and its files; `deleted` is false when there was none. */
57
+ async delete(sandboxId, options = {}) {
58
+ const path = this.path(sandboxId);
59
+ await this.transport.requireFeature(FEATURE, options.signal);
60
+ return this.transport.json(path, "DELETE", undefined, options.signal);
61
+ }
62
+ /** The sandbox's lifecycle events, from `from` on. */
63
+ async events(sandboxId, options = {}) {
64
+ const path = this.path(sandboxId);
65
+ await this.transport.requireFeature(FEATURE, options.signal);
66
+ const reply = await this.transport.json(`${path}/events${options.from === undefined ? "" : `?from=${options.from}`}`, "GET", undefined, options.signal);
67
+ return reply.events;
68
+ }
69
+ /**
70
+ * One sandbox for one session: creates the sandbox (`sessions/<session id>` unless
71
+ * `sandboxId` names another), opens the session attached to it, and returns both with
72
+ * `release()`, which deletes the sandbox. If the session cannot be opened, the sandbox is
73
+ * deleted again.
74
+ */
75
+ async forSession(options) {
76
+ const sessionId = options.session.id ?? id();
77
+ const sandboxId = options.sandboxId ??
78
+ (isSandboxId(`sessions/${sessionId}`) ? `sessions/${sessionId}` : `sessions/${id()}`);
79
+ const sandbox = await this.ensure(sandboxId, options.spec ?? {}, {
80
+ ...(options.signal ? { signal: options.signal } : {}),
81
+ });
82
+ let session;
83
+ try {
84
+ session = await this.createSession({
85
+ ...options.session,
86
+ id: sessionId,
87
+ sandbox: { id: sandboxId },
88
+ });
89
+ }
90
+ catch (error) {
91
+ await this.delete(sandboxId).catch(() => undefined);
92
+ throw error;
93
+ }
94
+ return {
95
+ session,
96
+ sandbox,
97
+ release: (release = {}) => this.delete(sandboxId, release),
98
+ };
99
+ }
100
+ }
@@ -4,8 +4,10 @@
4
4
  * connection from the environment or the Project link.
5
5
  */
6
6
  import type { AgentManifest, BuiltAgent, BuiltWorkflow, JsonValue, SandboxToolName, WorkflowManifest } from "@nylorun/core/define";
7
- import { type SubjectScope, type CredentialInfo, type CredentialSelection, type SandboxRequest, type SessionCommand, type VaultInfo } from "@nylorun/core/contracts";
7
+ import { type SubjectScope, type CredentialInfo, type CredentialSelection, type MessagePart, type SandboxRequest, type SessionCommand, type VaultInfo } from "@nylorun/core/contracts";
8
8
  import { AccessClient, TokensClient } from "./access.js";
9
+ import { SandboxesClient } from "./sandboxes.js";
10
+ import { ArtifactsClient } from "./artifacts.js";
9
11
  import { Transport, type Destination } from "./http.js";
10
12
  /** Application-principal access to a session's sandbox built-ins (workflows.md §8). */
11
13
  export type SessionSandbox = {
@@ -28,6 +30,24 @@ export interface SessionView {
28
30
  waits?: unknown;
29
31
  [key: string]: unknown;
30
32
  }
33
+ /** `AgentsClient.createSession`'s options. */
34
+ export interface CreateSessionOptions {
35
+ id?: string;
36
+ agentId: string;
37
+ ownerUserId: string;
38
+ info?: Record<string, unknown>;
39
+ requestId?: string;
40
+ vaultIds?: readonly string[];
41
+ credentialSelections?: readonly CredentialSelection[];
42
+ /**
43
+ * The session's sandbox: omit for the Tenant default, `false` for none, `{ id }` to attach a
44
+ * sandbox resource (`client.sandboxes`, Host feature `sandboxes`), or an inline sandbox
45
+ * (`image`, `network.allow`, `resources`) checked against the Tenant's limits. `{ session }`
46
+ * shares another session's and is deprecated: use `client.sandboxes.forSession()` or
47
+ * `ensure()` and `{ id }`. Fixed once the session exists.
48
+ */
49
+ sandbox?: SandboxRequest;
50
+ }
31
51
  export interface ActAsOptions {
32
52
  /** What the subject may do; the Runtime grants nothing else. Default `["sessions:own"]`. */
33
53
  scopes?: readonly SubjectScope[];
@@ -51,6 +71,13 @@ export declare class AgentsClient {
51
71
  get tokens(): TokensClient;
52
72
  /** The access policy, signing keys and revocations (Host feature `subject-tokens`). */
53
73
  get access(): AccessClient;
74
+ /**
75
+ * Sandboxes as a resource (Host feature `sandboxes`): `ensure` one by id, `list` them by
76
+ * label, `delete` one, or give a session its own with `forSession`.
77
+ */
78
+ get sandboxes(): SandboxesClient;
79
+ /** File artifacts: upload, list, download with Range, capability links (protocol 6). */
80
+ get artifacts(): ArtifactsClient;
54
81
  /** The Runtime's protocol features, including optional ones such as `transcript-events`. */
55
82
  hostFeatures(options?: {
56
83
  signal?: AbortSignal;
@@ -72,21 +99,7 @@ export declare class AgentsClient {
72
99
  implementationVersion: string;
73
100
  requestId?: string;
74
101
  }): Promise<unknown>;
75
- createSession(options: {
76
- id?: string;
77
- agentId: string;
78
- ownerUserId: string;
79
- info?: Record<string, unknown>;
80
- requestId?: string;
81
- vaultIds?: readonly string[];
82
- credentialSelections?: readonly CredentialSelection[];
83
- /**
84
- * The session's sandbox: omit for the Tenant default, `false` for none, `{ session }` to share
85
- * another session's, or an inline sandbox (`image`, `network.allow`, `resources`) checked
86
- * against the Tenant's limits. Fixed once the session exists.
87
- */
88
- sandbox?: SandboxRequest;
89
- }): Promise<SessionClient>;
102
+ createSession(options: CreateSessionOptions): Promise<SessionClient>;
90
103
  createVault(options: {
91
104
  name: string;
92
105
  ownerUserId: string;
@@ -181,6 +194,17 @@ export declare class SessionClient {
181
194
  cursor: string | null;
182
195
  requestId: string;
183
196
  }>;
197
+ /**
198
+ * Start a turn with text and file parts (protocol 6): a file part names an artifact (upload
199
+ * it with `client.artifacts.upload`), at a version or its latest, and the model reads it, an
200
+ * image as an image and a text file as text.
201
+ */
202
+ inputParts(parts: readonly MessagePart[], options: CommandOptions): Promise<{
203
+ status: "accepted";
204
+ turnId: string | null;
205
+ cursor: string | null;
206
+ requestId: string;
207
+ }>;
184
208
  approve(interactionId: string, approved: boolean, options: CommandOptions): Promise<{
185
209
  status: "accepted";
186
210
  turnId: string | null;
@@ -280,3 +304,5 @@ export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
280
304
  export type { Destination, IncompatibleReason } from "./http.js";
281
305
  export type { LiveEvent } from "@nylorun/core/contracts";
282
306
  export { AccessClient, PublishableKeysClient, SigningKeysClient, TokensClient, } from "./access.js";
307
+ export { SandboxesClient } from "./sandboxes.js";
308
+ export type { ForSessionOptions, SandboxSpec, SessionSandboxHandle } from "./sandboxes.js";
@@ -3,6 +3,8 @@ import { z } from "zod";
3
3
  import { SCOPES_HEADER, SUBJECT_HEADER } from "@nylorun/core/compatibility";
4
4
  import { parseSessionEvent, parseSubjectHeaders, } from "@nylorun/core/contracts";
5
5
  import { AccessClient, TokensClient } from "./access.js";
6
+ import { SandboxesClient } from "./sandboxes.js";
7
+ import { ArtifactsClient } from "./artifacts.js";
6
8
  import { Transport, id, segment } from "./http.js";
7
9
  import { observeSSE } from "./sse.js";
8
10
  function sessionSandboxOf(transport, sessionId) {
@@ -104,6 +106,17 @@ export class AgentsClient {
104
106
  get access() {
105
107
  return new AccessClient(this.transport);
106
108
  }
109
+ /**
110
+ * Sandboxes as a resource (Host feature `sandboxes`): `ensure` one by id, `list` them by
111
+ * label, `delete` one, or give a session its own with `forSession`.
112
+ */
113
+ get sandboxes() {
114
+ return new SandboxesClient(this.transport, (options) => this.createSession(options));
115
+ }
116
+ /** File artifacts: upload, list, download with Range, capability links (protocol 6). */
117
+ get artifacts() {
118
+ return new ArtifactsClient(this.transport);
119
+ }
107
120
  /** The Runtime's protocol features, including optional ones such as `transcript-events`. */
108
121
  hostFeatures(options = {}) {
109
122
  return this.transport.hostFeatures(options.signal);
@@ -255,6 +268,19 @@ export class SessionClient {
255
268
  ? { ...base, content: value }
256
269
  : { ...base, data: value }, options.signal);
257
270
  }
271
+ /**
272
+ * Start a turn with text and file parts (protocol 6): a file part names an artifact (upload
273
+ * it with `client.artifacts.upload`), at a version or its latest, and the model reads it, an
274
+ * image as an image and a text file as text.
275
+ */
276
+ inputParts(parts, options) {
277
+ return this.command({
278
+ type: "message",
279
+ requestId: options.requestId ?? id(),
280
+ idempotencyKey: options.idempotencyKey,
281
+ parts: [...parts],
282
+ }, options.signal);
283
+ }
258
284
  approve(interactionId, approved, options) {
259
285
  return this.command({
260
286
  type: "approve",
@@ -373,3 +399,4 @@ export class SessionClient {
373
399
  }
374
400
  export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
375
401
  export { AccessClient, PublishableKeysClient, SigningKeysClient, TokensClient, } from "./access.js";
402
+ export { SandboxesClient } from "./sandboxes.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/agents",
3
- "version": "0.11.0-beta",
3
+ "version": "0.13.0-beta",
4
4
  "description": "Nylorun agent definitions, session client and Action endpoint",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -47,7 +47,7 @@
47
47
  "test": "vitest run --passWithNoTests"
48
48
  },
49
49
  "dependencies": {
50
- "@nylorun/core": "0.10.0-beta",
50
+ "@nylorun/core": "0.12.0-beta",
51
51
  "gray-matter": "^4.0.3"
52
52
  },
53
53
  "peerDependencies": {
@@ -56,7 +56,7 @@
56
56
  "devDependencies": {
57
57
  "@types/node": "^24.0.0",
58
58
  "typescript": "npm:@typescript/typescript6@^6.0.2",
59
- "vitest": "^5.0.1"
59
+ "vitest": "^5.0.2"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=24"