nylorun 0.15.0-beta → 0.16.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,124 @@
1
1
  # nylorun
2
2
 
3
+ ## 0.16.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 0a2fbc5: **Artifacts reach the harness through the Harness API; `read_artifact` and `save_artifact` run in the harness (protocol 15, Harness API 4).** The first step of the harness boundary: model-facing work belongs to the harness, and the Runtime offers primitives and holds limits and access. A fresh start for agent sessions: the Runtime speaks protocol 15 only. See MIGRATION.md.
8
+
9
+ - **The artifact seam.** A harness reads and writes the artifacts of the run it holds: `artifact.read {artifactId, version?, offset?, length?}` answers the version's metadata (`ArtifactMetaSchema`) and its bytes from `offset`, at most `turn.start.limits.chunkBytes`; `artifact.write` (`begin`, `chunk`, `commit`, `abort`) makes a file artifact of the session or a new version of one of its own, streamed with backpressure into the Object store and committed in Postgres within the Tenant's and the Environment's artifact limits. The scope is always the run's session: its own artifacts, and the Tenant-wide ones its messages named; anything else is the new error code `not_found`, alike. A write's `key` makes it idempotent within the session (`artifact_versions.write_key`, migration 0028), and its `effectId` links the artifact to a tool call of the turn, whose call id `artifact.created` carries. An upload the run leaves open commits nothing. Client helpers: `readArtifact`, `openArtifactWrite`, `writeArtifactBytes`, `toBase64` and `fromBase64` in `@nylorun/core/harness-api`.
10
+ - **Our harness runs the artifact tools.** `read_artifact` (every agent) and `save_artifact` (with a sandbox) are our engine's tools, defined in `@nylorun/harness` (`artifactToolsFor`, `runArtifactTool`) and run over the seam and the run's sandbox files (`HarnessExecutors.files`, `SandboxManager.writeBytes`); their calls are kind `harness`. `save_artifact` keys its write by its effect id. `read_artifact` takes `path` with a sandbox, which copies the whole artifact into it, and hands a file of any other type to the model as a file of the result (the Model Gate writes the note for one the model does not read; `artifact.unsupported` is gone). Its page is harness configuration, `createHarness({ artifactPageBytes })` (32 KiB by default), and now always ends on a whole character. `runDurable` takes `engineTools`; `apiHost` takes the engine's configuration.
11
+ - **The Runtime pins no artifact tools.** `runtimeManifestFor` has no `save` or `read`; `RUNTIME_TOOL_NAMES` no longer holds the two names, so an author tool may take one (our harness then refuses the turn). A session without a sandbox or skills pins its definition unchanged. Core no longer runs either tool (`tenant/artifact-tool.ts` is gone). Migration 0029 closes every agent session that pinned them (`closedReason: "protocol_15"`, `409 session_closed`); workflow sessions carry over.
12
+ - **Media types are core's.** `contentTypeFor`, `essence`, `normalizeContentType`, `isText`, `isModelImage`, `modelInputOf`, `inputLabel`, `extensionFor`, `unreadableReason`, `MODEL_IMAGE_TYPES` and `DEFAULT_CONTENT_TYPE` are exported from `@nylorun/core/contracts`, for the Runtime and harnesses alike.
13
+ - `PROTOCOL_VERSION` 15 with required feature `harness-boundary`; `HARNESS_API_VERSION` 4. The SDKs and the CLI send protocol 15.
14
+
15
+ - b1e815d: **Runtime limits are a Tenant setting, and harnesses get them in `turn.start.limits`.**
16
+
17
+ - What one record entry, model call, model file, tool answer, custom harness state and Harness API chunk may hold is the new Management API setting `GET`/`PUT /v1/tenant/limits` (`{ limits: { recordEntryBytes, modelCallBytes, modelFileBytes, toolAnswerBytes, harnessStateBytes, chunkBytes } }`), also `admin.settings.limits` and `nylorun tenant settings limits [get | put <file|->]`.
18
+ - The defaults are today's sizes (`RUNTIME_LIMIT_DEFAULTS`): 960 KiB, 32 MiB, 8 MiB, 8 MiB, 256 KiB and 16 MiB. A limit left out of a `PUT` takes its default.
19
+ - Each limit has a ceiling a dependency sets (`RUNTIME_LIMIT_CEILINGS`): S2's 1 MiB record less 64 KiB for the event's envelope, the 64 MiB Harness API frame, the longest JavaScript string. A limit above its ceiling is `400`, naming the ceiling.
20
+ - Each is read from the Tenant's database where it is checked; nothing keeps it.
21
+ - **The record holds every write to `recordEntryBytes`.** An event whose payload, or an effect outcome whose value, is larger, whoever wrote it, is refused with `limit_exceeded`: `413` (`details.limit: "recordEntryBytes"`) to an API client, the new Harness API error code `limit_exceeded` to a harness. A harness's claimed event past it is refused rather than dropped. Before, such an event reached a record S2 could not take.
22
+ - **The gates read the setting.** The Model Gate refuses a body past `modelCallBytes` and a file past `modelFileBytes`; the Tool Gate keeps no MCP or HTTP answer past `toolAnswerBytes` (`mcp.too-large`, `http.too-large`) and reads no tool body past it. A custom harness's state past `harnessStateBytes` fails its turn with `limit_exceeded` (it was `invalid`), and a `definition.file` range longer than `chunkBytes` is `limit_exceeded`. Core's step budget for tool results is unchanged (256 KiB), or half `recordEntryBytes` when that is less.
23
+ - **`turn.start` carries `limits`** (`RuntimeLimitsSchema`), the limits in force when the segment started. Harness API 3 and protocol 14 are unchanged, but `TurnStartSchema` is strict: a harness built against an older `@nylorun/core` refuses a `turn.start` with `limits`, so upgrade a custom harness with the Runtime. Our harness asks for skill files `limits.chunkBytes` at a time, and holds MCP answers on its own connections to `limits.toolAnswerBytes`. `createHarness` takes `onStart(grant, start)`.
24
+ - **Removed exports:** `MAX_MODEL_CALL_BYTES`, `MAX_TOOL_BODY_BYTES`, `MAX_HARNESS_STATE_BYTES` and `DEFINITION_FILE_CHUNK_BYTES` from `@nylorun/core`. Added: `RuntimeLimitsSchema`, `RUNTIME_LIMIT_DEFAULTS`, `RUNTIME_LIMIT_CEILINGS`, `runtimeLimitsOf`, `runtimeLimitIssues`, `limitExceededMessage`, `S2_RECORD_MAX_BYTES`, `RECORD_ENVELOPE_RESERVE_BYTES`, `JS_STRING_MAX_LENGTH` and the `TenantLimits*` schemas.
25
+
26
+ - f2e9b0a: **Skill and artifact limits are set per Tenant and per Environment manifest; a skill holds up to 2 GiB by default, with no limit per file or on the number of files.**
27
+
28
+ - The 10 MiB limit per skill file and the 500-file limit are gone. A skill's files may hold up to the Tenant's skill limit together: 2 GiB by default (`SKILL_TOTAL_BYTES_DEFAULT`). A file at two paths counts twice, since a sandbox holds both copies. `SKILL_FILES_MAX` and `DEFINITION_FILE_MAX_BYTES` are no longer exported.
29
+ - The Tenant's skill limit is the new Management API setting `GET`/`PUT /v1/tenant/skills` (`{ limits: { totalBytes } }`), also available as `admin.settings.skills` and `nylorun tenant settings skills [get | put <file|->]`.
30
+ - `PUT /v1/files/sha256:<hex>` takes a file up to that limit.
31
+ - `PUT /v1/agents/{id}` refuses a skill over it, an agent used as a tool included: `413 limit_exceeded`, with `details.skills` listing each skill and its size.
32
+ - An environment manifest, or the inline fields of `PUT /v1/environments/{id}`, may set `limits`: `artifacts.fileBytes`, `artifacts.totalBytes` and `skills.totalBytes`. Like the manifest's other fields, they are fixed for the Environment's life.
33
+ - Each limit must be at most the Tenant's. A higher one is refused with `400` when the Environment is made or the manifest is saved.
34
+ - A limit left out is the Tenant's at the time it is checked. A Tenant that lowers a limit lowers it for every Environment.
35
+ - The Environment view's new `limits` field gives the limits in force.
36
+ - An Environment's artifacts are its sessions' artifacts and its disk versions. Uploads, `save_artifact`, turn-end exports and disk snapshots hold them to the Environment's per-file limit and to its own total, as well as to the Tenant's total. A turn-end export past the Environment's total is skipped with the new reason `environment_total`, and a disk snapshot past it is `413` with `details.limit: "environment_total"`.
37
+ - A session opens on an Environment only when its agent's skills fit the Environment's skill limit; otherwise it is `409 environment_unsuitable`, with `details.skills`.
38
+ - `PUT /v1/files` streams the body to the Object store instead of holding it in memory. Only bytes that match the named hash reach the file's key. Environment files named by hash are held to the same per-file maximum.
39
+ - `@nylorun/agents`: loading a skills folder no longer checks a size, since the limit is the Tenant's; the Runtime refuses an oversized skill. The `skills.file-too-large` and `skills.too-many-files` errors are gone. Files are hashed and read a chunk at a time.
40
+ - Sandboxes get skill files as a stream. A harness fetches them from core 16 MiB at a time: `definition.file` takes optional `offset` and `length` (at most the Runtime limit `chunkBytes`).
41
+ - `load_skill` and `read_skill_resource` read files of up to 10 MiB into the model's context. A larger file fails with `skill.resource_too_large`, and the error names the file's path in the sandbox when there is one.
42
+
43
+ ### Patch Changes
44
+
45
+ - cf425c6: **The harness mounts skills and saves outputs; the in-process harness uses the gates; compaction is a transcript edit (protocol 15).** The fifth step of the harness boundary, still protocol 15 and Harness API 4. See MIGRATION.md.
46
+
47
+ - **Skill folders are our engine's.** Before each sandbox call our engine mounts each skill version of the turn's tree at `/skills/<name>@<hash>/` with `executors.files.mount`, reading the bytes over `definition.file`; the SandboxManager only writes them (`mountFolder`), once per open sandbox (`present` after). `SandboxManager.mountSkills`, its `definitionFile` port and `SandboxSessionRef.manifest` are gone, and the operator sandbox route mounts nothing.
48
+ - **The outputs folder is our engine's (Q5).** At a completed turn's end in a sandbox our engine saves `/workspace/outputs` as the session's `outputs` folder before `turn.completed`, over the new folder mode of `artifact.write` (`kind: "folder"`, `file`, chunks named by hash, `unchanged`, `refused`). Core takes every byte, checks each file's hash before content is stored under it, dedupes, and keeps the content epoch and quotas. Versions say `source: "engine"`; `artifact.export.skipped` and `artifact.export.failed` are harness claims. Core's export (`artifacts/export.ts`, the `WorkspaceReader`, `workspace.read` listing and bytes calls) is removed.
49
+ - **No limit literals.** The new Runtime limit `folderEntries` (10,000; a count, its ceiling from the manifest's one string) replaces `EXPORT_MAX_FILES`, and `createHarness({ outputs: { dir, maxBytes } })` (`/workspace/outputs`, 1 GiB) replaces `EXPORT_MAX_BYTES`. `RUNTIME_LIMIT_CEILINGS` entries are `{value, unit, why}`.
50
+ - **In-process parity.** Every model call goes through the Model Gate: a scripted model is served by `providerModelGate`, metered like any. The MCP pool opens every server through the Tool Gate (`openRemote` is required; the in-process gate's `openMcp` finds the server in the session's pinned manifest and authorizes it with the vault). `ModelRoute` is `{tenantId, modelGate}`; `TenantContext.modelProvider`, `useVaultModel` and `workspaces` are gone.
51
+ - **Compaction is a transcript edit (Q4).** Summary calls say `purpose: "summary"` (no `message.assistant`), and the segment's edit that replaced the history says `kind: "compaction"` with its facts and summary calls (`CompactionFactsSchema`, `transcriptUpdates(..., compaction)`). Core checks the calls and writes one `context.compacted`, which loses `partial`.
52
+
53
+ - dc75e58: **Host timings, retries and engine settings are configuration with defaults (B15).** Still protocol 15 and Harness API 4. See MIGRATION.md and DEPLOYMENT.md ("Host tuning").
54
+
55
+ - **Host tuning.** The Runtime processes' timeouts, retries, poll intervals and internal sizes are one typed schema with a default each (`HostTuningSchema`, `runtime/server/src/tuning.ts`): keys, the Harness API, the API's streams, the Host, the Object store, the Tenant, its sweep, Durable Session Execution, Restate, Postgres, Durable Streams and the relay, the gates, egress-gate, sandbox pods, harnesses and traces. An operator sets any part in `NYLORUN_TUNING` (JSON), parsed with the stack configuration and read once at start; every default is the value before. An unknown key or a value past its ceiling (Node's longest timer, S3's part sizes, S2's batch, the sandboxes service's wait) stops the process naming the key. Core hands a sandbox pod's engine the `harness` and `engine` groups the operator set. `nylorun start` passes `NYLORUN_TUNING` to the runtime, gateway and harness containers.
56
+ - **Our engine's loop is configuration.** `createHarness` takes `compaction` (`CompactionConfig`, `DEFAULT_COMPACTION_CONFIG`: the kept tail, the reply's reserve, the estimate), `partialOutputChars` (a failed agent-as-tool's output its parent sees), `resendDelaysMs` and `leaseRetryMs`; `runDurable` takes `loop` (`LoopConfig`), which an invocation hands its agents used as tools. `EngineToolConfig` is `EngineConfig`. The Runtime's harness host passes the tuning's `engine` group to `createHarness`.
57
+ - **Kept as constants**, each justified where it is defined: Node's longest timer, S3's part sizes and object key, S2's batch, the sandboxes service's longest wait and pod variable, the `environment.egress` event's 16 running turns, W3C's 512-character `tracestate`, and the 24-hour floor of a wake's dedupe key.
58
+
59
+ - 23774c4: **The last hard-coded limits are configuration (harness boundary B15c).** Still protocol 15 and Harness API 4. Every default is the value before. See MIGRATION.md, and DEPLOYMENT.md's Runtime limits and Host tuning.
60
+
61
+ - **21 more Runtime limits** (`GET`/`PUT /v1/tenant/limits`): a message's parts (`messageParts`), page sizes (`pageItems`, `exportPageItems`, `environmentEventItems`; a page asked larger holds that many), labels (`artifactLabels`, `labelKeyChars`, `labelValueChars`), a manifest's setup command, descriptions and secret hosts (`setupCommandChars`, `manifestDescriptionChars`, `secretDescriptionChars`, `secretHosts`), a failed pod's reason (`setupReasonChars`, `setupCommandExcerptChars`), secret values (`secretValueChars`), the tool policy and budgets (`toolRules`, `toolRuleFieldChars`, `toolRuleTextChars`, `modelBudgets`) and flows (`flowConcurrency`, `flowMapItems`, `flowLoopIterations`, replacing the `NYLORUN_FLOW_*` variables no Host read). Request schemas lose these maxima; the Runtime checks the Tenant's.
62
+ - **More Host tuning** (`NYLORUN_TUNING`): token lifetimes and clock tolerance (`keys.*`), the advance deadline and grace (`execution.*`), `nylorun-operate`'s waits, the credential store's lock, sweep arming, Postgres batches and the replication pool, a basin repair's log cadence, egress sockets, the engine poll and sandbox memory floor, MCP listing pages and diagnostics, sandbox helper commands, the scrub threshold, exec event and local stream sizes; and in the engine, `flowValueChars` and `toolResults.minPreviewBytes`.
63
+ - **The CLI's own waits are `NYLORUN_CLI_TUNING`**: `nylorun start`'s waits and polls, the Compose healthchecks and WAL cap, and `nylorun local sandbox`'s waits.
64
+ - **Studio follows the Runtime's limits**: no upload cap of its own, the Tenant's `requestBodyBytes` read for a write it parses past the default (`@nylorun/agents` exports `RUNTIME_LIMIT_DEFAULTS`), no CPU cap in its form.
65
+ - **Kept, each justified where it is defined**: Linux's `MAX_ARG_STRLEN` (a setup command's ceiling) and `PATH_MAX` (a link target), a model-facing tool name's 64 characters, RFC 6838's media type length, Postgres's 65,535 bind parameters, a tmpfs page, and identifier grammars (the protocol's).
66
+
67
+ - 6772612: **The remaining request, upload, token and sandbox limits are Tenant settings (harness boundary B15).** Still protocol 15 and Harness API 4. See MIGRATION.md and DEPLOYMENT.md's Runtime limits.
68
+
69
+ - **27 more Runtime limits** in `GET`/`PUT /v1/tenant/limits`, each with today's value as its default and a ceiling from a dependency (a JavaScript string or timer, a request read as one string, a Harness API frame, a JavaScript number): request bodies and inline files (`requestBodyBytes`, `inlineFileBytes`), `a2aWaitMs`, `errorExcerptChars`, `traceContentBytes`, an Environment's labels, setup and input files (`environmentLabels`, `environmentSetupCommands`, `environmentFiles`, `environmentFilesPerEntry`, `environmentSecretSlots`, `setupCommandMs`, `setupOutputChars`), disk versions (`diskVersionEntries`, `diskVersionBytes`, `diskLeaseMs`, `diskPodWaitMs`; an Environment's manifest may lower the first two with `limits.disk`), model calls (`modelRequestMs`, `modelIdleMs`, `modelAttempts`, `modelRetryDelayMs`, `modelProviderRetries`, `modelRetryDelayMaxMs`, read by the gates for each call) and tools (`toolAttempts`, `httpToolTimeoutMs`, `mcpPreviewMs`, `mcpMetadataBytes`, and the new `mcpSnapshotBytes`: what a session pins of its MCP servers' tools and instructions, past which a server is left out with a diagnostic).
70
+ - **Schema maxima are checked against the Tenant's limits**: an Environment's labels, setup commands, input files, files per entry, secret slots and setup timeout, and an HTTP tool's `timeoutMs` (at deploy and at each call). `limits.environments` and `lifecycle.stopGrace` lose their arbitrary caps; an identity file's issuer takes any number of grants and `jwksFetch` settings.
71
+ - **`turn.start.limits` carries only a harness's limits** (`HarnessLimitsSchema`, `harnessLimitsOf`), and a harness drops one it does not know. `transcriptUpdates` takes `recordEntryBytes` and fills each event up to it; `CHUNK_BYTES` is gone.
72
+ - **Constants a dependency or a standard fixes stay, named for it**: a token is read up to Node's `http.maxHeaderSize`; artifact names and folder paths keep `FILE_NAME_MAX` (255) and `FILE_PATH_MAX` (1,024); a skill file's path is what `FILE_PATH_MAX` leaves under its mount folder; `tracestate` keeps W3C's 512.
73
+ - **Removed**: `ENVIRONMENT_LABELS_MAX`, `ENVIRONMENT_SETUP_MAX`, `ENVIRONMENT_SETUP_TIMEOUT_MAX_MS` (now `ENVIRONMENT_SETUP_TIMEOUT_DEFAULT`), `ENVIRONMENT_FILES_MAX`, `ENVIRONMENT_SECRET_SLOTS_MAX`, `TOKEN_ENVIRONMENT_GRANTS_MAX`, `HTTP_TOOL_MAX_TIMEOUT_MS`, `READ_ARTIFACT_MAX_BYTES` and `CHUNK_BYTES` from `@nylorun/core`; the image checks (`decodeImageBase64`, `validateImageBytes`, `IMAGE_MEDIA_TYPES`, `MAX_IMAGE_BYTES`), the Tenant configuration's `modelCall` and `TrustedIssuersOptions`' `refetchMs`/`maxAgeMs` from `@nylorun/runtime`. Studio's Environment form no longer caps setup commands and input files at 16: the Runtime checks the Tenant's limit.
74
+
75
+ - Pin core to the tested release.
76
+ - Pin agents to the tested release.
77
+ - Pin admin to the tested release.
78
+ - Pin runtime to the tested release.
79
+ - Pin studio to the tested release.
80
+ - Updated dependencies [0a2fbc5]
81
+ - Updated dependencies [ebe7348]
82
+ - Updated dependencies [ff6470e]
83
+ - Updated dependencies [f443475]
84
+ - Updated dependencies [cf425c6]
85
+ - Updated dependencies [23774c4]
86
+ - Updated dependencies [b1e815d]
87
+ - Updated dependencies [2459430]
88
+ - Updated dependencies [f2e9b0a]
89
+ - Updated dependencies [6772612]
90
+ - Updated dependencies
91
+ - Updated dependencies
92
+ - @nylorun/core@0.23.0-beta
93
+ - @nylorun/agents@0.23.0-beta
94
+ - @nylorun/admin@0.18.0-beta
95
+
96
+ ## 0.15.1-beta
97
+
98
+ ### Patch Changes
99
+
100
+ - 000ff75: **READMEs and help text match protocol 14.**
101
+
102
+ - `nylorun --help` lists `environments fork`, `manifests` and `delete-version`. The package description no longer offers `nylo` as the Runtime client, and the README says nylorun depends on `@nylorun/agents` too.
103
+ - The READMEs name the current commands: `nylorun tenant keys`, `nylorun tenant mcp inspect`, `nylorun local logs` and `nylorun local reset`. `@nylorun/admin` says it speaks protocol 14.
104
+ - `@nylorun/agents`: a lost MCP or HTTP tool call is sent again with its key, then fails to the model as `tool.interrupted` (it is not left `uncertain`). Every manifest is v7. A2A takes `raw` files. A delegated agent cannot pause.
105
+ - `@nylorun/runtime`: the README describes protocol 14, `PUT /v1/sessions/{id}` says it answers an identical session (vaults are no longer attached), and the database-layout error names `nylorun local reset`.
106
+ - `@nylorun/create-agent`: `nylorun start` writes the project's keys (no key is derived any more).
107
+
108
+ - Pin core to the tested release.
109
+ - Pin agents to the tested release.
110
+ - Pin admin to the tested release.
111
+ - Pin runtime to the tested release.
112
+ - Pin studio to the tested release.
113
+ - Updated dependencies [620072d]
114
+ - Updated dependencies [a0cf8b2]
115
+ - Updated dependencies [000ff75]
116
+ - Updated dependencies
117
+ - Updated dependencies
118
+ - @nylorun/core@0.22.0-beta
119
+ - @nylorun/agents@0.22.0-beta
120
+ - @nylorun/admin@0.17.1-beta
121
+
3
122
  ## 0.15.0-beta
