nylorun 0.7.0-beta → 0.8.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 +51 -0
- package/README.md +50 -10
- package/dist/cli.js +7 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +10 -0
- package/dist/sandbox/cluster-file.d.ts +47 -0
- package/dist/sandbox/cluster-file.js +55 -0
- package/dist/sandbox/commands.d.ts +41 -0
- package/dist/sandbox/commands.js +281 -0
- package/dist/sandbox/kubectl.d.ts +39 -0
- package/dist/sandbox/kubectl.js +70 -0
- package/dist/sandbox/manifests.d.ts +41 -0
- package/dist/sandbox/manifests.js +138 -0
- package/dist/sandbox/probe.d.ts +44 -0
- package/dist/sandbox/probe.js +137 -0
- package/dist/stack/commands.d.ts +53 -1
- package/dist/stack/commands.js +141 -23
- package/dist/stack/compose-file.d.ts +42 -5
- package/dist/stack/compose-file.js +207 -15
- package/dist/stack/env-file.d.ts +48 -1
- package/dist/stack/env-file.js +81 -4
- package/dist/stack/host-files.js +11 -1
- package/dist/stack/images.d.ts +8 -2
- package/dist/stack/images.js +8 -2
- package/dist/stack/index.d.ts +3 -0
- package/dist/stack/index.js +6 -0
- package/dist/stack/paths.d.ts +15 -0
- package/dist/stack/paths.js +7 -0
- package/dist/stack/prepare.d.ts +10 -2
- package/dist/stack/prepare.js +20 -2
- package/dist/stack/sandbox.d.ts +4 -0
- package/dist/stack/sandbox.js +106 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# nylorun
|
|
2
2
|
|
|
3
|
+
## 0.8.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
|
+
- e58e6ec: **egress-gate: pod sandboxes reach only the hosts their spec allows (F7.2).** A new `egress` service runs in the gateway's process (`--service gates,keys,egress`) on `NYLORUN_EGRESS_LISTEN_PORT` (default 4200). It is a CONNECT proxy: each tunnel needs an egress token (`Proxy-Authorization`, Basic with the token as password or Bearer) for a live sandbox at its current host epoch, a host name (no IP literal) that the sandbox's `network.allow` names exactly or by `*.suffix`, port 443 or 80, and a name that resolves to a public address; the gate connects to that checked address and pipes bytes, with no TLS interception and no credential injection. Plain HTTP is answered `405`; a sandbox holds at most 64 tunnels; idle tunnels close after 5 minutes; refusals are logged, never emitted as events. Egress tokens (`typ: nylorun-egress+jwt`), minted with each host token at a pod's join, are accepted nowhere else; egress-gate also refuses a token of a sandbox that is deleted, lost or expired, or of a pod other than the one that joined. Sandbox spec resolution lets `pod` sandboxes allow `*.suffix` hosts within the Tenant's ceiling (virtual ones keep exact names). With sandboxes enabled, `nylorun` runs the gateway with `egress` and publishes it on `NYLORUN_SANDBOX_BIND` at `NYLORUN_SANDBOX_EGRESS_PORT`, next to the gates.
|
|
20
|
+
- 186ec3f: **A local Tenant has an Object store: RustFS, behind the Runtime's new `BlobStore` seam.** `nylorun start` adds a `rustfs` container (RustFS 1.0.1, single node and single drive, pinned by digest) on the `nylorun-<tenant>-rustfs` volume, unpublished and without its console. Its secret key is generated once into `docker/.env` (`NYLORUN_OBJECT_STORE_SECRET_KEY`), and only the `runtime` and `gateway` containers receive the credential (`NYLORUN_OBJECT_STORE_ENDPOINT`, `_ACCESS_KEY`, `_SECRET_KEY`); the runtime creates the bucket at boot. The Runtime reaches the store through `BlobStore` (put with a streamed body and a size cap, get with a byte range, head, delete, list by prefix) with an `s3` adapter over the plain S3 API and an `fs` adapter, which a Runtime without `NYLORUN_OBJECT_STORE_ENDPOINT` (embedded, ephemeral, tests) uses under the Tenant directory's `blobs/`. Nothing stores files there yet; file artifacts build on it.
|
|
21
|
+
- 3fd1cda: **The harness container, the network split, and Restate's UI closed by default (F6.2).** A local Tenant now runs agent turns, stdio MCP servers and workspaces in a `harness` container: the runtime image as `--service harness`, connected to the runtime's Harness API (`ws://runtime:4200/nylorun/harness/v1`, `NYLORUN_HARNESS=remote`) with `NYLORUN_HARNESS_TOKEN`, which `nylorun start` generates once and keeps in `.env`. The harness holds no other credential, mounts only the Tenant directory's `sandboxes/`, `plugin-data/`, `home/` and `tmp/` under `/harness`, publishes no port and is healthy once connected; `nylorun status` and `nylorun doctor` report it, and `nylorun logs harness` shows its log. `NYLORUN_HARNESS=in-process` in `.env` rolls back to turns in the runtime container (the harness service is then removed).
|
|
22
|
+
|
|
23
|
+
- Plugin roots: the Host root's `plugins/` directory is mounted read-only at its own path into the harness and runtime containers, so a stdio MCP server from a plugin under `~/.nylorun/tenants/<name>/plugins/` runs in the harness.
|
|
24
|
+
- Networks: `<project>-store` (internal) joins Postgres, s2-lite, Restate and RustFS to the runtime and the gateway only; `<project>-harness` joins the harness to the runtime and the gateway only; the default network keeps egress and the published ports.
|
|
25
|
+
- Restate's admin API and UI (unauthenticated) are no longer published. `nylorun start --restate-ui` (or `NYLORUN_RESTATE_UI=1`) publishes them on loopback for that start and prints the URL.
|
|
26
|
+
|
|
27
|
+
- 678e085: **Pod sandboxes (F7.2, second part).** With `nylorun sandbox enable`, a sandbox resource of kind `pod` (`PUT /v1/sandboxes/{id}` with `kind: "pod"`, an `image`, `*.suffix` hosts, `storage` and `lifecycle.ttl`) is an agent-sandbox pod on the Tenant's cluster, created at once. The turns of the sessions attached to it run in the pod: the engine is copied from the Runtime image into the pod (the Runtime image now carries tini for it), waits until the pod's NetworkPolicy is in force, exchanges its join token for a host token at the Harness API listener (`POST /nylorun/harness/v1/host/join`, published for pods on the Docker host's address, with the gates), and serves its sandbox alone. New: `POST /v1/sandboxes/{id}/stop` and `/reset`; the Tenant's `limits.ttl`, `lifecycle.onExpiry`, `lifecycle.stopGrace` and `placement`; idle stop; lifecycle events `sandbox.running`, `.suspended`, `.expired`, `.relaunched`, `.lost`, `.reset` and `.failed`; error codes `placement_refused`, `sandbox_lost` and `sandbox_expired`; `cluster` in `GET /v1/tenant/sandbox`; Host feature `sandbox-pods` (additive, protocol unchanged). Without a cluster, kind `pod` is `409 sandbox_unavailable` (it was 400). Migration `0007_sandbox_pods` adds the pod lifecycle columns to `sandbox_resources`.
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
30
|
+
- `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.
|
|
31
|
+
- 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.
|
|
32
|
+
- 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.
|
|
33
|
+
- The Tenant holds at most `limits.sandboxes` sandboxes (`PUT /v1/tenant/sandbox`, default 100); one more is `409 limit_exceeded`.
|
|
34
|
+
- 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.
|
|
35
|
+
- 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.
|
|
36
|
+
- `@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`.
|
|
37
|
+
- `nylorun sandbox ls [--label key=value]... [--json]` and `nylorun sandbox rm <id>` list and delete the running local Tenant's sandboxes.
|
|
38
|
+
|
|
39
|
+
- db045cd: **The sandboxes service and `nylorun sandbox enable` (F7.2, first part).** A new image, `ghcr.io/nylorun/sandboxes`, versioned with the Runtime, drives agent-sandbox v1.0.5 Sandboxes in one namespace per Tenant: `PUT`, `GET ?wait=` and `DELETE /v1/pods/{name}` with an operation id, `/ready` and `/v1/info`, behind a bearer token only the runtime container holds. `nylorun sandbox enable --context <name>` installs into that kubeconfig context only (the pinned controller when absent, the namespace `nylorun-sbx-<tenant>`, a ServiceAccount whose Role covers Sandbox lifecycle and join Secrets, no NetworkPolicy or exec rights), refuses a cluster whose NetworkPolicy it cannot prove enforced, records `<Host root>/sandboxes/cluster.json` and the token, and adds the `sandboxes` service to the Tenant; `nylorun sandbox disable` and `nylorun sandbox status` remove and report it. Sessions do not run in pods yet: that comes with the Harness API and egress-gate.
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- Pin core to the tested release.
|
|
44
|
+
- Pin runtime to the tested release.
|
|
45
|
+
- Pin studio to the tested release.
|
|
46
|
+
- Updated dependencies [b352feb]
|
|
47
|
+
- Updated dependencies [6077272]
|
|
48
|
+
- Updated dependencies [b6bf1f5]
|
|
49
|
+
- Updated dependencies [8ed4ea6]
|
|
50
|
+
- Updated dependencies [678e085]
|
|
51
|
+
- Updated dependencies [926711b]
|
|
52
|
+
- @nylorun/core@0.12.0-beta
|
|
53
|
+
|
|
3
54
|
## 0.7.0-beta
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -34,17 +34,23 @@ npx nylorun doctor # Node 24+, Docker, Compose v2, and the Tenan
|
|
|
34
34
|
## Commands
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
nylorun up|start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]
|
|
37
|
+
nylorun up|start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset] [--restate-ui]
|
|
38
38
|
# create (first run) and start the Tenant; link the project; print the URLs; open Studio signed in
|
|
39
|
+
# --restate-ui (or NYLORUN_RESTATE_UI=1): publish Restate's UI on loopback for this start
|
|
39
40
|
nylorun down|stop [--tenant <name> | --all] # stop the containers (--all: every Tenant's); keep volumes
|
|
40
41
|
nylorun status [--tenant <name>] [--json] # the Tenant, its Host root and id, services, endpoints, Runtime health
|
|
41
|
-
nylorun logs [service] [--tenant <name>] [-f] [--tail <n>] # postgres, restate, s2-lite, gateway, runtime, studio
|
|
42
|
+
nylorun logs [service] [--tenant <name>] [-f] [--tail <n>] # postgres, restate, s2-lite, rustfs, gateway, runtime, harness, studio, sandboxes
|
|
42
43
|
nylorun studio [--tenant <name>] [--no-open] # sign a browser in to Studio on the Tenant; starts it if needed
|
|
43
44
|
nylorun reset [--tenant <name>] [--yes] # delete the Tenant's volumes, Tenant directory and vault key; the next start creates it anew
|
|
44
45
|
nylorun ls [--json] # the Tenants on this machine, with their state, memory and URLs
|
|
45
46
|
nylorun delete <tenant> --yes # remove a Tenant: containers, volumes, Host root and vault key
|
|
47
|
+
nylorun sandbox ls [--tenant <name>] [--label <key=value>]... [--json] # the running Tenant's sandboxes
|
|
48
|
+
nylorun sandbox rm <id> [--tenant <name>] # delete a sandbox and its files
|
|
46
49
|
nylorun doctor [--json] # prerequisites and the Tenant's health
|
|
47
50
|
nylorun telemetry [status|enable|disable] # Studio's anonymous usage analytics
|
|
51
|
+
nylorun sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--no-pull]
|
|
52
|
+
nylorun sandbox disable [--tenant <name>] [--delete-namespace]
|
|
53
|
+
nylorun sandbox status [--tenant <name>] [--json] # pods on a Kubernetes context (see Sandboxes)
|
|
48
54
|
```
|
|
49
55
|
|
|
50
56
|
`up` and `down` are the Docker Compose spellings of `start` and `stop`: `down`
|
|
@@ -157,15 +163,24 @@ The Runtime registers the derived principals of `NYLORUN_DERIVED_PRINCIPALS`
|
|
|
157
163
|
`nylorun start` writes `compose.yaml` and `.env` (mode 0600) under
|
|
158
164
|
`~/.nylorun/tenants/<name>/docker/` on every start; `.env` keeps the ports and
|
|
159
165
|
secrets chosen on the first run. The services are `postgres`, `restate`,
|
|
160
|
-
`s2-lite`, `gateway`, `runtime` and `studio`. Container, network and volume
|
|
166
|
+
`s2-lite`, `rustfs`, `gateway`, `runtime`, `harness` and `studio`. Container, network and volume
|
|
161
167
|
names are global on the Docker engine, so each carries the Tenant's Compose
|
|
162
168
|
project, and each has the label `dev.nylorun.tenant: <name>`. For Tenant `shop`:
|
|
163
169
|
|
|
164
170
|
| Thing | Name |
|
|
165
171
|
| --- | --- |
|
|
166
|
-
| Containers | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-gateway`, `nylorun-shop-runtime`, `nylorun-shop-studio` |
|
|
167
|
-
|
|
|
168
|
-
| Volumes | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-workspaces` |
|
|
172
|
+
| Containers | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-rustfs`, `nylorun-shop-gateway`, `nylorun-shop-runtime`, `nylorun-shop-harness`, `nylorun-shop-studio` |
|
|
173
|
+
| Networks | `nylorun-shop` (egress, published ports), `nylorun-shop-store` (internal: the stores), `nylorun-shop-harness` (the harness, the runtime and the gateway) |
|
|
174
|
+
| Volumes | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-rustfs`, `nylorun-shop-workspaces` |
|
|
175
|
+
|
|
176
|
+
The `harness` container (the Runtime image as `--service harness`) runs agent
|
|
177
|
+
turns, stdio MCP servers and workspaces, apart from the `runtime` container. It
|
|
178
|
+
holds only the harness token (`NYLORUN_HARNESS_TOKEN` in `.env`), mounts only
|
|
179
|
+
the Tenant directory's `sandboxes/`, `plugin-data/`, `home/` and `tmp/`, and the
|
|
180
|
+
Host root's `plugins/` read-only at its own path: put a plugin whose stdio MCP
|
|
181
|
+
server the agent runs under `~/.nylorun/tenants/<name>/plugins/` and load it from
|
|
182
|
+
there. `NYLORUN_HARNESS=in-process` in `.env` rolls back to running them in the
|
|
183
|
+
`runtime` container (no `harness` container); `nylorun status` shows which.
|
|
169
184
|
|
|
170
185
|
Postgres initialises the Tenant's database with C collation. Restate runs with
|
|
171
186
|
its own defaults. A running Tenant uses about 1.2 GB, most of it Restate; stop
|
|
@@ -173,9 +188,11 @@ the Tenants you are not using (`nylorun stop`, or `nylorun stop --all`). The Run
|
|
|
173
188
|
this release (`package.json` `nylorun.runtime` and `nylorun.studio`);
|
|
174
189
|
`NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them (local builds,
|
|
175
190
|
CI). Ports publish on loopback only: the Runtime on `8787`, its operator
|
|
176
|
-
listener (Admin API) on `8788
|
|
177
|
-
|
|
178
|
-
|
|
191
|
+
listener (Admin API) on `8788` and Studio on `4161`, or free ports chosen on the
|
|
192
|
+
Tenant's first start, avoiding the ports other Tenants keep, and kept in its
|
|
193
|
+
`.env`. Restate's UI and admin API (unauthenticated) are not published; `nylorun
|
|
194
|
+
start --restate-ui` (or `NYLORUN_RESTATE_UI=1`) publishes them for that start on
|
|
195
|
+
`9070` (or the port kept in `.env`), and the next start without it closes them.
|
|
179
196
|
|
|
180
197
|
`start`, `status`, `ls` and `nylorun studio` give Studio as
|
|
181
198
|
`http://localhost:<port>` (`studio.url` in `status --json`). Studio has no
|
|
@@ -233,6 +250,29 @@ default names one (`PUT /v1/tenant/sandbox`). A local Tenant runs them on the
|
|
|
233
250
|
Runtime's **virtual** backend: an emulated shell in the Runtime process, with no
|
|
234
251
|
extra containers. It is not a VM or container boundary.
|
|
235
252
|
|
|
253
|
+
A sandbox can also be a resource with its own id (`PUT /v1/sandboxes/{id}`,
|
|
254
|
+
`client.sandboxes` in `@nylorun/agents`) that sessions attach to and that
|
|
255
|
+
outlives them. `nylorun sandbox ls` lists the running Tenant's sandboxes with
|
|
256
|
+
their state, attached sessions and labels; `nylorun sandbox rm <id>` deletes one
|
|
257
|
+
and its files (refused while a turn runs in it). Neither starts a stopped Tenant
|
|
258
|
+
(exit 3).
|
|
259
|
+
|
|
260
|
+
### Pods on a cluster (preview)
|
|
261
|
+
|
|
262
|
+
`nylorun sandbox enable --context <name>` prepares a Kubernetes context for the
|
|
263
|
+
Tenant's sandboxes as pods ([agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox)
|
|
264
|
+
v1.0.5): Docker Desktop's cluster (`--context docker-desktop`) or kind. It only
|
|
265
|
+
touches the context you name. It installs the pinned agent-sandbox controller
|
|
266
|
+
when the cluster has none (and refuses another version), creates the namespace
|
|
267
|
+
`nylorun-sbx-<tenant>` with a ServiceAccount limited to Sandbox lifecycle, proves
|
|
268
|
+
with a probe that the cluster enforces NetworkPolicy (and refuses it otherwise),
|
|
269
|
+
lets pods reach only three ports on the Docker host, and adds the `sandboxes`
|
|
270
|
+
service to the Tenant: the one container holding the cluster credentials
|
|
271
|
+
(`<Host root>/sandboxes/`). On kind under Linux, pass `--host-address 172.17.0.1`.
|
|
272
|
+
`nylorun sandbox disable` removes the service (`--delete-namespace` also deletes
|
|
273
|
+
the namespace and every sandbox in it). Sessions keep using the virtual backend
|
|
274
|
+
in this release.
|
|
275
|
+
|
|
236
276
|
## Host root
|
|
237
277
|
|
|
238
278
|
The **Host root** is `~/.nylorun/tenants/<name>/`, or `NYLORUN_HOME`. It is
|
|
@@ -255,7 +295,7 @@ containers, volumes and Host root: the vault key (KEK) and all its data go with
|
|
|
255
295
|
| 0 | Success |
|
|
256
296
|
| 1 | Generic failure, including a missing Docker or Compose v2 |
|
|
257
297
|
| 2 | Usage error (including no Tenant selected, or `delete` without `--yes`), or a removed or moved command |
|
|
258
|
-
| 3 | `status`: the Runtime is not answering; `stop`/`logs`: no Tenant yet; `delete`: no such Tenant; `start`: a Tenant created by nylorun 0.5 (reset it) |
|
|
298
|
+
| 3 | `status`: the Runtime is not answering; `sandbox`: the Tenant is not running; `stop`/`logs`: no Tenant yet; `delete`: no such Tenant; `start`: a Tenant created by nylorun 0.5 (reset it) |
|
|
259
299
|
| 4 | The Runtime port is held by another Host |
|
|
260
300
|
| 5 | `up`/`start`/`studio`: this nylorun pins a Runtime older than the one that last ran the Tenant; update nylorun, or `start --allow-downgrade` |
|
|
261
301
|
| 7 | The containers, the Tenant or Studio did not become ready (an unavailable Tenant's cause is printed) |
|
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import "./baseline.js";
|
|
3
3
|
import { baselineEnv } from "./baseline.js";
|
|
4
4
|
import { CliError } from "./errors.js";
|
|
5
|
-
import { isStackCommand, stackCommand, stackUsage, studioCommand, } from "./stack/index.js";
|
|
6
|
-
const usage = `nylorun <up|down|start|stop|status|logs|studio|reset|ls|delete|doctor|telemetry>
|
|
5
|
+
import { isStackCommand, sandboxCommand, sandboxUsage, stackCommand, stackUsage, studioCommand, } from "./stack/index.js";
|
|
6
|
+
const usage = `nylorun <up|down|start|stop|status|logs|studio|reset|ls|delete|sandbox|doctor|telemetry>
|
|
7
7
|
|
|
8
8
|
Local Tenants (Docker Compose), one per project:
|
|
9
9
|
${stackUsage}
|
|
10
|
+
${sandboxUsage}
|
|
10
11
|
doctor [--json] check Node, Docker and Compose v2, and the Tenant's health
|
|
11
12
|
telemetry [status|enable|disable] Studio's anonymous usage analytics (on unless disabled,
|
|
12
13
|
NYLORUN_TELEMETRY_DISABLED=1, DO_NOT_TRACK=1 or CI)
|
|
@@ -45,6 +46,10 @@ async function main() {
|
|
|
45
46
|
process.exitCode = await studioCommand(args, baselineEnv());
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
49
|
+
if (command === "sandbox") {
|
|
50
|
+
process.exitCode = await sandboxCommand(args, baselineEnv());
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
48
53
|
if (isStackCommand(command)) {
|
|
49
54
|
process.exitCode = await stackCommand(command, args, baselineEnv());
|
|
50
55
|
return;
|
package/dist/doctor.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface StackDoctorReport {
|
|
|
6
6
|
};
|
|
7
7
|
docker: Check;
|
|
8
8
|
compose?: Check;
|
|
9
|
-
tenant?: Pick<StackStatus, "name" | "project" | "home" | "state" | "runtime" | "tenant" | "studio" | "gateway">;
|
|
9
|
+
tenant?: Pick<StackStatus, "name" | "project" | "home" | "state" | "runtime" | "tenant" | "studio" | "gateway" | "harness">;
|
|
10
10
|
/** Why no Tenant was checked: none is selected here (a project without a link). */
|
|
11
11
|
noTenant?: string;
|
|
12
12
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -29,6 +29,7 @@ export async function doctorStack(options) {
|
|
|
29
29
|
...(status.tenant ? { tenant: status.tenant } : {}),
|
|
30
30
|
studio: status.studio,
|
|
31
31
|
gateway: status.gateway,
|
|
32
|
+
harness: status.harness,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
catch (error) {
|
|
@@ -40,6 +41,7 @@ export async function doctorStack(options) {
|
|
|
40
41
|
const broken = report.tenant?.state === "running" &&
|
|
41
42
|
(!report.tenant.runtime.healthy ||
|
|
42
43
|
!report.tenant.gateway.healthy ||
|
|
44
|
+
(report.tenant.harness.mode === "remote" && !report.tenant.harness.healthy) ||
|
|
43
45
|
report.tenant.tenant?.cause !== undefined);
|
|
44
46
|
const failed = !nodeOk || !checks.docker.ok || checks.compose?.ok === false || broken;
|
|
45
47
|
if (options.json) {
|
|
@@ -88,6 +90,14 @@ export async function doctorStack(options) {
|
|
|
88
90
|
? `✓ ${local.gateway.state} · combined packing (runtime: core,loop; gateway: gates)`
|
|
89
91
|
: `✗ ${local.gateway.state}: model calls fail; see nylorun logs gateway`,
|
|
90
92
|
]);
|
|
93
|
+
rows.push([
|
|
94
|
+
"harness",
|
|
95
|
+
local.harness.mode === "in-process"
|
|
96
|
+
? "- in-process (NYLORUN_HARNESS=in-process in .env)"
|
|
97
|
+
: local.harness.healthy
|
|
98
|
+
? `✓ ${local.harness.state} · remote (agent turns, MCP servers, workspaces)`
|
|
99
|
+
: `✗ ${local.harness.state}: turns do not run; see nylorun logs harness`,
|
|
100
|
+
]);
|
|
91
101
|
if (local.tenant)
|
|
92
102
|
rows.push([
|
|
93
103
|
"tenant id",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<Host root>/sandboxes/`: what the sandboxes service mounts read-only. `cluster.json`
|
|
3
|
+
* records the cluster `nylorun sandbox enable` installed into; `token` is the
|
|
4
|
+
* ServiceAccount token (0600). Only the sandboxes container reads them: the runtime
|
|
5
|
+
* container sees an empty mount there.
|
|
6
|
+
*/
|
|
7
|
+
export interface ClusterFile {
|
|
8
|
+
context: string;
|
|
9
|
+
/** The API server URL in the kubeconfig. */
|
|
10
|
+
server: string;
|
|
11
|
+
/** host:port the sandboxes container dials. */
|
|
12
|
+
dial: string;
|
|
13
|
+
/** The name the API server's certificate is verified against. */
|
|
14
|
+
tlsServerName: string;
|
|
15
|
+
/** The cluster CA, base64 PEM. */
|
|
16
|
+
caData: string;
|
|
17
|
+
namespace: string;
|
|
18
|
+
controllerVersion: string;
|
|
19
|
+
hostAddress: string;
|
|
20
|
+
bindAddress: string;
|
|
21
|
+
ports: {
|
|
22
|
+
harness: number;
|
|
23
|
+
gates: number;
|
|
24
|
+
egress: number;
|
|
25
|
+
};
|
|
26
|
+
networkPolicy: {
|
|
27
|
+
enforced: boolean;
|
|
28
|
+
probedAt: string;
|
|
29
|
+
};
|
|
30
|
+
enabledAt: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function sandboxesDir(root: string): string;
|
|
33
|
+
export declare function clusterFilePath(root: string): string;
|
|
34
|
+
/** Sandboxes are enabled for the Tenant under `root`. */
|
|
35
|
+
export declare function sandboxesEnabled(root: string): boolean;
|
|
36
|
+
export declare function readClusterFile(root: string): Promise<ClusterFile | undefined>;
|
|
37
|
+
export declare function writeClusterFiles(root: string, cluster: ClusterFile, token: string): Promise<void>;
|
|
38
|
+
export declare function removeClusterFiles(root: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* How the sandboxes container reaches the API server. One on this machine's loopback
|
|
41
|
+
* (Docker Desktop's `https://127.0.0.1:<port>`) is dialled at `host.docker.internal:<port>`
|
|
42
|
+
* while the certificate is still verified against the kubeconfig's host (Experiment 2, Q3).
|
|
43
|
+
*/
|
|
44
|
+
export declare function apiRoute(server: string): {
|
|
45
|
+
dial: string;
|
|
46
|
+
tlsServerName: string;
|
|
47
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export function sandboxesDir(root) {
|
|
6
|
+
return join(root, "sandboxes");
|
|
7
|
+
}
|
|
8
|
+
export function clusterFilePath(root) {
|
|
9
|
+
return join(sandboxesDir(root), "cluster.json");
|
|
10
|
+
}
|
|
11
|
+
/** Sandboxes are enabled for the Tenant under `root`. */
|
|
12
|
+
export function sandboxesEnabled(root) {
|
|
13
|
+
return existsSync(clusterFilePath(root));
|
|
14
|
+
}
|
|
15
|
+
export async function readClusterFile(root) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(await readFile(clusterFilePath(root), "utf8"));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function writeFileMode(path, text, mode) {
|
|
24
|
+
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
25
|
+
await writeFile(temporary, text, { mode });
|
|
26
|
+
await rename(temporary, path);
|
|
27
|
+
await chmod(path, mode);
|
|
28
|
+
}
|
|
29
|
+
export async function writeClusterFiles(root, cluster, token) {
|
|
30
|
+
const dir = sandboxesDir(root);
|
|
31
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
32
|
+
await chmod(dir, 0o700);
|
|
33
|
+
await writeFileMode(join(dir, "token"), token, 0o600);
|
|
34
|
+
await writeFileMode(clusterFilePath(root), `${JSON.stringify(cluster, null, 2)}\n`, 0o600);
|
|
35
|
+
}
|
|
36
|
+
export async function removeClusterFiles(root) {
|
|
37
|
+
await rm(sandboxesDir(root), { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
40
|
+
/**
|
|
41
|
+
* How the sandboxes container reaches the API server. One on this machine's loopback
|
|
42
|
+
* (Docker Desktop's `https://127.0.0.1:<port>`) is dialled at `host.docker.internal:<port>`
|
|
43
|
+
* while the certificate is still verified against the kubeconfig's host (Experiment 2, Q3).
|
|
44
|
+
*/
|
|
45
|
+
export function apiRoute(server) {
|
|
46
|
+
const url = new URL(server);
|
|
47
|
+
if (url.protocol !== "https:")
|
|
48
|
+
throw new Error(`the API server ${server} is not https`);
|
|
49
|
+
const port = url.port || "443";
|
|
50
|
+
const host = url.hostname;
|
|
51
|
+
const bare = host.replace(/^\[|\]$/g, "");
|
|
52
|
+
if (LOOPBACK.has(host) || LOOPBACK.has(bare))
|
|
53
|
+
return { dial: `host.docker.internal:${port}`, tlsServerName: bare };
|
|
54
|
+
return { dial: `${host}:${port}`, tlsServerName: bare };
|
|
55
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type StackDeps } from "../stack/commands.js";
|
|
2
|
+
import type { FetchLike } from "../stack/studio-login.js";
|
|
3
|
+
import { type ClusterFile } from "./cluster-file.js";
|
|
4
|
+
import { type KubectlRunner } from "./kubectl.js";
|
|
5
|
+
export declare const sandboxUsage = " sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--no-pull]\n run the Tenant's sandboxes as pods on that kubeconfig context (agent-sandbox v1.0.5)\n sandbox disable [--tenant <name>] [--delete-namespace]\n stop the sandboxes service and forget the cluster\n sandbox status [--tenant <name>] [--json]\n the cluster, the sandboxes service's readiness and what it reports";
|
|
6
|
+
/** The default workload image (D36), pre-pulled with the Runtime image. */
|
|
7
|
+
export declare const DEFAULT_SANDBOX_IMAGE = "python:3.13-slim";
|
|
8
|
+
export interface SandboxDeps {
|
|
9
|
+
stack: StackDeps;
|
|
10
|
+
kubectl: KubectlRunner;
|
|
11
|
+
/** Fetches the controller manifest; defaults to the stack's fetch. */
|
|
12
|
+
fetch?: FetchLike;
|
|
13
|
+
/** Poll interval and NetworkPolicy wait, for tests. */
|
|
14
|
+
pollMs?: number;
|
|
15
|
+
policyTimeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface InstallResult {
|
|
18
|
+
cluster: ClusterFile;
|
|
19
|
+
/** The ServiceAccount token. */
|
|
20
|
+
token: string;
|
|
21
|
+
/** Images the pre-pull could not pull. */
|
|
22
|
+
notPulled: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Steps 2–8 of `enable` against one context: controller, namespace and RBAC, NetworkPolicy
|
|
26
|
+
* probe, the namespace's policy, pre-pull. Returns what cluster.json records.
|
|
27
|
+
*/
|
|
28
|
+
export declare function installCluster(deps: SandboxDeps, input: {
|
|
29
|
+
context: string;
|
|
30
|
+
tenant: string;
|
|
31
|
+
hostAddress?: string;
|
|
32
|
+
bindAddress?: string;
|
|
33
|
+
prePull: readonly string[];
|
|
34
|
+
ports: {
|
|
35
|
+
harness: number;
|
|
36
|
+
gates: number;
|
|
37
|
+
egress: number;
|
|
38
|
+
};
|
|
39
|
+
}): Promise<InstallResult>;
|
|
40
|
+
/** `nylorun sandbox <enable|disable|status>`. */
|
|
41
|
+
export declare function runSandboxCommand(args: readonly string[], deps: SandboxDeps): Promise<number>;
|