@nylorun/runtime 0.28.0-beta → 0.29.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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.29.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - eeb6294: Session usage as one ledger (protocol 18). The usage ledger keeps each model call's cost by token kind (uncached input, cache read, cache write, output) at the prices the adapter applied (migration 0037). For a protocol 18 client, `GET /v1/sessions/{id}/calls/model` returns each call's `startedAt`, `endedAt` and `usage.costSplit` (null for a call recorded before), and `GET /v1/sessions/{id}/usage` adds `costSplit` with `splitCalls`, and the calls' durations added up as `modelTimeMs` with `timedCalls`. A protocol 16 or 17 client reads usage as before. Studio's Usage tab is rebuilt on them: a Session card with one tokens bar and a ledger of tokens and cost by kind adding up to a total row, then the model calls grouped by turn with the same columns (cached, uncached, output, total, time, cost), and a row menu whose View event opens the call's event in the Events tab.
8
+
9
+ ### Patch Changes
10
+
11
+ - ed08ba8: Credential failures name the session's Agent, not its Environment: an HTTP tool's `http.credential` failure says "The session's Agent holds no credential '…' for …", and an MCP server's `credential_rejected` says the credential came from, or is missing in, the session's Agent.
12
+ - ed08ba8: A flow's HTTP stage or HTTP verifier, and an HTTP tool of an agent the harness section embeds (a flow's `agents`, or an agent used as a tool), now has its `credential` checked when the manifest is saved, as an agent's own HTTP tool has: it must name a `bearer` or `headers` slot of `runtime.secrets` bound to its URL, or the save is `422 manifest_inconsistent` naming it (`harness.root stage "refund" http.credential "checks" is not a secret slot in runtime.secrets`). Before, such a manifest saved and each call failed at run time with `http.credential`.
13
+ - ed08ba8: The Runtime API's Agent manifests tag no longer says a save changes no Agent: a save moves every Agent that tracks the manifest to the new version, and a pinned one stays.
14
+ - ed08ba8: Public text names no internal roadmap codes: the OpenAPI documents' descriptions, the type comments of `@nylorun/core`, `@nylorun/agents` and `@nylorun/admin`, and the CLI's help, README and generated compose file. A Runtime test fails when either OpenAPI document names one.
15
+ - ed08ba8: Webhook URLs name the Runtime's public URL. A trigger's `webhookUrl` is at `NYLORUN_PUBLIC_URL`, else the origin the request reached, so behind a reverse proxy that rewrites `Host` to `localhost:<port>` it is the proxy's URL, not `http://localhost:<port>`; the Runtime API's `401` challenges name it too. `nylorun start` passes `NYLORUN_PUBLIC_URL` from its shell to the runtime container (default `http://localhost:<port>`).
16
+ - ed08ba8: The tool policy's documentation matches how rules match: `tool` is the tool's identity (an MCP tool's own name on its `server`, or an HTTP tool's declared name), never the model-facing `server__tool`, which is refused; `agent` is the agent manifest's name, or the id of an agent used as a tool or of a flow's agent. `PUT /v1/tenant/tool-policy` describes each selector, and `GET` documents its `409` for a stored rule it cannot apply.
17
+ - ed08ba8: `POST /v1/webhooks/{endpointId}` takes no credential and ignores one that is sent: a delivery with an `Authorization` header is no longer checked against the Tenant's keys, since its Standard Webhooks signature is its grant. The OpenAPI document says so: `security: []`, no `Nylorun-*` headers, and `webhook-id`, `webhook-timestamp` and `webhook-signature` required.
18
+ - Pin core to the tested release.
19
+ - Pin harness to the tested release.
20
+ - Updated dependencies [ed08ba8]
21
+ - Updated dependencies [ed08ba8]
22
+ - Updated dependencies [ed08ba8]
23
+ - Updated dependencies [eeb6294]
24
+ - Updated dependencies [ed08ba8]
25
+ - Updated dependencies
26
+ - @nylorun/core@0.25.0-beta
27
+ - @nylorun/harness@0.29.1-beta
28
+
3
29
  ## 0.28.0-beta
4
30
 
5
31
  ### Minor Changes
@@ -12,7 +38,7 @@
12
38
 
13
39
  - 1ded443: Track T3, Agents that evolve: an Agent moves between versions of its manifest in place, with its id, disk, vault and sessions. `PUT /v1/agents/{id}` with another version moves it (`?dryRun=true` answers the plan, `AgentMovePlan`), and saving a manifest moves every Agent that tracks it. A harness-only change applies at once; a runtime change a pod applies only when it starts (image, resources, network, setup, files) leaves the Agent outdated (`targetVersion`, `waitingFor: "start"`) until its pod's next start, on the same volume; a version that adds a required secret waits for its value (`waitingFor: "secrets"`, state `input_required`). Its manifest's name, kind, harness and storage are fixed: `409 agent_fixed_field` replaces `409 agent_upgrade_unavailable`. Only secrets that fill a slot of the running version are used; others stay in the vault, dormant. New events `agent.outdated` and `agent.version_changed`, `GET /v1/agents?outdated=true`, and the save plan's `moves` and `agents_move` warning. Migration 0033 adds the Agent's target and failed versions and its start check.
