nylorun 0.7.0-beta → 0.9.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 +109 -0
- package/README.md +95 -25
- package/dist/cli.js +17 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +10 -0
- package/dist/project/link.d.ts +13 -7
- package/dist/project/link.js +13 -2
- 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 +71 -1
- package/dist/stack/commands.js +209 -38
- package/dist/stack/compose-file.d.ts +47 -7
- package/dist/stack/compose-file.js +223 -18
- package/dist/stack/env-file.d.ts +50 -14
- package/dist/stack/env-file.js +87 -40
- 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 +9 -0
- package/dist/stack/index.js +18 -1
- package/dist/stack/keys.d.ts +4 -0
- package/dist/stack/keys.js +72 -0
- package/dist/stack/mcp.d.ts +8 -0
- package/dist/stack/mcp.js +114 -0
- package/dist/stack/operator-keys.d.ts +39 -0
- package/dist/stack/operator-keys.js +146 -0
- package/dist/stack/paths.d.ts +32 -0
- package/dist/stack/paths.js +11 -0
- package/dist/stack/prepare.d.ts +12 -6
- package/dist/stack/prepare.js +24 -6
- package/dist/stack/sandbox.d.ts +4 -0
- package/dist/stack/sandbox.js +106 -0
- package/package.json +5 -5
- package/dist/project/derived-key.d.ts +0 -8
- package/dist/project/derived-key.js +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,114 @@
|
|
|
1
1
|
# nylorun
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- bd478ee: **Subject tokens, browser keys and derived principals leave open source (F9 I3, protocol 7).** Upgrade these with the Runtime: they speak protocol 7, which this Runtime serves beside 4, 5 and 6; protocol 6 clients keep working on every route that remains. Browsers and apps present a trusted issuer's token (F9 I2), servers an operator key (F9 I1), and a person's own credentials come from the operator's credential resolver (F9 C1). See MIGRATION.md.
|
|
8
|
+
|
|
9
|
+
- **Breaking (`@nylorun/runtime`, `@nylorun/core`): subject tokens are gone.** `POST /v1/tokens`, `POST /v1/access/revocations`, subject-token verification, revocation epochs and the `subject.revoked` stream signal are removed; the routes answer `404`, and a JWT no trusted issuer signed is the opaque `404`. A stream opened with a token now ends only at its expiry (`event: nylorun.closed`, reason `token_expired`; `StreamClosedFrame` loses `revoked`). Migration `0010_oss_auth_removals` drops the `subject_epochs`, `subject_usage` and `publishable_keys` tables. The `token` caller is a trusted issuer's token only: it loses `role`, `limits` and `epoch`, and `issuer` is always set; `GET /v1/me` no longer reports `via: token`. Core drops `SUBJECT_TOKEN_TYPE`, `SUBJECT_TOKEN_AUDIENCE`, `subjectTokenIssuer` (the Runtime's own tokens use `tenantTokenIssuer`, same value), `CreateTokenRequest`/`Response`, `RevokeSubject*`, `SubjectTokenClaims`, `TOKEN_TTL_MIN_SECONDS` and `TOKEN_TTL_DEFAULT_SECONDS`; `TOKEN_SCOPES` is `agents:read`, `sessions:own`, `sandboxes:write`.
|
|
10
|
+
- **Breaking (`@nylorun/runtime`, `@nylorun/core`): the access policy and role limits are gone.** `GET`/`PUT /v1/access/policy` answer `404`, a turn is never refused with `429 limit_exceeded` for a role (limit requests at your proxy), and an existing `access.policy` setting is ignored. Core drops `AccessPolicy*`, `AccessRole*`, `RoleLimits*`, `DEFAULT_ACCESS_POLICY`, `PutAccessPolicyRequest` and `ROLE_NAME_PATTERN`.
|
|
11
|
+
- **Breaking (`@nylorun/runtime`, `@nylorun/core`): publishable keys and the Runtime's CORS are gone.** `/v1/access/publishable-keys*` answer `404`, `Nylorun-Key` is ignored, `NYLORUN_BROWSER_ACCESS` and `browserAccess` (host.json, `createHost`, `startEphemeralRuntime`) are removed, and the Runtime sends no CORS headers. A request with `Origin` reaches the Tenant routes of the public listener with no toggle: a trusted issuer's token is served, an application key or delivery token is `403 origin_rejected`; admin routes, `/health`, `/ready` and the operator listener still refuse `Origin`. Any `OPTIONS` request is `204` with `Allow` and no CORS header: the operator's proxy answers preflights. Route declarations lose `browser` (`x-nylorun-browser` leaves the OpenAPI document) and the `publishable` credential; the token security scheme is `issuerToken`. Core drops `PUBLISHABLE_KEY_HEADER`, `PUBLISHABLE_KEY_PATTERN`, `PUBLISHABLE_KEY_ID_PATTERN`, `tenantOfPublishableKey`, `newPublishableKey`, `newPublishableKeyId`, `PublishableKey*`, `OriginEntrySchema`, `originAllowed` and `LOOPBACK_ORIGIN_WILDCARDS`.
|
|
12
|
+
- **Breaking (`@nylorun/runtime`, `@nylorun/core`, `@nylorun/agents`): vault routes are application-only, and `vaults:own` is retired.** Every `/v1/vaults` route takes an application key acting for no one (operator keys, Studio); a request acting for a subject or a trusted issuer's token gets `403 scope_required`. `vaults:own` leaves `SUBJECT_SCOPES`, `TOKEN_SCOPES` and the issuer scopes; `Nylorun-Scopes` may still name it and it grants nothing. Existing user vaults stay attachable to their own owner's sessions. `client.createVault` takes `{ scope: "installation" }` or `ownerUserId`, and `client.listVaults()` takes an optional owner.
|
|
13
|
+
- **Breaking (`@nylorun/runtime`, `@nylorun/core`, `@nylorun/admin`, `nylorun`): derived principals are gone.** Only Studio's key is derived from the admin key. `NYLORUN_DERIVED_PRINCIPALS` is ignored and no longer written, `hostPrincipals` takes no `derived`, `startEphemeralRuntime` no `derivedPrincipals`, and `@nylorun/admin` drops `deriveTenantKey`, `PROJECT_PRINCIPAL_ID` and `admin.deriveTenantKey`. Keys an earlier Host derived stay in the database and keep working as ordinary keys; replace them with operator keys. Core renames `DERIVED_PRINCIPAL_ID_PATTERN` to `APPLICATION_KEY_ID_PATTERN`.
|
|
14
|
+
- **Breaking (`@nylorun/agents`):** `createTokenEndpoint`, `createBrowserClient` and the `@nylorun/agents/browser` entry, `client.tokens`, `TokensClient`, `PublishableKeysClient`, `client.access.getPolicy`/`putPolicy`/`revokeSubject`/`publishableKeys` and `Destination.publishableKey` are removed. `client.access.signingKeys` and `client.access.jwks()` stay, and `Destination.token` takes a trusted issuer's tokens.
|
|
15
|
+
- **Breaking (`@nylorun/cli`):** `nylo access policy|keys|revoke|token` are removed and name what replaces them; `nylo access signing-keys list|rotate|revoke` stays.
|
|
16
|
+
- **Breaking (`@nylorun/core`, `@nylorun/agents`, `@nylorun/studio`): the Studio embed message `open.babai` is now `open.session`** (`{ sessionId }`): it asks the embedding app to open that session in its own UI. Studio sends no such message itself.
|
|
17
|
+
- **Breaking (`nylorun`): Studio embedding is opt-in.** No origin may frame Studio by default; list your app's origins with `nylorun start --studio-embed-origin <origin>`. A Tenant's `.env` from an earlier nylorun keeps the origins it had until `--studio-embed-origin-reset`.
|
|
18
|
+
- `PROTOCOL_VERSION` is 7 and `HOST_PROTOCOL` 4–7; the Host features `subject-tokens`, `browser-access` and `derived-principals` are removed. `@nylorun/studio` and `@nylorun/create-agent` speak protocol 7.
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- 8773586: **Installation vaults and a credential resolver (F9 C1).** A session's MCP credential now comes from its attached vaults, then from the operator's own credential resolver; nothing changes for existing vaults, and the protocol stays at 6.
|
|
23
|
+
|
|
24
|
+
- Installation vaults: `POST /v1/vaults` takes `scope: "installation"` (no `ownerUserId`) from an application key acting for no one; a request acting for a subject gets `403`. The vault is owned by `installation`, now a reserved subject like `host`. Any session may attach one and select its credentials. `GET /v1/vaults` from an application key lists them after the named person's vaults, and lists only them without `ownerUserId`; a request acting for a subject never sees one (the opaque `404`). The host model vault stays hidden and unattachable. Migration 0008 adds the scope.
|
|
25
|
+
- The credential resolver: `NYLORUN_RESOLVER_URL` and `NYLORUN_RESOLVER_TOKEN` on the gateway (`TenantConfig.resolver` and `startEphemeralRuntime({ resolver })` in process). When the session's vaults hold nothing for a remote MCP server's URL, the Runtime POSTs `{ owner, session, turn, target: { kind: "mcp", server, agent, url } }` with the resolver's bearer: `200 { headers, expiresAt? }` is used, `404` goes without a credential, and anything else or no answer within 5 s refuses the server with `credential_unavailable`. Owner and turn come from the session row. Answers are cached per owner and URL until `expiresAt`, at most 5 minutes (60 s without one), and concurrent misses share one request. See DEPLOYMENT.md, Credentials.
|
|
26
|
+
- `nylorun`: the gateway's Compose service passes `NYLORUN_RESOLVER_URL` and `NYLORUN_RESOLVER_TOKEN` from the shell that runs `nylorun start` (unset by default).
|
|
27
|
+
- Studio: the Vault page is now **Connections**, and creates installation vaults.
|
|
28
|
+
|
|
29
|
+
- e1bfb4b: **MCP OAuth connect for installation vaults (F9 C2).** The installation can sign in once to a remote MCP server that uses OAuth, and every session that attaches the vault uses the credential. Additive; the protocol stays at 6.
|
|
30
|
+
|
|
31
|
+
- `POST /v1/vaults/{vaultId}/oauth/start` (application keys acting for no one, installation vaults only) takes `{ url, server, clientId? }` and answers `{ authorizeUrl, expiresAt }`. The Runtime discovers the server's authorization server (RFC 9728, then RFC 8414), registers itself (RFC 7591) unless `clientId` names a registered client, and starts an S256 PKCE sign-in whose `state` works once, for ten minutes. A server without registration and no `clientId` is `400 oauth_client_required`.
|
|
32
|
+
- `GET /v1/oauth/callback` takes the browser back: anonymous and unversioned like an artifact link, it exchanges the code and stores an `oauth` credential bound to the URL, named after `server` (connecting again rotates it), and answers a small HTML page. An unknown, used or expired `state` is `oauth_state_invalid`; the authorization server's refusal is `oauth_failed`. The callback's base is `NYLORUN_PUBLIC_URL` (`TenantConfig.publicUrl`), else the start request's origin.
|
|
33
|
+
- Every OAuth step runs in the gateway's keys module (F9-D14): the runtime container never sees a token, the PKCE verifier or a client secret, and makes no outbound call. Migration 0009 adds `oauth_pending`, with the verifier and secret sealed under the vault key. `Keys` gains `startOAuth` and `finishOAuth`.
|
|
34
|
+
- OAuth requests, including refresh of every OAuth vault credential, now go through `guardedFetch` (`tenant/outbound.ts`): the `NYLORUN_ENDPOINT_*` address policy checked on the address connected to, no redirects, a bounded answer. With `NYLORUN_ENDPOINT_PRIVATE=refuse` a token endpoint on a private address is refused, where refresh used to call it.
|
|
35
|
+
- `@nylorun/core`: `ERROR_CODES` adds `oauth_client_required`, `oauth_state_invalid` and `oauth_failed`; `StartOAuthRequestSchema` and `StartOAuthResponseSchema`.
|
|
36
|
+
- `nylorun mcp connect <url> --server <name> [--vault <id>] [--client-id <id>]`: creates the installation vault `mcp` if needed, opens the sign-in page and waits up to 10 minutes for the credential. See DEPLOYMENT.md, "Connecting a remote MCP server with OAuth".
|
|
37
|
+
|
|
38
|
+
- 5cfaed9: **Operator keys, and project links on them (F9 I1).** The Admin API manages the Tenant's application keys by name: `PUT /v1/admin/keys/{id}` creates a key or rotates it and returns it once, `GET /v1/admin/keys` lists every key's id, role and issue time (never the keys), and `DELETE /v1/admin/keys/{id}` removes one. A rotated or deleted key stops authenticating on its next request. Ids follow `^[a-z][a-z0-9-]{0,31}$`; `studio` (derived from the admin key) is refused. Keys keep today's format (64 hex) and only their SHA-256 is stored, in the existing principals table (no migration). Host feature `operator-keys` (additive, protocol unchanged); core adds the `OperatorKey`, `ListOperatorKeysResponse`, `PutOperatorKeyResponse` and `DeleteOperatorKeyResponse` schemas.
|
|
39
|
+
|
|
40
|
+
- `@nylorun/admin`: `admin.keys.put(id)`, `admin.keys.list()` and `admin.keys.delete(id)`; they refuse with `incompatible_host` when the Host lacks `operator-keys`.
|
|
41
|
+
- `nylorun key put|list|rm <id>` manages a running local Tenant's keys; `put` prints the key once on stdout.
|
|
42
|
+
- `nylorun start` no longer derives the project's key. It keeps `.nylorun/credentials.json` while its key still reaches the Tenant (one authenticated read); otherwise it gives the project the operator key `project`, which the Host root keeps in `project-credentials.json` (0600) so every checkout linked to the Tenant shares it. An existing derived project key keeps working and is adopted.
|
|
43
|
+
- `nylorun sandbox` uses the linked project's key, or the operator key `cli` it puts once and keeps in `<Host root>/cli-credentials.json` (0600).
|
|
44
|
+
- The ephemeral Runtime (`startEphemeralRuntime`) no longer registers the derived `project` principal; pass `derivedPrincipals: ["project"]` to keep it. Derived principals (`NYLORUN_DERIVED_PRINCIPALS`, `deriveTenantKey`) still work on a Host.
|
|
45
|
+
|
|
46
|
+
- b906050: **Studio for a team (F9 S1).** Studio can sit behind a sign-in proxy such as oauth2-proxy. A request with no Studio session that carries a JWT, in `X-Forwarded-Access-Token` or as an `Authorization` bearer that is not a Studio session, signs in when the Runtime's `GET /v1/me` verifies it and reports the `studio` scope: Studio sets its usual signed session cookie, which records the subject for its write log and ends no later than the token (at most the usual 30 days), and serves the request. Studio never trusts the header unverified and keeps nothing else. A token without the scope gets `403` naming `studio`; a token the Runtime refuses, or one that is not a JWT, gets `401`. Admitted people get Studio's Tenant-wide view; the CLI sign-in and embedding are unchanged, and a valid embed bearer session still wins. New `NYLORUN_STUDIO_ALLOWED_HOSTS`: extra `Host` values Studio serves, comma-separated (`studio.acme.dev`); its state changes accept that host's `https` and `http` origins, and the cookie is `Secure` when the proxy sends `X-Forwarded-Proto: https`. The default is unchanged: only `localhost` and `127.0.0.1` on the published port. `nylorun start` passes `NYLORUN_STUDIO_ALLOWED_HOSTS` from its environment to the Studio container.
|
|
47
|
+
- c0b604e: **Trusted issuers (F9 I2).** The Tenant API accepts JWTs from the operator's own identity provider as bearers, configured in an identity file: `NYLORUN_IDENTITY_FILE` names a YAML file listing each issuer's `name`, `issuer`, `audience`, `jwks` URL or static PEM `keys`, a `subject` template over scalar claims (`u:{sub}`), `scopes` from a claim or a fixed list, `allowedScopes`, an optional `agents` allowlist, optional `sandboxes` grant templates (`{org_id}/*`) and `maxLifetime`. A malformed file stops the boot, naming the issuer and the field; a subject template without a claim is malformed. A bearer whose unverified `iss` names an issuer is verified with RS256, ES256 or EdDSA only, up to 16 KiB, `aud` matching, `exp` and `iat` required and `exp − iat` within `maxLifetime`, and becomes a token caller (`role: issuer:<name>`) with the issuer's scopes, agents and rendered sandbox grants; subject revocation does not reach it. JWKS keys are fetched only from the configured URL (no redirects), cached by `kid`, refetched at most once a minute for an unknown `kid`; while a JWKS is unreachable cached keys keep working and a new `kid` is `401 issuer_unavailable` (new error code). A browser request with an issuer token needs no publishable key (CORS comes from the operator's proxy). New: `GET /v1/me` reports the subject, scopes, agents, sandbox grants and `via` (`application:<id>`, `subject`, `token` or `issuer:<name>`) of any credential; the issuer-only scope `studio`; `ISSUER_SCOPES`, `CALLER_SCOPES` and `MeResponseSchema` in `@nylorun/core/contracts`; `parseIdentityFile` and `createTrustedIssuers` in `@nylorun/runtime`, and an `issuers` option on `startEphemeralRuntime`; Host feature `trusted-issuers` (additive, protocol unchanged). `nylorun start` sets `NYLORUN_IDENTITY_FILE=/nylorun/identity.yaml` on the runtime container when `<Host root>/identity.yaml` exists.
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- Pin core to the tested release.
|
|
52
|
+
- Pin runtime to the tested release.
|
|
53
|
+
- Pin studio to the tested release.
|
|
54
|
+
- Updated dependencies [8773586]
|
|
55
|
+
- Updated dependencies [e1bfb4b]
|
|
56
|
+
- Updated dependencies [5cfaed9]
|
|
57
|
+
- Updated dependencies [bd478ee]
|
|
58
|
+
- Updated dependencies [c0b604e]
|
|
59
|
+
- @nylorun/core@0.13.0-beta
|
|
60
|
+
|
|
61
|
+
## 0.8.0-beta
|
|
62
|
+
|
|
63
|
+
### Major Changes
|
|
64
|
+
|
|
65
|
+
- 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.
|
|
66
|
+
|
|
67
|
+
- **`@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.
|
|
68
|
+
- **`@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.
|
|
69
|
+
- **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`.
|
|
70
|
+
- **`@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.
|
|
71
|
+
- **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.
|
|
72
|
+
- **`@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.
|
|
73
|
+
- `@nylorun/admin`, `@nylorun/cli`, `nylorun`, `@nylorun/studio`: speak protocol 6.
|
|
74
|
+
|
|
75
|
+
### Minor Changes
|
|
76
|
+
|
|
77
|
+
- 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.
|
|
78
|
+
- 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.
|
|
79
|
+
- 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).
|
|
80
|
+
|
|
81
|
+
- 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.
|
|
82
|
+
- 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.
|
|
83
|
+
- 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.
|
|
84
|
+
|
|
85
|
+
- 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`.
|
|
86
|
+
- 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.
|
|
87
|
+
|
|
88
|
+
- `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.
|
|
89
|
+
- 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.
|
|
90
|
+
- 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.
|
|
91
|
+
- The Tenant holds at most `limits.sandboxes` sandboxes (`PUT /v1/tenant/sandbox`, default 100); one more is `409 limit_exceeded`.
|
|
92
|
+
- 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.
|
|
93
|
+
- 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.
|
|
94
|
+
- `@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`.
|
|
95
|
+
- `nylorun sandbox ls [--label key=value]... [--json]` and `nylorun sandbox rm <id>` list and delete the running local Tenant's sandboxes.
|
|
96
|
+
|
|
97
|
+
- 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.
|
|
98
|
+
|
|
99
|
+
### Patch Changes
|
|
100
|
+
|
|
101
|
+
- Pin core to the tested release.
|
|
102
|
+
- Pin runtime to the tested release.
|
|
103
|
+
- Pin studio to the tested release.
|
|
104
|
+
- Updated dependencies [b352feb]
|
|
105
|
+
- Updated dependencies [6077272]
|
|
106
|
+
- Updated dependencies [b6bf1f5]
|
|
107
|
+
- Updated dependencies [8ed4ea6]
|
|
108
|
+
- Updated dependencies [678e085]
|
|
109
|
+
- Updated dependencies [926711b]
|
|
110
|
+
- @nylorun/core@0.12.0-beta
|
|
111
|
+
|
|
3
112
|
## 0.7.0-beta
|
|
4
113
|
|
|
5
114
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -34,17 +34,27 @@ 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
|
|
49
|
+
nylorun key put <id> [--tenant <name>] # create or rotate the Tenant's operator key <id>; prints it once
|
|
50
|
+
nylorun key list [--tenant <name>] [--json] # the Tenant's keys: id, role, when issued
|
|
51
|
+
nylorun key rm <id> [--tenant <name>] # delete a key: it stops working at once
|
|
52
|
+
nylorun mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open] # sign the Tenant in to a remote MCP server with OAuth
|
|
46
53
|
nylorun doctor [--json] # prerequisites and the Tenant's health
|
|
47
54
|
nylorun telemetry [status|enable|disable] # Studio's anonymous usage analytics
|
|
55
|
+
nylorun sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--no-pull]
|
|
56
|
+
nylorun sandbox disable [--tenant <name>] [--delete-namespace]
|
|
57
|
+
nylorun sandbox status [--tenant <name>] [--json] # pods on a Kubernetes context (see Sandboxes)
|
|
48
58
|
```
|
|
49
59
|
|
|
50
60
|
`up` and `down` are the Docker Compose spellings of `start` and `stop`: `down`
|
|
@@ -131,8 +141,12 @@ its own `.gitignore` of `*`):
|
|
|
131
141
|
- `link.json`: `{ "format": 3, "tenant", "tenantId", "hostUrl", "hostId" }`.
|
|
132
142
|
`tenant` is the local Tenant's name; the Tenant id is information only:
|
|
133
143
|
nothing in a request selects a Tenant.
|
|
134
|
-
- `credentials.json` (mode 0600):
|
|
135
|
-
|
|
144
|
+
- `credentials.json` (mode 0600): `{ "format": 1, "applicationKey",
|
|
145
|
+
"principalId" }`, the operator key `project`. A later `start` keeps the file
|
|
146
|
+
while its key still reaches the Tenant (one authenticated read); otherwise it
|
|
147
|
+
writes the Tenant's `project` key, which the Host root keeps in
|
|
148
|
+
`project-credentials.json` (mode 0600) so every checkout linked to the Tenant
|
|
149
|
+
shares one key, or puts a new one through the Admin API.
|
|
136
150
|
|
|
137
151
|
Later starts reuse the Tenant and rewrite the link only when the Tenant's name,
|
|
138
152
|
URL, Host or id changed (after `nylorun reset`, for example). When it writes a
|
|
@@ -148,24 +162,52 @@ both share it. `--no-link` starts a Tenant without linking the working
|
|
|
148
162
|
directory (scripts). A link of an older nylorun (format 0 to 2) counts as no
|
|
149
163
|
link: `start` replaces it.
|
|
150
164
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
No key is derived from the admin key but Studio's. A project holds the operator
|
|
166
|
+
key `project`; a key an older nylorun derived for it keeps working while it
|
|
167
|
+
authenticates and is replaced by the operator key otherwise.
|
|
168
|
+
|
|
169
|
+
## Operator keys
|
|
170
|
+
|
|
171
|
+
`nylorun key put <id>` creates the running Tenant's key `<id>` (or rotates it:
|
|
172
|
+
the previous key stops working at once) and prints it once on stdout.
|
|
173
|
+
`nylorun key list [--json]` shows each key's id, role and when it was issued,
|
|
174
|
+
never the keys; `nylorun key rm <id>` deletes one. Ids match
|
|
175
|
+
`^[a-z][a-z0-9-]{0,31}$`; `studio` belongs to Studio and is refused. Give each
|
|
176
|
+
app server its own key. `nylorun sandbox` and `nylorun mcp` use the linked
|
|
177
|
+
project's key, or the key `cli` they put once and keep in
|
|
178
|
+
`<Host root>/cli-credentials.json` (mode 0600).
|
|
179
|
+
|
|
180
|
+
`nylorun mcp connect <url> --server <name>` signs the running Tenant in to a
|
|
181
|
+
remote MCP server that uses OAuth: it opens the server's sign-in page in the
|
|
182
|
+
browser and waits until the Runtime has stored the credential in the
|
|
183
|
+
installation vault `mcp` (created if needed; `--vault <id>` picks another),
|
|
184
|
+
which sessions attach with `vaultIds`. Pass `--client-id` when the server does
|
|
185
|
+
not let clients register themselves. See
|
|
186
|
+
[Connecting a remote MCP server with OAuth](../DEPLOYMENT.md#connecting-a-remote-mcp-server-with-oauth).
|
|
154
187
|
|
|
155
188
|
## The containers
|
|
156
189
|
|
|
157
190
|
`nylorun start` writes `compose.yaml` and `.env` (mode 0600) under
|
|
158
191
|
`~/.nylorun/tenants/<name>/docker/` on every start; `.env` keeps the ports and
|
|
159
192
|
secrets chosen on the first run. The services are `postgres`, `restate`,
|
|
160
|
-
`s2-lite`, `gateway`, `runtime` and `studio`. Container, network and volume
|
|
193
|
+
`s2-lite`, `rustfs`, `gateway`, `runtime`, `harness` and `studio`. Container, network and volume
|
|
161
194
|
names are global on the Docker engine, so each carries the Tenant's Compose
|
|
162
195
|
project, and each has the label `dev.nylorun.tenant: <name>`. For Tenant `shop`:
|
|
163
196
|
|
|
164
197
|
| Thing | Name |
|
|
165
198
|
| --- | --- |
|
|
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` |
|
|
199
|
+
| 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` |
|
|
200
|
+
| Networks | `nylorun-shop` (egress, published ports), `nylorun-shop-store` (internal: the stores), `nylorun-shop-harness` (the harness, the runtime and the gateway) |
|
|
201
|
+
| Volumes | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-rustfs`, `nylorun-shop-workspaces` |
|
|
202
|
+
|
|
203
|
+
The `harness` container (the Runtime image as `--service harness`) runs agent
|
|
204
|
+
turns, stdio MCP servers and workspaces, apart from the `runtime` container. It
|
|
205
|
+
holds only the harness token (`NYLORUN_HARNESS_TOKEN` in `.env`), mounts only
|
|
206
|
+
the Tenant directory's `sandboxes/`, `plugin-data/`, `home/` and `tmp/`, and the
|
|
207
|
+
Host root's `plugins/` read-only at its own path: put a plugin whose stdio MCP
|
|
208
|
+
server the agent runs under `~/.nylorun/tenants/<name>/plugins/` and load it from
|
|
209
|
+
there. `NYLORUN_HARNESS=in-process` in `.env` rolls back to running them in the
|
|
210
|
+
`runtime` container (no `harness` container); `nylorun status` shows which.
|
|
169
211
|
|
|
170
212
|
Postgres initialises the Tenant's database with C collation. Restate runs with
|
|
171
213
|
its own defaults. A running Tenant uses about 1.2 GB, most of it Restate; stop
|
|
@@ -173,9 +215,11 @@ the Tenants you are not using (`nylorun stop`, or `nylorun stop --all`). The Run
|
|
|
173
215
|
this release (`package.json` `nylorun.runtime` and `nylorun.studio`);
|
|
174
216
|
`NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them (local builds,
|
|
175
217
|
CI). Ports publish on loopback only: the Runtime on `8787`, its operator
|
|
176
|
-
listener (Admin API) on `8788
|
|
177
|
-
|
|
178
|
-
|
|
218
|
+
listener (Admin API) on `8788` and Studio on `4161`, or free ports chosen on the
|
|
219
|
+
Tenant's first start, avoiding the ports other Tenants keep, and kept in its
|
|
220
|
+
`.env`. Restate's UI and admin API (unauthenticated) are not published; `nylorun
|
|
221
|
+
start --restate-ui` (or `NYLORUN_RESTATE_UI=1`) publishes them for that start on
|
|
222
|
+
`9070` (or the port kept in `.env`), and the next start without it closes them.
|
|
179
223
|
|
|
180
224
|
`start`, `status`, `ls` and `nylorun studio` give Studio as
|
|
181
225
|
`http://localhost:<port>` (`studio.url` in `status --json`). Studio has no
|
|
@@ -195,14 +239,16 @@ out of another.
|
|
|
195
239
|
|
|
196
240
|
### Embedding Studio in a desktop app
|
|
197
241
|
|
|
198
|
-
A desktop
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`~/.nylorun/tenants/<name>/docker/.env`) may frame it
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`
|
|
205
|
-
|
|
242
|
+
A desktop or web app can show Studio inside its own window, in an iframe loaded
|
|
243
|
+
from Studio's URL (`studio.url` in `nylorun status --json`). Embedding is
|
|
244
|
+
opt-in: only exact origins listed in `NYLORUN_STUDIO_FRAME_ANCESTORS` (in
|
|
245
|
+
`~/.nylorun/tenants/<name>/docker/.env`) may frame it, and the list is empty by
|
|
246
|
+
default. Add the app's origins once with `nylorun start --studio-embed-origin
|
|
247
|
+
<origin>` (for example `app://localhost`, or a dev server's
|
|
248
|
+
`http://localhost:1420`); the list is kept across starts until
|
|
249
|
+
`--studio-embed-origin-reset`, and `nylorun status` shows it under `Embeds`.
|
|
250
|
+
Wildcards are refused. A Tenant started by an older nylorun keeps the origins it
|
|
251
|
+
had.
|
|
206
252
|
|
|
207
253
|
The app's backend mints a single-use login token for the Tenant with
|
|
208
254
|
`mintStudioLoginToken` from `@nylorun/admin` (it needs the admin key), and its
|
|
@@ -233,14 +279,38 @@ default names one (`PUT /v1/tenant/sandbox`). A local Tenant runs them on the
|
|
|
233
279
|
Runtime's **virtual** backend: an emulated shell in the Runtime process, with no
|
|
234
280
|
extra containers. It is not a VM or container boundary.
|
|
235
281
|
|
|
282
|
+
A sandbox can also be a resource with its own id (`PUT /v1/sandboxes/{id}`,
|
|
283
|
+
`client.sandboxes` in `@nylorun/agents`) that sessions attach to and that
|
|
284
|
+
outlives them. `nylorun sandbox ls` lists the running Tenant's sandboxes with
|
|
285
|
+
their state, attached sessions and labels; `nylorun sandbox rm <id>` deletes one
|
|
286
|
+
and its files (refused while a turn runs in it). Neither starts a stopped Tenant
|
|
287
|
+
(exit 3).
|
|
288
|
+
|
|
289
|
+
### Pods on a cluster (preview)
|
|
290
|
+
|
|
291
|
+
`nylorun sandbox enable --context <name>` prepares a Kubernetes context for the
|
|
292
|
+
Tenant's sandboxes as pods ([agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox)
|
|
293
|
+
v1.0.5): Docker Desktop's cluster (`--context docker-desktop`) or kind. It only
|
|
294
|
+
touches the context you name. It installs the pinned agent-sandbox controller
|
|
295
|
+
when the cluster has none (and refuses another version), creates the namespace
|
|
296
|
+
`nylorun-sbx-<tenant>` with a ServiceAccount limited to Sandbox lifecycle, proves
|
|
297
|
+
with a probe that the cluster enforces NetworkPolicy (and refuses it otherwise),
|
|
298
|
+
lets pods reach only three ports on the Docker host, and adds the `sandboxes`
|
|
299
|
+
service to the Tenant: the one container holding the cluster credentials
|
|
300
|
+
(`<Host root>/sandboxes/`). On kind under Linux, pass `--host-address 172.17.0.1`.
|
|
301
|
+
`nylorun sandbox disable` removes the service (`--delete-namespace` also deletes
|
|
302
|
+
the namespace and every sandbox in it). Sessions keep using the virtual backend
|
|
303
|
+
in this release.
|
|
304
|
+
|
|
236
305
|
## Host root
|
|
237
306
|
|
|
238
307
|
The **Host root** is `~/.nylorun/tenants/<name>/`, or `NYLORUN_HOME`. It is
|
|
239
308
|
bind-mounted into the Runtime and Studio containers, and holds `tenant.json`
|
|
240
309
|
(`{ "format": 1, "name", "project"? }`: the Tenant's name and the project it
|
|
241
310
|
was created for), `host.json` (the client-facing host and port),
|
|
242
|
-
`host-credentials.json` (the admin key, mode 0600),
|
|
243
|
-
`
|
|
311
|
+
`host-credentials.json` (the admin key, mode 0600), `project-credentials.json`
|
|
312
|
+
and `cli-credentials.json` (the operator keys `project` and `cli`, mode 0600, when
|
|
313
|
+
they were put), the Docker Compose files in `docker/`, the Tenant directory `tenant/` (homes, logs) and `keys/vault-kek`,
|
|
244
314
|
the Tenant's vault key, which only the gateway container mounts. The Tenant's data lives in its Postgres, Restate and S2 volumes.
|
|
245
315
|
|
|
246
316
|
`nylorun reset` deletes the selected Tenant's volumes, `tenant/` and `keys/`, and keeps
|
|
@@ -255,7 +325,7 @@ containers, volumes and Host root: the vault key (KEK) and all its data go with
|
|
|
255
325
|
| 0 | Success |
|
|
256
326
|
| 1 | Generic failure, including a missing Docker or Compose v2 |
|
|
257
327
|
| 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) |
|
|
328
|
+
| 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
329
|
| 4 | The Runtime port is held by another Host |
|
|
260
330
|
| 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
331
|
| 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,14 @@
|
|
|
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, keyCommand, keyUsage, mcpCommand, mcpUsage, sandboxCommand, sandboxUsage, stackCommand, stackUsage, studioCommand, } from "./stack/index.js";
|
|
6
|
+
const usage = `nylorun <up|down|start|stop|status|logs|studio|reset|ls|delete|sandbox|key|mcp|doctor|telemetry>
|
|
7
7
|
|
|
8
8
|
Local Tenants (Docker Compose), one per project:
|
|
9
9
|
${stackUsage}
|
|
10
|
+
${sandboxUsage}
|
|
11
|
+
${keyUsage}
|
|
12
|
+
${mcpUsage}
|
|
10
13
|
doctor [--json] check Node, Docker and Compose v2, and the Tenant's health
|
|
11
14
|
telemetry [status|enable|disable] Studio's anonymous usage analytics (on unless disabled,
|
|
12
15
|
NYLORUN_TELEMETRY_DISABLED=1, DO_NOT_TRACK=1 or CI)
|
|
@@ -45,6 +48,18 @@ async function main() {
|
|
|
45
48
|
process.exitCode = await studioCommand(args, baselineEnv());
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
51
|
+
if (command === "sandbox") {
|
|
52
|
+
process.exitCode = await sandboxCommand(args, baselineEnv());
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (command === "mcp") {
|
|
56
|
+
process.exitCode = await mcpCommand(args, baselineEnv());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (command === "key") {
|
|
60
|
+
process.exitCode = await keyCommand(args, baselineEnv());
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
48
63
|
if (isStackCommand(command)) {
|
|
49
64
|
process.exitCode = await stackCommand(command, args, baselineEnv());
|
|
50
65
|
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",
|
package/dist/project/link.d.ts
CHANGED
|
@@ -22,13 +22,19 @@ export declare function writeProjectLink(projectRoot: string, link: {
|
|
|
22
22
|
hostId: string;
|
|
23
23
|
tenantId: string;
|
|
24
24
|
}): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
export
|
|
25
|
+
/** An application key and its principal: `credentials.json`'s content (format 1). */
|
|
26
|
+
export interface KeyCredentials {
|
|
27
27
|
applicationKey: string;
|
|
28
28
|
principalId: string;
|
|
29
|
-
}
|
|
29
|
+
}
|
|
30
|
+
/** The Project's application key and principal, or undefined when absent or unreadable. */
|
|
31
|
+
export declare function readProjectCredentials(projectRoot: string): Promise<KeyCredentials | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* A credentials file (`{ format: 1, applicationKey, principalId }`): the Project's, or one the
|
|
34
|
+
* nylorun commands keep in the Host root. Undefined when absent or unreadable.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readCredentialsFile(path: string): Promise<KeyCredentials | undefined>;
|
|
30
37
|
/** `.nylorun/credentials.json` (format 1, mode 0600). */
|
|
31
|
-
export declare function writeProjectCredentials(projectRoot: string, credentials:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}): Promise<void>;
|
|
38
|
+
export declare function writeProjectCredentials(projectRoot: string, credentials: KeyCredentials): Promise<void>;
|
|
39
|
+
/** A credentials file (format 1), written atomically with mode 0600. */
|
|
40
|
+
export declare function writeCredentialsFile(path: string, credentials: KeyCredentials): Promise<void>;
|
package/dist/project/link.js
CHANGED
|
@@ -69,8 +69,15 @@ export async function writeProjectLink(projectRoot, link) {
|
|
|
69
69
|
}
|
|
70
70
|
/** The Project's application key and principal, or undefined when absent or unreadable. */
|
|
71
71
|
export async function readProjectCredentials(projectRoot) {
|
|
72
|
+
return await readCredentialsFile(credentialsPath(projectRoot));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A credentials file (`{ format: 1, applicationKey, principalId }`): the Project's, or one the
|
|
76
|
+
* nylorun commands keep in the Host root. Undefined when absent or unreadable.
|
|
77
|
+
*/
|
|
78
|
+
export async function readCredentialsFile(path) {
|
|
72
79
|
try {
|
|
73
|
-
const value = JSON.parse(await readFile(
|
|
80
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
74
81
|
return typeof value.applicationKey === "string" && typeof value.principalId === "string"
|
|
75
82
|
? { applicationKey: value.applicationKey, principalId: value.principalId }
|
|
76
83
|
: undefined;
|
|
@@ -82,7 +89,11 @@ export async function readProjectCredentials(projectRoot) {
|
|
|
82
89
|
/** `.nylorun/credentials.json` (format 1, mode 0600). */
|
|
83
90
|
export async function writeProjectCredentials(projectRoot, credentials) {
|
|
84
91
|
await ensureProjectDir(projectRoot);
|
|
85
|
-
await
|
|
92
|
+
await writeCredentialsFile(credentialsPath(projectRoot), credentials);
|
|
93
|
+
}
|
|
94
|
+
/** A credentials file (format 1), written atomically with mode 0600. */
|
|
95
|
+
export async function writeCredentialsFile(path, credentials) {
|
|
96
|
+
await writePrivate(path, {
|
|
86
97
|
format: 1,
|
|
87
98
|
applicationKey: credentials.applicationKey,
|
|
88
99
|
principalId: credentials.principalId,
|
|
@@ -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
|
+
}
|