@nylorun/runtime 0.14.0-beta → 0.15.1-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 (198) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +82 -40
  3. package/dist/adapters/execution/restate.js +2 -2
  4. package/dist/adapters/replication/pgoutput.d.ts +5 -0
  5. package/dist/adapters/replication/pgoutput.js +4 -3
  6. package/dist/admin-openapi.json +185 -686
  7. package/dist/api/components.d.ts +308 -68
  8. package/dist/api/components.js +4 -5
  9. package/dist/api/gate/keys.d.ts +14 -0
  10. package/dist/api/gate/keys.js +59 -0
  11. package/dist/api/gate/routes.d.ts +34 -0
  12. package/dist/api/gate/routes.js +235 -0
  13. package/dist/api/http/define.js +0 -8
  14. package/dist/api/http/routes/access.js +25 -18
  15. package/dist/api/http/routes/tenant.d.ts +2 -1
  16. package/dist/api/http/routes/tenant.js +77 -5
  17. package/dist/api/http/routes/vaults.js +2 -2
  18. package/dist/api/openapi.js +6 -7
  19. package/dist/gates/contract.d.ts +51 -0
  20. package/dist/gates/contract.js +37 -0
  21. package/dist/gates/handler.d.ts +21 -0
  22. package/dist/gates/handler.js +55 -0
  23. package/dist/gates/http-client.d.ts +12 -0
  24. package/dist/gates/http-client.js +160 -0
  25. package/dist/gates/in-process.d.ts +32 -0
  26. package/dist/gates/in-process.js +42 -0
  27. package/dist/gates/inflight.d.ts +39 -0
  28. package/dist/gates/inflight.js +70 -0
  29. package/dist/gates/mcp-handler.d.ts +35 -0
  30. package/dist/gates/mcp-handler.js +142 -0
  31. package/dist/gates/meter.d.ts +24 -0
  32. package/dist/gates/meter.js +165 -0
  33. package/dist/gates/model-gate.d.ts +45 -0
  34. package/dist/gates/model-gate.js +1 -0
  35. package/dist/gates/tenant-vaults.d.ts +64 -0
  36. package/dist/gates/tenant-vaults.js +94 -0
  37. package/dist/gates/tool-calls.d.ts +51 -0
  38. package/dist/gates/tool-calls.js +64 -0
  39. package/dist/gates/tool-client.d.ts +12 -0
  40. package/dist/gates/tool-client.js +201 -0
  41. package/dist/gates/tool-contract.d.ts +116 -0
  42. package/dist/gates/tool-contract.js +64 -0
  43. package/dist/gates/tool-gate.d.ts +45 -0
  44. package/dist/gates/tool-gate.js +10 -0
  45. package/dist/host/admin-api.d.ts +7 -6
  46. package/dist/host/admin-api.js +9 -113
  47. package/dist/host/app.d.ts +1 -1
  48. package/dist/host/app.js +20 -24
  49. package/dist/host/config-for.d.ts +1 -1
  50. package/dist/host/config-for.js +2 -2
  51. package/dist/host/create-host.d.ts +5 -5
  52. package/dist/host/create-host.js +5 -15
  53. package/dist/host/execution.d.ts +20 -18
  54. package/dist/host/execution.js +2 -2
  55. package/dist/host/gates.d.ts +58 -0
  56. package/dist/host/gates.js +127 -0
  57. package/dist/host/http.d.ts +6 -1
  58. package/dist/host/http.js +17 -0
  59. package/dist/host/index.d.ts +1 -1
  60. package/dist/host/main.js +151 -71
  61. package/dist/host/stack-config.d.ts +71 -19
  62. package/dist/host/stack-config.js +192 -18
  63. package/dist/infra/database.d.ts +2 -2
  64. package/dist/infra/execution.d.ts +5 -5
  65. package/dist/infra/execution.js +8 -6
  66. package/dist/infra/index.d.ts +1 -1
  67. package/dist/infra/workers.d.ts +17 -17
  68. package/dist/infra/workers.js +9 -9
  69. package/dist/keys/client.d.ts +9 -0
  70. package/dist/keys/client.js +70 -0
  71. package/dist/keys/contract.d.ts +24 -0
  72. package/dist/keys/contract.js +12 -0
  73. package/dist/keys/keys.d.ts +41 -0
  74. package/dist/keys/keys.js +50 -0
  75. package/dist/mcp/connect.d.ts +43 -3
  76. package/dist/mcp/connect.js +12 -5
  77. package/dist/mcp/pool.d.ts +21 -3
  78. package/dist/mcp/pool.js +15 -2
  79. package/dist/openapi.json +888 -695
  80. package/dist/record/append.d.ts +51 -0
  81. package/dist/record/append.js +20 -0
  82. package/dist/{store/event.js → record/envelope.js} +1 -1
  83. package/dist/record/index.d.ts +11 -0
  84. package/dist/record/index.js +11 -0
  85. package/dist/store/postgres/connect.d.ts +15 -12
  86. package/dist/store/postgres/connect.js +24 -12
  87. package/dist/store/postgres/db.d.ts +16 -0
  88. package/dist/store/postgres/db.js +28 -0
  89. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  90. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  91. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  92. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  93. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  94. package/dist/store/postgres/migrate.d.ts +68 -0
  95. package/dist/store/postgres/migrate.js +183 -0
  96. package/dist/store/postgres/record-writer.d.ts +4 -0
  97. package/dist/store/postgres/record-writer.js +34 -0
  98. package/dist/store/postgres/record.d.ts +2 -8
  99. package/dist/store/postgres/record.js +50 -54
  100. package/dist/store/postgres/schema.d.ts +3096 -0
  101. package/dist/store/postgres/schema.js +363 -0
  102. package/dist/store/postgres/store.d.ts +2 -4
  103. package/dist/store/postgres/store.js +481 -567
  104. package/dist/store/postgres/tenant.d.ts +56 -0
  105. package/dist/store/postgres/tenant.js +154 -0
  106. package/dist/store/types.d.ts +43 -87
  107. package/dist/store/types.js +0 -1
  108. package/dist/streams/control.d.ts +1 -1
  109. package/dist/streams/control.js +6 -2
  110. package/dist/streams/relay/types.d.ts +2 -1
  111. package/dist/streams/relay/types.js +2 -1
  112. package/dist/streams/types.d.ts +5 -0
  113. package/dist/tenant/advance.d.ts +7 -2
  114. package/dist/tenant/advance.js +95 -16
  115. package/dist/tenant/cause.d.ts +25 -0
  116. package/dist/tenant/cause.js +45 -0
  117. package/dist/tenant/commands.js +4 -2
  118. package/dist/tenant/context.d.ts +26 -3
  119. package/dist/tenant/delivery-token.js +17 -18
  120. package/dist/tenant/delivery.d.ts +1 -1
  121. package/dist/tenant/delivery.js +18 -13
  122. package/dist/tenant/effects.d.ts +15 -1
  123. package/dist/tenant/effects.js +93 -17
  124. package/dist/tenant/ephemeral.d.ts +16 -7
  125. package/dist/tenant/ephemeral.js +45 -111
  126. package/dist/tenant/history.d.ts +64 -0
  127. package/dist/tenant/history.js +120 -0
  128. package/dist/tenant/model-setting.d.ts +2 -2
  129. package/dist/tenant/module.d.ts +14 -28
  130. package/dist/tenant/module.js +131 -271
  131. package/dist/tenant/paths.d.ts +11 -16
  132. package/dist/tenant/paths.js +24 -68
  133. package/dist/tenant/pool.d.ts +1 -1
  134. package/dist/tenant/pool.js +1 -1
  135. package/dist/tenant/principals.d.ts +23 -11
  136. package/dist/tenant/principals.js +61 -49
  137. package/dist/tenant/runtime.d.ts +19 -1
  138. package/dist/tenant/runtime.js +40 -26
  139. package/dist/tenant/scheduler.d.ts +4 -2
  140. package/dist/tenant/scheduler.js +4 -4
  141. package/dist/tenant/session-streams.d.ts +2 -0
  142. package/dist/tenant/session-streams.js +8 -3
  143. package/dist/tenant/status.d.ts +3 -0
  144. package/dist/tenant/status.js +1 -1
  145. package/dist/tenant/store-pg.d.ts +9 -6
  146. package/dist/tenant/store-pg.js +60 -125
  147. package/dist/tenant/streams.d.ts +9 -25
  148. package/dist/tenant/streams.js +7 -22
  149. package/dist/tenant/tokens.js +16 -14
  150. package/dist/tenant/types.d.ts +41 -92
  151. package/dist/tenant/types.js +4 -15
  152. package/dist/tenant/worker.js +4 -4
  153. package/dist/vault/host-model.d.ts +31 -0
  154. package/dist/vault/host-model.js +57 -0
  155. package/dist/vault/service.d.ts +26 -15
  156. package/dist/vault/service.js +70 -109
  157. package/dist/version.d.ts +1 -1
  158. package/dist/version.js +1 -1
  159. package/package.json +9 -5
  160. package/dist/store/memory.d.ts +0 -69
  161. package/dist/store/memory.js +0 -810
  162. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  163. package/dist/store/postgres/migrations/001_initial.js +0 -196
  164. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  165. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  166. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
  167. package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
  168. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
  169. package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
  170. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
  171. package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
  172. package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
  173. package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
  174. package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
  175. package/dist/store/postgres/migrations/007_event_time.js +0 -14
  176. package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
  177. package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
  178. package/dist/store/postgres/migrations/index.d.ts +0 -53
  179. package/dist/store/postgres/migrations/index.js +0 -96
  180. package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
  181. package/dist/store/postgres/migrations/shared/index.js +0 -92
  182. package/dist/store/postgres/names.d.ts +0 -11
  183. package/dist/store/postgres/names.js +0 -42
  184. package/dist/store/postgres/tenants.d.ts +0 -90
  185. package/dist/store/postgres/tenants.js +0 -228
  186. package/dist/streams/relay/memory.d.ts +0 -69
  187. package/dist/streams/relay/memory.js +0 -176
  188. package/dist/tenant/envelope.d.ts +0 -7
  189. package/dist/tenant/envelope.js +0 -22
  190. package/dist/tenant/quarantine-error.d.ts +0 -6
  191. package/dist/tenant/quarantine-error.js +0 -9
  192. package/dist/tenant/quarantine.d.ts +0 -27
  193. package/dist/tenant/quarantine.js +0 -72
  194. package/dist/tenant/store-memory.d.ts +0 -16
  195. package/dist/tenant/store-memory.js +0 -111
  196. /package/dist/{store → record}/cursor.d.ts +0 -0
  197. /package/dist/{store → record}/cursor.js +0 -0
  198. /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The loop's client of the Tool Gate in the gates service (F4.1, `tool-contract.ts`): remote
