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.
Files changed (43) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +95 -25
  3. package/dist/cli.js +17 -2
  4. package/dist/doctor.d.ts +1 -1
  5. package/dist/doctor.js +10 -0
  6. package/dist/project/link.d.ts +13 -7
  7. package/dist/project/link.js +13 -2
  8. package/dist/sandbox/cluster-file.d.ts +47 -0
  9. package/dist/sandbox/cluster-file.js +55 -0
  10. package/dist/sandbox/commands.d.ts +41 -0
  11. package/dist/sandbox/commands.js +281 -0
  12. package/dist/sandbox/kubectl.d.ts +39 -0
  13. package/dist/sandbox/kubectl.js +70 -0
  14. package/dist/sandbox/manifests.d.ts +41 -0
  15. package/dist/sandbox/manifests.js +138 -0
  16. package/dist/sandbox/probe.d.ts +44 -0
  17. package/dist/sandbox/probe.js +137 -0
  18. package/dist/stack/commands.d.ts +71 -1
  19. package/dist/stack/commands.js +209 -38
  20. package/dist/stack/compose-file.d.ts +47 -7
  21. package/dist/stack/compose-file.js +223 -18
  22. package/dist/stack/env-file.d.ts +50 -14
  23. package/dist/stack/env-file.js +87 -40
  24. package/dist/stack/host-files.js +11 -1
  25. package/dist/stack/images.d.ts +8 -2
  26. package/dist/stack/images.js +8 -2
  27. package/dist/stack/index.d.ts +9 -0
  28. package/dist/stack/index.js +18 -1
  29. package/dist/stack/keys.d.ts +4 -0
  30. package/dist/stack/keys.js +72 -0
  31. package/dist/stack/mcp.d.ts +8 -0
  32. package/dist/stack/mcp.js +114 -0
  33. package/dist/stack/operator-keys.d.ts +39 -0
  34. package/dist/stack/operator-keys.js +146 -0
  35. package/dist/stack/paths.d.ts +32 -0
  36. package/dist/stack/paths.js +11 -0
  37. package/dist/stack/prepare.d.ts +12 -6
  38. package/dist/stack/prepare.js +24 -6
  39. package/dist/stack/sandbox.d.ts +4 -0
  40. package/dist/stack/sandbox.js +106 -0
  41. package/package.json +5 -5
  42. package/dist/project/derived-key.d.ts +0 -8
  43. 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): the key of the derived principal `project`,
135
- derived from the Tenant's admin key, and its id.
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
- The Runtime registers the derived principals of `NYLORUN_DERIVED_PRINCIPALS`
152
- (comma-separated, from the environment of `nylorun start`, kept in the Tenant's
153
- `.env`) when it creates the Tenant; `project` is always among them.
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
- | Network | `nylorun-shop` |
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`, Studio on `4161` and the Restate UI on `9070`,
177
- or free ports chosen on the Tenant's first start, avoiding the ports other
178
- Tenants keep, and kept in its `.env`.
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 app such as Babai Desktop can show Studio inside its own window, in
199
- an iframe loaded from Studio's URL (`studio.url` in `nylorun status --json`).
200
- Only exact origins listed in `NYLORUN_STUDIO_FRAME_ANCESTORS` (in
201
- `~/.nylorun/tenants/<name>/docker/.env`) may frame it. The default is Babai's
202
- `nylorun://localhost http://nylorun.localhost`; `nylorun status` lists them
203
- under `Embeds`. While building such an app, add its dev server once with
204
- `nylorun start --studio-embed-origin http://localhost:1420`; the list is kept
205
- across starts until `--studio-embed-origin-reset`. Wildcards are refused.
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), the Docker Compose files in
243
- `docker/`, the Tenant directory `tenant/` (homes, logs) and `keys/vault-kek`,
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",
@@ -22,13 +22,19 @@ export declare function writeProjectLink(projectRoot: string, link: {
22
22
  hostId: string;
23
23
  tenantId: string;
24
24
  }): Promise<void>;
25
- /** The Project's application key and principal, or undefined when absent or unreadable. */
26
- export declare function readProjectCredentials(projectRoot: string): Promise<{
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
- } | undefined>;
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
- applicationKey: string;
33
- principalId: string;
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>;
@@ -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(credentialsPath(projectRoot), "utf8"));
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 writePrivate(credentialsPath(projectRoot), {
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
+ }