4
123
 
5
124
  ### Minor Changes
package/README.md CHANGED
@@ -17,10 +17,10 @@ it on its first start) and links the project to it; anywhere else, it starts
17
17
  the Tenant `default`. Projects that must not share agents, credentials or
18
18
  history run separate Tenants. The `tenant` and `environments` groups act on a
19
19
  Tenant over its APIs, so they work on a remote installation too
20
- ([below](#remote-installations)). Depends on `@nylorun/core` and
21
- `@nylorun/admin` among Nylorun packages and never imports `@nylorun/runtime`;
22
- `start`, `stop` and the `local` commands load neither the API client nor
23
- `@nylorun/admin`. Vocabulary:
20
+ ([below](#remote-installations)). Depends on `@nylorun/core`,
21
+ `@nylorun/admin` and `@nylorun/agents` among Nylorun packages and never imports
22
+ `@nylorun/runtime`; `start`, `stop` and the `local` commands load neither
23
+ `@nylorun/agents` nor `@nylorun/admin`. Vocabulary:
24
24
  [runtime/server/src/CONTEXT.md](../../runtime/server/src/CONTEXT.md).
25
25
 
26
26
  Prerequisites, installed by the developer (nylorun never downloads them): Node
@@ -96,7 +96,7 @@ nylorun tenant usage [--scope tenant|agent|turn] [--id <id>] [--period day|month
96
96
  nylorun tenant calls [--after <cursor>] [--limit <n>] [--tenant <name>] # every recorded model call as NDJSON, until caught up
97
97
  nylorun tenant budgets [get | put <file|->] [--tenant <name>] # the model spend caps
98
98
  nylorun tenant tool-policy [get | put <file|->] [--tenant <name>] # the Tool Gate's rules
99
- nylorun tenant settings sandbox|artifacts [get | put <file|->] [--tenant <name>] # Environment and artifact store settings
99
+ nylorun tenant settings sandbox|artifacts|skills|limits [get | put <file|->] [--tenant <name>] # Environment, artifact store and skill settings, Runtime limits
100
100
  nylorun tenant coverage --agent <id> [--environment <id>] [--tenant <name>] [--json] # which secret each MCP server and HTTP tool would get
101
101
  nylorun tenant mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]
102
102
 
@@ -510,13 +510,14 @@ already has a credential, keeping its model (or `--model`) and endpoint.
510
510
 
511
511
  ### Files in, files out
512
512
 
513
- `budgets`, `tool-policy`, `settings sandbox` and `settings artifacts` are
514
- documents: `get` (the default) prints one as JSON, and `put <file>` (or `put -`
515
- for stdin) replaces the whole document with the JSON body in the file, the
516
- Management API's request body (`PUT /v1/tenant/budgets`, `/tool-policy`,
517
- `/sandbox`, `/artifacts`). The body is checked against its schema before
518
- anything is sent; one that does not parse exits 2. What `budgets`, `tool-policy`
519
- and `settings artifacts` print, `put` takes back (their `updatedAt` and
513
+ `budgets`, `tool-policy`, `settings sandbox`, `settings artifacts`,
514
+ `settings skills` and `settings limits` are documents: `get` (the default)
515
+ prints one as JSON, and `put <file>` (or `put -` for stdin) replaces the whole
516
+ document with the JSON body in the file, the Management API's request body
517
+ (`PUT /v1/tenant/budgets`, `/tool-policy`, `/sandbox`, `/artifacts`, `/skills`,
518
+ `/limits`). The body is checked against its schema before anything is sent; one
519
+ that does not parse exits 2. What `budgets`, `tool-policy`, `settings artifacts`,
520
+ `settings skills` and `settings limits` print, `put` takes back (their `updatedAt` and
520
521
  `usedBytes` are dropped), so they can be kept in a repository and applied from
521
522
  CI:
522
523
 
@@ -528,11 +529,28 @@ nylorun tenant tool-policy put tool-policy.json
528
529
  `settings sandbox get` prints the effective settings and what the Host offers;
529
530
  its `put` takes the settings alone (`limits`, `lifecycle`, `placement`).
530
531
 
532
+ `settings limits` is the Tenant's Runtime limits: the sizes, counts and
533
+ deadlines the Runtime holds what crosses it to, such as `recordEntryBytes`,
534
+ `requestBodyBytes`, `environmentLabels`, `pageItems`, `modelRequestMs` and
535
+ `httpToolTimeoutMs` (each one, with its default and ceiling, in DEPLOYMENT.md's
536
+ Runtime limits). A
537
+ limit left out of a `put` takes its default; one above its ceiling (S2's 1 MiB
538
+ record, the Harness API's 64 MiB frame, a JavaScript timer) is refused, and the
539
+ error names the ceiling.
540
+
531
541
  `nylorun tenant calls` prints each recorded model call as one JSON line, page
532
542
  after page, until it is caught up, then the cursor on stderr: pass it as
533
543
  `--after` to read only the newer calls. A call may come twice across runs;
534
544
  deduplicate by `id`.
535
545
 
546
+ ### The CLI's own waits
547
+
548
+ How long `nylorun start` waits for the stack, how often it polls, the healthchecks
549
+ it writes into the Compose file and the waits of `nylorun local sandbox` are the
550
+ CLI's tuning: a JSON object in `NYLORUN_CLI_TUNING`, each value left out keeping
551
+ its default (`{"stack":{"healthWaitMs":120000}}`). DEPLOYMENT.md ("The CLI") lists
552
+ them. The Runtime's own are `NYLORUN_TUNING`, which `nylorun start` passes on.
553
+
536
554
  ## Exit codes
537
555
 
538
556
  | Code | Meaning |
package/dist/cli.js CHANGED
@@ -17,8 +17,9 @@ Groups, each with its own --help:
17
17
  sessions list | create | get | send | approve | respond | cancel | watch | history
18
18
  usage | calls | trace | exec
19
19
  the Tenant's sessions (Runtime API)
20
- environments list | get | put | stop | reset | delete | snapshot | restore | versions
21
- events | secrets the Tenant's Environments (Runtime API)
20
+ environments list | get | put | fork | stop | reset | delete | snapshot | restore
21
+ versions | delete-version | events | secrets | manifests
22
+ the Tenant's Environments (Runtime API)
22
23
  artifacts list | get | upload | download | tree | diff | zip | link | rm
23
24
  the Tenant's file artifacts (Runtime API)
24
25
  tenant reset | keys | signing-keys | models | usage | calls | budgets | tool-policy |
@@ -22,8 +22,6 @@ const SECRET_HINT = "write it as a bearer or headers secret of an Environment (n
22
22
  export const MCP_VAULT_REMOVED = "--vault was removed in protocol 11: a credential is a secret of an Environment, in its own vault. Pass --environment <id> to send that Environment's credential for the URL.";
23
23
  /** What the removed `connect` subcommand says since protocol 10. */
24
24
  export const MCP_CONNECT_REMOVED = `The connect subcommand of nylorun mcp was removed with MCP OAuth (protocol 10): ${SECRET_HINT.replace("write it", "write the server's key")}, or reach a server that needs a person's sign-in through a gateway (a secret with via). See its tools with nylorun tenant mcp inspect <url> --environment <id>.`;
25
- /** The preview takes up to 15 s in the Runtime; the hop gets a margin. */
26
- const PREVIEW_TIMEOUT_MS = 30_000;
27
25
  const DESCRIPTION_CHARS = 60;
28
26
  const usageError = (message) => new CliError(message, 2);
29
27
  async function inspect(deps, args) {
@@ -53,8 +51,8 @@ async function inspect(deps, args) {
53
51
  ...(name === undefined ? {} : { name }),
54
52
  ...(environmentId === undefined ? {} : { environmentId }),
55
53
  }),
54
+ // The Runtime bounds the preview (its Runtime limit mcpPreviewMs, each step); the CLI waits.
56
55
  redirect: "error",
57
- signal: AbortSignal.timeout(PREVIEW_TIMEOUT_MS),
58
56
  });
59
57
  const body = (await response.json().catch(() => ({})));
60
58
  if (response.status === 404 && body.code === "not_found")
@@ -46,7 +46,7 @@ export declare function configureProvider(options?: {
46
46
  root?: string;
47
47
  input?: Readable;
48
48
  output?: Writable;
49
- /** Tenant API catalog from GET /v1/tenant/models. */
49
+ /** Management API catalog from GET /v1/tenant/models. */
50
50
  catalog?: ModelCatalog;
51
51
  /** This installation's stable id, for OAuth flows that need one (OpenAI). */
52
52
  deviceId?: () => string;
@@ -16,7 +16,7 @@ export class ConfigurationCancelled extends CliError {
16
16
  }
17
17
  /**
18
18
  * Build a pi-ai registry for interactive login. Catalog listing prefers the
19
- * Tenant API payload when provided (F2-4).
19
+ * Management API payload when provided (F2-4).
20
20
  */
21
21
  function modelsFor({ builtinModels, createProvider, stream, streamSimple }, selection, credentials, catalog) {
22
22
  const models = builtinModels({ credentials });
@@ -185,8 +185,9 @@ export async function callsCommand(deps, args) {
185
185
  const after = flags.values.get("--after");
186
186
  const limitFlag = flags.values.get("--limit");
187
187
  const limit = limitFlag === undefined ? undefined : Number(limitFlag);
188
- if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1 && limit <= 1000))
189
- throw usageError("--limit is a whole number from 1 to 1000.");
188
+ // A page holds at most the Runtime limit exportPageItems; a larger --limit gets that many.
189
+ if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1))
190
+ throw usageError("--limit is a whole number of at least 1.");
190
191
  const admin = await client(deps, flags.values.get("--tenant"));
191
192
  let next = null;
192
193
  for await (const page of admin.models.exportCalls({
@@ -1,8 +1,8 @@
1
1
  import { type StackDeps } from "../stack/commands.js";
2
- export declare const settingsUsage = " budgets [get | put <file|->] [--tenant <name>]\n the model spend caps; put replaces them all\n tool-policy [get | put <file|->] [--tenant <name>]\n the Tool Gate's rules; put replaces them all\n settings sandbox [get | put <file|->] [--tenant <name>]\n the Environment settings: limits, stop grace, placement\n settings artifacts [get | put <file|->] [--tenant <name>]\n the artifact store's limits\n coverage --agent <id> [--environment <id>] [--tenant <name>] [--json]\n which secret of the Environment each of the agent's MCP\n servers and HTTP tools would get";
2
+ export declare const settingsUsage = " budgets [get | put <file|->] [--tenant <name>]\n the model spend caps; put replaces them all\n tool-policy [get | put <file|->] [--tenant <name>]\n the Tool Gate's rules; put replaces them all\n settings sandbox [get | put <file|->] [--tenant <name>]\n the Environment settings: limits, stop grace, placement\n settings artifacts [get | put <file|->] [--tenant <name>]\n the artifact store's limits\n settings skills [get | put <file|->] [--tenant <name>]\n the most bytes one skill may hold\n settings limits [get | put <file|->] [--tenant <name>]\n the Runtime limits: sizes, counts and deadlines (record\n entries, request bodies, Environments, model calls, tools)\n coverage --agent <id> [--environment <id>] [--tenant <name>] [--json]\n which secret of the Environment each of the agent's MCP\n servers and HTTP tools would get";
3
3
  export declare const budgetsCommand: (deps: StackDeps, args: readonly string[]) => Promise<number>;
4
4
  export declare const toolPolicyCommand: (deps: StackDeps, args: readonly string[]) => Promise<number>;
5
- /** `nylorun tenant settings sandbox | artifacts ...` */
5
+ /** `nylorun tenant settings sandbox | artifacts | skills | limits ...` */
6
6
  export declare function settingsCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
7
7
  /**
8
8
  * `nylorun tenant coverage`: which secret of `--environment` each remote MCP server and HTTP tool
@@ -1,4 +1,4 @@
1
- import { PutModelBudgetsRequestSchema, PutTenantArtifactsRequestSchema, PutTenantEnvironmentsRequestSchema, PutToolPolicyRequestSchema, } from "@nylorun/core/contracts";
1
+ import { PutModelBudgetsRequestSchema, PutTenantArtifactsRequestSchema, PutTenantEnvironmentsRequestSchema, PutTenantLimitsRequestSchema, PutTenantSkillsRequestSchema, PutToolPolicyRequestSchema, } from "@nylorun/core/contracts";
2
2
  import { CliError } from "../errors.js";
3
3
  import { parseStackFlags } from "../stack/commands.js";
4
4
  import { printJson, printTable, readBody } from "./io.js";
@@ -11,6 +11,11 @@ export const settingsUsage = ` budgets [get | put <file|->] [--tenant <name>]
11
11
  the Environment settings: limits, stop grace, placement
12
12
  settings artifacts [get | put <file|->] [--tenant <name>]
13
13
  the artifact store's limits
14
+ settings skills [get | put <file|->] [--tenant <name>]
15
+ the most bytes one skill may hold
16
+ settings limits [get | put <file|->] [--tenant <name>]
17
+ the Runtime limits: sizes, counts and deadlines (record
18
+ entries, request bodies, Environments, model calls, tools)
14
19
  coverage --agent <id> [--environment <id>] [--tenant <name>] [--json]
15
20
  which secret of the Environment each of the agent's MCP
16
21
  servers and HTTP tools would get`;
@@ -42,6 +47,18 @@ const artifacts = {
42
47
  get: (admin) => admin.settings.artifacts.get(),
43
48
  put: (admin, body) => admin.settings.artifacts.put(body),
44
49
  };
50
+ const skillSettings = {
51
+ name: "settings skills",
52
+ schema: PutTenantSkillsRequestSchema,
53
+ get: (admin) => admin.settings.skills.get(),
54
+ put: (admin, body) => admin.settings.skills.put(body),
55
+ };
56
+ const limitSettings = {
57
+ name: "settings limits",
58
+ schema: PutTenantLimitsRequestSchema,
59
+ get: (admin) => admin.settings.limits.get(),
60
+ put: (admin, body) => admin.settings.limits.put(body),
61
+ };
45
62
  /** `<document> [get | put <file|->]`: `get` when no verb is given. */
46
63
  async function documentCommand(deps, document, args) {
47
64
  const usage = `nylorun tenant ${document.name} [get | put <file|->] [--tenant <name>]`;
@@ -56,13 +73,17 @@ async function documentCommand(deps, document, args) {
56
73
  }
57
74
  export const budgetsCommand = (deps, args) => documentCommand(deps, budgets, args);
58
75
  export const toolPolicyCommand = (deps, args) => documentCommand(deps, toolPolicy, args);
59
- /** `nylorun tenant settings sandbox | artifacts ...` */
76
+ /** `nylorun tenant settings sandbox | artifacts | skills | limits ...` */
60
77
  export async function settingsCommand(deps, args) {
61
78
  const [which, ...rest] = args;
62
79
  if (which === "sandbox")
63
80
  return documentCommand(deps, sandbox, rest);
64
81
  if (which === "artifacts")
65
82
  return documentCommand(deps, artifacts, rest);
83
+ if (which === "skills")
84
+ return documentCommand(deps, skillSettings, rest);
85
+ if (which === "limits")
86
+ return documentCommand(deps, limitSettings, rest);
66
87
  throw usageError();
67
88
  }
68
89
  const COVERAGE_USAGE = "nylorun tenant coverage --agent <id> [--environment <id>] [--tenant <name>] [--json]";
@@ -6,7 +6,7 @@
6
6
  import type { ManagementClient } from "@nylorun/admin";
7
7
  import { type StackDeps } from "../stack/commands.js";
8
8
  import { type Connection } from "./target.js";
9
- export declare const tenantUsage = "nylorun tenant <command>\n\nThe Management API of the installation's one Tenant, with a management key: the local Tenant\n--tenant names (else the linked project's, else the default one), or the installation that\nNYLORUN_RUNTIME_URL and NYLORUN_MANAGEMENT_KEY name.\n\n reset [--sessions|--environments|--all] [--yes] [--tenant <name>]\n clear the Tenant's sessions, Environments or all its data\n (the Project link and keys stay)\n keys put <id> [--management] [--tenant <name>]\n create the Tenant's application key <id> (--management: a\n management key, on the Runtime's machine only), or rotate it\n (the old key stops working); prints the key once\n keys list [--tenant <name>] [--json]\n the Tenant's keys: id, role, when issued (never the keys)\n keys rm <id> [--tenant <name>] delete key <id>: it stops working at once\n signing-keys list [--tenant <name>]\n the Tenant's token signing keys\n signing-keys rotate [--force] [--tenant <name>]\n rotate; --force ends outstanding links and run tokens\n signing-keys revoke <kid> [--tenant <name>]\n revoke a previous or standby key\n models catalog [--tenant <name>] [--json]\n every provider and model the Runtime knows\n models providers [--tenant <name>] [--json]\n the providers with a credential, and the one in use\n models get [--tenant <name>] [--json]\n the Tenant's model\n models set [--tenant <name>] sign in to a model provider and set the Tenant's model\n models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]\n the same with an API key piped in on stdin (CI)\n models select <provider> [--model <id>] [--tenant <name>]\n use a provider that has a credential (default: its model)\n usage [--scope tenant|agent|turn] [--id <id>] [--period day|month|total] [--tenant <name>]\n what the model calls used and cost (JSON)\n calls [--after <cursor>] [--limit <n>] [--tenant <name>]\n every recorded model call, one JSON line each, until\n caught up; prints the cursor to resume from on stderr\n budgets [get | put <file|->] [--tenant <name>]\n the model spend caps; put replaces them all\n tool-policy [get | put <file|->] [--tenant <name>]\n the Tool Gate's rules; put replaces them all\n settings sandbox [get | put <file|->] [--tenant <name>]\n the Environment settings: limits, stop grace, placement\n settings artifacts [get | put <file|->] [--tenant <name>]\n the artifact store's limits\n coverage --agent <id> [--environment <id>] [--tenant <name>] [--json]\n which secret of the Environment each of the agent's MCP\n servers and HTTP tools would get\n mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]\n list a remote MCP server's tools, with Environment <id>'s\n credential for <url> (none without it): model names, schema\n sizes, renames, or that it needs a person's sign-in (the\n Runtime calls no tool)\n\nA put reads a JSON body from a file, or from stdin for -, and replaces the whole document; what\nbudgets, tool-policy and settings artifacts get print, put takes back. An API key is read from\nstdin, never from the arguments.";
9
+ export declare const tenantUsage = "nylorun tenant <command>\n\nThe Management API of the installation's one Tenant, with a management key: the local Tenant\n--tenant names (else the linked project's, else the default one), or the installation that\nNYLORUN_RUNTIME_URL and NYLORUN_MANAGEMENT_KEY name.\n\n reset [--sessions|--environments|--all] [--yes] [--tenant <name>]\n clear the Tenant's sessions, Environments or all its data\n (the Project link and keys stay)\n keys put <id> [--management] [--tenant <name>]\n create the Tenant's application key <id> (--management: a\n management key, on the Runtime's machine only), or rotate it\n (the old key stops working); prints the key once\n keys list [--tenant <name>] [--json]\n the Tenant's keys: id, role, when issued (never the keys)\n keys rm <id> [--tenant <name>] delete key <id>: it stops working at once\n signing-keys list [--tenant <name>]\n the Tenant's token signing keys\n signing-keys rotate [--force] [--tenant <name>]\n rotate; --force ends outstanding links and run tokens\n signing-keys revoke <kid> [--tenant <name>]\n revoke a previous or standby key\n models catalog [--tenant <name>] [--json]\n every provider and model the Runtime knows\n models providers [--tenant <name>] [--json]\n the providers with a credential, and the one in use\n models get [--tenant <name>] [--json]\n the Tenant's model\n models set [--tenant <name>] sign in to a model provider and set the Tenant's model\n models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]\n the same with an API key piped in on stdin (CI)\n models select <provider> [--model <id>] [--tenant <name>]\n use a provider that has a credential (default: its model)\n usage [--scope tenant|agent|turn] [--id <id>] [--period day|month|total] [--tenant <name>]\n what the model calls used and cost (JSON)\n calls [--after <cursor>] [--limit <n>] [--tenant <name>]\n every recorded model call, one JSON line each, until\n caught up; prints the cursor to resume from on stderr\n budgets [get | put <file|->] [--tenant <name>]\n the model spend caps; put replaces them all\n tool-policy [get | put <file|->] [--tenant <name>]\n the Tool Gate's rules; put replaces them all\n settings sandbox [get | put <file|->] [--tenant <name>]\n the Environment settings: limits, stop grace, placement\n settings artifacts [get | put <file|->] [--tenant <name>]\n the artifact store's limits\n settings skills [get | put <file|->] [--tenant <name>]\n the most bytes one skill may hold\n settings limits [get | put <file|->] [--tenant <name>]\n the Runtime limits: sizes, counts and deadlines (record\n entries, request bodies, Environments, model calls, tools)\n coverage --agent <id> [--environment <id>] [--tenant <name>] [--json]\n which secret of the Environment each of the agent's MCP\n servers and HTTP tools would get\n mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]\n list a remote MCP server's tools, with Environment <id>'s\n credential for <url> (none without it): model names, schema\n sizes, renames, or that it needs a person's sign-in (the\n Runtime calls no tool)\n\nA put reads a JSON body from a file, or from stdin for -, and replaces the whole document; what\nbudgets, tool-policy, settings artifacts, settings skills and settings limits get print, put takes back. An API key is read from\nstdin, never from the arguments.";
10
10
  /** The Management API client of the installation `--tenant` (or the environment) names. */
11
11
  export declare function tenantClient(deps: StackDeps, tenant: string | undefined): Promise<{
12
12
  connection: Connection;
@@ -24,7 +24,7 @@ ${settingsUsage}
24
24
  ${mcpUsage}
25
25
 
26
26
  A put reads a JSON body from a file, or from stdin for -, and replaces the whole document; what
27
- budgets, tool-policy and settings artifacts get print, put takes back. An API key is read from
27
+ budgets, tool-policy, settings artifacts, settings skills and settings limits get print, put takes back. An API key is read from
28
28
  stdin, never from the arguments.`;
29
29
  const usageError = (message) => new CliError(message, 2);
30
30
  /** The Management API client of the installation `--tenant` (or the environment) names. */
@@ -3,6 +3,7 @@ import { readFileSync, statSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { parseEnv } from "node:util";
5
5
  import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
6
+ import { cliTuning } from "../tuning.js";
6
7
  /**
7
8
  * The Project's `.env` as a value map. It is never applied to this process or written back:
8
9
  * a Project's `.env` never configures the Host.
@@ -56,7 +57,7 @@ async function call(fetch, url, managementKey, init = {}) {
56
57
  },
57
58
  ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
58
59
  redirect: "error",
59
- signal: AbortSignal.timeout(15_000),
60
+ signal: AbortSignal.timeout(cliTuning().stack.seedRequestMs),
60
61
  });
61
62
  const body = (await response.json().catch(() => ({})));
62
63
  if (!response.ok)
@@ -10,6 +10,7 @@ import { apiRoute, readClusterFile, removeClusterFiles, writeClusterFiles, } fro
10
10
  import { contexts, kube } from "./kubectl.js";
11
11
  import { CONTROLLER_DEPLOYMENT, CONTROLLER_NAMESPACE, CONTROLLER_SHA256, CONTROLLER_URL, CONTROLLER_VERSION, networkPolicy, SANDBOX_API_VERSION, SANDBOX_CRD, tenantLabel, tenantManifests, tenantNamespace, TOKEN_SECRET, } from "./manifests.js";
12
12
  import { assertIPv4, runProbe } from "./probe.js";
13
+ import { cliTuning } from "../tuning.js";
13
14
  export const sandboxUsage = ` sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--no-pull]
14
15
  run the Tenant's sandboxes as pods on that kubeconfig context (agent-sandbox ${CONTROLLER_VERSION})
15
16
  sandbox disable [--tenant <name>] [--delete-namespace]
@@ -52,7 +53,7 @@ async function ensureController(k, fetch, err) {
52
53
  return;
53
54
  }
54
55
  err(`Installing agent-sandbox ${CONTROLLER_VERSION} into ${k.context}.`);
55
- const response = await fetch(CONTROLLER_URL, { signal: AbortSignal.timeout(60_000) });
56
+ const response = await fetch(CONTROLLER_URL, { signal: AbortSignal.timeout(cliTuning().sandbox.manifestDownloadMs) });
56
57
  if (!response.ok)
57
58
  throw new CliError(`Downloading ${CONTROLLER_URL} failed (HTTP ${response.status}).`, 1);
58
59
  const manifest = await response.text();
@@ -60,12 +61,13 @@ async function ensureController(k, fetch, err) {
60
61
  if (digest !== CONTROLLER_SHA256)
61
62
  throw new CliError(`${CONTROLLER_URL} has sha256 ${digest}, not the pinned ${CONTROLLER_SHA256}; not applying it.`, 1);
62
63
  await k.check("Installing agent-sandbox", ["apply", "--server-side", "-f", "-"], { input: manifest });
63
- await k.check("Waiting for the Sandbox CRD", ["wait", "--for=condition=Established", `crd/${SANDBOX_CRD}`, "--timeout=60s"]);
64
- await k.check("Waiting for the agent-sandbox controller", ["rollout", "status", `deployment/${CONTROLLER_DEPLOYMENT}`, "-n", CONTROLLER_NAMESPACE, "--timeout=180s"], { timeoutMs: 200_000 });
64
+ await k.check("Waiting for the Sandbox CRD", ["wait", "--for=condition=Established", `crd/${SANDBOX_CRD}`, `--timeout=${cliTuning().sandbox.crdWaitSeconds}s`]);
65
+ const tuning = cliTuning().sandbox;
66
+ await k.check("Waiting for the agent-sandbox controller", ["rollout", "status", `deployment/${CONTROLLER_DEPLOYMENT}`, "-n", CONTROLLER_NAMESPACE, `--timeout=${tuning.rolloutWaitSeconds}s`], { timeoutMs: tuning.rolloutWaitSeconds * 1000 + tuning.kubectlMarginMs });
65
67
  }
66
68
  /** The ServiceAccount token the token controller writes into the Secret. */
67
69
  async function serviceAccountToken(k, namespace, pollMs) {
68
- const deadline = Date.now() + 60_000;
70
+ const deadline = Date.now() + cliTuning().sandbox.tokenWaitMs;
69
71
  for (;;) {
70
72
  const secret = await k.getJson(["secret", TOKEN_SECRET, "-n", namespace]);
71
73
  if (secret?.data?.token)
@@ -82,7 +84,7 @@ async function serviceAccountToken(k, namespace, pollMs) {
82
84
  export async function installCluster(deps, input) {
83
85
  const { stack } = deps;
84
86
  const k = kube(deps.kubectl, input.context);
85
- const pollMs = deps.pollMs ?? 1000;
87
+ const pollMs = deps.pollMs ?? cliTuning().sandbox.pollMs;
86
88
  const { server, caData } = await readKubeconfig(k);
87
89
  let route;
88
90
  try {
@@ -189,7 +191,7 @@ async function enable(deps, args) {
189
191
  bind: installed.cluster.bindAddress,
190
192
  };
191
193
  await restartTenant(stack, { name: tenant.name, sandboxes: settings });
192
- const ready = await stack.docker.stream(tenant.compose("up", "--detach", "--wait", "--wait-timeout", "120", "sandboxes"));
194
+ const ready = await stack.docker.stream(tenant.compose("up", "--detach", "--wait", "--wait-timeout", String(cliTuning().sandbox.composeWaitSeconds), "sandboxes"));
193
195
  if (ready !== 0)
194
196
  throw new CliError(`The sandboxes service did not become ready (image ${images.sandboxes}). See "nylorun local logs sandboxes" and "nylorun local sandbox status".`, 7);
195
197
  const c = installed.cluster;
@@ -226,7 +228,7 @@ async function disable(deps, args) {
226
228
  return 0;
227
229
  }
228
230
  /** Asks the sandboxes service from inside the runtime container, which holds the token. */
229
- const INFO_SCRIPT = "fetch(process.env.NYLORUN_SANDBOXES_URL+'/v1/info',{headers:{authorization:'Bearer '+process.env.NYLORUN_SANDBOXES_TOKEN},signal:AbortSignal.timeout(5000)}).then(async r=>{process.stdout.write(await r.text());process.exit(r.ok?0:1)},()=>process.exit(1))";
231
+ const infoScript = (timeoutMs) => `fetch(process.env.NYLORUN_SANDBOXES_URL+'/v1/info',{headers:{authorization:'Bearer '+process.env.NYLORUN_SANDBOXES_TOKEN},signal:AbortSignal.timeout(${timeoutMs})}).then(async r=>{process.stdout.write(await r.text());process.exit(r.ok?0:1)},()=>process.exit(1))`;
230
232
  async function status(deps, args) {
231
233
  const { stack } = deps;
232
234
  const usage = "nylorun local sandbox status [--tenant <name>] [--json]";
@@ -245,7 +247,7 @@ async function status(deps, args) {
245
247
  }
246
248
  await dockerPreflight(stack.docker);
247
249
  const ready = (await stack.docker.run(tenant.compose("exec", "-T", "sandboxes", "/sandboxes", "healthcheck"))).code === 0;
248
- const answer = await stack.docker.run(tenant.compose("exec", "-T", "runtime", "node", "-e", INFO_SCRIPT));
250
+ const answer = await stack.docker.run(tenant.compose("exec", "-T", "runtime", "node", "-e", infoScript(cliTuning().sandbox.infoRequestMs)));
249
251
  let info;
250
252
  try {
251
253
  info = answer.code === 0 ? JSON.parse(answer.stdout) : undefined;
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { CliError } from "../errors.js";
3
+ import { cliTuning } from "../tuning.js";
3
4
  /** The real `kubectl` on PATH. */
4
5
  export function spawnKubectl(env, binary = "kubectl") {
5
6
  const childEnv = Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
@@ -9,7 +10,7 @@ export function spawnKubectl(env, binary = "kubectl") {
9
10
  let stdout = "";
10
11
  let stderr = "";
11
12
  const child = spawn(binary, [...args], { env: childEnv, stdio: ["pipe", "pipe", "pipe"] });
12
- const timer = setTimeout(() => child.kill("SIGKILL"), options.timeoutMs ?? 300_000);
13
+ const timer = setTimeout(() => child.kill("SIGKILL"), options.timeoutMs ?? cliTuning().sandbox.kubectlTimeoutMs);
13
14
  child.stdout.on("data", (chunk) => (stdout += chunk));
14
15
  child.stderr.on("data", (chunk) => (stderr += chunk));
15
16
  child.once("error", (error) => {
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { cliTuning } from "../tuning.js";
2
3
  /**
3
4
  * What `nylorun local sandbox enable` installs into a cluster. The agent-sandbox controller is
4
5
  * pinned (D32, Experiment 2): v1.0.5, API agents.x-k8s.io/v1beta1, its release manifest
@@ -119,7 +120,7 @@ export function probePod(namespace, name, options) {
119
120
  spec: {
120
121
  automountServiceAccountToken: false,
121
122
  enableServiceLinks: false,
122
- terminationGracePeriodSeconds: 1,
123
+ terminationGracePeriodSeconds: cliTuning().sandbox.probeGraceSeconds,
123
124
  restartPolicy: options.command ? "Never" : "Always",
124
125
  ...(options.nodeName ? { nodeName: options.nodeName } : {}),
125
126
  securityContext: { runAsNonRoot: true, runAsUser: 1000, seccompProfile: { type: "RuntimeDefault" } },
@@ -130,7 +131,7 @@ export function probePod(namespace, name, options) {
130
131
  imagePullPolicy: "IfNotPresent",
131
132
  command,
132
133
  securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } },
133
- resources: { limits: { cpu: "200m", memory: "64Mi" } },
134
+ resources: { limits: { cpu: cliTuning().sandbox.probeCpu, memory: cliTuning().sandbox.probeMemory } },
134
135
  },
135
136
  ],
136
137
  },
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { CliError } from "../errors.js";
3
3
  import { networkPolicy, PROBE_IMAGE, probePod } from "./manifests.js";
4
+ import { cliTuning } from "../tuning.js";
4
5
  const DOCKER_DESKTOP_HOST = "192.168.65.254";
5
6
  /** Docker Desktop and OrbStack forward the host address to this machine's loopback. */
6
7
  export function defaultBind(context, hostAddress) {
@@ -27,7 +28,7 @@ async function sleep(ms) {
27
28
  }
28
29
  /** `wget` from a probe pod; true when the connection succeeds. */
29
30
  async function reaches(kube, namespace, pod, address, port) {
30
- const result = await kube.run(["exec", "-n", namespace, pod, "--", "wget", "-q", "-O", "-", "-T", "3", `http://${address}:${port}/`], { timeoutMs: 20_000 });
31
+ const result = await kube.run(["exec", "-n", namespace, pod, "--", "wget", "-q", "-O", "-", "-T", String(cliTuning().sandbox.probeWgetSeconds), `http://${address}:${port}/`], { timeoutMs: cliTuning().sandbox.probeExecMs });
31
32
  return result.code === 0;
32
33
  }
33
34
  export async function runProbe(deps, options) {
@@ -35,7 +36,8 @@ export async function runProbe(deps, options) {
35
36
  const suffix = randomBytes(3).toString("hex");
36
37
  const namespace = `nylorun-sbx-probe-${suffix}`;
37
38
  const listener = namespace;
38
- const poll = deps.pollMs ?? 1000;
39
+ const tuning = cliTuning().sandbox;
40
+ const poll = deps.pollMs ?? tuning.pollMs;
39
41
  let listenerStarted = false;
40
42
  const fail = (message) => new CliError(`Refusing context ${kube.context}: ${message}`, 6);
41
43
  try {
@@ -46,8 +48,8 @@ export async function runProbe(deps, options) {
46
48
  probePod(namespace, "restricted", { role: "restricted" }),
47
49
  ]);
48
50
  await kube.check("Waiting for the probe pods", [
49
- "wait", "--for=condition=Ready", "pod", "--all", "-n", namespace, "--timeout=180s",
50
- ], { timeoutMs: 200_000 });
51
+ "wait", "--for=condition=Ready", "pod", "--all", "-n", namespace, `--timeout=${tuning.probeReadySeconds}s`,
52
+ ], { timeoutMs: tuning.probeReadySeconds * 1000 + tuning.kubectlMarginMs });
51
53
  let hostAddress = options.hostAddress;
52
54
  if (!hostAddress) {
53
55
  const lookup = await kube.run(["exec", "-n", namespace, "control", "--", "nslookup", "host.docker.internal"]);
@@ -80,7 +82,7 @@ export async function runProbe(deps, options) {
80
82
  await kube.apply("Applying the probe NetworkPolicy", [
81
83
  networkPolicy(namespace, hostAddress, [allowed], { name: "probe", podSelector: { "nylorun.dev/probe": "restricted" } }),
82
84
  ]);
83
- const deadline = Date.now() + (deps.policyTimeoutMs ?? 30_000);
85
+ const deadline = Date.now() + (deps.policyTimeoutMs ?? tuning.policyTimeoutMs);
84
86
  while (await reaches(kube, namespace, "restricted", peer, 8080)) {
85
87
  if (Date.now() > deadline)
86
88
  throw fail("NetworkPolicy is not enforced: a pod under a deny policy still reached another pod. Sandboxes need a CNI that enforces NetworkPolicy (kindnet with policy support, Calico, Cilium).");
@@ -117,7 +119,7 @@ async function prePull(deps, namespace, images) {
117
119
  await kube.apply("Pre-pulling images", pods.map((pod) => pod.manifest));
118
120
  const failed = new Set();
119
121
  const done = new Set();
120
- const deadline = Date.now() + 300_000;
122
+ const deadline = Date.now() + cliTuning().sandbox.prepullMs;
121
123
  while (done.size + failed.size < pods.length && Date.now() < deadline) {
122
124
  const list = await kube.getJson(["pods", "-n", namespace, "-l", "nylorun.dev/probe=pull"]);
123
125
  for (const item of list?.items ?? []) {
@@ -131,7 +133,7 @@ async function prePull(deps, namespace, images) {
131
133
  done.add(pod.name);
132
134
  }
133
135
  if (done.size + failed.size < pods.length)
134
- await sleep(deps.pollMs ?? 2000);
136
+ await sleep(deps.pollMs ?? cliTuning().sandbox.prepullPollMs);
135
137
  }
136
138
  return images.filter((image) => pods.some((pod) => pod.image === image && !done.has(pod.name)));
137
139
  }
@@ -15,6 +15,7 @@ import { prepareStack, readStackEnv } from "./prepare.js";
15
15
  import { assertTenantName, chooseTenantName, DEFAULT_TENANT, defaultNylorunRoot, listTenants, moveStackRoots, readTenantRecord, removeOldProxy, sanitizeTenantName, tenantRoot, tenantsDir, writeTenantRecord, } from "./stacks.js";
16
16
  import { mintStudioLogin, studioOrigin } from "./studio-login.js";
17
17
  import { CLI_KEY_ID, hostKey, keyAuthenticates, PROJECT_KEY_ID, } from "./operator-keys.js";
18
+ import { cliTuning } from "../tuning.js";
18
19
  export const STACK_SERVICES = [
19
20
  "postgres",
20
21
  "restate",
@@ -199,7 +200,7 @@ async function sleep(ms) {
199
200
  async function fetchHealth(deps, runtimeUrl) {
200
201
  try {
201
202
  const response = await deps.fetch(`${runtimeUrl}/health`, {
202
- signal: AbortSignal.timeout(3000),
203
+ signal: AbortSignal.timeout(cliTuning().stack.probeTimeoutMs),
203
204
  redirect: "error",
204
205
  });
205
206
  if (!response.ok)
@@ -211,7 +212,7 @@ async function fetchHealth(deps, runtimeUrl) {
211
212
  }
212
213
  }
213
214
  async function waitForHealth(ctx, runtimeUrl, hostId) {
214
- const deadline = Date.now() + (ctx.deps.healthTimeoutMs ?? 60_000);
215
+ const deadline = Date.now() + (ctx.deps.healthTimeoutMs ?? cliTuning().stack.healthWaitMs);
215
216
  for (;;) {
216
217
  const health = await fetchHealth(ctx.deps, runtimeUrl);
217
218
  if (health?.status === "ok") {
@@ -221,7 +222,7 @@ async function waitForHealth(ctx, runtimeUrl, hostId) {
221
222
  }
222
223
  if (Date.now() > deadline)
223
224
  throw new CliError(`The Runtime did not answer ${runtimeUrl}/health. See "nylorun local logs runtime".`, 7);
224
- await sleep(ctx.deps.pollMs ?? 500);
225
+ await sleep(ctx.deps.pollMs ?? cliTuning().stack.pollMs);
225
226
  }
226
227
  }
227
228
  /** The Runtime's `/ready` (503 while not ready, same body); undefined when it does not answer. */
@@ -229,7 +230,7 @@ async function fetchReady(deps, runtimeUrl) {
229
230
  try {
230
231
  const response = await deps.fetch(`${runtimeUrl}/ready`, {
231
232
  headers: { accept: "application/json" },
232
- signal: AbortSignal.timeout(3000),
233
+ signal: AbortSignal.timeout(cliTuning().stack.probeTimeoutMs),
233
234
  redirect: "error",
234
235
  });
235
236
  const body = (await response.json());
@@ -289,11 +290,11 @@ function tenantCauseMessage(ctx, tenant) {
289
290
  * Tenant cannot open (`schema-too-new`, `database-layout-old`), which ends the wait.
290
291
  */
291
292
  async function waitForTenant(ctx, runtimeUrl) {
292
- const deadline = Date.now() + (ctx.deps.healthTimeoutMs ?? 60_000);
293
+ const deadline = Date.now() + (ctx.deps.healthTimeoutMs ?? cliTuning().stack.tenantWaitMs);
293
294
  for (let poll = 1;; poll += 1) {
294
295
  const open = (await fetchReady(ctx.deps, runtimeUrl))?.tenant === true;
295
296
  const late = Date.now() > deadline;
296
- if (open || late || poll % 10 === 0) {
297
+ if (open || late || poll % cliTuning().stack.operateEveryPolls === 0) {
297
298
  const tenant = await operateStatus(ctx);
298
299
  if (tenant?.state === "open" && tenant.id)
299
300
  return { ...tenant, id: tenant.id };
@@ -302,7 +303,7 @@ async function waitForTenant(ctx, runtimeUrl) {
302
303
  if (late)
303
304
  throw new CliError(`Tenant ${ctx.name} did not open (${runtimeUrl}/ready: ${open ? "open, but nylorun-operate status did not answer" : "not open"})${tenant?.message ? `: ${tenant.message.replace(/\.$/, "")}` : ""}. See "nylorun status" and "nylorun local logs runtime".`, 7);
304
305
  }
305
- await sleep(ctx.deps.pollMs ?? 500);
306
+ await sleep(ctx.deps.pollMs ?? cliTuning().stack.pollMs);
306
307
  }
307
308
  }
308
309
  /**
@@ -427,7 +428,7 @@ async function bringUp(ctx, options) {
427
428
  const up = await deps.docker.stream(
428
429
  // --remove-orphans: a service this start's file no longer has (the harness after a rollback
429
430
  // to NYLORUN_HARNESS=in-process, sandboxes after disable) is removed, not left running.
430
- composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", "300", "--remove-orphans", ...coreServices(prepared.env.harness)));
431
+ composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", String(cliTuning().stack.composeWaitSeconds), "--remove-orphans", ...coreServices(prepared.env.harness)));
431
432
  if (up !== 0) {
432
433
  // A Runtime whose Tenant cannot open fails readiness; nylorun-operate names the cause.
433
434
  const tenant = await operateStatus(ctx);
@@ -444,7 +445,7 @@ async function bringUp(ctx, options) {
444
445
  }
445
446
  let studioStarted = false;
446
447
  if (options.studio) {
447
- const studio = await deps.docker.stream(composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", "120", "studio"));
448
+ const studio = await deps.docker.stream(composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", String(cliTuning().stack.studioWaitSeconds), "studio"));
448
449
  studioStarted = studio === 0;
449
450
  if (!studioStarted)
450
451
  deps.err(`Warning: Studio did not start (image ${prepared.env.studioImage}). The Runtime is up; see "nylorun local logs studio".`);
@@ -466,7 +467,7 @@ const errorText = (error) => (error instanceof Error ? error.message : String(er
466
467
  async function tryStudioLogin(ctx, studioPort, adminKey) {
467
468
  const { deps } = ctx;
468
469
  const origin = studioOrigin(studioPort);
469
- const deadline = Date.now() + (deps.loginTimeoutMs ?? 15_000);
470
+ const deadline = Date.now() + (deps.loginTimeoutMs ?? cliTuning().stack.loginWaitMs);
470
471
  let lastError = "";
471
472
  for (;;) {
472
473
  try {
@@ -477,7 +478,7 @@ async function tryStudioLogin(ctx, studioPort, adminKey) {
477
478
  }
478
479
  if (Date.now() > deadline)
479
480
  break;
480
- await sleep(deps.pollMs ?? 500);
481
+ await sleep(deps.pollMs ?? cliTuning().stack.pollMs);
481
482
  }
482
483
  deps.err(`Warning: Studio at ${origin} is not reachable for a login (${lastError}).`);
483
484
  return undefined;
@@ -68,6 +68,9 @@
68
68
  *
69
69
  * With `identity` (when `<Host root>/identity.yaml` exists) the runtime reads its trusted issuers
70
70
  * from it, through the Host root mount (NYLORUN_IDENTITY_FILE=/nylorun/identity.yaml).
71
+ *
72
+ * The healthchecks and Postgres's WAL cap come from the CLI tuning (`NYLORUN_CLI_TUNING`). The
73
+ * `tmpfs` masks are empty: 4096 bytes is one page, the least a tmpfs takes (Linux's page size).
71
74
  */
72
75
  export declare function renderComposeFile(project: string, name: string, options?: {
73
76
  sandboxes?: true;
@@ -1,4 +1,15 @@
1
1
  import { PINNED_IMAGES } from "./images.js";
2
+ import { cliTuning } from "../tuning.js";
3
+ /** A service's healthcheck lines, from the CLI tuning (`compose.healthchecks`). */
4
+ function healthcheck(service) {
5
+ const { intervalSeconds, timeoutSeconds, retries } = cliTuning().compose.healthchecks[service];
6
+ return ` interval: ${intervalSeconds}s\n timeout: ${timeoutSeconds}s\n retries: ${retries}\n`;
7
+ }
8
+ /** Postgres's `max_slot_wal_keep_size` (`compose.walKeepSizeMb`), in GB when it is whole GB. */
9
+ function walKeepSize() {
10
+ const mb = cliTuning().compose.walKeepSizeMb;
11
+ return mb % 1024 === 0 ? `${mb / 1024}GB` : `${mb}MB`;
12
+ }
2
13
  /**
3
14
  * A local Tenant's Compose file (Runtime Architecture §14.3), written to
4
15
  * `<Host root>/docker/compose.yaml` by `nylorun start`. Each local Tenant is one installation
@@ -69,6 +80,9 @@ import { PINNED_IMAGES } from "./images.js";
69
80
  *
70
81
  * With `identity` (when `<Host root>/identity.yaml` exists) the runtime reads its trusted issuers
71
82
  * from it, through the Host root mount (NYLORUN_IDENTITY_FILE=/nylorun/identity.yaml).
83
+ *
84
+ * The healthchecks and Postgres's WAL cap come from the CLI tuning (`NYLORUN_CLI_TUNING`). The
85
+ * `tmpfs` masks are empty: 4096 bytes is one page, the least a tmpfs takes (Linux's page size).
72
86
  */
73
87
  export function renderComposeFile(project, name, options = {}) {
74
88
  const sandboxes = options.sandboxes === true;
@@ -86,8 +100,8 @@ services:
86
100
  image: ${PINNED_IMAGES.postgres}
87
101
  container_name: ${project}-postgres
88
102
  labels: *tenant
89
- # Logical replication feeds the stream relay; a stuck slot is capped at 4 GB of WAL.
90
- command: ["postgres", "-c", "wal_level=logical", "-c", "max_slot_wal_keep_size=4GB"]
103
+ # Logical replication feeds the stream relay; a stuck slot's WAL is capped (compose.walKeepSizeMb).
104
+ command: ["postgres", "-c", "wal_level=logical", "-c", "max_slot_wal_keep_size=${walKeepSize()}"]
91
105
  environment:
92
106
  POSTGRES_USER: nylorun
93
107
  POSTGRES_PASSWORD: \${NYLORUN_POSTGRES_PASSWORD:?run nylorun start}
@@ -98,10 +112,7 @@ services:
98
112
  networks: [store]
99
113
  healthcheck:
100
114
  test: ["CMD-SHELL", "pg_isready -U nylorun -d nylorun"]
101
- interval: 2s
102
- timeout: 5s
103
- retries: 30
104
- restart: unless-stopped
115
+ ${healthcheck("postgres")} restart: unless-stopped
105
116
 
106
117
  restate: # Durable Session Execution
107
118
  image: ${PINNED_IMAGES.restate}
@@ -115,10 +126,7 @@ services:
115
126
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/docker/restate-identity.pem:/run/nylorun/restate-identity.pem:ro
116
127
  ${restateUi ? RESTATE_UI : RESTATE_CLOSED} healthcheck:
117
128
  test: ["CMD", "curl", "-fsS", "http://localhost:9070/health"]
118
- interval: 2s
119
- timeout: 5s
120
- retries: 30
121
- restart: unless-stopped
129
+ ${healthcheck("restate")} restart: unless-stopped
122
130
 
123
131
  s2-lite: # Durable Streams; not published, only the Runtime reaches it
124
132
  image: ${PINNED_IMAGES.s2}
@@ -150,10 +158,7 @@ ${restateUi ? RESTATE_UI : RESTATE_CLOSED} healthcheck:
150
158
  networks: [store]
151
159
  healthcheck:
152
160
  test: ["CMD", "curl", "-fsS", "http://localhost:9000/health"]
153
- interval: 2s
154
- timeout: 5s
155
- retries: 30
156
- restart: unless-stopped
161
+ ${healthcheck("rustfs")} restart: unless-stopped
157
162
 
158
163
  gateway: # the Model and Tool Gates and keys: the vault key, credentials, signing, outbound calls; not published${sandboxes ? ", but for sandbox pods" : ""}
159
164
  image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
@@ -175,7 +180,9 @@ ${restateUi ? RESTATE_UI : RESTATE_CLOSED} healthcheck:
175
180
  NYLORUN_OBJECT_STORE_ACCESS_KEY: nylorun
176
181
  NYLORUN_OBJECT_STORE_SECRET_KEY: \${NYLORUN_OBJECT_STORE_SECRET_KEY:?run nylorun start}
177
182
  # MCP servers and HTTP tools on this machine: \`localhost\` in their URLs means the Docker host.
178
- NYLORUN_ENDPOINT_LOOPBACK: docker-host${sandboxes ? SANDBOXES_GATEWAY_ENV : ""}
183
+ NYLORUN_ENDPOINT_LOOPBACK: docker-host
184
+ # Host tuning (timings, retries, internal sizes; JSON): from the shell that runs \`nylorun start\`.
185
+ NYLORUN_TUNING: \${NYLORUN_TUNING:-}${sandboxes ? SANDBOXES_GATEWAY_ENV : ""}
179
186
  extra_hosts:
180
187
  host.docker.internal: host-gateway # model servers, MCP servers and HTTP tools on this machine
181
188
  volumes:
@@ -186,10 +193,7 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
186
193
  networks: [default, store, harness]
187
194
  healthcheck:
188
195
  test: ["CMD", "node", "-e", "fetch('http://localhost:4100/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
189
- interval: 2s
190
- timeout: 5s
191
- retries: 60
192
- restart: unless-stopped
196
+ ${healthcheck("gateway")} restart: unless-stopped
193
197
 
194
198
  runtime:
195
199
  image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
@@ -247,7 +251,10 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
247
251
  OTEL_SERVICE_NAME: \${OTEL_SERVICE_NAME:-}
248
252
  OTEL_RESOURCE_ATTRIBUTES: \${OTEL_RESOURCE_ATTRIBUTES:-}
249
253
  OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: \${OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT:-}
250
- NYLORUN_TRACE_CONTENT: \${NYLORUN_TRACE_CONTENT:-}${sandboxes ? SANDBOXES_RUNTIME_ENV : ""}${identity ? IDENTITY_RUNTIME_ENV : ""}
254
+ NYLORUN_TRACE_CONTENT: \${NYLORUN_TRACE_CONTENT:-}
255
+ # Host tuning (timings, retries, internal sizes; JSON): from the shell that runs \`nylorun start\`.
256
+ # Sandbox pods get its harness and engine groups from the runtime.
257
+ NYLORUN_TUNING: \${NYLORUN_TUNING:-}${sandboxes ? SANDBOXES_RUNTIME_ENV : ""}${identity ? IDENTITY_RUNTIME_ENV : ""}
251
258
  extra_hosts:
252
259
  host.docker.internal: host-gateway # the Docker host, also on Linux Docker Engine
253
260
  volumes:
@@ -268,10 +275,7 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
268
275
  - "127.0.0.1:\${NYLORUN_PORT:?run nylorun start}:4000" # Runtime and Management APIs, SSE, browsers${sandboxes ? SANDBOXES_RUNTIME_PORT : ""}
269
276
  healthcheck: # liveness: a Tenant that cannot open is reported by nylorun start (/ready, nylorun-operate status), not by a 300 s wait
270
277
  test: ["CMD", "node", "-e", "fetch('http://localhost:4000/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
271
- interval: 2s
272
- timeout: 5s
273
- retries: 60
274
- restart: unless-stopped
278
+ ${healthcheck("runtime")} restart: unless-stopped
275
279
 
276
280
  studio: # dashboard + trusted proxy
277
281
  image: \${NYLORUN_STUDIO_IMAGE:?run nylorun start}
@@ -303,10 +307,7 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
303
307
  - "127.0.0.1:\${NYLORUN_STUDIO_PORT:?run nylorun start}:3000" # opened as http://localhost:<port>
304
308
  healthcheck: # /healthz needs no session; Host studio:3000 is accepted
305
309
  test: ["CMD", "node", "-e", "require('http').get({port:3000,path:'/healthz',headers:{host:'studio:3000'}},r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
306
- interval: 2s
307
- timeout: 5s
308
- retries: 30
309
- restart: unless-stopped
310
+ ${healthcheck("studio")} restart: unless-stopped
310
311
  ${remote ? harnessService(project) : ""}${sandboxes ? sandboxesService(project) : ""}
311
312
  networks:
312
313
  default:
@@ -357,16 +358,14 @@ function harnessService(project) {
357
358
  NYLORUN_HARNESS_TOKEN: \${NYLORUN_HARNESS_TOKEN:?run nylorun start}
358
359
  NYLORUN_GATES_URL: http://gateway:4100
359
360
  NYLORUN_HARNESS_ROOT: /harness
361
+ NYLORUN_TUNING: \${NYLORUN_TUNING:-} # Host tuning, as the runtime has it
360
362
  volumes:
361
363
  # The Tenant's workspaces; nothing else.
362
364
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/sandboxes:/harness/sandboxes
363
365
  networks: [harness]
364
366
  healthcheck: # healthy once connected to core's Harness API
365
367
  test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4300/health').then(r=>r.json()).then(b=>process.exit(b.connected?0:1),()=>process.exit(1))"]
366
- interval: 2s
367
- timeout: 5s
368
- retries: 90
369
- restart: unless-stopped
368
+ ${healthcheck("harness")} restart: unless-stopped
370
369
  `;
371
370
  }
372
371
  /**
@@ -428,9 +427,6 @@ function sandboxesService(project) {
428
427
  networks: [default] # the cluster's API server, through host.docker.internal
429
428
  healthcheck: # /ready: informers synced and the API server answers
430
429
  test: ["CMD", "/sandboxes", "healthcheck"]
431
- interval: 5s
432
- timeout: 5s
433
- retries: 24
434
- restart: unless-stopped
430
+ ${healthcheck("sandboxes")} restart: unless-stopped
435
431
  `;
436
432
  }
@@ -8,7 +8,7 @@ export declare const DEFAULT_STUDIO_FRAME_ANCESTORS: readonly string[];
8
8
  * persist; the Restate identity public key is derived from `restate-identity.pem`.
9
9
  */
10
10
  export interface StackEnv {
11
- /** Published Runtime port (loopback): the Tenant API. */
11
+ /** Published Runtime port (loopback): the Runtime and Management APIs. */
12
12
  runtimePort: number;
13
13
  /** Published Studio port (loopback). */
14
14
  studioPort: number;
@@ -8,6 +8,7 @@ import { open, rm, stat } from "node:fs/promises";
8
8
  import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
9
9
  import { CliError } from "../errors.js";
10
10
  import { readCredentialsFile, writeCredentialsFile, } from "../project/link.js";
11
+ import { cliTuning } from "../tuning.js";
11
12
  /** The keys `nylorun start` gives the projects it links (and `-management`). */
12
13
  export const PROJECT_KEY_ID = "project";
13
14
  /** The keys nylorun commands use outside a linked project (and `-management`). */
@@ -56,7 +57,7 @@ export async function keyAuthenticates(fetch, runtimeUrl, key) {
56
57
  accept: "application/json",
57
58
  },
58
59
  redirect: "error",
59
- signal: AbortSignal.timeout(10_000),
60
+ signal: AbortSignal.timeout(cliTuning().stack.keyCheckMs),
60
61
  });
61
62
  }
62
63
  catch (error) {
@@ -69,12 +70,12 @@ export async function keyAuthenticates(fetch, runtimeUrl, key) {
69
70
  return false;
70
71
  throw new CliError(`GET ${runtimeUrl}/v1/me returned ${response.status}.`, 7);
71
72
  }
72
- const LOCK_TIMEOUT_MS = 30_000;
73
+ const lockTimeoutMs = () => cliTuning().stack.lockWaitMs;
73
74
  /** A lock older than this was left by a process that died. */
74
- const LOCK_STALE_MS = 60_000;
75
+ const lockStaleMs = () => cliTuning().stack.lockStaleMs;
75
76
  /** Runs `fn` holding the lock file `path` (created exclusively), so one process puts a key. */
76
77
  async function withLock(path, fn) {
77
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
78
+ const deadline = Date.now() + lockTimeoutMs();
78
79
  for (;;) {
79
80
  try {
80
81
  const handle = await open(path, "wx", 0o600);
@@ -85,13 +86,13 @@ async function withLock(path, fn) {
85
86
  if (error.code !== "EEXIST")
86
87
  throw error;
87
88
  const held = await stat(path).catch(() => undefined);
88
- if (held && Date.now() - held.mtimeMs > LOCK_STALE_MS) {
89
+ if (held && Date.now() - held.mtimeMs > lockStaleMs()) {
89
90
  await rm(path, { force: true });
90
91
  continue;
91
92
  }
92
93
  if (Date.now() > deadline)
93
94
  throw new CliError(`Another nylorun command holds ${path}. Remove it if none runs.`, 1);
94
- await new Promise((resolve) => setTimeout(resolve, 100));
95
+ await new Promise((resolve) => setTimeout(resolve, cliTuning().stack.lockPollMs));
95
96
  }
96
97
  }
97
98
  try {
@@ -1,4 +1,5 @@
1
1
  import { createServer } from "node:net";
2
+ import { cliTuning } from "../tuning.js";
2
3
  export const DEFAULT_PORTS = {
3
4
  runtime: 8787,
4
5
  studio: 4161,
@@ -36,7 +37,7 @@ export async function choosePort(probe, preferred, persisted, taken) {
36
37
  return persisted;
37
38
  if (!taken.has(preferred) && (await probe.isFree(preferred)))
38
39
  return preferred;
39
- for (let attempt = 0; attempt < 20; attempt += 1) {
40
+ for (let attempt = 0; attempt < cliTuning().stack.portAttempts; attempt += 1) {
40
41
  const port = await probe.pickFree();
41
42
  if (!taken.has(port))
42
43
  return port;
@@ -1,4 +1,5 @@
1
1
  import { STACK_CLIENT_HOST } from "./host-files.js";
2
+ import { cliTuning } from "../tuning.js";
2
3
  /** Studio's published origin: always `localhost` (the cookie's host). */
3
4
  export function studioOrigin(port) {
4
5
  return `http://${STACK_CLIENT_HOST}:${port}`;
@@ -20,7 +21,7 @@ export async function mintStudioLogin(input) {
20
21
  },
21
22
  body: "{}",
22
23
  redirect: "error",
23
- signal: AbortSignal.timeout(input.timeoutMs ?? 5000),
24
+ signal: AbortSignal.timeout(input.timeoutMs ?? cliTuning().stack.loginRequestMs),
24
25
  });
25
26
  if (!response.ok)
26
27
  throw new Error(`Studio refused a login token (HTTP ${response.status}).`);
@@ -0,0 +1,90 @@
1
+ /** A Compose healthcheck: seconds between checks, seconds one check may take, and failures in a row. */
2
+ export interface Healthcheck {
3
+ readonly intervalSeconds: number;
4
+ readonly timeoutSeconds: number;
5
+ readonly retries: number;
6
+ }
7
+ export declare const DEFAULT_CLI_TUNING: {
8
+ /** `nylorun start` and the commands that reach the local stack. */
9
+ stack: {
10
+ /** One `/health` or `/ready` probe of the Runtime. */
11
+ probeTimeoutMs: number;
12
+ /** How long `start` waits for the Runtime's `/health`, and then for the Tenant to open. */
13
+ healthWaitMs: number;
14
+ tenantWaitMs: number;
15
+ /** How often `start` polls while it waits. */
16
+ pollMs: number;
17
+ /** While the Tenant opens, `nylorun-operate status` is asked every this many polls. */
18
+ operateEveryPolls: number;
19
+ /** `docker compose up --wait-timeout` of the core services, and of Studio, in seconds. */
20
+ composeWaitSeconds: number;
21
+ studioWaitSeconds: number;
22
+ /** How long a Studio login is retried, and how long one request for it may take. */
23
+ loginWaitMs: number;
24
+ loginRequestMs: number;
25
+ /** How long a key check against the Runtime (`/v1/me`) may take. */
26
+ keyCheckMs: number;
27
+ /** The local key file's lock: how long to wait for it, when a held one is stale, how often to look. */
28
+ lockWaitMs: number;
29
+ lockStaleMs: number;
30
+ lockPollMs: number;
31
+ /** Tries at picking a free port. */
32
+ portAttempts: number;
33
+ /** How long one seed or model call to the Management API may take (`nylorun dev`). */
34
+ seedRequestMs: number;
35
+ };
36
+ /** What `nylorun start` writes into the Compose file. */
37
+ compose: {
38
+ /** Postgres's `max_slot_wal_keep_size`: the WAL a stuck replication slot may hold, in MiB. */
39
+ walKeepSizeMb: number;
40
+ healthchecks: {
41
+ postgres: Healthcheck;
42
+ restate: Healthcheck;
43
+ rustfs: Healthcheck;
44
+ gateway: Healthcheck;
45
+ runtime: Healthcheck;
46
+ studio: Healthcheck;
47
+ harness: Healthcheck;
48
+ sandboxes: Healthcheck;
49
+ };
50
+ };
51
+ /** `nylorun local sandbox` (agent-sandbox on a local Kubernetes cluster). */
52
+ sandbox: {
53
+ /** How long a `kubectl` call may run before it is killed, and how much longer than a wait it was told. */
54
+ kubectlTimeoutMs: number;
55
+ kubectlMarginMs: number;
56
+ /** Downloading the agent-sandbox manifest. */
57
+ manifestDownloadMs: number;
58
+ /** `kubectl wait` for the CRD, and `rollout status` of the controller, in seconds. */
59
+ crdWaitSeconds: number;
60
+ rolloutWaitSeconds: number;
61
+ /** How long to wait for the service account's token Secret. */
62
+ tokenWaitMs: number;
63
+ /** How often install and the network probe poll. */
64
+ pollMs: number;
65
+ /** `docker compose up --wait-timeout` of the sandboxes service, in seconds. */
66
+ composeWaitSeconds: number;
67
+ /** How long the sandboxes service's `/v1/info`, read from the runtime container, may take. */
68
+ infoRequestMs: number;
69
+ /** The reachability probe: `wget -T` in seconds, and the `kubectl exec` around it. */
70
+ probeWgetSeconds: number;
71
+ probeExecMs: number;
72
+ /** How long probe pods may take to be Ready, in seconds, and the NetworkPolicy to apply. */
73
+ probeReadySeconds: number;
74
+ policyTimeoutMs: number;
75
+ /** How long image pre-pulls may take, and how often they are checked. */
76
+ prepullMs: number;
77
+ prepullPollMs: number;
78
+ /** A probe pod's `terminationGracePeriodSeconds`, and its resources (Kubernetes quantities). */
79
+ probeGraceSeconds: number;
80
+ probeCpu: string;
81
+ probeMemory: string;
82
+ };
83
+ };
84
+ type Widen<T> = T extends number ? number : T extends string ? string : {
85
+ readonly [K in keyof T]: Widen<T[K]>;
86
+ };
87
+ export type CliTuning = Widen<typeof DEFAULT_CLI_TUNING>;
88
+ /** The CLI's tuning: `NYLORUN_CLI_TUNING` over the defaults, read now. */
89
+ export declare function cliTuning(env?: Readonly<Record<string, string | undefined>>): CliTuning;
90
+ export {};
package/dist/tuning.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The CLI's own timings and sizes (B15): how long `nylorun` waits for the local stack, how often
3
+ * it polls, the healthchecks it writes into the Compose file, and the waits of `nylorun local
4
+ * sandbox`. Each has a default, the value the CLI used before it was configuration. Set any part
5
+ * in `NYLORUN_CLI_TUNING`, a JSON object of the groups below
6
+ * (`{"stack":{"healthWaitMs":120000}}`), read each time a value is used; an unknown key or a value
7
+ * that is not a positive whole number stops the command naming it.
8
+ *
9
+ * The Runtime's own timings are its Host tuning (`NYLORUN_TUNING`), which `nylorun start` passes
10
+ * to its containers; Tenant-facing limits are the Runtime limits (`nylorun tenant settings
11
+ * limits`). Neither is here.
12
+ */
13
+ import { CliError } from "./errors.js";
14
+ const check = (retries, intervalSeconds = 2, timeoutSeconds = 5) => ({
15
+ intervalSeconds,
16
+ timeoutSeconds,
17
+ retries,
18
+ });
19
+ export const DEFAULT_CLI_TUNING = deepFreeze({
20
+ /** `nylorun start` and the commands that reach the local stack. */
21
+ stack: {
22
+ /** One `/health` or `/ready` probe of the Runtime. */
23
+ probeTimeoutMs: 3_000,
24
+ /** How long `start` waits for the Runtime's `/health`, and then for the Tenant to open. */
25
+ healthWaitMs: 60_000,
26
+ tenantWaitMs: 60_000,
27
+ /** How often `start` polls while it waits. */
28
+ pollMs: 500,
29
+ /** While the Tenant opens, `nylorun-operate status` is asked every this many polls. */
30
+ operateEveryPolls: 10,
31
+ /** `docker compose up --wait-timeout` of the core services, and of Studio, in seconds. */
32
+ composeWaitSeconds: 300,
33
+ studioWaitSeconds: 120,
34
+ /** How long a Studio login is retried, and how long one request for it may take. */
35
+ loginWaitMs: 15_000,
36
+ loginRequestMs: 5_000,
37
+ /** How long a key check against the Runtime (`/v1/me`) may take. */
38
+ keyCheckMs: 10_000,
39
+ /** The local key file's lock: how long to wait for it, when a held one is stale, how often to look. */
40
+ lockWaitMs: 30_000,
41
+ lockStaleMs: 60_000,
42
+ lockPollMs: 100,
43
+ /** Tries at picking a free port. */
44
+ portAttempts: 20,
45
+ /** How long one seed or model call to the Management API may take (`nylorun dev`). */
46
+ seedRequestMs: 15_000,
47
+ },
48
+ /** What `nylorun start` writes into the Compose file. */
49
+ compose: {
50
+ /** Postgres's `max_slot_wal_keep_size`: the WAL a stuck replication slot may hold, in MiB. */
51
+ walKeepSizeMb: 4_096,
52
+ healthchecks: {
53
+ postgres: check(30),
54
+ restate: check(30),
55
+ rustfs: check(30),
56
+ gateway: check(60),
57
+ runtime: check(60),
58
+ studio: check(30),
59
+ harness: check(90),
60
+ sandboxes: check(24, 5, 5),
61
+ },
62
+ },
63
+ /** `nylorun local sandbox` (agent-sandbox on a local Kubernetes cluster). */
64
+ sandbox: {
65
+ /** How long a `kubectl` call may run before it is killed, and how much longer than a wait it was told. */
66
+ kubectlTimeoutMs: 300_000,
67
+ kubectlMarginMs: 20_000,
68
+ /** Downloading the agent-sandbox manifest. */
69
+ manifestDownloadMs: 60_000,
70
+ /** `kubectl wait` for the CRD, and `rollout status` of the controller, in seconds. */
71
+ crdWaitSeconds: 60,
72
+ rolloutWaitSeconds: 180,
73
+ /** How long to wait for the service account's token Secret. */
74
+ tokenWaitMs: 60_000,
75
+ /** How often install and the network probe poll. */
76
+ pollMs: 1_000,
77
+ /** `docker compose up --wait-timeout` of the sandboxes service, in seconds. */
78
+ composeWaitSeconds: 120,
79
+ /** How long the sandboxes service's `/v1/info`, read from the runtime container, may take. */
80
+ infoRequestMs: 5_000,
81
+ /** The reachability probe: `wget -T` in seconds, and the `kubectl exec` around it. */
82
+ probeWgetSeconds: 3,
83
+ probeExecMs: 20_000,
84
+ /** How long probe pods may take to be Ready, in seconds, and the NetworkPolicy to apply. */
85
+ probeReadySeconds: 180,
86
+ policyTimeoutMs: 30_000,
87
+ /** How long image pre-pulls may take, and how often they are checked. */
88
+ prepullMs: 300_000,
89
+ prepullPollMs: 2_000,
90
+ /** A probe pod's `terminationGracePeriodSeconds`, and its resources (Kubernetes quantities). */
91
+ probeGraceSeconds: 1,
92
+ probeCpu: "200m",
93
+ probeMemory: "64Mi",
94
+ },
95
+ });
96
+ function deepFreeze(value) {
97
+ if (value && typeof value === "object") {
98
+ for (const item of Object.values(value))
99
+ deepFreeze(item);
100
+ Object.freeze(value);
101
+ }
102
+ return value;
103
+ }
104
+ function merge(defaults, set, path) {
105
+ if (set === undefined)
106
+ return defaults;
107
+ if (typeof defaults === "number") {
108
+ // A grace period may be 0; everything else is at least 1.
109
+ const least = path.endsWith("GraceSeconds") ? 0 : 1;
110
+ if (typeof set !== "number" || !Number.isSafeInteger(set) || set < least)
111
+ throw new CliError(`NYLORUN_CLI_TUNING: ${path} must be a whole number of at least ${least}`, 2);
112
+ return set;
113
+ }
114
+ if (typeof defaults === "string") {
115
+ if (typeof set !== "string" || set.trim() === "")
116
+ throw new CliError(`NYLORUN_CLI_TUNING: ${path} must be a non-empty string`, 2);
117
+ return set;
118
+ }
119
+ if (!set || typeof set !== "object" || Array.isArray(set))
120
+ throw new CliError(`NYLORUN_CLI_TUNING: ${path || "(root)"} must be an object`, 2);
121
+ const out = {};
122
+ const known = defaults;
123
+ for (const key of Object.keys(set))
124
+ if (!(key in known))
125
+ throw new CliError(`NYLORUN_CLI_TUNING: ${path ? `${path}.` : ""}${key} is not a setting`, 2);
126
+ for (const [key, value] of Object.entries(known))
127
+ out[key] = merge(value, set[key], path ? `${path}.${key}` : key);
128
+ return out;
129
+ }
130
+ /** The CLI's tuning: `NYLORUN_CLI_TUNING` over the defaults, read now. */
131
+ export function cliTuning(env = process.env) {
132
+ const raw = env.NYLORUN_CLI_TUNING;
133
+ if (raw === undefined || raw.trim() === "")
134
+ return DEFAULT_CLI_TUNING;
135
+ let set;
136
+ try {
137
+ set = JSON.parse(raw);
138
+ }
139
+ catch (error) {
140
+ throw new CliError(`NYLORUN_CLI_TUNING must be a JSON object: ${error instanceof Error ? error.message : String(error)}`, 2);
141
+ }
142
+ return merge(DEFAULT_CLI_TUNING, set, "");
143
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "nylorun",
3
- "version": "0.15.0-beta",
3
+ "version": "0.16.0-beta",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
- "description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose (nylorun), and the Runtime client of the linked installation (nylo)",
6
+ "description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose, and act on a Tenant through its Runtime and Management APIs",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/nylorun/agents.git",
@@ -30,9 +30,9 @@
30
30
  "prepack": "npm run build"
31
31
  },
32
32
  "dependencies": {
33
- "@nylorun/admin": "0.17.0-beta",
34
- "@nylorun/agents": "0.21.0-beta",
35
- "@nylorun/core": "0.21.0-beta"
33
+ "@nylorun/admin": "0.18.0-beta",
34
+ "@nylorun/agents": "0.23.0-beta",
35
+ "@nylorun/core": "0.23.0-beta"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@earendil-works/pi-ai": "0.99.1",
@@ -46,7 +46,7 @@
46
46
  "nylo": "dist/nylo.js"
47
47
  },
48
48
  "nylorun": {
49
- "runtime": "0.25.0-beta",
50
- "studio": "0.25.0-beta"
49
+ "runtime": "0.27.0-beta",
50
+ "studio": "0.26.0-beta"
51
51
  }
52
52
  }