@nylorun/runtime 0.23.0-beta → 0.24.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +79 -28
  3. package/dist/adapters/execution/restate.js +1 -1
  4. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  5. package/dist/adapters/replication/pgoutput.js +6 -3
  6. package/dist/adapters/sandbox/local.d.ts +9 -1
  7. package/dist/adapters/sandbox/local.js +16 -4
  8. package/dist/adapters/sandbox/virtual.js +4 -1
  9. package/dist/api/a2a/endpoint.js +5 -1
  10. package/dist/api/a2a/routes.d.ts +2 -0
  11. package/dist/api/a2a/routes.js +2 -2
  12. package/dist/api/a2a/tasks.js +2 -1
  13. package/dist/api/ag-ui/endpoint.js +4 -2
  14. package/dist/api/ag-ui/history.js +4 -1
  15. package/dist/api/ag-ui/routes.d.ts +4 -1
  16. package/dist/api/ag-ui/routes.js +11 -10
  17. package/dist/api/ag-ui/translate.js +3 -2
  18. package/dist/api/components.d.ts +4953 -1204
  19. package/dist/api/components.js +25 -16
  20. package/dist/api/gate/keys.js +7 -1
  21. package/dist/api/gate/routes.d.ts +6 -0
  22. package/dist/api/gate/routes.js +39 -3
  23. package/dist/api/http/app.js +4 -4
  24. package/dist/api/http/define.d.ts +6 -0
  25. package/dist/api/http/define.js +13 -1
  26. package/dist/api/http/respond.js +1 -1
  27. package/dist/api/http/routes/artifacts.js +35 -8
  28. package/dist/api/http/routes/coverage.d.ts +9 -0
  29. package/dist/api/http/routes/coverage.js +34 -0
  30. package/dist/api/http/routes/environments.d.ts +11 -0
  31. package/dist/api/http/routes/environments.js +283 -0
  32. package/dist/api/http/routes/mcp.d.ts +1 -1
  33. package/dist/api/http/routes/mcp.js +2 -2
  34. package/dist/api/http/routes/me.js +1 -1
  35. package/dist/api/http/routes/reads.d.ts +45 -9
  36. package/dist/api/http/routes/reads.js +54 -5
  37. package/dist/api/http/routes/sessions.js +11 -6
  38. package/dist/api/http/routes/tenant.js +32 -10
  39. package/dist/api/openapi.js +31 -26
  40. package/dist/artifacts/disk.d.ts +59 -0
  41. package/dist/artifacts/disk.js +563 -0
  42. package/dist/artifacts/export.d.ts +1 -1
  43. package/dist/artifacts/export.js +2 -3
  44. package/dist/artifacts/folders.d.ts +6 -3
  45. package/dist/artifacts/folders.js +17 -12
  46. package/dist/artifacts/service.d.ts +2 -1
  47. package/dist/artifacts/service.js +6 -1
  48. package/dist/artifacts/workspace.d.ts +1 -1
  49. package/dist/artifacts/workspace.js +1 -1
  50. package/dist/core/flow-host.d.ts +2 -3
  51. package/dist/core/flow-host.js +3 -0
  52. package/dist/core/provider.d.ts +3 -2
  53. package/dist/core/provider.js +5 -37
  54. package/dist/core/sandbox-routes.d.ts +2 -21
  55. package/dist/core/sandbox-routes.js +10 -43
  56. package/dist/execution/types.d.ts +1 -1
  57. package/dist/gates/contract.d.ts +3 -24
  58. package/dist/gates/contract.js +1 -36
  59. package/dist/gates/egress-credentials.d.ts +9 -0
  60. package/dist/gates/egress-credentials.js +11 -4
  61. package/dist/gates/egress.d.ts +28 -1
  62. package/dist/gates/egress.js +115 -10
  63. package/dist/gates/fixture-model.d.ts +28 -0
  64. package/dist/gates/fixture-model.js +92 -0
  65. package/dist/gates/handler.js +4 -4
  66. package/dist/gates/http-tool.d.ts +6 -2
  67. package/dist/gates/http-tool.js +3 -5
  68. package/dist/gates/in-process.d.ts +5 -0
  69. package/dist/gates/in-process.js +12 -1
  70. package/dist/gates/journal.d.ts +30 -0
  71. package/dist/gates/journal.js +52 -0
  72. package/dist/gates/mcp-handler.d.ts +4 -0
  73. package/dist/gates/mcp-handler.js +5 -1
  74. package/dist/gates/meter.d.ts +10 -2
  75. package/dist/gates/meter.js +20 -5
  76. package/dist/gates/policy.d.ts +49 -0
  77. package/dist/gates/policy.js +134 -0
  78. package/dist/gates/tenant-vaults.d.ts +7 -7
  79. package/dist/gates/tenant-vaults.js +5 -5
  80. package/dist/gates/tool-calls.d.ts +10 -5
  81. package/dist/gates/tool-calls.js +28 -10
  82. package/dist/gates/tool-client.d.ts +8 -0
  83. package/dist/gates/tool-client.js +27 -11
  84. package/dist/gates/tool-contract.d.ts +10 -68
  85. package/dist/gates/tool-contract.js +1 -85
  86. package/dist/gates/tool-gate.d.ts +1 -1
  87. package/dist/harness/calls.d.ts +12 -5
  88. package/dist/harness/calls.js +21 -12
  89. package/dist/harness/executors.js +12 -9
  90. package/dist/harness/main.js +18 -5
  91. package/dist/harness/pod.d.ts +34 -0
  92. package/dist/harness/pod.js +41 -0
  93. package/dist/harness/service.d.ts +0 -1
  94. package/dist/harness/service.js +13 -5
  95. package/dist/harness/setup.d.ts +27 -0
  96. package/dist/harness/setup.js +150 -0
  97. package/dist/harness/ws-client.js +3 -2
  98. package/dist/harness/ws-port.d.ts +1 -5
  99. package/dist/harness/ws-port.js +1 -5
  100. package/dist/harness-api/in-process.js +3 -3
  101. package/dist/harness-api/record.d.ts +15 -1
  102. package/dist/harness-api/record.js +174 -44
  103. package/dist/harness-api/redact.d.ts +3 -0
  104. package/dist/harness-api/redact.js +45 -0
  105. package/dist/harness-api/server.d.ts +8 -0
  106. package/dist/harness-api/server.js +93 -13
  107. package/dist/harness-api/start.d.ts +10 -3
  108. package/dist/harness-api/start.js +40 -10
  109. package/dist/harness-api/tool-journal.d.ts +96 -0
  110. package/dist/harness-api/tool-journal.js +102 -0
  111. package/dist/harness-api/tool-status.d.ts +13 -0
  112. package/dist/harness-api/tool-status.js +26 -0
  113. package/dist/harness-api/workspace.d.ts +6 -3
  114. package/dist/harness-api/workspace.js +27 -11
  115. package/dist/harness-api/ws-server.d.ts +11 -3
  116. package/dist/harness-api/ws-server.js +94 -15
  117. package/dist/host/gates.d.ts +2 -1
  118. package/dist/host/gates.js +10 -2
  119. package/dist/host/main.js +54 -5
  120. package/dist/host/operate.js +67 -5
  121. package/dist/host/stack-config.d.ts +64 -0
  122. package/dist/host/stack-config.js +61 -1
  123. package/dist/keys/client.js +4 -1
  124. package/dist/keys/keys.d.ts +5 -5
  125. package/dist/keys/keys.js +5 -7
  126. package/dist/management-openapi.json +999 -2211
  127. package/dist/mcp/connect.d.ts +25 -20
  128. package/dist/mcp/connect.js +29 -30
  129. package/dist/mcp/pool.d.ts +2 -2
  130. package/dist/mcp/pool.js +1 -1
  131. package/dist/mcp/preview.d.ts +3 -3
  132. package/dist/mcp/preview.js +10 -20
  133. package/dist/mcp/snapshot.d.ts +5 -11
  134. package/dist/mcp/snapshot.js +6 -7
  135. package/dist/model/pi-model.js +8 -0
  136. package/dist/openapi.json +6955 -2526
  137. package/dist/reads/types.d.ts +16 -3
  138. package/dist/record/append.d.ts +3 -1
  139. package/dist/record/append.js +1 -0
  140. package/dist/record/envelope.d.ts +5 -1
  141. package/dist/record/envelope.js +7 -3
  142. package/dist/record/environment.d.ts +25 -0
  143. package/dist/record/environment.js +44 -0
  144. package/dist/record/index.d.ts +1 -1
  145. package/dist/record/index.js +1 -1
  146. package/dist/sandbox/disk-fs.d.ts +95 -0
  147. package/dist/sandbox/disk-fs.js +206 -0
  148. package/dist/sandbox/join.d.ts +17 -18
  149. package/dist/sandbox/join.js +42 -15
  150. package/dist/sandbox/manager.d.ts +16 -8
  151. package/dist/sandbox/manager.js +33 -17
  152. package/dist/sandbox/placement.d.ts +13 -12
  153. package/dist/sandbox/placement.js +29 -12
  154. package/dist/sandbox/pods/client.d.ts +6 -1
  155. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  156. package/dist/sandbox/pods/lifecycle.js +14 -26
  157. package/dist/sandbox/pods/reconcile.js +18 -12
  158. package/dist/sandbox/pods/spec.d.ts +15 -13
  159. package/dist/sandbox/pods/spec.js +16 -20
  160. package/dist/sandbox/records.d.ts +2 -2
  161. package/dist/sandbox/records.js +3 -3
  162. package/dist/sandbox/resolve.d.ts +35 -27
  163. package/dist/sandbox/resolve.js +66 -41
  164. package/dist/sandbox/session-sandbox.js +1 -1
  165. package/dist/sandbox/setup.d.ts +61 -0
  166. package/dist/sandbox/setup.js +123 -0
  167. package/dist/sandbox/share.d.ts +11 -21
  168. package/dist/sandbox/share.js +12 -41
  169. package/dist/sandbox/tenant-config.d.ts +12 -14
  170. package/dist/sandbox/tenant-config.js +12 -17
  171. package/dist/sandbox/types.d.ts +6 -0
  172. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  173. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  174. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  175. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  176. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  177. package/dist/store/postgres/reads.js +117 -17
  178. package/dist/store/postgres/record-writer.d.ts +4 -4
  179. package/dist/store/postgres/record-writer.js +9 -9
  180. package/dist/store/postgres/record.d.ts +7 -0
  181. package/dist/store/postgres/record.js +23 -2
  182. package/dist/store/postgres/schema.d.ts +934 -761
  183. package/dist/store/postgres/schema.js +102 -73
  184. package/dist/store/postgres/store.js +119 -66
  185. package/dist/store/types.d.ts +109 -48
  186. package/dist/tenant/advance.d.ts +7 -6
  187. package/dist/tenant/advance.js +115 -42
  188. package/dist/tenant/artifact-tool.js +3 -4
  189. package/dist/tenant/commands.d.ts +15 -2
  190. package/dist/tenant/commands.js +93 -12
  191. package/dist/tenant/context.d.ts +48 -22
  192. package/dist/tenant/context.js +4 -15
  193. package/dist/tenant/effects.d.ts +1 -1
  194. package/dist/tenant/effects.js +37 -20
  195. package/dist/tenant/environments.d.ts +167 -0
  196. package/dist/tenant/environments.js +823 -0
  197. package/dist/tenant/harness-tokens.d.ts +21 -0
  198. package/dist/tenant/harness-tokens.js +61 -0
  199. package/dist/tenant/host-token.d.ts +1 -1
  200. package/dist/tenant/host-token.js +2 -4
  201. package/dist/tenant/identity-file.d.ts +4 -4
  202. package/dist/tenant/identity-file.js +17 -8
  203. package/dist/tenant/issuers.js +3 -3
  204. package/dist/tenant/principals.js +1 -1
  205. package/dist/tenant/reset.js +3 -3
  206. package/dist/tenant/runtime.d.ts +6 -1
  207. package/dist/tenant/runtime.js +7 -1
  208. package/dist/tenant/session.d.ts +3 -3
  209. package/dist/tenant/session.js +0 -14
  210. package/dist/tenant/sessions.d.ts +13 -13
  211. package/dist/tenant/sessions.js +61 -138
  212. package/dist/tenant/signing-keys.d.ts +5 -1
  213. package/dist/tenant/status.js +3 -3
  214. package/dist/tenant/sweep.js +12 -6
  215. package/dist/tenant/transcript.d.ts +210 -20
  216. package/dist/tenant/transcript.js +62 -11
  217. package/dist/tenant/turn-timing.d.ts +43 -0
  218. package/dist/tenant/turn-timing.js +39 -0
  219. package/dist/tenant/types.d.ts +2 -0
  220. package/dist/traces/ids.d.ts +42 -0
  221. package/dist/traces/ids.js +85 -0
  222. package/dist/traces/key.d.ts +6 -0
  223. package/dist/traces/key.js +37 -0
  224. package/dist/traces/map.d.ts +113 -0
  225. package/dist/traces/map.js +317 -0
  226. package/dist/traces/options.d.ts +7 -0
  227. package/dist/traces/options.js +1 -0
  228. package/dist/traces/otlp.d.ts +42 -0
  229. package/dist/traces/otlp.js +84 -0
  230. package/dist/traces/projector.d.ts +58 -0
  231. package/dist/traces/projector.js +200 -0
  232. package/dist/traces/read.d.ts +23 -0
  233. package/dist/traces/read.js +16 -0
  234. package/dist/vault/coverage.d.ts +26 -0
  235. package/dist/vault/coverage.js +64 -0
  236. package/dist/vault/error.d.ts +11 -1
  237. package/dist/vault/error.js +5 -1
  238. package/dist/vault/service.d.ts +47 -48
  239. package/dist/vault/service.js +321 -382
  240. package/dist/vault/sources.d.ts +5 -16
  241. package/dist/vault/sources.js +6 -8
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +2 -2
  244. package/package.json +13 -13
  245. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  246. package/dist/api/http/routes/sandboxes.js +0 -153
  247. package/dist/api/http/routes/vaults.d.ts +0 -10
  248. package/dist/api/http/routes/vaults.js +0 -139
  249. package/dist/record/sandbox.d.ts +0 -25
  250. package/dist/record/sandbox.js +0 -43
  251. package/dist/tenant/sandboxes.d.ts +0 -123
  252. package/dist/tenant/sandboxes.js +0 -566
