nylorun 0.15.1-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 +93 -0
- package/README.md +26 -8
- package/dist/client/mcp.js +1 -3
- package/dist/client/models.js +3 -2
- package/dist/client/settings.d.ts +2 -2
- package/dist/client/settings.js +23 -2
- package/dist/client/tenant.d.ts +1 -1
- package/dist/client/tenant.js +1 -1
- package/dist/project/seed.js +2 -1
- package/dist/sandbox/commands.js +10 -8
- package/dist/sandbox/kubectl.js +2 -1
- package/dist/sandbox/manifests.js +3 -2
- package/dist/sandbox/probe.js +9 -7
- package/dist/stack/commands.js +12 -11
- package/dist/stack/compose-file.d.ts +3 -0
- package/dist/stack/compose-file.js +32 -36
- package/dist/stack/operator-keys.js +7 -6
- package/dist/stack/ports.js +2 -1
- package/dist/stack/studio-login.js +2 -1
- package/dist/tuning.d.ts +90 -0
- package/dist/tuning.js +143 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
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
|
+
|
|
3
96
|
## 0.15.1-beta
|
|
4
97
|
|
|
5
98
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -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
|
|
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
|
|
514
|
-
documents: `get` (the default)
|
|
515
|
-
|
|
516
|
-
Management API's request body
|
|
517
|
-
`/sandbox`, `/artifacts
|
|
518
|
-
|
|
519
|
-
|
|
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/client/mcp.js
CHANGED
|
@@ -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")
|
package/dist/client/models.js
CHANGED
|
@@ -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
|
-
|
|
189
|
-
|
|
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
|
package/dist/client/settings.js
CHANGED
|
@@ -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]";
|
package/dist/client/tenant.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/client/tenant.js
CHANGED
|
@@ -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
|
|
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. */
|
package/dist/project/seed.js
CHANGED
|
@@ -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(
|
|
60
|
+
signal: AbortSignal.timeout(cliTuning().stack.seedRequestMs),
|
|
60
61
|
});
|
|
61
62
|
const body = (await response.json().catch(() => ({})));
|
|
62
63
|
if (!response.ok)
|
package/dist/sandbox/commands.js
CHANGED
|
@@ -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(
|
|
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}`,
|
|
64
|
-
|
|
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() +
|
|
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 ??
|
|
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",
|
|
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
|
|
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",
|
|
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;
|
package/dist/sandbox/kubectl.js
CHANGED
|
@@ -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 ??
|
|
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:
|
|
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:
|
|
134
|
+
resources: { limits: { cpu: cliTuning().sandbox.probeCpu, memory: cliTuning().sandbox.probeMemory } },
|
|
134
135
|
},
|
|
135
136
|
],
|
|
136
137
|
},
|
package/dist/sandbox/probe.js
CHANGED
|
@@ -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",
|
|
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
|
|
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,
|
|
50
|
-
], { timeoutMs:
|
|
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 ??
|
|
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() +
|
|
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 ??
|
|
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
|
}
|
package/dist/stack/commands.js
CHANGED
|
@@ -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(
|
|
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 ??
|
|
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 ??
|
|
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(
|
|
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 ??
|
|
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 %
|
|
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 ??
|
|
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",
|
|
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",
|
|
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 ??
|
|
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 ??
|
|
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
|
|
90
|
-
command: ["postgres", "-c", "wal_level=logical", "-c", "max_slot_wal_keep_size
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:-}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
timeout: 5s
|
|
433
|
-
retries: 24
|
|
434
|
-
restart: unless-stopped
|
|
430
|
+
${healthcheck("sandboxes")} restart: unless-stopped
|
|
435
431
|
`;
|
|
436
432
|
}
|
|
@@ -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(
|
|
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
|
|
73
|
+
const lockTimeoutMs = () => cliTuning().stack.lockWaitMs;
|
|
73
74
|
/** A lock older than this was left by a process that died. */
|
|
74
|
-
const
|
|
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() +
|
|
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 >
|
|
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,
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, cliTuning().stack.lockPollMs));
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
try {
|
package/dist/stack/ports.js
CHANGED
|
@@ -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 <
|
|
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 ??
|
|
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}).`);
|
package/dist/tuning.d.ts
ADDED
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nylorun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
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",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"prepack": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@nylorun/admin": "0.
|
|
34
|
-
"@nylorun/agents": "0.
|
|
35
|
-
"@nylorun/core": "0.
|
|
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.
|
|
50
|
-
"studio": "0.
|
|
49
|
+
"runtime": "0.27.0-beta",
|
|
50
|
+
"studio": "0.26.0-beta"
|
|
51
51
|
}
|
|
52
52
|
}
|