3
+ * MCP servers opened and called through the gate, which holds their connections and
4
+ * credentials, and Action deliveries POSTed by the gate. Over `node:http`, like the model gate's
5
+ * client (`http-client.ts`): a call answers only when it has finished.
6
+ *
7
+ * Failures keep the meaning they had in the loop's own process:
8
+ * - An MCP request that fails, at the server or on the hop, throws, so the pool reports a
9
+ * diagnostic and `resolveEffect` marks a call `uncertain`, as before.
10
+ * - A delivery never throws. A hop that failed before the request was sent is `not_sent`
11
+ * (the deliverer sends it again, even a tool); one that failed after is `lost`.
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { request as httpRequest } from "node:http";
15
+ import { request as httpsRequest } from "node:https";
16
+ import { TENANT_HEADER } from "./contract.js";
17
+ import { GATE_CLIENT_TIMEOUT_MS } from "./http-client.js";
18
+ import { DELIVERIES_PATH, MCP_CLOSE_PATH, MCP_CONNECT_PATH, MCP_LIST_PATH, TOOL_CALLS_PATH, } from "./tool-contract.js";
19
+ /** How long a close or a cancel may take; neither changes an outcome. */
20
+ const SHORT_TIMEOUT_MS = 2_000;
21
+ /** Extra silence allowed on a delivery beyond the endpoint's own timeout. */
22
+ const DELIVERY_SLACK_MS = 30_000;
23
+ export function httpToolGate(options) {
24
+ const where = new URL(options.url).origin;
25
+ const timeoutMs = options.timeoutMs ?? GATE_CLIENT_TIMEOUT_MS;
26
+ function exchange(path, body, request) {
27
+ const url = new URL(path, options.url);
28
+ const send = url.protocol === "https:" ? httpsRequest : httpRequest;
29
+ const payload = body === undefined ? Buffer.alloc(0) : Buffer.from(JSON.stringify(body), "utf8");
30
+ const signal = request.signal;
31
+ return new Promise((resolve) => {
32
+ if (signal?.aborted)
33
+ return resolve({ kind: "aborted", sent: false, reason: signal.reason });
34
+ let settled = false;
35
+ let sent = false;
36
+ let responded = false;
37
+ let timedOut = false;
38
+ const settle = (result) => {
39
+ if (settled)
40
+ return;
41
+ settled = true;
42
+ signal?.removeEventListener("abort", onAbort);
43
+ resolve(result);
44
+ };
45
+ const onAbort = () => {
46
+ settle({ kind: "aborted", sent, reason: signal.reason });
47
+ outgoing.destroy();
48
+ };
49
+ const failed = (error) => settle(sent
50
+ ? {
51
+ kind: "lost",
52
+ message: timedOut
53
+ ? `Tool gate at ${where} sent nothing for ${Math.round(request.timeoutMs / 1000)} s`
54
+ : `Tool gate connection lost${error ? ` (${error.message})` : ""}`,
55
+ }
56
+ : { kind: "unreachable", message: `Tool gate unreachable at ${where}${error ? ` (${error.message})` : ""}` });
57
+ const outgoing = send(url, {
58
+ method: "POST",
59
+ headers: {
60
+ authorization: `Bearer ${options.token}`,
61
+ "content-type": "application/json",
62
+ "content-length": payload.byteLength,
63
+ ...(options.tenantId ? { [TENANT_HEADER]: options.tenantId } : {}),
64
+ ...(request.idempotencyKey ? { "idempotency-key": request.idempotencyKey } : {}),
65
+ },
66
+ timeout: request.timeoutMs,
67
+ }, (response) => {
68
+ responded = true;
69
+ readBody(response).then((text) => settle({ kind: "answer", status: response.statusCode ?? 0, text }), (error) => failed(error));
70
+ });
71
+ outgoing.on("socket", (socket) => socket.setKeepAlive(true, 30_000));
72
+ outgoing.on("finish", () => {
73
+ sent = true;
74
+ });
75
+ outgoing.on("timeout", () => {
76
+ timedOut = true;
77
+ outgoing.destroy();
78
+ });
79
+ outgoing.on("error", (error) => {
80
+ if (!signal?.aborted)
81
+ failed(error);
82
+ });
83
+ outgoing.on("close", () => {
84
+ if (!signal?.aborted && !responded)
85
+ failed();
86
+ });
87
+ signal?.addEventListener("abort", onAbort, { once: true });
88
+ outgoing.end(payload);
89
+ });
90
+ }
91
+ /** The result of an MCP request, or a throw the pool and `resolveEffect` read as before. */
92
+ async function mcp(path, body, request) {
93
+ const result = await exchange(path, body, { ...request, timeoutMs });
94
+ if (result.kind === "aborted")
95
+ throw result.reason ?? new Error("aborted");
96
+ if (result.kind !== "answer")
97
+ throw new Error(result.message);
98
+ const parsed = parse(result.text);
99
+ if (result.status !== 200 || !parsed || !("ok" in parsed))
100
+ throw new Error(gateRefusal(result.status, parsed));
101
+ if (parsed.ok)
102
+ return parsed.result;
103
+ throw mcpError(parsed.error);
104
+ }
105
+ async function fireAndForget(path, body) {
106
+ await exchange(path, body, { timeoutMs: SHORT_TIMEOUT_MS });
107
+ }
108
+ return {
109
+ recovers: true,
110
+ async openMcp(server) {
111
+ await mcp(MCP_CONNECT_PATH, { server }, {});
112
+ const client = {
113
+ listTools: (params, request) => mcp(MCP_LIST_PATH, { server, ...(params?.cursor ? { cursor: params.cursor } : {}) }, request?.signal ? { signal: request.signal } : {}),
114
+ callTool: (params, request) => {
115
+ const body = {
116
+ server: { ...server },
117
+ effectId: request?.key ?? randomUUID(),
118
+ name: params.name,
119
+ arguments: params.arguments,
120
+ };
121
+ return mcp(TOOL_CALLS_PATH, body, {
122
+ ...(request?.signal ? { signal: request.signal } : {}),
123
+ ...(request?.key ? { idempotencyKey: request.key } : {}),
124
+ });
125
+ },
126
+ };
127
+ return { client, close: () => fireAndForget(MCP_CLOSE_PATH, { server }) };
128
+ },
129
+ async cancel(request) {
130
+ await fireAndForget(`${TOOL_CALLS_PATH}/${encodeURIComponent(request.effectId)}/cancel`, undefined);
131
+ },
132
+ async post(delivery, signal) {
133
+ const result = await exchange(DELIVERIES_PATH, {
134
+ url: delivery.url,
135
+ body: delivery.body,
136
+ headers: { ...delivery.headers },
137
+ timeoutMs: delivery.timeoutMs,
138
+ }, { signal, timeoutMs: delivery.timeoutMs + DELIVERY_SLACK_MS });
139
+ if (result.kind === "aborted")
140
+ return {
141
+ kind: result.sent ? "lost" : "not_sent",
142
+ code: "ABORTED",
143
+ message: "The delivery was aborted",
144
+ };
145
+ if (result.kind === "unreachable")
146
+ return { kind: "not_sent", code: "gateway.unreachable", message: result.message };
147
+ if (result.kind === "lost")
148
+ return { kind: "lost", code: "gateway.lost", message: result.message };
149
+ const parsed = parse(result.text);
150
+ if (result.status !== 200 || !parsed?.result)
151
+ // The gate refused the request before sending anything.
152
+ return { kind: "not_sent", code: "gateway.refused", message: gateRefusal(result.status, parsed) };
153
+ const wire = parsed.result;
154
+ if (wire.kind !== "response")
155
+ return wire;
156
+ return {
157
+ kind: "response",
158
+ status: wire.status,
159
+ headers: wire.headers,
160
+ body: Buffer.from(wire.body, "base64"),
161
+ };
162
+ },
163
+ };
164
+ }
165
+ /** An MCP failure the gate reported, as the loop's own MCP code would have thrown it. */
166
+ function mcpError(error) {
167
+ const thrown = new Error(error.uncertain ? `${error.message} (the call may have run; it is not sent again)` : error.message);
168
+ if (error.code !== undefined)
169
+ thrown.code = error.code;
170
+ if (error.credentialIds !== undefined)
171
+ thrown.credentialIds = error.credentialIds;
172
+ return thrown;
173
+ }
174
+ function gateRefusal(status, parsed) {
175
+ const message = parsed?.error?.message;
176
+ if (status === 401)
177
+ return "The tool gate refused this Runtime's token: the runtime and gateway containers must share NYLORUN_GATES_TOKEN";
178
+ return `Tool gate answered ${status}${typeof message === "string" ? `: ${message}` : ""}`;
179
+ }
180
+ function parse(text) {
181
+ try {
182
+ return JSON.parse(text);
183
+ }
184
+ catch {
185
+ return undefined;
186
+ }
187
+ }
188
+ function readBody(response) {
189
+ return new Promise((resolve, reject) => {
190
+ const chunks = [];
191
+ response.on("data", (chunk) => chunks.push(chunk));
192
+ response.on("end", () => {
193
+ if (response.complete)
194
+ resolve(Buffer.concat(chunks).toString("utf8"));
195
+ else
196
+ reject(new Error("response ended early"));
197
+ });
198
+ response.on("error", reject);
199
+ response.on("aborted", () => reject(new Error("response aborted")));
200
+ });
201
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The Tool Gate's wire format (blueprint §12, F4.1): the routes the loop calls on the gates
3
+ * service for remote MCP servers and Action deliveries. Internal, like the model call route
4
+ * (`contract.ts`): same token, same optional Tenant header, not in the published OpenAPI
5
+ * documents.
6
+ *
7
+ * Remote MCP. The gate holds the connection and the vault credential; the loop names the
8
+ * server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
9
+ * - `POST /nylorun/v1/mcp/connect` `{server}`: opens the server's connection (vault
10
+ * authorization included), or keeps the open one.
11
+ * - `POST /nylorun/v1/mcp/list` `{server, cursor?}`: one page of `tools/list`.
12
+ * - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
13
+ * `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
14
+ * once (G3); a re-send joins it or gets its outcome.
15
+ * - `POST /nylorun/v1/tool-calls/{key}/cancel`: aborts a keyed call; `204` either way.
16
+ * - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
17
+ * Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
18
+ * answer, not a gate error. A keyed call whose earlier attempt was lost with the gateway answers
19
+ * `{ok: false, error: {uncertain: true}}` and is never run again.
20
+ *
21
+ * Action deliveries. `POST /nylorun/v1/deliveries` `{url, body, headers, timeoutMs}`: the gate
22
+ * POSTs the signed delivery under the gateway's own address policy and answers `200 {result}`,
23
+ * what `tenant/outbound.ts` `post` returned, with the answer's body in base64.
24
+ */
25
+ import { z } from "zod";
26
+ export declare const MCP_CONNECT_PATH = "/nylorun/v1/mcp/connect";
27
+ export declare const MCP_LIST_PATH = "/nylorun/v1/mcp/list";
28
+ export declare const MCP_CLOSE_PATH = "/nylorun/v1/mcp/close";
29
+ export declare const TOOL_CALLS_PATH = "/nylorun/v1/tool-calls";
30
+ export declare const DELIVERIES_PATH = "/nylorun/v1/deliveries";
31
+ /** Largest tool call or delivery body the gate reads. */
32
+ export declare const MAX_TOOL_BODY_BYTES: number;
33
+ /** The longest delivery timeout the gate accepts: an endpoint's maximum `timeoutMs`. */
34
+ export declare const MAX_DELIVERY_TIMEOUT_MS: number;
35
+ /** The only headers the gate forwards on a delivery, lower-cased. */
36
+ export declare const DELIVERY_HEADERS: ReadonlySet<string>;
37
+ export declare const McpServerRefSchema: z.ZodObject<{
38
+ sessionId: z.ZodString;
39
+ agentId: z.ZodOptional<z.ZodString>;
40
+ capabilityId: z.ZodString;
41
+ serverName: z.ZodString;
42
+ }, z.core.$strip>;
43
+ export declare const McpServerBodySchema: z.ZodObject<{
44
+ server: z.ZodObject<{
45
+ sessionId: z.ZodString;
46
+ agentId: z.ZodOptional<z.ZodString>;
47
+ capabilityId: z.ZodString;
48
+ serverName: z.ZodString;
49
+ }, z.core.$strip>;
50
+ }, z.core.$strip>;
51
+ export declare const McpListBodySchema: z.ZodObject<{
52
+ server: z.ZodObject<{
53
+ sessionId: z.ZodString;
54
+ agentId: z.ZodOptional<z.ZodString>;
55
+ capabilityId: z.ZodString;
56
+ serverName: z.ZodString;
57
+ }, z.core.$strip>;
58
+ cursor: z.ZodOptional<z.ZodString>;
59
+ }, z.core.$strip>;
60
+ export declare const ToolCallBodySchema: z.ZodObject<{
61
+ server: z.ZodObject<{
62
+ sessionId: z.ZodString;
63
+ agentId: z.ZodOptional<z.ZodString>;
64
+ capabilityId: z.ZodString;
65
+ serverName: z.ZodString;
66
+ }, z.core.$strip>;
67
+ effectId: z.ZodString;
68
+ name: z.ZodString;
69
+ arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
70
+ }, z.core.$strip>;
71
+ export type ToolCallBody = z.infer<typeof ToolCallBodySchema>;
72
+ /** Why an MCP request failed, as the loop's diagnostics read it (`mcp/connect.ts`). */
73
+ export interface McpGateError {
74
+ readonly message: string;
75
+ /** The MCP SDK's error code, an HTTP status for transport errors (401 for a refused token). */
76
+ readonly code?: number;
77
+ /** The vault credentials a refused authorization names. */
78
+ readonly credentialIds?: readonly string[];
79
+ /** The call may have run: its earlier attempt was lost with the gateway. Never re-run. */
80
+ readonly uncertain?: boolean;
81
+ }
82
+ export type McpAnswer<T> = {
83
+ readonly ok: true;
84
+ readonly result: T;
85
+ } | {
86
+ readonly ok: false;
87
+ readonly error: McpGateError;
88
+ };
89
+ export declare const DeliveryBodySchema: z.ZodObject<{
90
+ url: z.ZodString;
91
+ body: z.ZodString;
92
+ headers: z.ZodRecord<z.ZodString, z.ZodString>;
93
+ timeoutMs: z.ZodNumber;
94
+ }, z.core.$strip>;
95
+ export type DeliveryBody = z.infer<typeof DeliveryBodySchema>;
96
+ /** `OutboundResult` on the wire: the answer's body in base64. */
97
+ export type WireOutboundResult = {
98
+ readonly kind: "response";
99
+ readonly status: number;
100
+ readonly headers: Record<string, string | string[] | undefined>;
101
+ readonly body: string;
102
+ } | {
103
+ readonly kind: "too_large";
104
+ readonly status: number;
105
+ } | {
106
+ readonly kind: "not_sent";
107
+ readonly code: string;
108
+ readonly message: string;
109
+ } | {
110
+ readonly kind: "lost";
111
+ readonly code: string;
112
+ readonly message: string;
113
+ };
114
+ export interface DeliveryAnswer {
115
+ readonly result: WireOutboundResult;
116
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The Tool Gate's wire format (blueprint §12, F4.1): the routes the loop calls on the gates
3
+ * service for remote MCP servers and Action deliveries. Internal, like the model call route
4
+ * (`contract.ts`): same token, same optional Tenant header, not in the published OpenAPI
5
+ * documents.
6
+ *
7
+ * Remote MCP. The gate holds the connection and the vault credential; the loop names the
8
+ * server, never a URL or a credential, and the gate finds it in the session's pinned manifest.
9
+ * - `POST /nylorun/v1/mcp/connect` `{server}`: opens the server's connection (vault
10
+ * authorization included), or keeps the open one.
11
+ * - `POST /nylorun/v1/mcp/list` `{server, cursor?}`: one page of `tools/list`.
12
+ * - `POST /nylorun/v1/tool-calls` `{server, effectId, name, arguments}`: one
13
+ * `tools/call`. With an `Idempotency-Key` (the effect id) the call outlives its client and runs
14
+ * once (G3); a re-send joins it or gets its outcome.
15
+ * - `POST /nylorun/v1/tool-calls/{key}/cancel`: aborts a keyed call; `204` either way.
16
+ * - `POST /nylorun/v1/mcp/close` `{server}`: closes the server's connection; `204`.
17
+ * Each answers `200 {ok: true, result}` or `200 {ok: false, error}`: an MCP failure is an
18
+ * answer, not a gate error. A keyed call whose earlier attempt was lost with the gateway answers
19
+ * `{ok: false, error: {uncertain: true}}` and is never run again.
20
+ *
21
+ * Action deliveries. `POST /nylorun/v1/deliveries` `{url, body, headers, timeoutMs}`: the gate
22
+ * POSTs the signed delivery under the gateway's own address policy and answers `200 {result}`,
23
+ * what `tenant/outbound.ts` `post` returned, with the answer's body in base64.
24
+ */
25
+ import { z } from "zod";
26
+ import { PROTOCOL_HEADER, SIGNATURE_HEADER } from "@nylorun/core/compatibility";
27
+ export const MCP_CONNECT_PATH = "/nylorun/v1/mcp/connect";
28
+ export const MCP_LIST_PATH = "/nylorun/v1/mcp/list";
29
+ export const MCP_CLOSE_PATH = "/nylorun/v1/mcp/close";
30
+ export const TOOL_CALLS_PATH = "/nylorun/v1/tool-calls";
31
+ export const DELIVERIES_PATH = "/nylorun/v1/deliveries";
32
+ /** Largest tool call or delivery body the gate reads. */
33
+ export const MAX_TOOL_BODY_BYTES = 8 * 1024 * 1024;
34
+ /** The longest delivery timeout the gate accepts: an endpoint's maximum `timeoutMs`. */
35
+ export const MAX_DELIVERY_TIMEOUT_MS = 15 * 60_000;
36
+ /** The only headers the gate forwards on a delivery, lower-cased. */
37
+ export const DELIVERY_HEADERS = new Set([
38
+ SIGNATURE_HEADER.toLowerCase(),
39
+ PROTOCOL_HEADER.toLowerCase(),
40
+ "idempotency-key",
41
+ ]);
42
+ export const McpServerRefSchema = z.object({
43
+ sessionId: z.string().min(1),
44
+ agentId: z.string().min(1).optional(),
45
+ capabilityId: z.string().min(1),
46
+ serverName: z.string().min(1),
47
+ });
48
+ export const McpServerBodySchema = z.object({ server: McpServerRefSchema });
49
+ export const McpListBodySchema = z.object({
50
+ server: McpServerRefSchema,
51
+ cursor: z.string().optional(),
52
+ });
53
+ export const ToolCallBodySchema = z.object({
54
+ server: McpServerRefSchema,
55
+ effectId: z.string().min(1),
56
+ name: z.string().min(1),
57
+ arguments: z.record(z.string(), z.unknown()),
58
+ });
59
+ export const DeliveryBodySchema = z.object({
60
+ url: z.string().url(),
61
+ body: z.string(),
62
+ headers: z.record(z.string(), z.string()),
63
+ timeoutMs: z.number().int().positive().max(MAX_DELIVERY_TIMEOUT_MS),
64
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The Tool Gate seam (blueprint §12, F4.1). Every tool call that leaves the loop crosses one
3
+ * `ToolGate`: a remote MCP server's connection and calls, and every Action delivery to an
4
+ * Action endpoint. The gate owns what those need that the loop must not hold: the MCP
5
+ * credential and its OAuth refresh, and the outbound route.
6
+ *
7
+ * Two implementations: `inProcessToolGate` (embedding, the ephemeral Runtime, tests), which
8
+ * opens remote servers and POSTs deliveries in this process, and the HTTP client of the gates
9
+ * service (`tool-client.ts`, the `gateway` container). Stdio MCP servers and the sandbox tools
10
+ * never cross it: they run beside the loop until the session sandbox takes them (P4).
11
+ */
12
+ import type { LiveConnection } from "../mcp/connect.js";
13
+ import type { McpServerRef } from "../mcp/pool.js";
14
+ import { type OutboundPolicy, type OutboundResult } from "../tenant/outbound.js";
15
+ /** One Action delivery or endpoint ping, signed by the caller. */
16
+ export interface DeliveryRequest {
17
+ readonly url: string;
18
+ readonly body: string;
19
+ /** `Nylorun-Signature`, `Nylorun-Protocol` and `Idempotency-Key`; nothing else. */
20
+ readonly headers: Readonly<Record<string, string>>;
21
+ /** How long the endpoint may take to answer. */
22
+ readonly timeoutMs: number;
23
+ }
24
+ export interface ToolGate {
25
+ /**
26
+ * Opens a remote (`streamable-http` or `sse`) MCP server of a session. Absent: the loop's
27
+ * pool opens it itself, with the Tenant vault.
28
+ */
29
+ openMcp?(server: McpServerRef): Promise<LiveConnection>;
30
+ /** POSTs a delivery. Never throws; `signal` aborts it (see `tenant/outbound.ts`). */
31
+ post(request: DeliveryRequest, signal: AbortSignal): Promise<OutboundResult>;
32
+ /**
33
+ * True when a keyed MCP call outlives the caller's process (the gates service, G3): after a
34
+ * takeover or a shutdown, re-sending it joins the call or returns its outcome, and a call
35
+ * lost with the gateway answers `uncertain` instead of running again.
36
+ */
37
+ readonly recovers?: boolean;
38
+ /** Stops a keyed MCP call that outlives its caller (a user cancel). Never rejects. */
39
+ cancel?(request: {
40
+ tenantId: string;
41
+ effectId: string;
42
+ }): Promise<void>;
43
+ }
44
+ /** The Tool Gate in the caller's process: remote MCP through the pool, deliveries by `post`. */
45
+ export declare function inProcessToolGate(policy?: OutboundPolicy): ToolGate;
@@ -0,0 +1,10 @@
1
+ import { post } from "../tenant/outbound.js";
2
+ /** The Tool Gate in the caller's process: remote MCP through the pool, deliveries by `post`. */
3
+ export function inProcessToolGate(policy = {}) {
4
+ return {
5
+ post: (request, signal) => post(request.url, request.body, { ...request.headers }, {
6
+ policy,
7
+ signal: AbortSignal.any([signal, AbortSignal.timeout(request.timeoutMs)]),
8
+ }),
9
+ };
10
+ }
@@ -1,13 +1,14 @@
1
1
  /**
2
- * The Admin API (`/v1/admin/**`): Tenants and the Host's status, for a caller with the admin
3
- * key. The Host checks the listener, the protocol and the admin key first (`app.ts`); what
4
- * gets here is authorized. Routes are declared once, for serving and for the OpenAPI document.
2
+ * The Admin API (`/v1/admin/**`): the Host's status and shutdown, for a caller with the admin
3
+ * key. A Host serves one Tenant, so there are no Tenant routes: the status names the Tenant
4
+ * and, when it could not be opened, why (tenancy.md §9). The Host checks the listener, the
5
+ * protocol and the admin key first (`app.ts`); what gets here is authorized. Routes are
6
+ * declared once, for serving and for the OpenAPI document.
5
7
  */
