@nylorun/agents 0.21.0-beta → 0.23.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,136 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.23.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
+ - ebe7348: **The harness decides how files reach the model; the Model Gate only fetches and translates (protocol 15).** The second step of the harness boundary, still protocol 15 and Harness API 4. See MIGRATION.md.
16
+
17
+ - **Any file may be sent.** A message naming a file the model does not read is accepted: `422 attachment_unsupported` and its error code are gone, and AG-UI and A2A accept such files too. Files are still held to the artifact size limits at upload.
18
+ - **`turn.start.model`** is the model descriptor (`ModelDescriptorSchema`): the provider, id, inputs, context window and output limit of the model the turn's calls use, absent for flows and without a model. One resolution (`model/descriptor.ts`) serves it, the catalog's inputs and the gate.
19
+ - **Our engine shows each file.** An image or document the model reads goes as its artifact reference, a text file is read over the artifact seam and inlined as a `<file>` block, and anything else (past `modelFileBytes`, a kind the model does not read) is a note it writes. `fileView`, `representFiles`, `fileBlock`, `fileNote` and `mediaReferenceLine` (moved from `@nylorun/core/define`) are exported from `@nylorun/harness`. A budget from the descriptor bounds the prompt before the first answer, and compaction counts files as the model gets them.
20
+ - **The Model Gate refuses instead of writing notes.** A text file as a media part, or a kind the model or its API does not read, fails the call with the new model failure code `unsupported_input`; a reference it cannot fetch fails `invalid_request`. Nothing is sent, and neither is retried.
21
+ - **Structured answers are the engine's.** `ModelCall.outputSchema` is removed: our engine writes the JSON instruction and parses the answer (fenced, with control characters repaired; `parseJsonWithRepair` ported from pi-ai). `message.assistant` for a structured answer now carries its JSON text.
22
+ - **`read_artifact`** refuses a file the turn's model does not read (`artifact.unsupported`, with the reason).
23
+ - `unreadableReason` is removed; `isOffice` joins the media type helpers in `@nylorun/core/contracts`.
24
+ - Studio's file picker accepts every file, and the refusal chips are gone.
25
+
26
+ - ff6470e: **The harness writes the model's instructions and defines the sandbox and skill tools; no `runtime` block (protocol 15).** The third step of the harness boundary, still protocol 15 and Harness API 4. See MIGRATION.md.
27
+
28
+ - **No `runtime` block.** The Runtime no longer pins model-facing text or tools on a session's manifest: a session pins its definition as registered (`manifestHash` equals `definitionHash`) and keeps its sandbox spec on the session. The manifest schema refuses `runtime` with what replaces it (`REMOVED_MANIFEST_FIELDS`). `runtimeManifestFor`, `runtimeToolOf`, `RuntimeManifest`, `RUNTIME_TOOL_NAMES`, `isRuntimeToolName`, `createSandboxTools`, `SANDBOX_INSTRUCTIONS`, the skill tool names, `isSkillTool`, `SKILLS_USAGE`, `formatSkillCatalog` and `skillOfToolCall` leave `@nylorun/core/define`; `RUNTIME_PART_ID` is `SESSION_PART_ID` (`"session"`), and `agentFrom` takes `sessionInstructions`. `@nylorun/agents` no longer re-exports the skill catalog helpers. Migration 0030 closes the agent sessions that still pinned a `runtime` block (`closedReason: "protocol_15"`).
29
+ - **`turn.start` states facts.** `environment` (`TurnEnvironmentSchema`: `{id, kind, sandbox?: {workspace}}`) and `routing.sandbox.spec` (the pinned spec) join the agent's `skills`.
30
+ - **Our engine words them.** `@nylorun/harness` writes the skill catalog, the sandbox note and the skills' folder note, and defines the six sandbox tools, `load_skill` and `read_skill_resource`, in the order and bytes the `runtime` block had, so the prompt's prefix is unchanged (`engineToolsFor`, `sandboxTools`, `sandboxInstructions`, `skillCatalogText`, `skillFolderNote`, `skillTools`, all exported for a harness of your own). The skill tools run in the harness over `definition.file` (`runSkillTool`), and core no longer runs them (`tenant/skill-tool.ts` is gone). The sandbox tools' limits are harness configuration (`createHarness({ sandboxTools })`, `harnessExecutors({ sandboxTools })`, `DEFAULT_SANDBOX_TOOL_LIMITS`), as is the most a skill read takes into the context (`createHarness({ skillReadBytes })`, 10 MiB by default), held to the run's `limits.recordEntryBytes`.
31
+ - **Effects name their target.** A tool effect carries `target: {kind: "mcp" | "http" | "sandbox" | "harness" | "platform"}` (`EffectTargetSchema`) and may carry `skill`; the record routes by the kind, never by the tool's name, and a call without a target or one the session lacks is `tool.unavailable`. `hello.features.tools` and `NativeToolSchema` are removed, no tool name is reserved, and the record keeps the skill the intent names when the agent has it. A skill tool's `tool.completed` is `kind: "harness"` (it was `platform`).
32
+ - **Studio** drops the manifest panel's Runtime section: it shows the sandbox tools when the session's Environment has a sandbox.
33
+
34
+ - f443475: **The harness builds the MCP tool list and shapes tool results; tools are identified by server and tool (protocol 15).** The fourth step of the harness boundary, still protocol 15 and Harness API 4. See MIGRATION.md.
35
+
36
+ - **Tools have an identity.** An MCP tool call's target is `{kind: "mcp", server, tool}` (the server's own name for the tool) and an HTTP tool call's `{kind: "http", tool}` (its declared name); `platform` is gone. `toolName` is the model's name, for display. The record checks an MCP call against the session's discovery (`tool.unknown`) and the turn's tool settings (`tool.disabled`), and `tool.completed`, tool-call rows (`tool`), approvals (`Approve server / tool?`, `interaction.tool`) and traces (`nylorun.tool.identity`) carry the identity. The Tool Gate matches a call to its journaled target.
37
+ - **Tool policy keys on identity.** A rule's `tool` is the tool's identity, never the model's name. A rule whose `tool` has `__` in it is refused at `PUT /v1/tenant/tool-policy`; a stored one makes the policy unreadable, so every call is denied (and `GET` answers `409`) until the policy names the tool by `server` and `tool`.
38
+ - **`turn.start.mcp` states facts.** The session pins what its servers listed, raw (`McpSnapshot` v2: servers, scrubbed instructions, tools as named); each turn gets `mcp` (`McpFactsSchema`: every tool with `enabled` and the manifest's `deferred` hint) in place of `sessionTools` and `routing.mcpSnapshot`, and `session.mcp` answers `{mcp}`. `mcp.discovered` has no `renamed` or `deferred`. Migration 0031 closes the sessions that pinned the former snapshot (`closedReason: "protocol_15"`).
39
+ - **Our engine names, defers and searches.** `@nylorun/harness` names MCP tools (`mcpToolName`, `mcpToolNames`, within the 64-character provider ceiling), defers them by the manifest's setting or the model's window and pins that layout in its state (`mcpToolSet`, `McpLayout`), and runs `tool_search` (BM25) itself; core's `tool_search`, `deferredToolsTools`, `TOOL_SEARCH_TOOL` and `TOOL_CALL_TOOL` leave `@nylorun/core/define`, and `runDurable` takes `mcp` instead of `sessionTools`.
40
+ - **Our engine shapes tool results.** Core no longer stores large or binary tool results: the record refuses an outcome past `limits.recordEntryBytes` (`limit_exceeded`). Our engine spills them with `artifact.write`, keyed by the call and its bytes (`spillKey`), with a preview, an image as a file for a model that reads images, and one budget per step (`shapeToolOutcome`, `StepBudget`, exported for a harness of your own). The Tool Gate still scrubs credentials and refuses answers past `limits.toolAnswerBytes`.
41
+ - **Harness configuration.** `createHarness({ mcpTools, toolResults })` sets the deferral share, characters per token, fallback window, the note's instruction length, `tool_search`'s limits, the inline cap, preview head and tail, step budget, stub, small size and entry envelope (`DEFAULT_MCP_TOOL_CONFIG`, `DEFAULT_TOOL_RESULT_CONFIG`).
42
+ - **The MCP preview** names tools as Nylorun's engine would (`modelName`, `renamed`) and shows a server's instructions uncut.
43
+ - **Studio** shows an MCP tool as `server / tool` in tool and approval cards, with the model's name, and links the artifacts a tool's result was stored in.
44
+ - **`examples/harness-minimal`** spills a large `bash` output with `artifact.write` and keeps a preview; its README shows how a harness of your own offers MCP tools.
45
+
46
+ - 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.**
47
+
48
+ - 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.
49
+ - 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|->]`.
50
+ - `PUT /v1/files/sha256:<hex>` takes a file up to that limit.
51
+ - `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.
52
+ - 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.
53
+ - 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.
54
+ - 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.
55
+ - The Environment view's new `limits` field gives the limits in force.
56
+ - 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"`.
57
+ - 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`.
58
+ - `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.
59
+ - `@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.
60
+ - 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`).
61
+ - `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.
62
+
63
+ ### Patch Changes
64
+
65
+ - 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.
66
+
67
+ - **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.
68
+ - **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.
69
+ - **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}`.
70
+ - **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.
71
+ - **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`.
72
+
73
+ - 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.
74
+
75
+ - **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.
76
+ - **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`.
77
+ - **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.
78
+ - **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.
79
+ - **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).
80
+
81
+ - 2459430: **An `environment_secret` reads `nylorun_managed` in the sandbox instead of `nylorun-managed`, so CLIs that check a token's format send their requests.**
82
+
83
+ - The Vercel CLI refuses a `VERCEL_TOKEN` with any character outside letters, digits and `_` (`Must not contain: "-"`) and exits before egress-gate can set the header. `ENVIRONMENT_SECRET_SENTINEL` is now `nylorun_managed`.
84
+ - egress-gate never compares the value: it replaces the credential's header whatever the CLI sent, so nothing else changes. A script that checks for the literal `nylorun-managed` should check for `nylorun_managed`.
85
+
86
+ - Pin core to the tested release.
87
+ - Updated dependencies [0a2fbc5]
88
+ - Updated dependencies [ebe7348]
89
+ - Updated dependencies [ff6470e]
90
+ - Updated dependencies [f443475]
91
+ - Updated dependencies [cf425c6]
92
+ - Updated dependencies [23774c4]
93
+ - Updated dependencies [b1e815d]
94
+ - Updated dependencies [2459430]
95
+ - Updated dependencies [f2e9b0a]
96
+ - Updated dependencies [6772612]
97
+ - @nylorun/core@0.23.0-beta
98
+
99
+ ## 0.22.0-beta
100
+
101
+ ### Minor Changes
102
+
103
+ - a0cf8b2: **HTTP tools take `GET` and `DELETE`, and URL path parameters.**
104
+
105
+ - `method` is `GET`, `POST` (default), `PUT`, `PATCH` or `DELETE`. `POST`, `PUT` and `PATCH` send the input as the JSON body, as before. `GET` and `DELETE` send no body: the input becomes the query string, after any query string written in the URL. An array repeats its name (`tag=a&tag=b`) and a `null` is left out.
106
+ - The URL's path may name input fields as `{field}`, as in `https://api.example.com/orders/{orderId}`. Each value is percent-encoded into the path and is not sent again in the body or query string. A value that is empty, `.` or `..`, or not a string, number or boolean is refused before the call is sent (`tool.invalid-input`).
107
+ - `http()` and the manifest contract check a tool against its input schema. Each `{field}` must be a required scalar property. For `GET` and `DELETE`, a property must not be an object and must not repeat a query parameter the URL already sets.
108
+ - An HTTP verifier still takes only `POST`, `PUT` or `PATCH` and a URL without `{field}` parameters, because it is sent `{ input, output, iteration }` as a body. A credential with `via` cannot serve a tool with `{field}` parameters (`http.credential`).
109
+ - core exports `httpToolRequest`, which maps a call's input to the request, and `httpPathParameters`. The Runtime's outbound `post()` is now `send()`, which can send a request without a body.
110
+
111
+ ### Patch Changes
112
+
113
+ - 620072d: **Docs and schemas match what the CLI and the Runtime do.**
114
+
115
+ - `@nylorun/core`: the environment manifest's JSON Schema (`@nylorun/core/schemas/environment-manifest.json`) takes the CLI's `{ path, from }` `files` entry, so an editor no longer flags a manifest that `nylorun environments manifests save` accepts. The Runtime's own schema is unchanged: the CLI turns the folder into `{ path, files }` before saving. `manifest_changed` says to save without `If-Match` (`--if-match`, `ifMatch`); no command takes `--force`.
116
+ - Tool names, skill names and MCP server names are each unique within their kind, as the builder and the Runtime check; a tool, a skill and a server may share a name. The docs said they were unique across kinds.
117
+ - `harness_mismatch` is described as what it is since protocol 14: an Environment's harness is fixed for its life (fork onto a manifest version to run another), and a session's turn is refused when its Environment id was deleted and made again with another harness, or an Environment changed in place before protocol 14 applies that change.
118
+ - `@nylorun/agents`: the README names `nylorun environments list | delete`, not `nylorun local sandbox ls | rm`.
119
+ - MIGRATION.md no longer offers a tool's `approval`, removed in protocol 11, as a replacement for step middleware.
120
+
121
+ - 000ff75: **READMEs and help text match protocol 14.**
122
+
123
+ - `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.
124
+ - 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.
125
+ - `@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.
126
+ - `@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`.
127
+ - `@nylorun/create-agent`: `nylorun start` writes the project's keys (no key is derived any more).
128
+
129
+ - Pin core to the tested release.
130
+ - Updated dependencies [620072d]
131
+ - Updated dependencies [a0cf8b2]
132
+ - @nylorun/core@0.22.0-beta
133
+
3
134
  ## 0.21.0-beta