@@ -12,10 +12,16 @@ export interface McpRequestOptions {
12
12
  /** Options of one tool call. */
13
13
  export interface McpCallOptions extends McpRequestOptions {
14
14
  /**
15
- * The call's effect id. A gate runs a keyed call once and keeps its outcome, so a re-send
16
- * after a takeover joins it (F4.1). An in-process connection ignores it.
15
+ * The call's effect id. A gate keeps a keyed call's outcome, so a re-send after a takeover
16
+ * joins it (F4.1), and the server gets it in `_meta` (R5.5).
17
17
  */
18
18
  readonly key?: string;
19
+ /**
20
+ * The call's span context (R7.4), sent as the `traceparent` header on the call's requests where
21
+ * this process holds the connection. A client of the gates service leaves it out: the gate reads
22
+ * it from the call's journal row.
23
+ */
24
+ readonly traceparent?: string;
19
25
  }
20
26
  /** A page of `tools/list`, as the MCP SDK returns it. */
21
27
  export interface McpToolPage {
@@ -73,9 +79,8 @@ export type McpToolFailure = {
73
79
  readonly message: string;
74
80
  /** True when calling again may work (R2b C7). */
75
81
  readonly retryable?: boolean;
76
- /** With `credential_rejected`: the server, and the scope of the vault whose credential was sent. */
82
+ /** With `credential_rejected`: the server. */
77
83
  readonly server?: string;
78
- readonly vault?: "installation" | "user";
79
84
  /** How many credential values the message had in it, replaced (R2b C8). */
80
85
  readonly redacted?: number;
81
86
  };
@@ -93,8 +98,8 @@ export type McpToolOutcome = {
93
98
  * - `mcp.forbidden`: the server answered `403`.
94
99
  * - `mcp.error`: the server answered a JSON-RPC error; the message has its code.
95
100
  * - `mcp.status`: the server answered another HTTP error status, without a JSON-RPC error.
96
- * - `mcp.lost`: sent, and the answer was lost, so the tool may have run. The model sees it only
97
- * for a tool the server marks read-only or idempotent (Q15); any other call is `uncertain`.
101
+ * - `mcp.lost`: sent, and the answer was lost, so the tool may have run. The model sees it, and
102
+ * decides whether to call again (R5; it replaced R2b's read-only and idempotent exception).
98
103
  * - `mcp.too-large`: the answer passed `MCP_RESULT_MAX_BYTES` (R2b C11), as an HTTP tool's
99
104
  * answer is `http.too-large`. The tool ran; its answer was not kept.
100
105
  */
@@ -110,9 +115,8 @@ export interface McpCallFailure {
110
115
  /** The request reached the server, so it may have acted on it. */
111
116
  readonly sent: boolean;
112
117
  readonly retryable: boolean;
113
- /** With `credential_rejected`: the server, and the scope of the vault whose credential was sent. */
118
+ /** With `credential_rejected`: the server. */
114
119
  readonly server?: string;
115
- readonly vault?: "installation" | "user";
116
120
  /** How many credential values the message had in it, replaced (R2b C8). */
117
121
  readonly redacted?: number;
118
122
  }
@@ -134,16 +138,16 @@ export declare class McpCallFailed extends Error {
134
138
  */
135
139
  export declare class CredentialRejected extends McpCallFailed {
136
140
  readonly server: string;
137
- /** The scope of the vault whose credential was sent; absent when none was. */
138
- readonly vault?: "installation" | "user" | undefined;
141
+ /** Whether a credential of the session's Environment was sent. */
142
+ readonly sent: boolean;
139
143
  /**
140
144
  * The answer's `WWW-Authenticate`, which may name the server's protected-resource metadata
141
145
  * (RFC 9728 `resource_metadata`): a tool preview reads it (R2b C12). Never shown to the model.
142
146
  */
143
147
  readonly challenge?: string | undefined;
144
148
  constructor(server: string,
145
- /** The scope of the vault whose credential was sent; absent when none was. */
146
- vault?: "installation" | "user" | undefined,
149
+ /** Whether a credential of the session's Environment was sent. */
150
+ sent: boolean,
147
151
  /**
148
152
  * The answer's `WWW-Authenticate`, which may name the server's protected-resource metadata
149
153
  * (RFC 9728 `resource_metadata`): a tool preview reads it (R2b C12). Never shown to the model.
@@ -162,9 +166,16 @@ export declare class McpConnectionStale extends McpCallFailed {
162
166
  }
163
167
  /** A tool call that never reached its server: its connection could not be opened. */
164
168
  export declare function unreachable(server: string, error: unknown): McpCallFailed;
169
+ /**
170
+ * Where a `tools/call` carries its key (R5.5): MCP has no standard key field, and `_meta` is the
171
+ * spec's place for extra request data. The key is the call's effect id, the same on every send,
172
+ * so a server that reads it can run the call once.
173
+ */
174
+ export declare const IDEMPOTENCY_META_KEY = "nylorun/idempotency-key";
165
175
  /**
166
176
  * An SDK `Client` as an `McpClient`. Its answers and failures carry no credential value sent
167
- * with them (R2b C8), and a failed call is an `McpCallFailed` (C7).
177
+ * with them (R2b C8), and a failed call is an `McpCallFailed` (C7). A keyed call sends its key in
178
+ * `_meta` (R5.5).
168
179
  */
169
180
  export declare function sdkClient(client: Client, serverName: string): McpClient;
170
181
  export declare function openMcpServer(input: {
@@ -212,11 +223,5 @@ export declare function listMcpTools(client: McpClient, input: {
212
223
  * be opened again (the caller retries once), and with any error that leaves the call's fate
213
224
  * unknown.
214
225
  */
215
- export declare function callMcpTool(client: McpClient, serverToolName: string, args: unknown, options?: McpCallOptions & {
216
- /**
217
- * The tool is read-only or idempotent (`McpToolRecord.annotations`): a call whose answer was
218
- * lost after it was sent is `mcp.lost` to the model. Otherwise it is left uncertain (Q15).
219
- */
220
- readonly retrySafe?: boolean;
221
- }): Promise<McpToolOutcome>;
226
+ export declare function callMcpTool(client: McpClient, serverToolName: string, args: unknown, options?: McpCallOptions): Promise<McpToolOutcome>;
222
227
  export declare function diagnosticFromError(capabilityId: string, serverName: string, error: unknown): McpDiagnostic;
@@ -29,7 +29,7 @@ export class McpCallFailed extends Error {
29
29
  /** The failure the gate answered, as the class the loop's own call would have thrown. */
30
30
  static from(failure) {
31
31
  return failure.code === "credential_rejected" && failure.server !== undefined
32
- ? new CredentialRejected(failure.server, failure.vault)
32
+ ? new CredentialRejected(failure.server, !failure.message.includes("holds none"))
33
33
  : new McpCallFailed(failure);
34
34
  }
35
35
  /** The failed tool outcome the model sees. */
@@ -41,7 +41,6 @@ export class McpCallFailed extends Error {
41
41
  message: failure.message,
42
42
  retryable: failure.retryable,
43
43
  ...(failure.server === undefined ? {} : { server: failure.server }),
44
- ...(failure.vault === undefined ? {} : { vault: failure.vault }),
45
44
  ...(failure.redacted ? { redacted: failure.redacted } : {}),
46
45
  };
47
46
  }
@@ -54,11 +53,11 @@ export class McpCallFailed extends Error {
54
53
  */
55
54
  export class CredentialRejected extends McpCallFailed {
56
55
  server;
57
- vault;
56
+ sent;
58
57
  challenge;
59
58
  constructor(server,
60
- /** The scope of the vault whose credential was sent; absent when none was. */
61
- vault,
59
+ /** Whether a credential of the session's Environment was sent. */
60
+ sent,
62
61
  /**
63
62
  * The answer's `WWW-Authenticate`, which may name the server's protected-resource metadata
64
63
  * (RFC 9728 `resource_metadata`): a tool preview reads it (R2b C12). Never shown to the model.
@@ -66,16 +65,15 @@ export class CredentialRejected extends McpCallFailed {
66
65
  challenge) {
67
66
  super({
68
67
  code: "credential_rejected",
69
- message: vault === undefined
70
- ? `The MCP server '${server}' answered HTTP 401: it needs a credential, and the session's vaults hold none for it`
71
- : `The MCP server '${server}' answered HTTP 401: it rejected the credential from the ${vault} vault`,
68
+ message: sent
69
+ ? `The MCP server '${server}' answered HTTP 401: it rejected the credential from the session's Environment`
70
+ : `The MCP server '${server}' answered HTTP 401: it needs a credential, and the session's Environment holds none for it`,
72
71
  sent: true,
73
72
  retryable: false,
74
73
  server,
75
- ...(vault === undefined ? {} : { vault }),
76
74
  });
77
75
  this.server = server;
78
- this.vault = vault;
76
+ this.sent = sent;
79
77
  this.challenge = challenge;
80
78
  this.name = "CredentialRejected";
81
79
  }
@@ -106,18 +104,26 @@ export function unreachable(server, error) {
106
104
  }
107
105
  /** The operation a request of the transport's fetch belongs to. */
108
106
  const tracking = new AsyncLocalStorage();
109
- function tracked(call, run) {
107
+ function tracked(call, run, traceparent) {
110
108
  const tracker = {
111
109
  secrets: new Set(),
112
110
  sent: false,
113
111
  settled: false,
114
112
  ...(call ? { calling: new AbortController() } : {}),
113
+ ...(traceparent ? { traceparent } : {}),
115
114
  };
116
115
  return tracking.run(tracker, () => run(tracker));
117
116
  }
117
+ /**
118
+ * Where a `tools/call` carries its key (R5.5): MCP has no standard key field, and `_meta` is the
119
+ * spec's place for extra request data. The key is the call's effect id, the same on every send,
120
+ * so a server that reads it can run the call once.
121
+ */
122
+ export const IDEMPOTENCY_META_KEY = "nylorun/idempotency-key";
118
123
  /**
119
124
  * An SDK `Client` as an `McpClient`. Its answers and failures carry no credential value sent
120
- * with them (R2b C8), and a failed call is an `McpCallFailed` (C7).
125
+ * with them (R2b C8), and a failed call is an `McpCallFailed` (C7). A keyed call sends its key in
126
+ * `_meta` (R5.5).
121
127
  */
122
128
  export function sdkClient(client, serverName) {
123
129
  return {
@@ -133,7 +139,10 @@ export function sdkClient(client, serverName) {
133
139
  callTool: (params, options) => tracked(true, async (tracker) => {
134
140
  const lost = tracker.calling.signal;
135
141
  try {
136
- const result = (await client.callTool(params, undefined, {
142
+ const keyed = options?.key === undefined
143
+ ? params
144
+ : { ...params, _meta: { [IDEMPOTENCY_META_KEY]: options.key } };
145
+ const result = (await client.callTool(keyed, undefined, {
137
146
  signal: options?.signal ? AbortSignal.any([options.signal, lost]) : lost,
138
147
  }));
139
148
  // An answer the stream did not count (an `sse` server answers on its event stream).
@@ -151,7 +160,7 @@ export function sdkClient(client, serverName) {
151
160
  finally {
152
161
  tracker.settled = true;
153
162
  }
154
- }),
163
+ }, options?.traceparent),
155
164
  };
156
165
  }
157
166
  export async function openMcpServer(input) {
@@ -275,7 +284,6 @@ export async function listMcpTools(client, input) {
275
284
  continue;
276
285
  const { tool, inputSchema } = candidate;
277
286
  const outputSchema = tool.outputSchema ? usableSchema(tool.outputSchema) : undefined;
278
- const annotations = retryHints(tool.annotations);
279
287
  tools.push({
280
288
  capabilityId: input.capabilityId,
281
289
  serverName: input.serverName,
@@ -286,7 +294,6 @@ export async function listMcpTools(client, input) {
286
294
  : {}),
287
295
  inputSchema,
288
296
  ...(outputSchema === undefined ? {} : { outputSchema }),
289
- ...(annotations === undefined ? {} : { annotations }),
290
297
  });
291
298
  }
292
299
  return { tools, omitted, renamed, disabled, unknownTools };
@@ -307,6 +314,7 @@ export async function callMcpTool(client, serverToolName, args, options = {}) {
307
314
  }, {
308
315
  ...(options.key === undefined ? {} : { key: options.key }),
309
316
  ...(options.signal ? { signal: options.signal } : {}),
317
+ ...(options.traceparent === undefined ? {} : { traceparent: options.traceparent }),
310
318
  });
311
319
  }
312
320
  catch (error) {
@@ -315,11 +323,7 @@ export async function callMcpTool(client, serverToolName, args, options = {}) {
315
323
  // The connection's to handle: its holder opens it again and sends the call once more.
316
324
  if (error instanceof McpConnectionStale)
317
325
  throw error;
318
- // Sent, then lost: the tool may have run. Calling it again is the model's choice only for a
319
- // tool the server says is safe to repeat; any other waits for an operator.
320
- if (error.code === "mcp.lost" && !options.retrySafe)
321
- throw new Error(error.message);
322
- // Otherwise the outcome is known: the model sees it.
326
+ // The model sees it, `mcp.lost` included: sent, then lost, so the tool may have run (R5).
323
327
  return error.outcome();
324
328
  }
325
329
  const { result } = answer;
@@ -418,6 +422,9 @@ function authorizedFetch(server, endpoint, authorize, policy) {
418
422
  for (const [key, value] of Object.entries(result.headers))
419
423
  headers.set(key, value);
420
424
  const tracker = tracking.getStore();
425
+ // The tool call's span (R7.4): per request, so a pooled connection carries each call's own.
426
+ if (tracker?.traceparent)
427
+ headers.set("traceparent", tracker.traceparent);
421
428
  if (tracker && result.status === "authorized")
422
429
  for (const secret of credentialSecrets(result.headers, result.identity))
423
430
  tracker.secrets.add(secret);
@@ -436,7 +443,7 @@ function authorizedFetch(server, endpoint, authorize, policy) {
436
443
  tracker.sent = true;
437
444
  if (response.status === 401) {
438
445
  await response.body?.cancel().catch(() => undefined);
439
- throw new CredentialRejected(server.name, result.status === "authorized" ? result.vault : undefined, response.headers.get("www-authenticate") ?? undefined);
446
+ throw new CredentialRejected(server.name, result.status === "authorized", response.headers.get("www-authenticate") ?? undefined);
440
447
  }
441
448
  // The server ended the connection's session: the tool never saw the request.
442
449
  if (post && response.status === 404 && headers.has("mcp-session-id")) {
@@ -622,14 +629,6 @@ function rpcErrorOf(body) {
622
629
  }
623
630
  return undefined;
624
631
  }
625
- /** The hints that calling a tool again is safe, when the server gives any. */
626
- function retryHints(annotations) {
627
- const readOnly = annotations?.readOnlyHint === true;
628
- const idempotent = annotations?.idempotentHint === true;
629
- if (!readOnly && !idempotent)
630
- return undefined;
631
- return { ...(readOnly ? { readOnlyHint: true } : {}), ...(idempotent ? { idempotentHint: true } : {}) };
632
- }
633
632
  function sameOrigin(requestUrl, serverUrl) {
634
633
  try {
635
634
  return new URL(requestUrl).origin === new URL(serverUrl).origin;
@@ -58,8 +58,8 @@ export declare class McpPool {
58
58
  manifest: AgentManifest;
59
59
  /** The effect id: a gate runs the call once under it (F4.1). */
60
60
  effectId?: string;
61
- /** The tool is read-only or idempotent: a lost answer is the model's to retry (R2b C7). */
62
- retrySafe?: boolean;
61
+ /** The call's span context (R7.4), sent as `traceparent` on a connection this process holds. */
62
+ traceparent?: string;
63
63
  signal?: AbortSignal;
64
64
  }): Promise<McpToolOutcome>;
65
65
  /** The session's connection to the server, opened when there is none, or why it cannot be. */
package/dist/mcp/pool.js CHANGED
@@ -126,8 +126,8 @@ export class McpPool {
126
126
  try {
127
127
  return await callMcpTool(live.connection.client, input.serverToolName, input.args, {
128
128
  ...(input.effectId === undefined ? {} : { key: input.effectId }),
129
- ...(input.retrySafe ? { retrySafe: true } : {}),
130
129
  ...(input.signal ? { signal: input.signal } : {}),
130
+ ...(input.traceparent === undefined ? {} : { traceparent: input.traceparent }),
131
131
  });
132
132
  }
133
133
  catch (error) {
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * A remote MCP server's tools before an agent names it (R2b C12, `POST /v1/tenant/mcp/preview`,
3
- * `nylorun mcp inspect`, Studio's Credentials page). It runs where plaintext credentials live:
3
+ * `nylorun mcp inspect`, an Environment's Secrets tab in Studio). It runs where plaintext credentials live:
4
4
  * the keys service (`keys/keys.ts`, `previewMcp`), in the gateway or in process.
5
5
  *
6
- * One connection: `initialize` and `tools/list` with the installation vault's credential for the
6
+ * One connection: `initialize` and `tools/list` with the Environment's credential for the
7
7
  * URL (its headers, and its `via`), under the Host's address policy, within 15 s; then it closes.
8
8
  * It never calls a tool, and sends no identity header: no person asked. A server that answers
9
9
  * `401` is `authRequired`, with its RFC 9728 protected-resource metadata when it publishes any,
@@ -16,7 +16,7 @@ import { openMcpServer } from "./connect.js";
16
16
  /** How long a preview may take, `initialize` and every `tools/list` page together. */
17
17
  export declare const MCP_PREVIEW_TIMEOUT_MS = 15000;
18
18
  export interface McpPreviewOptions {
19
- readonly vault: Pick<VaultService, "authorize" | "getVault" | "listVaults">;
19
+ readonly vault: Pick<VaultService, "authorize" | "listEnvironmentSecrets">;
20
20
  /** The Host's address policy for developer URLs (`TenantConfig.delivery`). */
21
21
  readonly policy: OutboundPolicy;
22
22
  /** Default `MCP_PREVIEW_TIMEOUT_MS`. */
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * A remote MCP server's tools before an agent names it (R2b C12, `POST /v1/tenant/mcp/preview`,
3
- * `nylorun mcp inspect`, Studio's Credentials page). It runs where plaintext credentials live:
3
+ * `nylorun mcp inspect`, an Environment's Secrets tab in Studio). It runs where plaintext credentials live:
4
4
  * the keys service (`keys/keys.ts`, `previewMcp`), in the gateway or in process.
5
5
  *
6
- * One connection: `initialize` and `tools/list` with the installation vault's credential for the
6
+ * One connection: `initialize` and `tools/list` with the Environment's credential for the
7
7
  * URL (its headers, and its `via`), under the Host's address policy, within 15 s; then it closes.
8
8
  * It never calls a tool, and sends no identity header: no person asked. A server that answers
9
9
  * `401` is `authRequired`, with its RFC 9728 protected-resource metadata when it publishes any,
10
10
  * read under the same policy. No credential value sent is in the answer (`scrubValues`, C8).
11
11
  */
12
- import { INSTALLATION_OWNER, McpPreviewRequestSchema, } from "@nylorun/core/contracts";
12
+ import { McpPreviewRequestSchema, } from "@nylorun/core/contracts";
13
13
  import { scrubValues } from "../redact.js";
14
14
  import { HttpError } from "../tenant/http.js";
15
15
  import { guardedFetch, OutboundRefused } from "../tenant/outbound.js";
@@ -119,28 +119,18 @@ export async function previewMcpServer(body, options) {
119
119
  }
120
120
  }
121
121
  /**
122
- * The installation vault credential for the URL: from `vaultId`, else from every installation
123
- * vault. None is a preview without one; two or more without `vaultId` is a refusal that names
124
- * them.
122
+ * The credential of Environment `environmentId`'s vault for the URL; none without an
123
+ * Environment (a preview without a credential). An Environment that does not exist is a 404.
125
124
  */
126
125
  async function credentialFor(vault, request) {
127
- let vaultIds;
128
- if (request.vaultId !== undefined) {
129
- const found = await vault.getVault(request.vaultId);
130
- if (found.ownerUserId !== INSTALLATION_OWNER)
131
- throw new VaultError(400, "A preview uses an installation vault's credential: no person asked for it");
132
- vaultIds = [found.id];
133
- }
134
- else
135
- vaultIds = (await vault.listVaults(undefined, { installation: true })).map((found) => found.id);
136
- if (vaultIds.length === 0)
126
+ if (request.environmentId === undefined)
137
127
  return { status: "unauthenticated", url: request.url, headers: {} };
138
- const result = await vault.authorize({ vaultIds, credentialSelections: [], url: request.url });
128
+ // The 404 of a missing Environment, before any request leaves.
129
+ await vault.listEnvironmentSecrets(request.environmentId);
130
+ const result = await vault.authorize({ environmentId: request.environmentId, url: request.url });
139
131
  if (result.status !== "refused")
140
132
  return result;
141
- throw new HttpError(409, result.reason === "ambiguous"
142
- ? "Several installation vaults hold a credential for this URL: name one with vaultId"
143
- : `The installation vault's credential for this URL cannot be used (${result.reason})`, { code: "request_rejected", details: { reason: result.reason, credentialIds: result.credentialIds } });
133
+ throw new HttpError(409, `Environment ${request.environmentId}'s credential for this URL cannot be used (${result.reason})`, { code: "request_rejected", details: { reason: result.reason, credentialIds: result.credentialIds } });
144
134
  }
145
135
  /** A preview that listed nothing: `mcp_preview_failed`, with the code a tool call would have had. */
146
136
  function failed(server, error, signal, secrets, timeoutMs) {
@@ -1,6 +1,6 @@
1
1
  import type { EventPayload } from "@nylorun/core/contracts";
2
2
  import type { JsonObject } from "@nylorun/core/define";
3
- import { type AgentManifest } from "@nylorun/core/define";
3
+ import { type AgentManifest, type ResolvedMcpToolSettings } from "@nylorun/core/define";
4
4
  import type { DurableSessionTool, HostEffect } from "@nylorun/harness/run";
5
5
  export interface McpToolRecord {
6
6
  /** The agent used as a tool that declares the server; absent for the session's root agent. */
@@ -14,14 +14,6 @@ export interface McpToolRecord {
14
14
  readonly description?: string;
15
15
  readonly inputSchema: JsonObject;
16
16
  readonly outputSchema?: JsonObject;
17
- /**
18
- * The server's hints that calling the tool again is safe (R2b C7, Q15): a call whose answer
19
- * was lost after it was sent is then `mcp.lost` to the model, not `uncertain`.
20
- */
21
- readonly annotations?: {
22
- readonly readOnlyHint?: boolean;
23
- readonly idempotentHint?: boolean;
24
- };
25
17
  /**
26
18
  * Out of the model's tool list for the session's life (R2b C10): core decides it once, when it
27
19
  * records the snapshot (`pinDeferral`), so every model call of the session lists the same tools.
@@ -78,12 +70,14 @@ export declare function normalizeToolName(serverName: string, serverToolName: st
78
70
  readonly suffixed?: boolean;
79
71
  }): string;
80
72
  export declare function declaredToolNames(manifest: AgentManifest): Set<string>;
73
+ /** The tool's settings in `manifest` (R2b C9); the defaults when its server is not there. */
74
+ export declare function mcpToolSettingsIn(manifest: AgentManifest, tool: McpToolRecord): ResolvedMcpToolSettings;
81
75
  /**
82
76
  * The tools of a snapshot, as the engine gets them for a turn whose manifest is `manifest` (the
83
77
  * session's pin, or a turn variant that tightened its MCP servers' tools):
84
78
  *
85
- * - a tool its settings disable is left out (R2b C9); one whose approval resolves to `always`
86
- * waits for approval on each call;
79
+ * - a tool its settings disable is left out (R2b C9); the Tool Gate holds a call its approval
80
+ * or the Tenant's tool policy asks a person about (R5 step 2);
87
81
  * - a deferred tool (R2b C10) is marked so, out of the model's list, and its agent gets
88
82
  * `tool_search`, with the note on the servers it holds deferred tools of, and `tool_call` in
89
83
  * its `nylorun.tools` capability. An agent whose turn has no such capability keeps its tools in
@@ -58,7 +58,7 @@ function serverOf(manifest, tool) {
58
58
  return agent?.capabilities.find((item) => item.id === tool.capabilityId)?.mcpServers?.[tool.serverName];
59
59
  }
60
60
  /** The tool's settings in `manifest` (R2b C9); the defaults when its server is not there. */
61
- function settingsOf(manifest, tool) {
61
+ export function mcpToolSettingsIn(manifest, tool) {
62
62
  const server = serverOf(manifest, tool);
63
63
  return server ? mcpToolSettings(server, tool.serverToolName) : { enabled: true, approval: "never" };
64
64
  }
@@ -71,8 +71,8 @@ function reachesDeferred(manifest, agentId) {
71
71
  * The tools of a snapshot, as the engine gets them for a turn whose manifest is `manifest` (the
72
72
  * session's pin, or a turn variant that tightened its MCP servers' tools):
73
73
  *
74
- * - a tool its settings disable is left out (R2b C9); one whose approval resolves to `always`
75
- * waits for approval on each call;
74
+ * - a tool its settings disable is left out (R2b C9); the Tool Gate holds a call its approval
75
+ * or the Tenant's tool policy asks a person about (R5 step 2);
76
76
  * - a deferred tool (R2b C10) is marked so, out of the model's list, and its agent gets
77
77
  * `tool_search`, with the note on the servers it holds deferred tools of, and `tool_call` in
78
78
  * its `nylorun.tools` capability. An agent whose turn has no such capability keeps its tools in
@@ -87,7 +87,7 @@ export function sessionToolsOf(snapshot, manifest) {
87
87
  const deferred = new Map();
88
88
  const notes = new Map();
89
89
  for (const tool of snapshot.mcpTools) {
90
- const settings = settingsOf(manifest, tool);
90
+ const settings = mcpToolSettingsIn(manifest, tool);
91
91
  if (!settings.enabled)
92
92
  continue;
93
93
  const defer = tool.deferred === true && reachesDeferred(manifest, tool.agentId);
@@ -98,7 +98,6 @@ export function sessionToolsOf(snapshot, manifest) {
98
98
  ...(tool.description === undefined ? {} : { description: tool.description }),
99
99
  inputSchema: tool.inputSchema,
100
100
  ...(tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema }),
101
- ...(settings.approval === "always" ? { approval: "always" } : {}),
102
101
  ...(defer ? { deferred: true } : {}),
103
102
  };
104
103
  if (!defer) {
@@ -145,7 +144,7 @@ export const CHARS_PER_TOKEN = 4;
145
144
  export function pinDeferral(snapshot, manifest, contextWindow) {
146
145
  const limit = contextWindow * CHARS_PER_TOKEN * DEFER_CONTEXT_SHARE;
147
146
  const listed = new Map();
148
- const set = snapshot.mcpTools.map((tool) => settingsOf(manifest, tool).deferred);
147
+ const set = snapshot.mcpTools.map((tool) => mcpToolSettingsIn(manifest, tool).deferred);
149
148
  snapshot.mcpTools.forEach((tool, i) => {
150
149
  if (set[i] === true)
151
150
  return;
@@ -175,7 +174,7 @@ export function mcpToolOf(snapshot, request, manifest) {
175
174
  }
176
175
  /** True when the tool's settings in `manifest` leave it enabled (R2b C9). */
177
176
  export function isEnabledIn(manifest, tool) {
178
- return settingsOf(manifest, tool).enabled;
177
+ return mcpToolSettingsIn(manifest, tool).enabled;
179
178
  }
180
179
  /** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
181
180
  export function manifestFor(manifest, agent) {
@@ -38,6 +38,9 @@ export function piModel(options = {}) {
38
38
  };
39
39
  return async (call, context) => {
40
40
  context.signal.throwIfAborted();
41
+ // The first stream event's time, from here (R7.1): the meter keeps it on the usage row.
42
+ const calledAt = Date.now();
43
+ let firstEventAt;
41
44
  const root = options.root ?? "";
42
45
  const stored = await options.readHostModel?.();
43
46
  if (!stored)
@@ -100,6 +103,8 @@ export function piModel(options = {}) {
100
103
  timer = setTimeout(() => idle.abort(), settings.idleTimeoutMs);
101
104
  };
102
105
  arm();
106
+ // Only the answer's own attempt counts: a retried attempt's first event is not its.
107
+ firstEventAt = undefined;
103
108
  try {
104
109
  const stream = registry.streamSimple(selected, request, {
105
110
  ...invocationOptions,
@@ -107,6 +112,7 @@ export function piModel(options = {}) {
107
112
  });
108
113
  for await (const event of stream) {
109
114
  arm();
115
+ firstEventAt ??= Date.now();
110
116
  if (event.type === "text_delta" && options.onPreview) {
111
117
  try {
112
118
  void Promise.resolve(options.onPreview({
@@ -218,6 +224,8 @@ export function piModel(options = {}) {
218
224
  // The engine keeps the next prompt inside this window (Model Calls §7).
219
225
  contextWindow: selected.contextWindow,
220
226
  maxOutputTokens: selected.maxTokens,
227
+ // From the call to the provider's first stream event (R7.1); absent when none came.
228
+ ...(firstEventAt !== undefined ? { firstTokenMs: firstEventAt - calledAt } : {}),
221
229
  },
222
230
  },
223
231
  };