6
8
  import { OpenAPIHono } from "@hono/zod-openapi";
7
- import { type AdminStatus } from "@nylorun/core/contracts";
8
- import { type NodeBindings, type TenantModule } from "../tenant/types.js";
9
+ import type { AdminStatus } from "@nylorun/core/contracts";
10
+ import type { NodeBindings } from "../tenant/types.js";
9
11
  export interface AdminApiOptions {
10
- module: TenantModule;
11
12
  /** `GET /v1/admin/status`. */
12
13
  status(): Promise<AdminStatus>;
13
14
  /** Stops the Host once the shutdown answer is sent. */
@@ -1,16 +1,15 @@
1
1
  /**
2
- * The Admin API (`/v1/admin/**`): Tenants and the Host's status, for a caller with the admin
3
- * key. The Host checks the listener, the protocol and the admin key first (`app.ts`); what
4
- * gets here is authorized. Routes are declared once, for serving and for the OpenAPI document.
2
+ * The Admin API (`/v1/admin/**`): the Host's status and shutdown, for a caller with the admin
3
+ * key. A Host serves one Tenant, so there are no Tenant routes: the status names the Tenant
4
+ * and, when it could not be opened, why (tenancy.md §9). The Host checks the listener, the
5
+ * protocol and the admin key first (`app.ts`); what gets here is authorized. Routes are
6
+ * declared once, for serving and for the OpenAPI document.
5
7
  */
6
8
  import { OpenAPIHono } from "@hono/zod-openapi";
7
9
  import { z } from "zod";
8
- import { CreateTenantRequestSchema } from "@nylorun/core/contracts";
9
- import { AdminStatus as AdminStatusBody, AdminTenantList, AdminTenantStatus, CreateTenantRequest, HostShutdownResponse, ProtocolRejected, Rejected, TenantEnvelope, } from "../api/components.js";
10
+ import { AdminStatus as AdminStatusBody, HostShutdownResponse, ProtocolRejected, Rejected, } from "../api/components.js";
10
11
  import { serveRoute } from "../api/route.js";
11
- import { TenantBusyError, TenantConflictError } from "../tenant/quarantine.js";
12
- import { TenantNotFoundError, } from "../tenant/types.js";
13
- import { jsonResponse, opaqueNotFoundResponse, readJsonBody, rejectedResponse, } from "./http.js";
12
+ import { jsonResponse, rejectedResponse } from "./http.js";
14
13
  const json = (schema, description) => ({
15
14
  description,
16
15
  content: { "application/json": { schema } },
@@ -18,12 +17,9 @@ const json = (schema, description) => ({
18
17
  const rejected = (description) => json(Rejected, description);
19
18
  /** What every Admin route may answer before its own logic runs. */
20
19
  const gated = {
21
- 404: rejected("Not found: an unknown admin key, the public listener, or an unknown Tenant"),
20
+ 404: rejected("Not found: an unknown admin key, or the public listener"),
22
21
  426: json(ProtocolRejected, "`Nylorun-Protocol` missing or unsupported"),
23
22
  };
24
- const tenantId = z.object({
25
- tenantId: z.string().meta({ example: "tn_0123456789abcdefghjkmnpqrs" }),
26
- });
27
23
  function adminRoute(route) {
28
24
  return {
29
25
  ...route,
@@ -33,7 +29,6 @@ function adminRoute(route) {
33
29
  };
34
30
  }
35
31
  export function createAdminApi(options) {
36
- const { module } = options;
37
32
  const api = new OpenAPIHono();
38
33
  // HEAD is not a GET here: an Admin route answers only the methods it declares.
39
34
  api.use(async (c, next) => {
@@ -41,112 +36,13 @@ export function createAdminApi(options) {
41
36
  return notFound();
42
37
  await next();
43
38
  });
44
- serveRoute(api, adminRoute({
45
- method: "get",
46
- path: "/v1/admin/tenants",
47
- summary: "List Tenants",
48
- responses: { 200: json(AdminTenantList, "Every Tenant on the Host") },
49
- }), async () => jsonResponse(200, await module.list()));
50
- serveRoute(api, adminRoute({
51
- method: "post",
52
- path: "/v1/admin/tenants",
53
- summary: "Create a Tenant",
54
- description: "Idempotent on `idempotencyKey`: a replay answers 200 with the same Tenant.",
55
- request: {
56
- body: {
57
- required: true,
58
- content: { "application/json": { schema: CreateTenantRequest } },
59
- },
60
- },
61
- responses: {
62
- 200: json(TenantEnvelope, "The Tenant, created earlier by the same request"),
63
- 201: json(TenantEnvelope, "The Tenant, created"),
64
- 400: rejected("Invalid JSON or request body (`invalid_request`)"),
65
- 409: rejected("Another Tenant has this id or idempotency key (`tenant_conflict`)"),
66
- 413: rejected("Request body over 1 MiB"),
67
- },
68
- }), async (c) => {
69
- const body = CreateTenantRequestSchema.parse(await readJsonBody(c.req.raw));
70
- try {
71
- const result = await module.create({
72
- tenantId: body.tenantId,
73
- name: body.name,
74
- principalId: body.principalId,
75
- credentialHash: body.credentialHash,
76
- idempotencyKey: body.idempotencyKey,
77
- ...(body.studioCredentialHash
78
- ? { studioCredentialHash: body.studioCredentialHash }
79
- : {}),
80
- ...(body.derivedPrincipals?.length
81
- ? { derivedPrincipals: body.derivedPrincipals }
82
- : {}),
83
- });
84
- return jsonResponse(result.created ? 201 : 200, result.envelope);
85
- }
86
- catch (error) {
87
- const code = error.code;
88
- if (error instanceof TenantConflictError ||
89
- error.name === "TenantConflictError" ||
90
- code === "conflict" ||
91
- code === "tenant_conflict")
92
- return rejectedResponse(409, "tenant_conflict", error instanceof Error ? error.message : "Tenant conflict");
93
- throw error;
94
- }
95
- });
96
- serveRoute(api, adminRoute({
97
- method: "get",
98
- path: "/v1/admin/tenants/{tenantId}",
99
- summary: "Get a Tenant's status",
100
- request: { params: tenantId },
101
- responses: { 200: json(AdminTenantStatus, "The Tenant and its state") },
102
- }), async (c) => {
103
- const status = await module.status(c.req.param("tenantId"));
104
- return status ? jsonResponse(200, status) : opaqueNotFoundResponse();
105
- });
106
- serveRoute(api, adminRoute({
107
- method: "delete",
108
- path: "/v1/admin/tenants/{tenantId}",
109
- summary: "Delete a Tenant",
110
- request: {
111
- params: tenantId,
112
- query: z.object({
113
- activeWork: z.enum(["refuse", "drain", "cancel"]).optional().meta({
114
- description: "What to do with turns in progress: refuse to delete (default), wait for them, or cancel them",
115
- }),
116
- }),
117
- },
118
- responses: {
119
- 204: { description: "Deleted" },
120
- 400: rejected("`activeWork` is not refuse, drain or cancel (`invalid_request`)"),
121
- 409: rejected("The Tenant has work in progress and `activeWork` is refuse (`active_work`)"),
122
- },
123
- }), async (c) => {
124
- const activeWork = c.req.query("activeWork") ?? "refuse";
125
- if (activeWork !== "refuse" && activeWork !== "drain" && activeWork !== "cancel")
126
- return rejectedResponse(400, "invalid_request", "activeWork must be refuse, drain, or cancel");
127
- try {
128
- await module.delete(c.req.param("tenantId"), activeWork);
129
- return new Response(null, { status: 204 });
130
- }
131
- catch (error) {
132
- if (error instanceof TenantNotFoundError)
133
- return opaqueNotFoundResponse();
134
- const code = error.code;
135
- if (error instanceof TenantBusyError ||
136
- error.name === "TenantBusyError" ||
137
- code === "active_work" ||
138
- code === "conflict")
139
- return rejectedResponse(409, "active_work", error instanceof Error ? error.message : "Active work");
140
- throw error;
141
- }
142
- });
143
39
  // D12: `/v1/admin/status` and `/v1/admin/host` are one answer.
144
40
  for (const path of ["/v1/admin/status", "/v1/admin/host"])
145
41
  serveRoute(api, adminRoute({
146
42
  method: "get",
147
43
  path,
148
44
  summary: path.endsWith("host") ? "Get the Host's status (alias)" : "Get the Host's status",
149
- responses: { 200: json(AdminStatusBody, "Version, protocol, Tenants and the Host") },
45
+ responses: { 200: json(AdminStatusBody, "Version, protocol, the Tenant and the Host") },
150
46
  }), async () => jsonResponse(200, await options.status()));
151
47
  serveRoute(api, adminRoute({
152
48
  method: "post",
@@ -7,7 +7,7 @@ export type HostBindings = NodeBindings & {
7
7
  export type HostEnv = {
8
8
  Bindings: HostBindings;
9
9
  Variables: {
10
- /** The Tenant a request named, for the request log. */
10
+ /** The Tenant a request reached, for the request log. */
11
11
  tenantId?: string;
12
12
  /** The status to log when it is not the answer's (a rejection after a stream started). */
13
13
  status?: number;