14
40
  - 76dcd11: Track T3, the next start and its check: a pod Agent's start on a new version is checked until the new pod's harness joins (`startCheck` on the Agent). A failed setup, a failed pod, or a start slower than the Host's `tenant.upgradeStartMs` (10 minutes) takes the Agent back to the version it ran, its definition and not its disk, records `agent.start_failed`, and names the version `failedVersion` (state `failing`), which a tracking Agent does not move to again. A new version's setup runs on the same volume when its commands or files changed (`setupHash` in the setup plan and the volume's marker). While a pod waits to start on a new version it idles at most `tenant.upgradeIdleMs` (1 minute). `POST /v1/agents/{id}/restart` applies a pending version now; `retry: true` tries the failed version again.
15
- - 4fa160d: Agent memory, storage and API (track T5, D72). An agent manifest's `harness.memory` (`access`: `read_write` or `read_only`; `instructions`, at most 4,096 characters) turns on the Agent's memory: Markdown files under `/memories` it keeps across sessions, wakes and upgrades. The Runtime keeps them in Postgres (`memories`, `memory_versions`, migration 0036) with a version per change and who made it. `GET /v1/agents/{id}/memories` lists them (`pathPrefix`), `GET`, `PUT` (whole content, with an optional `contentSha256` precondition: `409 memory_conflict`) and `DELETE /v1/agents/{id}/memories/{path}` read, write and delete one (or a folder), and `GET /v1/agents/{id}/memory_versions` and `.../memory_versions/{versionId}` page the versions. A fork copies the memories (the fork plan adds `memories`); a delete removes them. `@nylorun/core` adds `memoryPathOf`, `memoryDescriptionOf` and the memory contracts; new Runtime limits `memoryBytes` (100 KB) and `memoriesPerAgent` (10,000); new error code `memory_conflict`. The SDK adds `client.agents.memories`; the CLI adds `nylorun agents memories ls|cat|put|rm|versions`. The model's `memory` tool arrives in the next steps of T5.
41
+ - 4fa160d: Agent memory, storage and API (track T5, D72). An agent manifest's `harness.memory` (`access`: `read_write` or `read_only`; `instructions`, at most 4,096 characters) turns on the Agent's memory: Markdown files under `/memories` it keeps across sessions, wakes and upgrades. The Runtime keeps them in Postgres (`memories`, `memory_versions`, migration 0036) with a version per change and who made it. `GET /v1/agents/{id}/memories` lists them (`pathPrefix`), `GET`, `PUT` (whole content, with an optional `contentSha256` precondition: `409 memory_conflict`) and `DELETE /v1/agents/{id}/memories/{path}` read, write and delete one (or a folder), and `GET /v1/agents/{id}/memory_versions` and `.../memory_versions/{versionId}` page the versions. A fork copies the memories (the fork plan adds `memories`); a delete removes them. `@nylorun/core` adds `memoryPathOf`, `memoryDescriptionOf` and the memory contracts; new Runtime limits `memoryBytes` (100 KiB) and `memoriesPerAgent` (10,000); new error code `memory_conflict`. The SDK adds `client.agents.memories`; the CLI adds `nylorun agents memories ls|cat|put|rm|versions`. The model's `memory` tool arrives in the next steps of T5.
16
42
  - 9fed948: The `memory` tool's calls (track T5, D72). A new effect target, `{kind: "runtime", operation: "memory"}` (additive on Harness API 5), carries Anthropic's memory tool input (`command`: `view`, `create`, `str_replace`, `insert`, `delete`, `rename`, and its parameters); core serves it in the intent's own transaction, so a replayed turn applies a write once. A malformed call fails with text for the model and changes nothing; a write from a `read_only` agent or a sub-agent is `memory.forbidden`. Each write is a memory version naming the turn's manifest version, session, turn and the trigger run that started the turn, with a new session event `memory.updated` and Agent event `agent.memory_updated`. `turn.start.memory` (`TurnMemorySchema`) carries the Agent's memory access, instructions and the index a harness pins, newest first, within the new Runtime limits `memoryIndexLines` (200) and `memoryIndexBytes` (25 KiB).
17
43
  - ffeb3bb: `schedule_wake` (track T4, D77). An agent manifest's `runtime.selfWake: true` offers the agent `schedule_wake`: `{in | at, note}` books its session's next turn, replacing the booking before, and `{cancel: true}` removes it. The Runtime serves the call in its own transaction (a new effect target, `{kind: "runtime", operation: "schedule_wake"}`, additive on Harness API 5; `turn.start.agent.selfWake` tells a harness to offer it), so a replayed turn books once. The booking fires as a trigger run of kind `wake`, its note the turn's message with `cause.kind: "self"`. New session events `wake.booked` and `wake.cancelled`, tool call kind `runtime`, and Runtime limits `wakeMaxAheadMs` and `wakeNoteChars`.
18
44
  - 9e60e40: Protocol 17, a queue per session (track T4, D76). A message sent while the session's turn runs or waits is queued (`{status: "queued", queuedMessageId, position}`, `message.queued`) and starts the next turn when this one ends, with every queued message, each its own user message (`queue.taken`); a cancel ends the turn, not the queue. `delivery: "reject"` keeps the refusal, now `409 session_busy`, and a protocol 16 client's messages default to it, so the Host serves protocol 16 and 17. The queue holds at most the Runtime limit `sessionQueuedMessages` (`429 queue_full`), and session reads count `queuedMessages`. Our engine takes `{kind: "messages", messages}` as a turn's input. The SDK's `input` and `inputParts` take `delivery`, and the CLI's `sessions send` takes `--reject-if-busy`. See MIGRATION.md.
package/README.md CHANGED
@@ -295,7 +295,7 @@ Postgres transactions persist session checkpoints, command receipts, individual
295
295
  effects, waits and the record of session events — in the Tenant's database; history is
296
296
  read from Durable Streams (S2). An advance owns its session through a lease with
297
297
  an epoch; a Worker that takes over after a crash marks in-flight model effects
298
- `uncertain`, and sends an interrupted MCP or HTTP tool call again with the same key (R5). Vault ciphertext needs that Tenant's own KEK.
298
+ `uncertain`, and sends an interrupted MCP or HTTP tool call again with the same key. Vault ciphertext needs that Tenant's own KEK.
299
299
 
300
300
  A session that names an Environment of kind `virtual` or `pod` gets a sandbox, its spec pinned
301
301
  on the session: our harness offers the sandbox tools and `save_artifact` and writes their notes
@@ -13964,6 +13964,8 @@ export declare const ModelCall: z.ZodObject<{
13964
13964
  provider: z.ZodNullable<z.ZodString>;
13965
13965
  model: z.ZodNullable<z.ZodString>;
13966
13966
  at: z.ZodString;
13967
+ startedAt: z.ZodNullable<z.ZodString>;
13968
+ endedAt: z.ZodNullable<z.ZodString>;
13967
13969
  outcome: z.ZodEnum<{
13968
13970
  completed: "completed";
13969
13971
  failed: "failed";
@@ -13979,6 +13981,12 @@ export declare const ModelCall: z.ZodObject<{
13979
13981
  duplicate: z.ZodBoolean;
13980
13982
  tokensReported: z.ZodNullable<z.ZodBoolean>;
13981
13983
  costKnown: z.ZodNullable<z.ZodBoolean>;
13984
+ costSplit: z.ZodNullable<z.ZodObject<{
13985
+ input: z.ZodNumber;
13986
+ cacheRead: z.ZodNumber;
13987
+ cacheWrite: z.ZodNumber;
13988
+ output: z.ZodNumber;
13989
+ }, z.core.$strict>>;
13982
13990
  }, z.core.$strict>>;
13983
13991
  }, z.core.$strict>;
13984
13992
  export declare const SessionPage: z.ZodObject<{
@@ -19499,6 +19507,15 @@ export declare const SessionUsageTotals: z.ZodObject<{
19499
19507
  unpricedCalls: z.ZodNumber;
19500
19508
  unreportedCalls: z.ZodNumber;
19501
19509
  unknownQualityCalls: z.ZodNumber;
19510
+ costSplit: z.ZodObject<{
19511
+ input: z.ZodNumber;
19512
+ cacheRead: z.ZodNumber;
19513
+ cacheWrite: z.ZodNumber;
19514
+ output: z.ZodNumber;
19515
+ }, z.core.$strict>;
19516
+ splitCalls: z.ZodNumber;
19517
+ modelTimeMs: z.ZodNumber;
19518
+ timedCalls: z.ZodNumber;
19502
19519
  }, z.core.$strict>;
19503
19520
  export declare const ModelCallsPage: z.ZodObject<{
19504
19521
  nextCursor: z.ZodNullable<z.ZodString>;
@@ -19512,6 +19529,8 @@ export declare const ModelCallsPage: z.ZodObject<{
19512
19529
  provider: z.ZodNullable<z.ZodString>;
19513
19530
  model: z.ZodNullable<z.ZodString>;
19514
19531
  at: z.ZodString;
19532
+ startedAt: z.ZodNullable<z.ZodString>;
19533
+ endedAt: z.ZodNullable<z.ZodString>;
19515
19534
  outcome: z.ZodEnum<{
19516
19535
  completed: "completed";
19517
19536
  failed: "failed";
@@ -19527,6 +19546,12 @@ export declare const ModelCallsPage: z.ZodObject<{
19527
19546
  duplicate: z.ZodBoolean;
19528
19547
  tokensReported: z.ZodNullable<z.ZodBoolean>;
19529
19548
  costKnown: z.ZodNullable<z.ZodBoolean>;
19549
+ costSplit: z.ZodNullable<z.ZodObject<{
19550
+ input: z.ZodNumber;
19551
+ cacheRead: z.ZodNumber;
19552
+ cacheWrite: z.ZodNumber;
19553
+ output: z.ZodNumber;
19554
+ }, z.core.$strict>>;
19530
19555
  }, z.core.$strict>>;
19531
19556
  }, z.core.$strict>>;
19532
19557
  }, z.core.$strict>;
@@ -19543,6 +19568,8 @@ export declare const ModelCallExportPage: z.ZodObject<{
19543
19568
  provider: z.ZodNullable<z.ZodString>;
19544
19569
  model: z.ZodNullable<z.ZodString>;
19545
19570
  at: z.ZodString;
19571
+ startedAt: z.ZodNullable<z.ZodString>;
19572
+ endedAt: z.ZodNullable<z.ZodString>;
19546
19573
  outcome: z.ZodEnum<{
19547
19574
  completed: "completed";
19548
19575
  failed: "failed";
@@ -19558,6 +19585,12 @@ export declare const ModelCallExportPage: z.ZodObject<{
19558
19585
  duplicate: z.ZodBoolean;
19559
19586
  tokensReported: z.ZodNullable<z.ZodBoolean>;
19560
19587
  costKnown: z.ZodNullable<z.ZodBoolean>;
19588
+ costSplit: z.ZodNullable<z.ZodObject<{
19589
+ input: z.ZodNumber;
19590
+ cacheRead: z.ZodNumber;
19591
+ cacheWrite: z.ZodNumber;
19592
+ output: z.ZodNumber;
19593
+ }, z.core.$strict>>;
19561
19594
  }, z.core.$strict>>;
19562
19595
  }, z.core.$strict>>;
19563
19596
  }, z.core.$strict>;
@@ -24,6 +24,10 @@ import type { TenantEnv } from "./app.js";
24
24
  */
25
25
  export type Credential = "application" | "subject" | "management" | "token";
26
26
  export interface RouteAccess {
27
+ /**
28
+ * The credentials the route takes. None (with `anonymous`): the route takes no credential and
29
+ * ignores one that is sent, as a webhook delivery, whose signature is its grant.
30
+ */
27
31
  readonly credentials: readonly Credential[];
28
32
  /** The subject scopes that reach the route: any one of them, none (`never`), or all (`any`). */
29
33
  readonly scopes: readonly SubjectScope[] | "never" | "any";
@@ -63,6 +63,11 @@ export function declaredRoute(method, segments) {
63
63
  (segment.startsWith("{") && segments[index] !== "")))?.access;
64
64
  }
65
65
  function authenticated(access) {
66
+ if (access.credentials.length === 0)
67
+ return async (c, next) => {
68
+ c.set("scope", { kind: "anonymous" });
69
+ await next();
70
+ };
66
71
  return async (c, next) => {
67
72
  const keys = keyAccess(access);
68
73
  const scope = await authenticateCaller(c, access.anonymous === true, keys);
@@ -82,9 +87,12 @@ export const TraceHeaders = z.object({
82
87
  export function tenantRoute(api, access, route, handler) {
83
88
  const takes = (credential) => access.credentials.includes(credential);
84
89
  const schemes = [...new Set(access.credentials.map((credential) => SCHEMES[credential]))];
90
+ // A route that takes no credential is called by others than Nylorun's clients: it documents
91
+ // neither a credential nor `Nylorun-*` headers.
92
+ const open = access.credentials.length === 0;
85
93
  const protocol = z.string().meta({ description: `The protocol version, \`${PROTOCOL_VERSION}\`` });
86
94
  const headers = z.object({
87
- "Nylorun-Protocol": access.unversioned ? protocol.optional() : protocol,
95
+ ...(open ? {} : { "Nylorun-Protocol": access.unversioned ? protocol.optional() : protocol }),
88
96
  ...(takes("subject")
89
97
  ? {
90
98
  "Nylorun-Subject": z
@@ -100,11 +108,10 @@ export function tenantRoute(api, access, route, handler) {
100
108
  });
101
109
  api.openAPIRegistry.registerPath({
102
110
  ...route,
103
- // `{}`: no credential needed (OpenAPI's optional security).
104
- security: [
105
- ...schemes.map((scheme) => ({ [scheme]: [] })),
106
- ...(access.anonymous ? [{}] : []),
107
- ],
111
+ // `{}`: no credential needed (OpenAPI's optional security); none at all: `[]`.
112
+ security: open
113
+ ? []
114
+ : [...schemes.map((scheme) => ({ [scheme]: [] })), ...(access.anonymous ? [{}] : [])],
108
115
  // A route's own headers (`traceparent`, R7.4) beside the common ones.
109
116
  request: {
110
117
  ...route.request,
@@ -115,9 +122,13 @@ export function tenantRoute(api, access, route, handler) {
115
122
  401: rejected(takes("token")
116
123
  ? "No credential (`credential_required`), one the Tenant does not accept (`credential_invalid`), a token that expired (`token_expired`), or a token whose issuer's keys cannot be fetched now (`issuer_unavailable`). `WWW-Authenticate: Bearer` names the protected resource metadata (`resource_metadata`)"
117
124
  : "No credential (`credential_required`), or one the Tenant does not accept (`credential_invalid`), with a `WWW-Authenticate: Bearer` challenge"),
118
- 403: rejected(takes("management")
119
- ? "Not allowed for this credential, a key of the other API (`key_role_mismatch`), a key from a browser (`origin_rejected`) or a management key acting for a subject"
120
- : "Not allowed for this credential or subject scope (`scope_required`; a token also gets `WWW-Authenticate: Bearer error=\"insufficient_scope\"` and the scope), a key of the other API (`key_role_mismatch`), or an application key from a browser (`origin_rejected`)"),
125
+ ...(open
126
+ ? {}
127
+ : {
128
+ 403: rejected(takes("management")
129
+ ? "Not allowed for this credential, a key of the other API (`key_role_mismatch`), a key from a browser (`origin_rejected`) or a management key acting for a subject"
130
+ : "Not allowed for this credential or subject scope (`scope_required`; a token also gets `WWW-Authenticate: Bearer error=\"insufficient_scope\"` and the scope), a key of the other API (`key_role_mismatch`), or an application key from a browser (`origin_rejected`)"),
131
+ }),
121
132
  404: rejected("Not found (`not_found`)"),
122
133
  426: { description: "`Nylorun-Protocol` missing or unsupported", content: { "application/json": { schema: ProtocolRejected } } },
123
134
  503: rejected("The Tenant's storage or streams are unavailable"),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent manifests (`/v1/agent-manifests/**`, protocol 16): save, read, list and delete the named,
3
3
  * versioned documents Agents are made from, each with a harness and a runtime section. A save
4
- * never changes an Agent. Reading needs `agents:read` (or a write scope), saving and deleting
4
+ * moves every Agent that tracks the manifest to the new version; a pinned one stays. Reading needs `agents:read` (or a write scope), saving and deleting
5
5
  * `agent-manifests:write`, from any client (Q12). A token caller reaches none: a manifest holds
6
6
  * an agent's instructions, tools and servers.
7
7
  */
@@ -59,7 +59,7 @@ export function agentManifestRoutes(api) {
59
59
  path: "/v1/agent-manifests/{name}",
60
60
  tags: ["Agent manifests"],
61
61
  summary: "Save an agent manifest",
62
- description: "Saves the manifest as its next version, or answers the latest unchanged when nothing differs. Every Agent that tracks the manifest moves to the new version (track T3): at once, at its pod's next start for a runtime change a pod applies only when it starts, or once a required secret the version adds is set; a pinned one stays, and so does one whose kind, harness or storage the version changes (`agents_behind`). The Runtime checks the harness and runtime sections against each other: a custom harness needs a pod with an image, every credential the harness names is a declared slot of the right type, and the runtime's kind gives the loop the sandbox it needs; every gap is named in one `422 manifest_inconsistent` (`details.gaps`). `If-Match: <version>` refuses the save with `412 manifest_changed` when the latest version is no longer the one you read (`0` for a manifest that does not exist yet). With `?dryRun=true` the answer is the plan: the changes by section, every reason a save would be refused (`errors`, the gaps first), warnings, the Agents it would move (`moves`) and how many stay on older versions (`behind`); nothing is saved. Clients compose the authoring files and drop their `$schema` before they send one document.",
62
+ description: "Saves the manifest as its next version, or answers the latest unchanged when nothing differs. Every Agent that tracks the manifest moves to the new version: at once, at its pod's next start for a runtime change a pod applies only when it starts, or once a required secret the version adds is set; a pinned one stays, and so does one whose kind, harness or storage the version changes (`agents_behind`). The Runtime checks the harness and runtime sections against each other: a custom harness needs a pod with an image, every credential the harness names is a declared slot of the right type, and the runtime's kind gives the loop the sandbox it needs; every gap is named in one `422 manifest_inconsistent` (`details.gaps`). `If-Match: <version>` refuses the save with `412 manifest_changed` when the latest version is no longer the one you read (`0` for a manifest that does not exist yet). With `?dryRun=true` the answer is the plan: the changes by section, every reason a save would be refused (`errors`, the gaps first), warnings, the Agents it would move (`moves`) and how many stay on older versions (`behind`); nothing is saved. Clients compose the authoring files and drop their `$schema` before they send one document.",
63
63
  request: {
64
64
  params: namePath,
65
65
  query: z.object({
@@ -72,7 +72,7 @@ export function agentRoutes(api) {
72
72
  path: "/v1/agents",
73
73
  tags: ["Agents"],
74
74
  summary: "List Agents",
75
- description: "Every Agent with all the labels asked for, in `state`, made from `manifest` and `outdated` (moving to another version, track T3) when given, by id. A token caller sees only the Agents its grants reach, and a caller acting for a person only that person's sessions on each. `limit` opts into pages.",
75
+ description: "Every Agent with all the labels asked for, in `state`, made from `manifest` and `outdated` (moving to another version) when given, by id. A token caller sees only the Agents its grants reach, and a caller acting for a person only that person's sessions on each. `limit` opts into pages.",
76
76
  request: {
77
77
  query: pageQuery.extend({
78
78
  label: z
@@ -122,7 +122,7 @@ export function agentRoutes(api) {
122
122
  path: "/v1/agents/{agentId}",
123
123
  tags: ["Agents"],
124
124
  summary: "Create, find or move an Agent",
125
- description: "Creates the Agent, its vault and its compute from a saved agent manifest, within the Tenant's limits, or answers the one with this id, so get-or-create is one call. `manifest: \"name\"` records that it tracks the manifest (its latest version, and each version saved later) and `\"name@n\"` that it is pinned; the version it runs is `resolvedVersion`. On an existing one, labels replace, `owner` sets or (with `null`) clears the owner guard, and `manifest` moves it (track T3) with its id, disk, vault and sessions: a harness-only change applies at once (each session's next turn runs it), a runtime change of a pod at its next start (`targetVersion`, `waitingFor: start`), and a version that adds a required secret once a value is set (`waitingFor: secrets`). Its manifest's name, kind, harness and storage are fixed (`409 agent_fixed_field`): fork it to change them. `?dryRun=true` answers the move's plan and changes nothing. Creating an Agent opens no session (D71). Kind `pod` needs sandbox pods (`nylorun local sandbox enable`) and is created at once.",
125
+ description: "Creates the Agent, its vault and its compute from a saved agent manifest, within the Tenant's limits, or answers the one with this id, so get-or-create is one call. `manifest: \"name\"` records that it tracks the manifest (its latest version, and each version saved later) and `\"name@n\"` that it is pinned; the version it runs is `resolvedVersion`. On an existing one, labels replace, `owner` sets or (with `null`) clears the owner guard, and `manifest` moves it with its id, disk, vault and sessions: a harness-only change applies at once (each session's next turn runs it), a runtime change of a pod at its next start (`targetVersion`, `waitingFor: start`), and a version that adds a required secret once a value is set (`waitingFor: secrets`). Its manifest's name, kind, harness and storage are fixed (`409 agent_fixed_field`): fork it to change them. `?dryRun=true` answers the move's plan and changes nothing. Creating an Agent opens no session. Kind `pod` needs sandbox pods (`nylorun local sandbox enable`) and is created at once.",
126
126
  request: {
127
127
  params: agentId,
128
128
  query: z.object({
@@ -161,7 +161,7 @@ export function agentRoutes(api) {
161
161
  path: "/v1/agents/{agentId}/events",
162
162
  tags: ["Agents"],
163
163
  summary: "Read an Agent's events",
164
- description: "`agent.created`, `.attached`, `.detached`, `.deleted`, a pod's `.running`, `.suspended`, `.relaunched`, `.lost`, `.reset` and `.failed`, `.forked`, a move's `.outdated`, `.version_changed` and `.start_failed` (track T3), and `agent.egress` for every connection its sandbox opens through egress-gate, naming the turns that were running (D67), in order, numbered from 0, at most the Tenant's Runtime limit `environmentEventItems` (default 1000) per call: `from` reads on.",
164
+ description: "`agent.created`, `.attached`, `.detached`, `.deleted`, a pod's `.running`, `.suspended`, `.relaunched`, `.lost`, `.reset` and `.failed`, `.forked`, a move's `.outdated`, `.version_changed` and `.start_failed`, and `agent.egress` for every connection its sandbox opens through egress-gate, naming the turns that were running, in order, numbered from 0, at most the Tenant's Runtime limit `environmentEventItems` (default 1000) per call: `from` reads on.",
165
165
  request: {
166
166
  params: agentId,
167
167
  query: z.object({
@@ -244,7 +244,7 @@ export function agentRoutes(api) {
244
244
  path: "/v1/agents/{agentId}/restart",
245
245
  tags: ["Agents"],
246
246
  summary: "Restart an Agent on its pending version",
247
- description: "Applies a version the Agent waits to apply (track T3): a running pod stops, so the next turn starts it on the version, on the same volume; a stopped or failed one is asked to run at once. `retry: true` first moves it to the version that failed to start (`failedVersion`) again. Kinds `virtual` and `none` apply a version at once, so there is nothing to wait for. Refused during a turn (`agent_busy`). The body is optional.",
247
+ description: "Applies a version the Agent waits to apply: a running pod stops, so the next turn starts it on the version, on the same volume; a stopped or failed one is asked to run at once. `retry: true` first moves it to the version that failed to start (`failedVersion`) again. Kinds `virtual` and `none` apply a version at once, so there is nothing to wait for. Refused during a turn (`agent_busy`). The body is optional.",
248
248
  request: {
249
249
  params: agentId,
250
250
  body: { required: false, content: { "application/json": { schema: RestartAgentRequest } } },
@@ -304,7 +304,7 @@ export function agentRoutes(api) {
304
304
  path: "/v1/agents/{agentId}/fork",
305
305
  tags: ["Agents"],
306
306
  summary: "Fork an Agent",
307
- description: "Copies the Agent into a new one (Q15), which is how an Agent changes what is fixed for its life: a new Agent `id`, made from `manifest` (default the source's manifest name, tracking, at its latest version), its disk started from a version of this one's (`disk`: `latest`, the default, takes a snapshot first; a version number; or `empty`) and its secrets copied inside the Runtime (`secrets`: `copy`, the default, copies those the new manifest declares; `none`). On a fork onto other setup the new manifest's setup runs on the copied disk before its first turn. This Agent keeps what it runs, its disk and its sessions. `dryRun` answers the plan and makes nothing. To move an Agent to another version of the same manifest, with the same kind, harness and storage, `PUT` it instead (track T3).",
307
+ description: "Copies the Agent into a new one, which is how an Agent changes what is fixed for its life: a new Agent `id`, made from `manifest` (default the source's manifest name, tracking, at its latest version), its disk started from a version of this one's (`disk`: `latest`, the default, takes a snapshot first; a version number; or `empty`) and its secrets copied inside the Runtime (`secrets`: `copy`, the default, copies those the new manifest declares; `none`). On a fork onto other setup the new manifest's setup runs on the copied disk before its first turn. This Agent keeps what it runs, its disk and its sessions. `dryRun` answers the plan and makes nothing. To move an Agent to another version of the same manifest, with the same kind, harness and storage, `PUT` it instead.",
308
308
  request: {
309
309
  params: agentId,
310
310
  body: { required: true, content: { "application/json": { schema: ForkAgentRequest } } },
@@ -378,7 +378,7 @@ export function agentRoutes(api) {
378
378
  path: "/v1/agents/{agentId}/secrets/{name}",
379
379
  tags: ["Agents"],
380
380
  summary: "Write an Agent's secret",
381
- description: "Creates or replaces one credential of the Agent's vault: an `environment_secret` (the variable `name` reads the sentinel `nylorun_managed` in the sandbox, and egress-gate sets the value as a header on requests to `allowedHosts`), an `environment_variable` (a plain variable), or a `bearer` token or `headers` map for the MCP servers and HTTP tools at `url`. The same name and type re-seals the value and binding; another type replaces it. A rotated value is used from the next request. A second credential for a host or a URL is `409 credential_conflict`. When the Agent's manifest declares secret slots (D64; while it moves to another version, that version's), send only the value (`{ value }`, or `{ headers }` for a `headers` slot): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`.",
381
+ description: "Creates or replaces one credential of the Agent's vault: an `environment_secret` (the variable `name` reads the sentinel `nylorun_managed` in the sandbox, and egress-gate sets the value as a header on requests to `allowedHosts`), an `environment_variable` (a plain variable), or a `bearer` token or `headers` map for the MCP servers and HTTP tools at `url`. The same name and type re-seals the value and binding; another type replaces it. A rotated value is used from the next request. A second credential for a host or a URL is `409 credential_conflict`. When the Agent's manifest declares secret slots (while it moves to another version, that version's), send only the value (`{ value }`, or `{ headers }` for a `headers` slot): the type and binding come from the slot, and a name it does not declare is `409 secret_not_in_manifest`.",
382
382
  request: {
383
383
  params: secretPath,
384
384
  body: {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { SESSION_STATUSES } from "@nylorun/core/contracts";
3
+ import { PROTOCOL_HEADER } from "@nylorun/core/compatibility";
3
4
  import { SessionManifestView, SessionUsageTotals, ModelCallsPage, ModelCallExportPage, ToolCallsPage, TurnTrace, } from "../../components.js";
4
5
  import { turnTrace } from "../../../traces/read.js";
5
6
  import { readAccess, readStoreOf } from "../../../reads/access.js";
@@ -8,6 +9,27 @@ import { fail } from "../../../tenant/http.js";
8
9
  import { readRuntimeLimits } from "../../../tenant/runtime-limits.js";
9
10
  import { tenantRoute } from "../define.js";
10
11
  import { jsonResponse } from "../respond.js";
12
+ /**
13
+ * Whether a client speaks protocol 18, whose usage reads carry the calls' timing and the cost by
14
+ * token kind. An older client's SDK parses these reads strictly, so it gets them without.
15
+ */
16
+ function usageByKind(header) {
17
+ const protocol = Number(header?.trim());
18
+ return Number.isInteger(protocol) && protocol >= 18;
19
+ }
20
+ /** A session's usage as a protocol 16 or 17 client reads it. */
21
+ function totalsBefore18(totals) {
22
+ const { costSplit: _split, splitCalls: _splitCalls, modelTimeMs: _time, timedCalls: _timed, ...rest } = totals;
23
+ return rest;
24
+ }
25
+ /** A model call as a protocol 16 or 17 client reads it. */
26
+ function callBefore18(call) {
27
+ const { startedAt: _started, endedAt: _ended, usage, ...rest } = call;
28
+ if (usage === null)
29
+ return { ...rest, usage };
30
+ const { costSplit: _split, ...counts } = usage;
31
+ return { ...rest, usage: counts };
32
+ }
11
33
  /** A page's `limit`, held to the Tenant's Runtime limit `pageItems` (`pageSize`). */
12
34
  const limitParam = (fallback) => z.coerce
13
35
  .number()
@@ -105,14 +127,16 @@ export function readRoutes(api) {
105
127
  },
106
128
  }, async (c) => {
107
129
  requirePrincipal(c.get("scope"));
108
- const store = readStoreOf(c.env.tenant), access = readAccess(c.get("scope")), id = c.req.param("sessionId");
130
+ const store = readStoreOf(c.env.tenant), access = readAccess(c.get("scope")), id = c.req.param("sessionId"), current = usageByKind(c.req.header(PROTOCOL_HEADER));
109
131
  if (kind === "usage") {
110
132
  const q = parseQuery(turnQuery, c.req.query());
111
- return jsonResponse(200, await store.usage(id, q.turnId, access));
133
+ const totals = await store.usage(id, q.turnId, access);
134
+ return jsonResponse(200, current ? totals : totalsBefore18(totals));
112
135
  }
113
136
  const q = parseQuery(callsQuery, c.req.query());
114
137
  const limit = await pageSize(c.env.tenant, q.limit, { fallback: CALLS_PAGE_DEFAULT });
115
- return jsonResponse(200, await store.modelCalls(id, q.turnId, { ...q, limit }, access));
138
+ const page = await store.modelCalls(id, q.turnId, { ...q, limit }, access);
139
+ return jsonResponse(200, current ? page : { ...page, calls: page.calls.map(callBefore18) });
116
140
  });
117
141
  }
118
142
  const toolCallsQuery = turnQuery
@@ -193,6 +217,7 @@ export function readRoutes(api) {
193
217
  }, async (c) => {
194
218
  const q = parseQuery(exportQuery, c.req.query());
195
219
  const limit = await pageSize(c.env.tenant, q.limit, { fallback: EXPORT_PAGE_DEFAULT, name: "exportPageItems" });
196
- return jsonResponse(200, await readStoreOf(c.env.tenant).exportModel(q.after, limit));
220
+ const page = await readStoreOf(c.env.tenant).exportModel(q.after, limit);
221
+ return jsonResponse(200, usageByKind(c.req.header(PROTOCOL_HEADER)) ? page : { ...page, calls: page.calls.map(callBefore18) });
197
222
  });
198
223
  }
@@ -307,7 +307,10 @@ export function tenantRoutes(api) {
307
307
  path: "/v1/tenant/tool-policy",
308
308
  tags: ["Tenant"],
309
309
  summary: "Get the Tenant's tool policy",
310
- responses: { 200: json(ToolPolicy, "The rules the Tool Gate applies to every MCP and HTTP tool call") },
310
+ responses: {
311
+ 200: json(ToolPolicy, "The rules the Tool Gate applies to every MCP and HTTP tool call"),
312
+ 409: { description: "A stored rule names an MCP tool by its model-facing `server__tool` name: every call is refused until the policy is set again" },
313
+ },
311
314
  }, async (c) => {
312
315
  try {
313
316
  return jsonResponse(200, await c.env.tenant.store.tx((t) => readToolPolicy(t)));
@@ -325,7 +328,7 @@ export function tenantRoutes(api) {
325
328
  path: "/v1/tenant/tool-policy",
326
329
  tags: ["Tenant"],
327
330
  summary: "Set the Tenant's tool policy",
328
- description: "Replaces every rule. The Tool Gate applies them to each MCP and HTTP tool call of every harness, before it sends the call: the strictest rule that matches decides. `deny` refuses the call (the model gets `tool.denied`), `ask` holds it and pauses the turn until a person answers it with an `approve` command on the session, `allow` runs it. A call no rule matches runs. A rule matches when every selector it sets matches: `tool` (the name the model calls it by; `*` matches any run of characters), `server` (an MCP server's name), `kind` (`mcp` or `http`), `agent` (the agent that calls it) and `when` (fields of the call's input by dotted path, each tested with `equals`, `in`, `gt`, `gte`, `lt`, `lte` or `matches`). Sandbox tools never cross the gate. An empty list allows every call.",
331
+ description: "Replaces every rule. The Tool Gate applies them to each MCP and HTTP tool call of every harness, before it sends the call: the strictest rule that matches decides. `deny` refuses the call (the model gets `tool.denied`), `ask` holds it and pauses the turn until a person answers it with an `approve` command on the session, `allow` runs it. A call no rule matches runs. A rule matches when every selector it sets matches: `tool` (the tool's identity: an MCP tool's own name on its `server`, or an HTTP tool's declared name, never the name the model calls it by; `*` matches any run of characters), `server` (an MCP server's name), `kind` (`mcp` or `http`), `agent` (the agent that calls it: the agent manifest's name, or the id of an agent used as a tool or of a flow's agent, never an Agent's id) and `when` (fields of the call's input by dotted path, each tested with `equals`, `in`, `gt`, `gte`, `lt`, `lte` or `matches`). A `tool` with `__` in it, a model-facing `server__tool` name, is refused (`400`): name an MCP tool by `server` and `tool`. A stored policy with such a rule refuses every call until it is set again. Sandbox tools never cross the gate. An empty list allows every call.",
329
332
  request: { body: body(PutToolPolicyRequest) },
330
333
  responses: { 200: json(ToolPolicy, "The policy now in force") },
331
334
  }, async (c) => {
@@ -44,7 +44,7 @@ export function triggerRoutes(api) {
44
44
  }, async (c) => {
45
45
  const id = idOf(c.req.param("agentId"));
46
46
  assertReaches(c.get("scope"), id);
47
- return jsonResponse(200, await listTriggers(c.env.tenant, id, originOf(c.req.url)));
47
+ return jsonResponse(200, await listTriggers(c.env.tenant, id, originOf(c)));
48
48
  });
49
49
  for (const action of ["pause", "resume"])
50
50
  tenantRoute(api, WRITE, {
@@ -63,7 +63,7 @@ export function triggerRoutes(api) {
63
63
  }, async (c) => {
64
64
  const id = idOf(c.req.param("agentId"));
65
65
  assertReaches(c.get("scope"), id);
66
- return jsonResponse(200, await setTriggerStatus(c.env.tenant, id, c.req.param("triggerId"), action, originOf(c.req.url)));
66
+ return jsonResponse(200, await setTriggerStatus(c.env.tenant, id, c.req.param("triggerId"), action, originOf(c)));
67
67
  });
68
68
  tenantRoute(api, WRITE, {
69
69
  method: "post",
@@ -111,7 +111,7 @@ export function triggerRoutes(api) {
111
111
  }
112
112
  /** A delivery: no credential, no protocol header, any body; the endpoint id and the signature are the grant. */
113
113
  const DELIVERY = {
114
- credentials: ["application", "subject", "token"],
114
+ credentials: [],
115
115
  scopes: "any",
116
116
  anonymous: true,
117
117
  unversioned: true,
@@ -132,20 +132,20 @@ export function webhookRoutes(api) {
132
132
  }, async (c) => {
133
133
  const id = idOf(c.req.param("agentId"));
134
134
  assertReaches(c.get("scope"), id);
135
- return jsonResponse(200, await rotateWebhookSecret(c.env.tenant, id, c.req.param("triggerId"), originOf(c.req.url)));
135
+ return jsonResponse(200, await rotateWebhookSecret(c.env.tenant, id, c.req.param("triggerId"), originOf(c)));
136
136
  });
137
137
  tenantRoute(api, DELIVERY, {
138
138
  method: "post",
139
139
  path: "/v1/webhooks/{endpointId}",
140
140
  tags: ["Triggers"],
141
141
  summary: "Deliver to a webhook trigger",
142
- description: "A sender's delivery, signed with Standard Webhooks (`webhook-id`, `webhook-timestamp` within 5 minutes by default, `webhook-signature`): no credential and no `Nylorun-Protocol`. The body, any media type, at most the Runtime limit `webhookBodyBytes`, is read as bytes and verified before anything else. It runs the trigger once per `webhook-id`: a retry answers the first run. The body reaches the model as data under the trigger's prompt. Other senders (GitHub, Stripe, Slack) are relayed by an app.",
142
+ description: "A sender's delivery, signed with Standard Webhooks (`webhook-id`, `webhook-timestamp` within 5 minutes by default, `webhook-signature`): no credential (an `Authorization` header is ignored) and no `Nylorun-Protocol`. The body, any media type, at most the Runtime limit `webhookBodyBytes`, is read as bytes and verified before anything else. It runs the trigger once per `webhook-id`: a retry answers the first run. The body reaches the model as data under the trigger's prompt. Other senders (GitHub, Stripe, Slack) are relayed by an app.",
143
143
  request: {
144
144
  params: z.object({ endpointId: z.string().meta({ description: "The trigger's endpoint id, `wh_…`" }) }),
145
145
  headers: z.object({
146
- "webhook-id": z.string().optional().meta({ description: "The delivery's id; a retry sends the same" }),
147
- "webhook-timestamp": z.string().optional().meta({ description: "Seconds since the epoch" }),
148
- "webhook-signature": z.string().optional().meta({ description: "`v1,<base64>`, space-separated" }),
146
+ "webhook-id": z.string().meta({ description: "The delivery's id; a retry sends the same" }),
147
+ "webhook-timestamp": z.string().meta({ description: "Seconds since the epoch" }),
148
+ "webhook-signature": z.string().meta({ description: "`v1,<base64>`, space-separated" }),
149
149
  }),
150
150
  body: {
151
151
  required: true,
@@ -155,7 +155,7 @@ export function webhookRoutes(api) {
155
155
  },
156
156
  responses: {
157
157
  202: json(WebhookAcceptedResponse, "The run it made: started, queued, skipped or failed"),
158
- 401: { description: "Not signed with the endpoint's secret, or a timestamp out of tolerance" },
158
+ 401: { description: "Unsigned, not signed with the endpoint's secret, or a timestamp out of tolerance" },
159
159
  404: { description: "No endpoint has this id" },
160
160
  409: { description: "The trigger is paused (`trigger_paused`)" },
161
161
  413: { description: "Over `webhookBodyBytes`" },
@@ -174,7 +174,10 @@ export function webhookRoutes(api) {
174
174
  }));
175
175
  });
176
176
  }
177
- /** The origin a request reached: where its webhook URLs live. */
178
- function originOf(url) {
179
- return new URL(url).origin;
177
+ /**
178
+ * Where its webhook URLs live: the Runtime's public URL (`NYLORUN_PUBLIC_URL`), else the origin
179
+ * the request reached, which behind a proxy that rewrites `Host` is the Runtime's own.
180
+ */
181
+ function originOf(c) {
182
+ return c.env.tenant.config.publicUrl ?? new URL(c.req.url).origin;
180
183
  }
@@ -112,7 +112,7 @@ const RUNTIME_TAGS = [
112
112
  {
113
113
  name: "Agent manifests",
114
114
  group: "Agents",
115
- description: "Named, versioned agent manifests (protocol 16): one document with what runs inside an agent's loop (`harness`) and what the Runtime provides around it (`runtime`), checked against each other when it is saved. Save one with a plan first (`?dryRun=true`) and `If-Match`; an Agent is made from one version and keeps it, so a save changes no Agent. `client.agentManifests` in `@nylorun/agents`, and `nylorun apply`.",
115
+ description: "Named, versioned agent manifests (protocol 16): one document with what runs inside an agent's loop (`harness`) and what the Runtime provides around it (`runtime`), checked against each other when it is saved. Save one with a plan first (`?dryRun=true`) and `If-Match`; an Agent runs one version at a time, and a save moves every Agent that tracks the manifest to the new version, while a pinned one stays. `client.agentManifests` in `@nylorun/agents`, and `nylorun apply`.",
116
116
  operations: [
117
117
  "PUT /v1/agent-manifests/{name}",
118
118
  "GET /v1/agent-manifests",
@@ -155,7 +155,7 @@ const RUNTIME_TAGS = [
155
155
  {
156
156
  name: "Triggers",
157
157
  group: "Agents",
158
- description: "What starts an Agent's turns with no client connected (track T4, D74): the schedules, one-shot times and webhook endpoints its manifest's `runtime.triggers` declare. Each trigger's status is the Agent's: pause and resume it, run it now, make a webhook trigger's signing secret, and read its runs (`started`, `queued`, `skipped` or `failed`, with a reason). In `@nylorun/agents` and `nylorun agents triggers`.",
158
+ description: "What starts an Agent's turns with no client connected: the schedules, one-shot times and webhook endpoints its manifest's `runtime.triggers` declare. Each trigger's status is the Agent's: pause and resume it, run it now, make a webhook trigger's signing secret, and read its runs (`started`, `queued`, `skipped` or `failed`, with a reason). In `@nylorun/agents` and `nylorun agents triggers`.",
159
159
  operations: [
160
160
  "GET /v1/agents/{agentId}/triggers",
161
161
  "POST /v1/agents/{agentId}/triggers/{triggerId}/pause",
@@ -169,7 +169,7 @@ const RUNTIME_TAGS = [
169
169
  {
170
170
  name: "Memory",
171
171
  group: "Agents",
172
- description: "An Agent's memories (track T5, D72): Markdown files under `/memories` it keeps across sessions, wakes and upgrades, shaped like Anthropic's memory stores. List and read them, write one whole (with a `content_sha256` precondition), delete one or a folder, and read the version every change leaves, with who made it. In `@nylorun/agents` and `nylorun agents memories`.",
172
+ description: "An Agent's memories: Markdown files under `/memories` it keeps across sessions, wakes and upgrades, shaped like Anthropic's memory stores. List and read them, write one whole (with a `content_sha256` precondition), delete one or a folder, and read the version every change leaves, with who made it. In `@nylorun/agents` and `nylorun agents memories`.",
173
173
  operations: [
174
174
  "GET /v1/agents/{agentId}/memories",
175
175
  "GET /v1/agents/{agentId}/memories/{path}",
@@ -198,7 +198,7 @@ export async function runHttpTool(tenant, call, options) {
198
198
  if (authorized.status === "refused")
199
199
  return failed("http.credential", `The credential '${http.credential}' was refused: ${authorized.reason}`);
200
200
  if (authorized.status === "unauthenticated")
201
- return failed("http.credential", `The session's Environment holds no credential '${http.credential}' for ${authorized.url}`);
201
+ return failed("http.credential", `The session's Agent holds no credential '${http.credential}' for ${authorized.url}`);
202
202
  credential = authorized.headers;
203
203
  identity = authorized.identity;
204
204
  // A credential with `via` sends the call there, a gateway (R2b C2, Q9): to its one URL, so
@@ -142,6 +142,22 @@ export function usageRow(request, outcome, at, timing) {
142
142
  ...(typeof firstTokenMs === "number" && Number.isFinite(firstTokenMs) && firstTokenMs >= 0
143
143
  ? { firstTokenMs: Math.round(firstTokenMs) }
144
144
  : {}),
145
+ ...costSplitColumns(outcome.evidence?.extras?.costSplit),
146
+ };
147
+ }
148
+ /** The ledger's cost-by-kind columns from the adapter's `costSplit`; none unless all four are costs. */
149
+ function costSplitColumns(split) {
150
+ if (!split || typeof split !== "object" || Array.isArray(split))
151
+ return {};
152
+ const { input, cacheRead, cacheWrite, output } = split;
153
+ const parts = [input, cacheRead, cacheWrite, output];
154
+ if (!parts.every((part) => typeof part === "number" && Number.isFinite(part) && part >= 0))
155
+ return {};
156
+ return {
157
+ costInputUsd: input,
158
+ costCacheReadUsd: cacheRead,
159
+ costCacheWriteUsd: cacheWrite,
160
+ costOutputUsd: output,
145
161
  };
146
162
  }
147
163
  /** The ledger rows a budget counts for `request`; undefined when it doesn't apply. */
@@ -29,7 +29,7 @@ export interface TenantVault {
29
29
  session(sessionId: string): Promise<Session | undefined>;
30
30
  /**
31
31
  * The authorization of one request to a session's remote MCP server or HTTP tool: its
32
- * credential from the vault of the session's Environment.
32
+ * credential from the vault of the session's Agent.
33
33
  */
34
34
  authorizeMcp(sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
35
35
  /** Vault writes and token signing with the Tenant's vault key (the keys service, F4.2). */
@@ -14,4 +14,6 @@ export declare function configForFactory(options: {
14
14
  delivery?: TenantConfig["delivery"];
15
15
  /** The trusted issuers of the identity file (`NYLORUN_IDENTITY_FILE`). */
16
16
  issuers?: TenantConfig["issuers"];
17
+ /** The URL clients reach the Runtime API at (`NYLORUN_PUBLIC_URL`). */
18
+ publicUrl?: string;
17
19
  }): (id: string) => TenantConfig;
@@ -16,6 +16,7 @@ export function configForFactory(options) {
16
16
  model: options.model ?? { kind: "vault" },
17
17
  ...(options.delivery ? { delivery: options.delivery } : {}),
18
18
  ...(options.issuers ? { issuers: options.issuers } : {}),
19
+ ...(options.publicUrl ? { publicUrl: options.publicUrl } : {}),
19
20
  logger: {
20
21
  info: (message, fields) => options.logger.info(message, { tenantId: id, ...fields }),
21
22
  warn: (message, fields) => options.logger.warn(message, { tenantId: id, ...fields }),
package/dist/host/main.js CHANGED
@@ -279,6 +279,7 @@ export async function main() {
279
279
  logger,
280
280
  ...(stack.delivery ? { delivery: stack.delivery } : {}),
281
281
  ...(issuers ? { issuers } : {}),
282
+ ...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
282
283
  });
283
284
  // The process's Durable Session Execution: Restate when its endpoints are set, else the
284
285
  // in-process memory execution. An invocation that arrives while the Tenant opens waits for it.