4
135
 
5
136
  ### Minor Changes
package/README.md CHANGED
@@ -46,7 +46,7 @@ const refundOrder = http({
46
46
  input: z.object({ orderId: z.string(), amount: z.number() }), // or a JSON Schema object
47
47
  output: z.object({ refundId: z.string() }), // optional
48
48
  url: "https://billing.example.com/refunds",
49
- method: "POST", // default; or PUT, PATCH
49
+ method: "POST", // default; or GET, PUT, PATCH, DELETE
50
50
  credential: "billing", // optional: the session's Environment holds a credential for this URL
51
51
  timeoutMs: 20_000, // default 60000, at most 300000
52
52
  });
@@ -54,9 +54,51 @@ const refundOrder = http({
54
54
  export const support = Agent({ id: "support" }).tools(refundOrder);
55
55
  ```
56
56
 
57
- The tool goes into the manifest as `http`, and the Runtime's
58
- Tool Gate sends the model's input as the JSON body, under the Host's address
59
- policy (`NYLORUN_ENDPOINT_*`), with `content-type: application/json`,
57
+ The method decides where the input goes:
58
+
59
+ | Method | Request |
60
+ | -------------------------------- | -------------------------------------------------------------- |
61
+ | `POST` (default), `PUT`, `PATCH` | The input as the JSON body (`content-type: application/json`) |
62
+ | `GET`, `DELETE` | No body: the input as the query string |
63
+
64
+ The URL's path may name input fields as `{field}`. Each one must be a required string,
65
+ number or boolean field of `input`. Its value goes into the path percent-encoded (so `/`,
66
+ `?` and `#` cannot change the request), and it is not sent again in the body or query
67
+ string. A query string written in the URL is kept, and the input's fields come after it.
68
+ In the query string an array repeats its name (`tag=a&tag=b`) and a `null` is left out.
69
+
70
+ `http()`, and the Runtime when it saves a manifest, refuse a tool whose URL names a field
71
+ the input does not require. For `GET` and `DELETE` they also refuse a field that is an object (a query string cannot hold one), and a
72
+ field that repeats a query parameter the URL already sets. A call is refused before it is
73
+ sent (`tool.invalid-input`) when a URL value is empty, `.` or `..`, or not a string, number
74
+ or boolean.
75
+
76
+ ```ts
77
+ // GET https://api.open-meteo.com/v1/forecast?current=…&latitude=22.57&longitude=88.36
78
+ const forecast = http({
79
+ name: "get_forecast",
80
+ description: "Current weather at a latitude and longitude.",
81
+ input: z.object({
82
+ latitude: z.number(),
83
+ longitude: z.number(),
84
+ temperature_unit: z.enum(["celsius", "fahrenheit"]).optional(),
85
+ }),
86
+ url: "https://api.open-meteo.com/v1/forecast?current=temperature_2m,relative_humidity_2m,wind_speed_10m&timezone=auto",
87
+ method: "GET",
88
+ });
89
+
90
+ // DELETE https://billing.example.com/orders/A-17?reason=duplicate
91
+ const cancelOrder = http({
92
+ name: "cancel_order",
93
+ input: z.object({ orderId: z.string(), reason: z.string() }),
94
+ url: "https://billing.example.com/orders/{orderId}",
95
+ method: "DELETE",
96
+ credential: "billing",
97
+ });
98
+ ```
99
+
100
+ The tool goes into the manifest as `http`, and the Runtime's Tool Gate sends
101
+ the request under the Host's address policy (`NYLORUN_ENDPOINT_*`), with
60
102
  `Nylorun-Session-Id`, `Nylorun-Turn-Id`, `Nylorun-Agent-Id` and an
61
103
  `Idempotency-Key` that is the same when the Runtime re-sends the call after a
62
104
  restart, so your service can drop duplicates. A `2xx` JSON answer is the output
@@ -81,10 +123,11 @@ turn say, that is a new call with a new key: put a business key (an order or
81
123
  charge id) in the tool's input schema to refuse it too.
82
124
 
83
125
  `credential` works as for a remote MCP server: the vault of the session's
84
- Environment must hold a credential bound to the tool's exact URL, else the call
85
- fails before it is sent (`http.credential`). A credential with `via`
86
- sends the call there instead, such as a gateway, with the session owner in its
87
- identity header, and a `401` is `credential_rejected` to the model, not retried.
126
+ Environment must hold a credential bound to the tool's exact URL (as written,
127
+ `{field}` parameters included), else the call fails before it is sent
128
+ (`http.credential`). A credential with `via` sends the call there instead, such
129
+ as a gateway, with the session owner in its identity header (a gateway has one
130
+ URL, so a tool with `{field}` parameters fails with `http.credential`), and a `401` is `credential_rejected` to the model, not retried.
88
131
  Whether a call waits for a person is not the tool's to say: it is the Tenant's
89
132
  [tool policy](#tool-policy). An HTTP tool is also a flow stage (see [Flow agents](#flow-agents)), and runs
90
133
  only on the Runtime (a local `run()` reports `http.runtime-only`).
@@ -199,9 +242,9 @@ const assistant = Agent({ id: "assistant", name: "Order assistant" })
199
242
  .skills("./assistant-skills");
200
243
  ```
201
244
 
202
- Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Every file of a skill's folder, binary included (not `.git/`, `node_modules/`, OS files like `.DS_Store`, or `.env` and `.env.*` files, which hold secrets), is part of the definition: the manifest names each one by path and SHA-256 (`skills.<name>.files`), at most 500 files of 10 MiB each. `saveAgent` uploads the files the Runtime does not hold (`client.files`, `PUT /v1/files/sha256:<hex>`) before it puts the definition, which the Runtime refuses while it names a file it lacks (`400 definition_files_missing`).
245
+ Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Every file of a skill's folder, binary included (not `.git/`, `node_modules/`, OS files like `.DS_Store`, or `.env` and `.env.*` files, which hold secrets), is part of the definition: the manifest names each one by path and SHA-256 (`skills.<name>.files`). A skill's files may hold together at most the Tenant's skill limit (`PUT /v1/tenant/skills`, default 2 GiB); no file and no count of files has a limit of its own, and an Environment's manifest may set a lower limit for its sessions (`limits.skills.totalBytes`; a session whose agent's skills do not fit is `409 environment_unsuitable`). `saveAgent` uploads the files the Runtime does not hold (`client.files`, `PUT /v1/files/sha256:<hex>`) before it puts the definition, which the Runtime refuses while it names a file it lacks (`400 definition_files_missing`) or a skill past the Tenant's limit (`413 limit_exceeded`). `read_skill_resource` reads files of up to 10 MiB into the model's context; a larger one is used in the sandbox.
203
246
 
204
- The Runtime serves the skills itself, with no call to your process. When a session pins the agent, the Runtime adds the skill catalog to the instructions and two tools to the manifest's `runtime` block: `load_skill` returns a skill's `SKILL.md` body and the paths of its other files, and `read_skill_resource` returns a text file. A session with a sandbox also has each skill's files read-only under `/skills/<name>@<hash>/`, so the model can run a skill's scripts with `bash`.
247
+ The Runtime serves the skills itself, with no call to your process. When our harness runs a turn, it adds the skill catalog to the instructions and two tools: `load_skill` returns a skill's `SKILL.md` body and the paths of its other files, and `read_skill_resource` returns a text file. A session with a sandbox also has each skill's files read-only under `/skills/<name>@<hash>/`, so the model can run a skill's scripts with `bash`.
205
248
 
206
249
  Declare MCP servers with `.mcp(...)` (same map shape as agent-plugins `mcpServers`):
207
250
 
@@ -215,11 +258,11 @@ const assistant = Agent({ id: "assistant", name: "Assistant" })
215
258
  });
216
259
  ```
217
260
 
218
- Each key names a server; its `name` defaults to the key and, when given, must equal it. Repeated `.mcp()` calls add servers to the manifest's `mcpServers`. Nylorun accepts remote servers only: `streamable-http` and `sse` ([Agent Plugins MCP servers](https://agent-plugins.org/plugin-authors/mcp-servers)); a `stdio` server is refused, so run it behind an HTTP transport and declare its URL. Attach an Agent Plugin package with `.plugin(path)`: its skills and remote MCP servers join the agent's `skills` and `mcpServers`, each labelled `plugin: { name, version }` from its `plugin.json`, and a stdio server in its `mcp.json` throws. Names are unique across the agent's tools, skills and servers.
261
+ Each key names a server; its `name` defaults to the key and, when given, must equal it. Repeated `.mcp()` calls add servers to the manifest's `mcpServers`. Nylorun accepts remote servers only: `streamable-http` and `sse` ([Agent Plugins MCP servers](https://agent-plugins.org/plugin-authors/mcp-servers)); a `stdio` server is refused, so run it behind an HTTP transport and declare its URL. Attach an Agent Plugin package with `.plugin(path)`: its skills and remote MCP servers join the agent's `skills` and `mcpServers`, each labelled `plugin: { name, version }` from its `plugin.json`, and a stdio server in its `mcp.json` throws. Tool names, skill names and server names are each unique within their kind; a tool, a skill and a server may share a name, since a server's tools are called `<server>__<tool>`.
219
262
 
220
- Set a server's tools one by one with `tools`, keyed by the server's own tool names, `"*"` for the rest: `{ "*": { enabled: false }, search_issues: { enabled: true }, create_issue: { enabled: true } }` is an allowlist (whether a call waits for a person is the [tool policy](#tool-policy)). When an agent's MCP tools would fill more than a tenth of the model's context window, the Runtime defers them: the model finds them with `tool_search` and runs them with `tool_call`. `deferred: true` or `false`, on a tool or on the server, decides instead. Either setting makes the manifest v6 (see MIGRATION.md).
263
+ Set a server's tools one by one with `tools`, keyed by the server's own tool names, `"*"` for the rest: `{ "*": { enabled: false }, search_issues: { enabled: true }, create_issue: { enabled: true } }` is an allowlist (whether a call waits for a person is the [tool policy](#tool-policy)). When an agent's MCP tools would fill more than a tenth of the model's context window, the Runtime defers them: the model finds them with `tool_search` and runs them with `tool_call`. `deferred: true` or `false`, on a tool or on the server, decides instead.
221
264
 
222
- A manifest never carries a server's credential, and its `headers` hold no secret. You write a credential for the server's URL into an Environment (a token or a header map, or a gateway's key with `via` and an identity header for servers that need each person's sign-in, with `client.environments.secrets.put`), and sessions that name the Environment use it: [DEPLOYMENT.md](../../guides/DEPLOYMENT.md#mcp-servers-and-http-tools), which also shows `nylorun mcp inspect` for listing a server's tools first. After `saveAgent`, `admin.coverage({ agentId, environmentId })` in `@nylorun/admin` says which credential each server and HTTP tool would get in that Environment ([Checking an agent's credentials](../../guides/DEPLOYMENT.md#checking-an-agents-credentials)). The model knows each tool as `server__tool`, with characters outside `[A-Za-z0-9_-]` replaced by `_`; a failed call reaches it with a code (`credential_rejected` on a `401`, `mcp.unreachable`, …), and a result past 32 KiB, or an image, becomes an artifact of the session that it reads with `read_artifact`.
265
+ A manifest never carries a server's credential, and its `headers` hold no secret. You write a credential for the server's URL into an Environment (a token or a header map, or a gateway's key with `via` and an identity header for servers that need each person's sign-in, with `client.environments.secrets.put`), and sessions that name the Environment use it: [DEPLOYMENT.md](../../guides/DEPLOYMENT.md#mcp-servers-and-http-tools), which also shows `nylorun tenant mcp inspect` for listing a server's tools first. After `saveAgent`, `admin.coverage({ agentId, environmentId })` in `@nylorun/admin` says which credential each server and HTTP tool would get in that Environment ([Checking an agent's credentials](../../guides/DEPLOYMENT.md#checking-an-agents-credentials)). The model knows each tool as `server__tool`, with characters outside `[A-Za-z0-9_-]` replaced by `_`; a failed call reaches it with a code (`credential_rejected` on a `401`, `mcp.unreachable`, …), and a result past 32 KiB, or an image, becomes an artifact of the session that it reads with `read_artifact`.
223
266
 
224
267
  A session runs in an **Environment**: one sandbox and one vault, with an id you choose, which outlives the sessions that name it (protocol 11). It is made from one version of an **environment manifest** or from inline fields, and never changes what it runs after that (protocol 14). The agent names no Environment, so the same agent runs in any Tenant; it may state what it needs (`.requires()`). Whether an Environment serves one session, one person or a project is your app's choice:
225
268
 
@@ -267,11 +310,11 @@ await release();
267
310
 
268
311
  With a sandbox (kind `virtual` or `pod`), the model gets `bash`, `read`, `write`, `edit`, `grep` and `glob` on a persistent `/workspace`. These tools run in the Runtime, not in your process. A `virtual` Environment is an emulated shell in the Runtime's harness, which is not a VM boundary and takes no `image`; a `pod` runs `image` with its own volume (`storage`) once the Tenant has sandbox pods (`nylorun local sandbox enable`). Kind `none` has a vault and no sandbox, for agents that use only remote MCP servers and HTTP tools. A session that names no Environment has no sandbox and no credentials.
269
312
 
270
- An Environment never changes what it runs (protocol 14, its view's `manifest`): a `put` on an existing one may change its `labels` and owner guard, fields that match are accepted (so get-or-create stays one call), and anything else is `409 environment_immutable`. A save of a new manifest version changes no Environment: make a new one, which can start from the old one's disk (`from`). The Runtime checks what an Environment runs against the Tenant's limits (`GET`/`PUT /v1/tenant/sandbox`: a network ceiling, a resource maximum, the idle timeout) and answers `400` with every problem it finds. `lifecycle.idle` stops a pod after that long without a turn (at most the Tenant's); `lifecycle.deleteAfter` deletes the Environment, its vault and its files once nothing has used it that long. Private networks, loopback, the host and cloud metadata endpoints are always blocked.
313
+ An Environment never changes what it runs (protocol 14, its view's `manifest`): a `put` on an existing one may change its `labels` and owner guard, fields that match are accepted (so get-or-create stays one call), and anything else is `409 environment_immutable`. A save of a new manifest version changes no Environment: make a new one, which can start from the old one's disk (`from`). The Runtime checks what an Environment runs against the Tenant's limits (`GET`/`PUT /v1/tenant/sandbox`: a network ceiling, a resource maximum, the idle timeout; `/v1/tenant/artifacts` and `/v1/tenant/skills` for its `limits`) and answers `400` with every problem it finds. `lifecycle.idle` stops a pod after that long without a turn (at most the Tenant's); `lifecycle.deleteAfter` deletes the Environment, its vault and its files once nothing has used it that long. Private networks, loopback, the host and cloud metadata endpoints are always blocked.
271
314
 
272
- Secrets are write-only: `client.environments.secrets.put(id, name, secret)` creates or replaces one, and `list`, `get` and `delete` never return a value. An `environment_secret` (`{ value, allowedHosts, inject? }`) shows its name to commands as the sentinel `nylorun-managed`, and egress-gate sets its header on HTTPS requests to `allowedHosts` (pods only); an `environment_variable` (`{ value }`) is a plain variable; `bearer` (`{ url, value, via?, identity? }`) and `headers` (`{ url, headers, via?, identity? }`) are credentials for the MCP servers and HTTP tools at `url`. A second secret for one host, or a second credential for one URL, is `409 credential_conflict` at the write ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#credentials)). When what an Environment runs declares a secret's slot (a manifest's `secrets`), send only `{ value }` (or `{ headers }`): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`. An agent built with `.requires({ sandbox: "pod", secrets: ["GH_TOKEN"] })` opens a session only on an Environment that has them (`409 environment_unsuitable`). `saveAgent(agent, { dryRun: true })` checks a definition and saves nothing.
315
+ Secrets are write-only: `client.environments.secrets.put(id, name, secret)` creates or replaces one, and `list`, `get` and `delete` never return a value. An `environment_secret` (`{ value, allowedHosts, inject? }`) shows its name to commands as the sentinel `nylorun_managed`, and egress-gate sets its header on HTTPS requests to `allowedHosts` (pods only); an `environment_variable` (`{ value }`) is a plain variable; `bearer` (`{ url, value, via?, identity? }`) and `headers` (`{ url, headers, via?, identity? }`) are credentials for the MCP servers and HTTP tools at `url`. A second secret for one host, or a second credential for one URL, is `409 credential_conflict` at the write ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#credentials)). When what an Environment runs declares a secret's slot (a manifest's `secrets`), send only `{ value }` (or `{ headers }`): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`. An agent built with `.requires({ sandbox: "pod", secrets: ["GH_TOKEN"] })` opens a session only on an Environment that has them (`409 environment_unsuitable`). `saveAgent(agent, { dryRun: true })` checks a definition and saves nothing.
273
316
 
274
- Sessions on one Environment share its `/workspace` and run their turns at the same time (one turn per session); which sessions work on one disk, and when, is your app's call. Stop, reset, snapshot and restore wait until no turn runs (`409 environment_busy`). Deleting a session only detaches it. With `ownerUserId` set, a session of another owner is `403 environment_owner_mismatch` (a caller acting for a person gets the `404` of a missing Environment), checked when it opens and at every turn start. `client.environments.list({ labels })`, `page`, `get(id)`, `events(id, { types, since, until })` (its lifecycle, and `environment.egress` for every connection its sandbox opens, naming the turns that were running), `stop(id)`, `reset(id)` and `delete(id)` manage them, and `npx nylorun local sandbox ls | rm` does the same on a local Tenant. Acting for a person (`app.as(...)`), changing an Environment or its secrets needs `environments:write`. A trusted issuer's token reaches only the Environments its issuer grants (the identity file's `environments` templates, exact ids or prefixes ending in `/*`), checked at every turn start. The Tenant holds at most `limits.environments` of them (`PUT /v1/tenant/sandbox`, default 100).
317
+ Sessions on one Environment share its `/workspace` and run their turns at the same time (one turn per session); which sessions work on one disk, and when, is your app's call. Stop, reset, snapshot and restore wait until no turn runs (`409 environment_busy`). Deleting a session only detaches it. With `ownerUserId` set, a session of another owner is `403 environment_owner_mismatch` (a caller acting for a person gets the `404` of a missing Environment), checked when it opens and at every turn start. `client.environments.list({ labels })`, `page`, `get(id)`, `events(id, { types, since, until })` (its lifecycle, and `environment.egress` for every connection its sandbox opens, naming the turns that were running), `stop(id)`, `reset(id)` and `delete(id)` manage them, and `npx nylorun environments list | delete` does the same from the command line. Acting for a person (`app.as(...)`), changing an Environment or its secrets needs `environments:write`. A trusted issuer's token reaches only the Environments its issuer grants (the identity file's `environments` templates, exact ids or prefixes ending in `/*`), checked at every turn start. The Tenant holds at most `limits.environments` of them (`PUT /v1/tenant/sandbox`, default 100).
275
318
 
276
319
  A pod's disk can start prepared: `files` copies versions of Tenant-wide artifacts in, then `setup` runs its commands, once per new disk and before its first turn (a failed command fails the pod, and the next turn start runs the setup again). Between turns, `client.environments.snapshot(id)` keeps a version of the disk, `restore(id, version)` puts one back (a pod on a new volume seeded before its next turn), `put(newId, { from: { environment: id, version: "latest" } })` starts a new Environment from it, and `versions(id)` and `deleteVersion(id, n)` manage them ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#environments)):
277
320
 
@@ -312,7 +355,7 @@ The tool is named after the agent's `id` and takes `{ task: string }`; the agent
312
355
 
313
356
  A flow agent can be a subagent too: `.subagents(researchFlow)` with a `description` like any other. Its manifest is inlined in the parent's, so it is saved with the parent. When the model calls it, the flow runs on the Runtime in its own linked session (a fresh one per call, linked from the parent's with a `node.agent` event at `support/<flow id>`), its agents in theirs, and the flow's output comes back as the tool result; a failed flow is a failed tool result. Cancelling the parent cancels the flow. Flow agents as subagents run on the Runtime only, not in a local `run()`. An approval asked for inside the flow waits on the flow's own session.
314
357
 
315
- Delegate when the parent should keep the answer. When a specialist should own the rest of the conversation, send the next message with a turn manifest that has the specialist's instructions instead. This version is one level deep and non-interactive: a delegated agent cannot use agents as tools, and its tools cannot declare `approval` (keep those on the parent). In the local engine (`@nylorun/harness/run`), a delegated agent's `tool({ run })` tools start with empty `ctx.state`, can read `ctx.agent` (`{ id, path, delegationId }`), and fail with `delegation.interaction-unsupported` on `ctx.ask`, `ctx.approve`, `ctx.sleep` or `ctx.waitFor`. Delegation is not an approval boundary; approvals live on tools.
358
+ Delegate when the parent should keep the answer. When a specialist should own the rest of the conversation, send the next message with a turn manifest that has the specialist's instructions instead. This version is one level deep and non-interactive: a delegated agent cannot use agents as tools, and cannot pause for input, an approval or a wait (keep those on the parent). In the local engine (`@nylorun/harness/run`), a delegated agent's `tool({ run })` tools start with empty `ctx.state`, can read `ctx.agent` (`{ id, path, delegationId }`), and fail with `delegation.interaction-unsupported` on `ctx.ask`, `ctx.approve`, `ctx.sleep` or `ctx.waitFor`. Delegation is not an approval boundary.
316
359
 
317
360
  ## Flow agents
318
361
 
@@ -398,7 +441,8 @@ an answer that does not match `output` fails the stage with its code (`http.stat
398
441
  is retried with the Loop's input, not the feedback.
399
442
 
400
443
  A Loop may be judged by your service instead of an agent: `http({ url, method?,
401
- credential?, timeoutMs? })`, with no name or input, is an HTTP verifier. The Runtime
444
+ credential?, timeoutMs? })`, with no name or input, is an HTTP verifier. Its method is
445
+ `POST`, `PUT` or `PATCH` and its URL has no `{field}` parameters, because the Runtime
402
446
  sends `{ input, output, iteration }` (the Loop's input, the attempt's output, its
403
447
  number) and reads a verdict, `{ pass, feedback? }`, from the answer; anything else, or a
404
448
  failed request, fails the Loop (`loop.verify-failed`). Its verdicts are recorded as
@@ -459,7 +503,8 @@ then `dev`. Agents do not hash the manifest; the Runtime pins the digest of the
459
503
  definition a session opens with. Model selection and model credentials belong to
460
504
  the Tenant.
461
505
 
462
- The Runtime makes a lost HTTP tool call `uncertain` instead of repeating its external effect.
506
+ The Runtime sends a lost MCP or HTTP tool call again with the same key, then fails it to the
507
+ model as `tool.interrupted` (see **How tool calls are delivered**).
463
508
  In the local engine (`@nylorun/harness/run`), `tool({ run })` tools receive state, info,
464
509
  identity, resume, signal, approval/response helpers and memoized `step`. Agent definitions
465
510
  have no `.run()`; explicit
@@ -590,9 +635,11 @@ What the Runtime answers (Host feature `a2a-endpoint`):
590
635
  on the same `taskId`.
591
636
  - The `messageId` is the idempotency key: a retried message returns the same
592
637
  task.
593
- - Text parts, or a single data part, in; the turn's output as the artifact
594
- `output` (text, or JSON as a data part). Files are
595
- `ContentTypeNotSupportedError`.
638
+ - Text parts, a single data part, or text and `raw` file parts in (at most
639
+ 20 MiB each, stored as artifacts of the context's session); the turn's
640
+ output as the artifact `output` (text, or JSON as a data part). A file by
641
+ `url` is `ContentTypeNotSupportedError`; a file of any type is accepted, and
642
+ the agent's harness decides how its model sees it.
596
643
  - Clients must send `A2A-Version: 1.0` (header or `?A2A-Version=`).
597
644
  - Not yet: `ListTasks`, streaming and `SubscribeToTask`
598
645
  (`UnsupportedOperationError`; the card says `streaming: false`), push
@@ -627,8 +674,10 @@ const { url } = await client.artifacts.link(files[0].artifactId); // no credenti
627
674
  A session with a sandbox gives the agent `save_artifact`, which saves a file it
628
675
  made as an artifact of the session; each new artifact or version appears in the
629
676
  session's events as `artifact.created` or `artifact.version.created`. The Tenant's
630
- limits (`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default)
631
- refuse a larger upload with `413 limit_exceeded`, and nothing is stored. Acting for
677
+ limits (`PUT /v1/tenant/artifacts`: 100 MiB per file and 10 GiB in all by default),
678
+ and those an Environment's manifest sets for its sessions' artifacts (`limits.artifacts`,
679
+ at most the Tenant's), refuse a larger upload with `413 limit_exceeded`, and nothing
680
+ is stored. Acting for
632
681
  a person (`as()`, or a trusted issuer's token), a client reaches only the artifacts of that
633
682
  person's sessions, and an upload names one of them.
634
683
 
@@ -657,11 +706,11 @@ oldest-first behavior and does not require `artifact-reads`.
657
706
 
658
707
  ### Outputs: folder artifacts
659
708
 
660
- At the end of each turn, the Runtime exports the files the agent wrote into
709
+ At the end of each turn, our harness saves the files the agent wrote into
661
710
  `/workspace/outputs` of its sandbox as a version of the session's **folder**
662
- artifact `outputs` (kind `folder`): the first export creates it, and each later
711
+ artifact `outputs` (kind `folder`): the first save creates it, and each later
663
712
  turn whose outputs changed adds a version, announced by `artifact.created` or
664
- `artifact.version.created` with `source: "export"`. A file that did not change is
713
+ `artifact.version.created` with `source: "engine"` (`"export"` before protocol 15). A file that did not change is
665
714
  stored once. Read a folder as a tree, one file by path, a diff or a zip:
666
715
 
667
716
  ```ts
@@ -673,8 +722,9 @@ const zip = await client.artifacts.zip(outputs.artifactId); // a streamed Respon
673
722
  const { url } = await client.artifacts.link(outputs.artifactId, { file: "app/index.html" }); // or the zip without `file`
674
723
  ```
675
724
 
676
- An export past a limit (10,000 files, 1 GiB, the per-file limit or the Tenant
677
- total) stores nothing and records `artifact.export.skipped`; the turn still
725
+ A save past a limit (the Runtime limit `folderEntries`, 10,000 files by default;
726
+ 1 GiB, our harness's own bound; the per-file limit or the Tenant total) stores
727
+ nothing, and our harness records `artifact.export.skipped`; the turn still
678
728
  completes.
679
729
 
680
730
  ## Acting for a person (app servers)
package/dist/client.d.ts CHANGED
@@ -2,7 +2,7 @@ export * from "./session-client.js";
2
2
  import { AgentsClient } from "./session-client.js";
3
3
  import type { Destination } from "./http.js";
4
4
  /**
5
- * Build a Tenant API client.
5
+ * Build a Runtime API client.
6
6
  * Explicit connection fields keep today's sync Transport resolution (including
7
7
  * environment fill-ins). With no connection fields, resolves via
8
8
  * `resolveConnection` (environment → project link), which reads the Project's files: it is
@@ -9,7 +9,7 @@ export declare class ConnectionError extends Error {
9
9
  constructor(message: string);
10
10
  }
11
11
  /**
12
- * Resolve Tenant API connection: options → environment → project link (D§7.1). A connection is a
12
+ * Resolve a Runtime API connection: options → environment → project link (D§7.1). A connection is a
13
13
  * Runtime URL and a key: the Runtime serves one Tenant, so nothing names it.
14
14
  */
15
15
  export declare function resolveConnection(options?: {
@@ -42,7 +42,7 @@ function readProjectLink(cwd) {
42
42
  };
43
43
  }
44
44
  /**
45
- * Resolve Tenant API connection: options → environment → project link (D§7.1). A connection is a
45
+ * Resolve a Runtime API connection: options → environment → project link (D§7.1). A connection is a
46
46
  * Runtime URL and a key: the Runtime serves one Tenant, so nothing names it.
47
47
  */
48
48
  export async function resolveConnection(options) {
@@ -116,7 +116,7 @@ export type EnvironmentFileBytes = Readonly<Record<string, Uint8Array>>;
116
116
  /**
117
117
  * A manifest `files` entry by content hash (Q6): `contents` (each file's path under `path` and
118
118
  * its bytes or text) hashed, as `entry` for the manifest and `bytes` for `manifests.save`'s
119
- * `files`, which uploads them. Each file holds at most 10 MiB.
119
+ * `files`, which uploads them. Each file holds at most 2 GiB.
120
120
  */
121
121
  export declare function environmentFiles(path: string, contents: Readonly<Record<string, Uint8Array | string>>): Promise<{
122
122
  entry: {
@@ -233,6 +233,19 @@ export declare class EnvironmentsClient {
233
233
  path: string;
234
234
  files: Record<string, string>;
235
235
  })[] | undefined;
236
+ limits?: {
237
+ artifacts?: {
238
+ fileBytes?: number | undefined;
239
+ totalBytes?: number | undefined;
240
+ } | undefined;
241
+ skills?: {
242
+ totalBytes?: number | undefined;
243
+ } | undefined;
244
+ disk?: {
245
+ versionBytes?: number | undefined;
246
+ versionEntries?: number | undefined;
247
+ } | undefined;
248
+ } | undefined;
236
249
  secrets?: Record<string, {
237
250
  required?: boolean | undefined;
238
251
  description?: string | undefined;
@@ -269,6 +282,19 @@ export declare class EnvironmentsClient {
269
282
  manifestName?: string | undefined;
270
283
  manifestVersion?: number | undefined;
271
284
  manifestHash?: string | undefined;
285
+ limits: {
286
+ artifacts: {
287
+ fileBytes: number;
288
+ totalBytes: number;
289
+ };
290
+ skills: {
291
+ totalBytes: number;
292
+ };
293
+ disk: {
294
+ versionBytes: number;
295
+ versionEntries: number;
296
+ };
297
+ };
272
298
  forkedFrom?: {
273
299
  environment: string;
274
300
  disk: number | null;
@@ -71,7 +71,7 @@ export class EnvironmentSecretsClient {
71
71
  /**
72
72
  * A manifest `files` entry by content hash (Q6): `contents` (each file's path under `path` and
73
73
  * its bytes or text) hashed, as `entry` for the manifest and `bytes` for `manifests.save`'s
74
- * `files`, which uploads them. Each file holds at most 10 MiB.
74
+ * `files`, which uploads them. Each file holds at most 2 GiB.
75
75
  */
76
76
  export async function environmentFiles(path, contents) {
77
77
  const files = {};
package/dist/files.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare class FilesClient {
17
17
  signal?: AbortSignal;
18
18
  }): Promise<boolean>;
19
19
  /**
20
- * Uploads one file, at most 10 MiB. The Runtime checks its bytes hash to `sha256` (default:
20
+ * Uploads one file, at most 2 GiB. The Runtime checks its bytes hash to `sha256` (default:
21
21
  * their hash) and keeps a file it already holds as it is.
22
22
  */
23
23
  upload(bytes: Uint8Array, options?: {
package/dist/files.js CHANGED
@@ -37,7 +37,7 @@ export class FilesClient {
37
37
  }
38
38
  }
39
39
  /**
40
- * Uploads one file, at most 10 MiB. The Runtime checks its bytes hash to `sha256` (default:
40
+ * Uploads one file, at most 2 GiB. The Runtime checks its bytes hash to `sha256` (default:
41
41
  * their hash) and keeps a file it already holds as it is.
42
42
  */
43
43
  async upload(bytes, options = {}) {
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { AgentBuildError, AgentLifecycleError, VerdictSchema, isVerdict, Workflo
7
7
  export type { FlowAgentBuilder, Flow, Named, LoopOptions, StageOptions, HttpTarget, } from "@nylorun/core/define";
8
8
  export type { AgentOptions, AgentManifest, BuiltAgent, BuiltWorkflow, Verdict, WorkflowBinding, WorkflowManifest, ToolDefinition, ToolExecutionContext, ToolOutcome, JsonValue, JsonObject, } from "@nylorun/core/define";
9
9
  export type { SessionPage, SessionListItem, SessionManifestView, HistoryPage, SessionUsageTotals, ModelCall, ModelCallsPage, EnvironmentPage, ToolCall, ToolCallsPage, LiveEvent, SessionEvent, SessionEventOf, EventType, SessionCommand, SubjectScope, EnvironmentView, EnvironmentEvent, EnvironmentEventType, EgressRunningTurn, EnvironmentKind, EnvironmentRecipe, EnvironmentSecretView, EnvironmentSnapshot, EnvironmentRestore, EnvironmentVersions, RestoreEnvironmentRequest, EnvironmentForkPlan, ForkEnvironmentRequest, ForkEnvironmentResponse, EnvironmentManifest, EnvironmentManifestPlan, EnvironmentManifestView, EnvironmentManifestVersions, SaveEnvironmentManifestResponse, MessagePart, ArtifactView, ArtifactKind, ArtifactListItem, ArtifactPage, ArtifactVersionView, ArtifactLink, UploadArtifactResponse, DefinitionFileView, ArtifactTree, ArtifactDiff, FolderEntry, } from "@nylorun/core/contracts";
10
- export { ENVIRONMENT_EVENT_TYPES, SUBJECT_SCOPES } from "@nylorun/core/contracts";
10
+ export { ENVIRONMENT_EVENT_TYPES, RUNTIME_LIMIT_DEFAULTS, SUBJECT_SCOPES } from "@nylorun/core/contracts";
11
11
  export { AgentsClient, SessionClient, createClient } from "./client.js";
12
12
  export { ArtifactsClient } from "./artifacts.js";
13
13
  export { FilesClient, definitionFileHash } from "./files.js";
@@ -21,7 +21,7 @@ export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
21
21
  export type { Destination, IncompatibleReason, TokenSource } from "./http.js";
22
22
  export { loadPlugin, PluginError } from "./plugins/load.js";
23
23
  export type { LoadedPlugin, PluginDiagnostic } from "./plugins/load.js";
24
- export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, formatSkillCatalog, SKILLS_USAGE, } from "./skills/index.js";
24
+ export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./skills/index.js";
25
25
  export type { SkillDiagnostic } from "./skills/index.js";
26
26
  export { McpError } from "./mcp/index.js";
27
27
  export type { McpServerSpec } from "./mcp/index.js";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { hashManifest, PROTOCOL_VERSION, PROTOCOL_FEATURES, HOST_PROTOCOL, DEFIN
4
4
  export { checkHealth, describeIncompatibility, parseProtocolRange, } from "@nylorun/core/transport";
5
5
  export { Agent, AgentBuilder } from "./builder.js";
6
6
  export { AgentBuildError, AgentLifecycleError, VerdictSchema, isVerdict, WorkflowBuildError, isBuiltWorkflow, flow, tool, http, defineSchema, ToolError, } from "@nylorun/core/define";
7
- export { ENVIRONMENT_EVENT_TYPES, SUBJECT_SCOPES } from "@nylorun/core/contracts";
7
+ export { ENVIRONMENT_EVENT_TYPES, RUNTIME_LIMIT_DEFAULTS, SUBJECT_SCOPES } from "@nylorun/core/contracts";
8
8
  export { AgentsClient, SessionClient, createClient } from "./client.js";
9
9
  export { ArtifactsClient } from "./artifacts.js";
10
10
  export { FilesClient, definitionFileHash } from "./files.js";
@@ -13,5 +13,5 @@ export { AccessClient } from "./access.js";
13
13
  export { resolveConnection, ConnectionError } from "./connection.js";
14
14
  export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
15
15
  export { loadPlugin, PluginError } from "./plugins/load.js";
16
- export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, formatSkillCatalog, SKILLS_USAGE, } from "./skills/index.js";
16
+ export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./skills/index.js";
17
17
  export { McpError } from "./mcp/index.js";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The Tenant API client classes, with no Node-only imports. `client.ts` adds `createClient`,
2
+ * The Runtime API client classes, with no Node-only imports. `client.ts` adds `createClient`,
3
3
  * which resolves the connection from the environment or the Project link.
4
4
  */
5
5
  import type { AgentManifest, BuiltAgent, BuiltWorkflow, JsonValue, SandboxToolName, WorkflowManifest } from "@nylorun/core/define";
@@ -242,8 +242,9 @@ export declare class SessionClient {
242
242
  turnId: string;
243
243
  callId: string | null;
244
244
  toolName: string | null;
245
- kind: "harness" | "http" | "mcp" | "platform" | "sandbox" | null;
245
+ kind: "harness" | "http" | "mcp" | "sandbox" | null;
246
246
  server: string | null;
247
+ tool: string | null;
247
248
  skill: string | null;
248
249
  plugin: {
249
250
  name: string;
@@ -1,3 +1,2 @@
1
1
  export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
2
2
  export type { LoadedSkill, SkillDiagnostic } from "./load.js";
3
- export { formatSkillCatalog, SKILLS_USAGE } from "@nylorun/core/define";
@@ -1,2 +1 @@
1
1
  export { loadSkillsFromDirectory, resolveSkillsRoot, parseSkill, SkillsError, } from "./load.js";
2
- export { formatSkillCatalog, SKILLS_USAGE } from "@nylorun/core/define";
@@ -24,8 +24,9 @@ export declare function resolveSkillsRoot(directory: string): string;
24
24
  /**
25
25
  * Load Agent Skills (agentskills.io) from a catalog directory.
26
26
  * Each immediate subdirectory with a valid SKILL.md becomes one skill, with every file of its
27
- * folder (text or binary) hashed. Throws `SkillsError` for a skill the Runtime cannot hold: a
28
- * file over 10 MiB, more than 500 files, or a path a manifest cannot name.
27
+ * folder (text or binary) hashed. Throws `SkillsError` for a path a manifest cannot name. How
28
+ * much a skill may hold is the Tenant's limit (`/v1/tenant/skills`, default 2 GiB), which the
29
+ * Runtime checks when the files are uploaded and the agent is saved.
29
30
  */
30
31
  export declare function loadSkillsFromDirectory(directory: string, diagnostics: SkillDiagnostic[], options?: {
31
32
  readonly boundary?: string;
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, readdirSync, readFileSync, realpathSync, statSync, } from "node:fs";
3
- import { readFile } from "node:fs/promises";
2
+ import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, realpathSync, statSync, } from "node:fs";
3
+ import { open } from "node:fs/promises";
4
4
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
- import { DEFINITION_FILE_MAX_BYTES, SKILL_ENTRY, SKILL_FILES_MAX, skillFilePathIssue, } from "@nylorun/core/define";
5
+ import { SKILL_ENTRY, skillFilePathIssue, } from "@nylorun/core/define";
6
6
  import matter from "gray-matter";
7
7
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
8
  /** Version control and operating system files, never part of a skill. */
@@ -33,8 +33,9 @@ export function resolveSkillsRoot(directory) {
33
33
  /**
34
34
  * Load Agent Skills (agentskills.io) from a catalog directory.
35
35
  * Each immediate subdirectory with a valid SKILL.md becomes one skill, with every file of its
36
- * folder (text or binary) hashed. Throws `SkillsError` for a skill the Runtime cannot hold: a
37
- * file over 10 MiB, more than 500 files, or a path a manifest cannot name.
36
+ * folder (text or binary) hashed. Throws `SkillsError` for a path a manifest cannot name. How
37
+ * much a skill may hold is the Tenant's limit (`/v1/tenant/skills`, default 2 GiB), which the
38
+ * Runtime checks when the files are uploaded and the agent is saved.
38
39
  */
39
40
  export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
40
41
  const root = resolveSkillsRoot(directory);
@@ -154,21 +155,50 @@ function readFiles(name, boundary, directory, diagnostics, prefix) {
154
155
  const issue = skillFilePathIssue(path);
155
156
  if (issue)
156
157
  throw new SkillsError(`${prefix}.invalid-path`, `Skill '${name}' has a file whose path ${issue}: ${path}`);
157
- if (stats.size > DEFINITION_FILE_MAX_BYTES)
158
- throw new SkillsError(`${prefix}.file-too-large`, `Skill '${name}' file ${path} is ${stats.size} bytes; a skill file may be at most ${DEFINITION_FILE_MAX_BYTES} (10 MiB)`);
159
- const hash = `sha256:${createHash("sha256").update(readFileSync(real)).digest("hex")}`;
158
+ const hash = hashFile(real);
160
159
  files[path] = hash;
161
- sources[hash] = {
162
- size: stats.size,
163
- read: async () => new Uint8Array(await readFile(real)),
164
- };
165
- if (Object.keys(files).length > SKILL_FILES_MAX)
166
- throw new SkillsError(`${prefix}.too-many-files`, `Skill '${name}' has more than ${SKILL_FILES_MAX} files; a skill may have at most ${SKILL_FILES_MAX}`);
160
+ sources[hash] = { size: stats.size, read: () => readWhole(real, stats.size) };
167
161
  }
168
162
  };
169
163
  walk(directory, "");
170
164
  return { files: sorted(files), sources };
171
165
  }
166
+ /** Read in chunks: neither the hash nor the read is bound by `readFile`'s 2 GiB limit. */
167
+ const CHUNK_BYTES = 1024 * 1024;
168
+ /** `sha256:<hex>` of the file at `path`, read a chunk at a time. */
169
+ function hashFile(path) {
170
+ const hash = createHash("sha256");
171
+ const chunk = Buffer.allocUnsafe(CHUNK_BYTES);
172
+ const fd = openSync(path, "r");
173
+ try {
174
+ for (let read; (read = readSync(fd, chunk, 0, CHUNK_BYTES, null)) > 0;)
175
+ hash.update(chunk.subarray(0, read));
176
+ }
177
+ finally {
178
+ closeSync(fd);
179
+ }
180
+ return `sha256:${hash.digest("hex")}`;
181
+ }
182
+ /** The `size` bytes of the file at `path`. */
183
+ async function readWhole(path, size) {
184
+ const bytes = new Uint8Array(size);
185
+ const file = await open(path, "r");
186
+ try {
187
+ let at = 0;
188
+ while (at < size) {
189
+ const { bytesRead } = await file.read(bytes, at, Math.min(CHUNK_BYTES, size - at), at);
190
+ if (bytesRead === 0)
191
+ break;
192
+ at += bytesRead;
193
+ }
194
+ if (at !== size)
195
+ throw new Error(`${path} changed while it was read: ${at} of ${size} bytes`);
196
+ }
197
+ finally {
198
+ await file.close();
199
+ }
200
+ return bytes;
201
+ }
172
202
  function sorted(files) {
173
203
  return Object.fromEntries(Object.entries(files).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
174
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/agents",
3
- "version": "0.21.0-beta",
3
+ "version": "0.23.0-beta",
4
4
  "description": "Nylorun agent definitions and session client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -43,7 +43,7 @@
43
43
  "test": "vitest run --passWithNoTests"
44
44
  },
45
45
  "dependencies": {
46
- "@nylorun/core": "0.21.0-beta",
46
+ "@nylorun/core": "0.23.0-beta",
47
47
  "gray-matter": "^4.0.3"
48
48
  },
49
49
  "peerDependencies": {