@nylorun/runtime 0.20.0-beta → 0.21.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 (109) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +19 -12
  3. package/dist/adapters/execution/restate.js +26 -1
  4. package/dist/api/components.d.ts +155 -36
  5. package/dist/api/components.js +3 -3
  6. package/dist/api/http/app.js +2 -0
  7. package/dist/api/http/routes/mcp.d.ts +9 -0
  8. package/dist/api/http/routes/mcp.js +32 -0
  9. package/dist/api/http/routes/vaults.d.ts +1 -9
  10. package/dist/api/http/routes/vaults.js +3 -103
  11. package/dist/api/openapi.js +6 -3
  12. package/dist/artifacts/media-types.d.ts +2 -0
  13. package/dist/artifacts/media-types.js +5 -0
  14. package/dist/core/flow-host.d.ts +2 -3
  15. package/dist/core/flow-host.js +10 -7
  16. package/dist/execution/types.d.ts +13 -5
  17. package/dist/execution/types.js +13 -5
  18. package/dist/gates/http-tool.d.ts +7 -0
  19. package/dist/gates/http-tool.js +34 -3
  20. package/dist/gates/mcp-handler.d.ts +4 -1
  21. package/dist/gates/mcp-handler.js +64 -8
  22. package/dist/gates/tenant-vaults.d.ts +9 -15
  23. package/dist/gates/tenant-vaults.js +9 -17
  24. package/dist/gates/tool-client.js +36 -17
  25. package/dist/gates/tool-contract.d.ts +19 -3
  26. package/dist/gates/tool-contract.js +9 -3
  27. package/dist/gates/tool-gate.d.ts +2 -2
  28. package/dist/harness/calls.d.ts +2 -0
  29. package/dist/harness/calls.js +13 -3
  30. package/dist/harness-api/record.d.ts +7 -6
  31. package/dist/harness-api/record.js +99 -17
  32. package/dist/harness-api/server.js +35 -27
  33. package/dist/harness-api/start.js +2 -1
  34. package/dist/host/config-for.d.ts +0 -2
  35. package/dist/host/config-for.js +0 -1
  36. package/dist/host/create-host.d.ts +1 -1
  37. package/dist/host/create-host.js +2 -1
  38. package/dist/host/gates.js +0 -1
  39. package/dist/host/http.d.ts +0 -1
  40. package/dist/host/http.js +0 -6
  41. package/dist/host/main.js +13 -3
  42. package/dist/host/stack-config.d.ts +7 -8
  43. package/dist/host/stack-config.js +50 -92
  44. package/dist/index.d.ts +0 -1
  45. package/dist/keys/client.js +1 -9
  46. package/dist/keys/keys.d.ts +11 -13
  47. package/dist/keys/keys.js +5 -7
  48. package/dist/management-openapi.json +315 -345
  49. package/dist/mcp/connect.d.ts +159 -13
  50. package/dist/mcp/connect.js +538 -55
  51. package/dist/mcp/pool.d.ts +11 -2
  52. package/dist/mcp/pool.js +74 -27
  53. package/dist/mcp/preview.d.ts +35 -0
  54. package/dist/mcp/preview.js +215 -0
  55. package/dist/mcp/snapshot.d.ts +79 -5
  56. package/dist/mcp/snapshot.js +145 -27
  57. package/dist/model/catalog.d.ts +14 -0
  58. package/dist/model/catalog.js +14 -1
  59. package/dist/model/pi-model.js +36 -2
  60. package/dist/openapi.json +362 -123
  61. package/dist/redact.d.ts +19 -1
  62. package/dist/redact.js +42 -13
  63. package/dist/sandbox/session-sandbox.d.ts +7 -0
  64. package/dist/sandbox/session-sandbox.js +89 -28
  65. package/dist/store/postgres/drizzle/0016_mcp_oauth_removed.sql +5 -0
  66. package/dist/store/postgres/drizzle/0017_wake_outbox.sql +9 -0
  67. package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
  68. package/dist/store/postgres/schema.d.ts +105 -261
  69. package/dist/store/postgres/schema.js +18 -36
  70. package/dist/store/postgres/store.js +54 -21
  71. package/dist/store/postgres/tenant.d.ts +14 -0
  72. package/dist/store/postgres/tenant.js +22 -2
  73. package/dist/store/types.d.ts +48 -13
  74. package/dist/tenant/advance.js +1 -4
  75. package/dist/tenant/artifact-tool.d.ts +9 -1
  76. package/dist/tenant/artifact-tool.js +113 -9
  77. package/dist/tenant/commands.js +5 -5
  78. package/dist/tenant/context.d.ts +15 -13
  79. package/dist/tenant/effects.d.ts +2 -2
  80. package/dist/tenant/effects.js +20 -5
  81. package/dist/tenant/ephemeral.d.ts +0 -6
  82. package/dist/tenant/ephemeral.js +0 -1
  83. package/dist/tenant/outbound.d.ts +14 -5
  84. package/dist/tenant/outbound.js +21 -9
  85. package/dist/tenant/runtime.js +11 -15
  86. package/dist/tenant/sessions.js +20 -3
  87. package/dist/tenant/store-pg.js +9 -0
  88. package/dist/tenant/sweep.d.ts +14 -0
  89. package/dist/tenant/sweep.js +69 -10
  90. package/dist/tenant/tool-results.d.ts +57 -0
  91. package/dist/tenant/tool-results.js +298 -0
  92. package/dist/tenant/tool-search.d.ts +43 -0
  93. package/dist/tenant/tool-search.js +100 -0
  94. package/dist/tenant/transcript.d.ts +108 -0
  95. package/dist/tenant/transcript.js +13 -0
  96. package/dist/tenant/types.d.ts +0 -14
  97. package/dist/vault/crypto.d.ts +0 -7
  98. package/dist/vault/crypto.js +0 -16
  99. package/dist/vault/headers.d.ts +17 -0
  100. package/dist/vault/headers.js +61 -0
  101. package/dist/vault/service.d.ts +11 -68
  102. package/dist/vault/service.js +83 -438
  103. package/dist/vault/sources.d.ts +20 -48
  104. package/dist/vault/sources.js +25 -189
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/package.json +3 -3
  108. package/dist/vault/oauth.d.ts +0 -55
  109. package/dist/vault/oauth.js +0 -186
@@ -29,7 +29,9 @@ import { HARNESS_API_VERSION, HARNESS_CLAIMS, HarnessApiError, } from "@nylorun/
29
29
  import { definitionFilesOf } from "@nylorun/core/contracts";
30
30
  import { isOwnershipLost } from "../store/ownership.js";
31
31
  import { ownedSession } from "../tenant/context.js";
32
- import { mcpDiscovered, sessionToolsOf } from "../mcp/snapshot.js";
32
+ import { mcpDiscovered, pinDeferral, sessionToolsOf, } from "../mcp/snapshot.js";
33
+ import { contextWindowOf } from "../model/catalog.js";
34
+ import { turnManifestOf } from "../tenant/session.js";
33
35
  import { sandboxWorkspaceKey, workspacePrefix } from "../sandbox/records.js";
34
36
  import { abortKind } from "../tenant/worker.js";
35
37
  import { readDefinitionFile } from "../tenant/definition-files.js";
@@ -194,36 +196,42 @@ export function createHarnessApiServer(ctx, options = {}) {
194
196
  * session wins, with `mcp.discovered` in its log; diagnostics replace those of the same server.
195
197
  * Answers the session's snapshot and the tools the engine advertises.
196
198
  */
197
- const recordMcp = (run, params) => ctx.store.tx(async (t) => {
198
- const { lease } = run.offer;
199
- const current = await ownedSession(t, lease, lease.sessionId);
200
- const diagnostics = params.diagnostics;
199
+ const recordMcp = async (run, params) => {
201
200
  const snapshot = params.snapshot;
202
201
  if (snapshot !== undefined && !isSnapshot(snapshot))
203
202
  throw new HarnessApiError("invalid", "The MCP snapshot is malformed");
204
- if (snapshot && !current.mcpSnapshot) {
205
- current.mcpSnapshot = snapshot;
206
- current.mcpDiagnostics = diagnostics;
207
- await t.put("sessions", current.id, current);
208
- await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(snapshot, diagnostics));
209
- }
210
- else if (diagnostics.length > 0) {
211
- const prior = [...(current.mcpDiagnostics ?? [])];
212
- for (const item of diagnostics) {
213
- const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId && existing.serverName === item.serverName);
214
- if (index >= 0)
215
- prior[index] = item;
216
- else
217
- prior.push(item);
203
+ // Which MCP tools the model reaches through tool_search, decided once for the session from
204
+ // the Tenant's model's context window (R2b C10, Q21).
205
+ const contextWindow = snapshot ? contextWindowOf(await ctx.vault.getHostModel()) : 0;
206
+ return ctx.store.tx(async (t) => {
207
+ const { lease } = run.offer;
208
+ const current = await ownedSession(t, lease, lease.sessionId);
209
+ const diagnostics = params.diagnostics;
210
+ if (snapshot && !current.mcpSnapshot) {
211
+ current.mcpSnapshot = pinDeferral(snapshot, current.manifest, contextWindow);
212
+ current.mcpDiagnostics = diagnostics;
213
+ await t.put("sessions", current.id, current);
214
+ await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics));
218
215
  }
219
- current.mcpDiagnostics = prior;
220
- await t.put("sessions", current.id, current);
221
- }
222
- return {
223
- ...(current.mcpSnapshot ? { snapshot: current.mcpSnapshot } : {}),
224
- sessionTools: [...(sessionToolsOf(current.mcpSnapshot, current.manifest) ?? [])],
225
- };
226
- });
216
+ else if (diagnostics.length > 0) {
217
+ const prior = [...(current.mcpDiagnostics ?? [])];
218
+ for (const item of diagnostics) {
219
+ const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId && existing.serverName === item.serverName);
220
+ if (index >= 0)
221
+ prior[index] = item;
222
+ else
223
+ prior.push(item);
224
+ }
225
+ current.mcpDiagnostics = prior;
226
+ await t.put("sessions", current.id, current);
227
+ }
228
+ return {
229
+ ...(current.mcpSnapshot ? { snapshot: current.mcpSnapshot } : {}),
230
+ // The turn's manifest: a variant may have disabled tools or required their approval.
231
+ sessionTools: [...(sessionToolsOf(current.mcpSnapshot, turnManifestOf(current)) ?? [])],
232
+ };
233
+ });
234
+ };
227
235
  /** A definition file the run's definition names (its skills', for its sandbox), base64. */
228
236
  const definitionFile = async (run, sha256, signal) => {
229
237
  if (!definitionFilesOf(run.offer.start.routing.rootManifest).has(sha256))
@@ -57,7 +57,8 @@ export function buildTurnStart(ctx, segment) {
57
57
  checkpoint: !flow && cp.state
58
58
  ? { ...cp, state: withTranscript(cp.state, []) }
59
59
  : cp,
60
- ...(flow ? {} : { sessionTools: sessionToolsOf(current.mcpSnapshot, current.manifest) ?? [] }),
60
+ // The turn's manifest: a variant may have disabled MCP tools or required their approval.
61
+ ...(flow ? {} : { sessionTools: sessionToolsOf(current.mcpSnapshot, turnManifestOf(current)) ?? [] }),
61
62
  outcomes: segment.outcomes,
62
63
  transcript: { cursor: segment.cursor },
63
64
  options: {
@@ -14,6 +14,4 @@ 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
- /** `NYLORUN_PUBLIC_URL`: the base of the MCP OAuth callback (F9 C2). */
18
- publicUrl?: string;
19
17
  }): (id: string) => TenantConfig;
@@ -16,7 +16,6 @@ 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 } : {}),
20
19
  logger: {
21
20
  info: (message, fields) => options.logger.info(message, { tenantId: id, ...fields }),
22
21
  warn: (message, fields) => options.logger.warn(message, { tenantId: id, ...fields }),
@@ -1,7 +1,7 @@
1
1
  import type { ResourceServerConfig } from "../tenant/resource-server.js";
2
2
  import type { Logger, TenantModule } from "../tenant/types.js";
3
3
  import type { HostConfigFile } from "./config.js";
4
- import type { ContainerListen } from "./stack-config.js";
4
+ import { type ContainerListen } from "./stack-config.js";
5
5
  export interface CreateHostOptions {
6
6
  hostRoot: string;
7
7
  module: TenantModule;
@@ -1,7 +1,8 @@
1
1
  import { createServer, } from "node:http";
2
2
  import { getRequestListener, RequestError } from "@hono/node-server";
3
3
  import { createHostApp } from "./app.js";
4
- import { bindListener as bind, EXIT_NON_LOOPBACK, headerValue, HostListenError, isAllowedRequestHost, isLoopbackHost, pathnameIsLogged, redactRoutePath, rejectedResponse, sendRejected, } from "./http.js";
4
+ import { isLoopbackHost } from "./stack-config.js";
5
+ import { bindListener as bind, EXIT_NON_LOOPBACK, headerValue, HostListenError, isAllowedRequestHost, pathnameIsLogged, redactRoutePath, rejectedResponse, sendRejected, } from "./http.js";
5
6
  export function createHost(options) {
6
7
  const { module, config, logger, coreVersion } = options;
7
8
  const pid = options.pid ?? process.pid;
@@ -38,7 +38,6 @@ export async function startGates(options) {
38
38
  vaults = createTenantVaults({
39
39
  sql: database,
40
40
  hostRoot: options.hostRoot,
41
- ...(gates.resolver ? { resolver: gates.resolver } : {}),
42
41
  ...(options.delivery ? { delivery: options.delivery } : {}),
43
42
  });
44
43
  }
@@ -54,4 +54,3 @@ export declare function adminKeyMatches(presented: string | undefined, adminKey:
54
54
  export declare function headerValue(incoming: IncomingMessage, name: string): string | undefined;
55
55
  /** A JSON request body, capped at `limit` bytes (413); `undefined` when there is none. */
56
56
  export declare function readJsonBody(request: Request, limit?: number): Promise<unknown>;
57
- export declare function isLoopbackHost(host: string): boolean;
package/dist/host/http.js CHANGED
@@ -191,9 +191,3 @@ export async function readJsonBody(request, limit = 1024 * 1024) {
191
191
  throw Object.assign(new Error("Invalid JSON"), { status: 400 });
192
192
  }
193
193
  }
194
- export function isLoopbackHost(host) {
195
- const normalized = host.trim().toLowerCase();
196
- return (normalized === "127.0.0.1" ||
197
- normalized === "::1" ||
198
- normalized === "localhost");
199
- }
package/dist/host/main.js CHANGED
@@ -139,6 +139,17 @@ async function ensureBucket(store, bucket, logger) {
139
139
  }
140
140
  }
141
141
  }
142
+ /**
143
+ * Settings of a removed feature that are still set: logged, never fatal. The credential
144
+ * resolver (`NYLORUN_RESOLVER_*`) is gone in protocol 10. Never logs the values.
145
+ */
146
+ function warnRemovedSettings(stack, logger) {
147
+ if (stack.removedResolverVariables)
148
+ logger.warn("resolver_removed", {
149
+ variables: stack.removedResolverVariables,
150
+ message: "The credential resolver is gone (protocol 10) and these variables are ignored: keep a person's keys in their user vault, or reach their accounts through a gateway",
151
+ });
152
+ }
142
153
  /**
143
154
  * The gateway's services: the Model Gate's listener (gates, keys) and egress-gate's (egress), over
144
155
  * the Postgres pool and the Host's tenant directory. Writes nothing to the Host root (the local
@@ -153,9 +164,8 @@ async function runGates(stack) {
153
164
  ...(stack.packing ? { packing: stack.packing } : {}),
154
165
  endpoints: describeEndpoints(stack.endpoints),
155
166
  objectStore: stack.objectStore ? "s3" : "none",
156
- // Never the resolver's URL or token: only whether one is set (F9 C1).
157
- resolver: stack.gates?.resolver ? "configured" : "none",
158
167
  });
168
+ warnRemovedSettings(stack, logger);
159
169
  const database = createDatabase(stack);
160
170
  const servers = [];
161
171
  try {
@@ -234,6 +244,7 @@ export async function main() {
234
244
  file: identityFile,
235
245
  issuers: issuers.issuers.map((issuer) => issuer.config.name),
236
246
  });
247
+ warnRemovedSettings(stack, logger);
237
248
  if (stack.deprecatedRole)
238
249
  logger.warn("deprecated_flag", {
239
250
  flag: "--role",
@@ -261,7 +272,6 @@ export async function main() {
261
272
  logger,
262
273
  ...(stack.delivery ? { delivery: stack.delivery } : {}),
263
274
  ...(issuers ? { issuers } : {}),
264
- ...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
265
275
  });
266
276
  // The process's Durable Session Execution: Restate when its endpoints are set, else the
267
277
  // in-process memory execution. An invocation that arrives while the Tenant opens waits for it.
@@ -54,14 +54,6 @@ export interface GatesConfig {
54
54
  listen: ContainerListen;
55
55
  /** `NYLORUN_GATES_TOKEN`: the bearer the loop presents; at least 32 bytes as hex. */
56
56
  token: string;
57
- /**
58
- * The operator's credential resolver (`NYLORUN_RESOLVER_URL`, `NYLORUN_RESOLVER_TOKEN`), asked
59
- * for a person's MCP credential when the session's vaults hold none (F9 C1). Absent: vaults only.
60
- */
61
- resolver?: {
62
- url: string;
63
- token: string;
64
- };
65
57
  }
66
58
  /** egress-gate's listener (`NYLORUN_EGRESS_LISTEN_HOST`, `NYLORUN_EGRESS_LISTEN_PORT`). */
67
59
  export interface EgressConfig {
@@ -168,6 +160,11 @@ export interface StackConfig {
168
160
  packing?: "combined" | "split";
169
161
  /** Set when the process was started with the deprecated `--role` (logged at startup). */
170
162
  deprecatedRole?: RuntimeRole;
163
+ /**
164
+ * The removed credential resolver's variables (`NYLORUN_RESOLVER_*`, protocol 10) the
165
+ * environment still sets: ignored, and logged at startup as `resolver_removed`.
166
+ */
167
+ removedResolverVariables?: string[];
171
168
  /** Present in container mode; absent means bind what host.json names. */
172
169
  listen?: ContainerListen;
173
170
  endpoints: StackEndpoints;
@@ -240,6 +237,8 @@ export declare function parseServices(argv: readonly string[]): ServiceSelection
240
237
  * IPv6 literals must be bracketed (`[::1]:4000`).
241
238
  */
242
239
  export declare function normalizeAllowedHost(name: string, entry: string): string;
240
+ /** A loopback bind address: `127.0.0.1`, `::1` or `localhost`. */
241
+ export declare function isLoopbackHost(host: string): boolean;
243
242
  /** Parse the stack configuration; throws `StackConfigError` naming the variable. */
244
243
  export declare function parseStackConfig(env: EnvSnapshot, argv: readonly string[]): StackConfig;
245
244
  /** Endpoint summary for logs: which endpoints are set, never their values. */
@@ -15,8 +15,8 @@
15
15
  * `NYLORUN_GATES_TOKEN`. In a container, loop requires the gate: the loop
16
16
  * process must never hold a model credential. `egress` (egress-gate, F7.2) joins gates and keys
17
17
  * in the gateway and parses its listener (`NYLORUN_EGRESS_LISTEN_*`, default `0.0.0.0:4200`).
18
- * Only gates reads the operator's credential resolver (`NYLORUN_RESOLVER_URL`,
19
- * `NYLORUN_RESOLVER_TOKEN`, F9 C1): remote MCP calls are authorized in the gateway.
18
+ * The credential resolver is gone (protocol 10): its `NYLORUN_RESOLVER_*` variables are ignored,
19
+ * and the Host logs `resolver_removed` when one is set.
20
20
  *
21
21
  * `harness` (F6.2) runs alone: it holds the harness credential and nothing else. It connects to
22
22
  * core's Harness API listener (`NYLORUN_HARNESS_URL`, `NYLORUN_HARNESS_TOKEN`), calls models and
@@ -179,9 +179,10 @@ export function normalizeAllowedHost(name, entry) {
179
179
  function loopbackForms(port) {
180
180
  return [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
181
181
  }
182
- function isLoopbackAddress(host) {
183
- const value = host.toLowerCase();
184
- return value === "127.0.0.1" || value === "::1" || value === "localhost";
182
+ /** A loopback bind address: `127.0.0.1`, `::1` or `localhost`. */
183
+ export function isLoopbackHost(host) {
184
+ const normalized = host.trim().toLowerCase();
185
+ return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
185
186
  }
186
187
  function parseUrl(env, name, protocols) {
187
188
  const raw = read(env, name);
@@ -198,32 +199,37 @@ function parseUrl(env, name, protocols) {
198
199
  throw new StackConfigError(`${name} must use ${protocols.map((p) => p.replace(/:$/, "")).join(" or ")}; got ${url.protocol.replace(/:$/, "")}`);
199
200
  return raw;
200
201
  }
201
- function parseListen(env) {
202
- const rawHost = read(env, "NYLORUN_LISTEN_HOST");
203
- const rawPort = read(env, "NYLORUN_LISTEN_PORT");
204
- const rawAllowed = read(env, "NYLORUN_ALLOWED_HOSTS");
205
- if (rawHost === undefined && rawPort === undefined && rawAllowed === undefined)
206
- return undefined;
207
- const host = rawHost ?? DEFAULT_CONTAINER_LISTEN_HOST;
202
+ function parseListener(env, prefix, defaultPort, hosts) {
203
+ const hostName = `${prefix}LISTEN_HOST`;
204
+ const host = read(env, hostName) ?? DEFAULT_CONTAINER_LISTEN_HOST;
208
205
  if (/\s|\//.test(host))
209
- throw new StackConfigError(`NYLORUN_LISTEN_HOST is not an address: ${host}`);
210
- const port = rawPort === undefined
211
- ? DEFAULT_CONTAINER_LISTEN_PORT
212
- : parsePort("NYLORUN_LISTEN_PORT", rawPort);
213
- const explicit = rawAllowed === undefined
214
- ? []
215
- : rawAllowed
216
- .split(",")
217
- .map((entry) => entry.trim())
218
- .filter((entry) => entry !== "")
219
- .map((entry) => normalizeAllowedHost("NYLORUN_ALLOWED_HOSTS", entry));
220
- if (explicit.length === 0 && !isLoopbackAddress(host))
221
- throw new StackConfigError(`NYLORUN_ALLOWED_HOSTS is required when NYLORUN_LISTEN_HOST is ${host}: list the Host headers clients send, e.g. runtime:${port},localhost:<published port>`);
206
+ throw new StackConfigError(`${hostName} is not an address: ${host}`);
207
+ const portName = `${prefix}LISTEN_PORT`;
208
+ const rawPort = read(env, portName);
209
+ const port = rawPort === undefined ? defaultPort : parsePort(portName, rawPort);
210
+ if (!hosts)
211
+ return { host, port };
212
+ const allowedName = `${prefix}ALLOWED_HOSTS`;
213
+ const explicit = (read(env, allowedName) ?? "")
214
+ .split(",")
215
+ .map((entry) => entry.trim())
216
+ .filter((entry) => entry !== "")
217
+ .map((entry) => normalizeAllowedHost(allowedName, entry));
218
+ if (explicit.length === 0 && !isLoopbackHost(host))
219
+ throw new StackConfigError(`${allowedName} is required when ${hostName} is ${host}: list the Host headers ${hosts.senders}, e.g. ${hosts.example(port)}`);
222
220
  // Loopback forms of the listen port serve probes from inside the container
223
221
  // (health checks). A browser on the Docker host never sends them: it sends
224
222
  // the published port.
225
- const allowedHosts = [...new Set([...explicit, ...loopbackForms(port)])];
226
- return { host, port, allowedHosts };
223
+ return { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] };
224
+ }
225
+ /** The API's listener (`NYLORUN_LISTEN_*`, `NYLORUN_ALLOWED_HOSTS`): none when none is set. */
226
+ function parseListen(env) {
227
+ if (["NYLORUN_LISTEN_HOST", "NYLORUN_LISTEN_PORT", "NYLORUN_ALLOWED_HOSTS"].every((name) => read(env, name) === undefined))
228
+ return undefined;
229
+ return parseListener(env, "NYLORUN_", DEFAULT_CONTAINER_LISTEN_PORT, {
230
+ senders: "clients send",
231
+ example: (port) => `runtime:${port},localhost:<published port>`,
232
+ });
227
233
  }
228
234
  const IDENTITY_KEY = /^publickeyv1_[1-9A-HJ-NP-Za-km-z]{32,64}$/;
229
235
  function parseIdentityKeys(env) {
@@ -304,9 +310,13 @@ export function parseStackConfig(env, argv) {
304
310
  const delivery = parseDelivery(env);
305
311
  const tenant = servesApi ? parseTenant(env) : undefined;
306
312
  const objectStore = parseObjectStore(env);
313
+ const removedResolverVariables = Object.keys(env)
314
+ .filter((name) => name.startsWith(REMOVED_RESOLVER_PREFIX) && read(env, name) !== undefined)
315
+ .sort();
307
316
  return {
308
317
  services,
309
318
  ...(deprecatedRole ? { deprecatedRole } : {}),
319
+ ...(removedResolverVariables.length ? { removedResolverVariables } : {}),
310
320
  ...(gates ? { gates } : {}),
311
321
  ...(egress ? { egress } : {}),
312
322
  ...(modelGate ? { modelGate } : {}),
@@ -394,30 +404,14 @@ function parseSandboxes(env) {
394
404
  * (required with `NYLORUN_HARNESS=remote`; optional when only sandbox pods connect).
395
405
  */
396
406
  function parseHarnessListener(env, tokenRequired = true) {
397
- const host = read(env, "NYLORUN_HARNESS_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
398
- if (/\s|\//.test(host))
399
- throw new StackConfigError(`NYLORUN_HARNESS_LISTEN_HOST is not an address: ${host}`);
400
- const rawPort = read(env, "NYLORUN_HARNESS_LISTEN_PORT");
401
- const port = rawPort === undefined
402
- ? DEFAULT_HARNESS_LISTEN_PORT
403
- : parsePort("NYLORUN_HARNESS_LISTEN_PORT", rawPort);
404
- const rawAllowed = read(env, "NYLORUN_HARNESS_ALLOWED_HOSTS");
405
- const explicit = rawAllowed === undefined
406
- ? []
407
- : rawAllowed
408
- .split(",")
409
- .map((entry) => entry.trim())
410
- .filter((entry) => entry !== "")
411
- .map((entry) => normalizeAllowedHost("NYLORUN_HARNESS_ALLOWED_HOSTS", entry));
412
- if (explicit.length === 0 && !isLoopbackAddress(host))
413
- throw new StackConfigError(`NYLORUN_HARNESS_ALLOWED_HOSTS is required when NYLORUN_HARNESS_LISTEN_HOST is ${host}: list the Host headers harnesses send, e.g. runtime:${port}`);
407
+ const listen = parseListener(env, "NYLORUN_HARNESS_", DEFAULT_HARNESS_LISTEN_PORT, {
408
+ senders: "harnesses send",
409
+ example: (port) => `runtime:${port}`,
410
+ });
414
411
  const token = tokenRequired || read(env, "NYLORUN_HARNESS_TOKEN") !== undefined
415
412
  ? parseHarnessToken(env, "with NYLORUN_HARNESS=remote: the credential harnesses present")
416
413
  : undefined;
417
- return {
418
- listen: { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] },
419
- ...(token ? { token } : {}),
420
- };
414
+ return { listen, ...(token ? { token } : {}) };
421
415
  }
422
416
  /** What a harness process must not hold: it runs next to agent code. */
423
417
  const HARNESS_REFUSED = ["NYLORUN_DATABASE_URL", "NYLORUN_GATES_TOKEN", "NYLORUN_KEYS_URL"];
@@ -487,58 +481,22 @@ function parsePodHost(env, url) {
487
481
  }
488
482
  /** `StackConfig.gates` from `NYLORUN_GATES_*`. */
489
483
  function parseGates(env) {
490
- const host = read(env, "NYLORUN_GATES_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
491
- if (/\s|\//.test(host))
492
- throw new StackConfigError(`NYLORUN_GATES_LISTEN_HOST is not an address: ${host}`);
493
- const rawPort = read(env, "NYLORUN_GATES_LISTEN_PORT");
494
- const port = rawPort === undefined
495
- ? DEFAULT_GATES_LISTEN_PORT
496
- : parsePort("NYLORUN_GATES_LISTEN_PORT", rawPort);
497
- const rawAllowed = read(env, "NYLORUN_GATES_ALLOWED_HOSTS");
498
- const explicit = rawAllowed === undefined
499
- ? []
500
- : rawAllowed
501
- .split(",")
502
- .map((entry) => entry.trim())
503
- .filter((entry) => entry !== "")
504
- .map((entry) => normalizeAllowedHost("NYLORUN_GATES_ALLOWED_HOSTS", entry));
505
- if (explicit.length === 0 && !isLoopbackAddress(host))
506
- throw new StackConfigError(`NYLORUN_GATES_ALLOWED_HOSTS is required when NYLORUN_GATES_LISTEN_HOST is ${host}: list the Host headers the loop sends, e.g. gateway:${port}`);
484
+ const listen = parseListener(env, "NYLORUN_GATES_", DEFAULT_GATES_LISTEN_PORT, {
485
+ senders: "the loop sends",
486
+ example: (port) => `gateway:${port}`,
487
+ });
507
488
  const token = read(env, "NYLORUN_GATES_TOKEN");
508
489
  if (token === undefined)
509
490
  throw new StackConfigError("NYLORUN_GATES_TOKEN is required for --service gates: the token the loop presents (`nylorun start` sets it)");
510
491
  if (!GATES_TOKEN.test(token))
511
492
  throw new StackConfigError("NYLORUN_GATES_TOKEN must be at least 32 bytes as hex");
512
- const resolver = parseResolver(env);
513
- return {
514
- listen: { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] },
515
- token,
516
- ...(resolver ? { resolver } : {}),
517
- };
518
- }
519
- /** `GatesConfig.resolver` from `NYLORUN_RESOLVER_URL` and `NYLORUN_RESOLVER_TOKEN`: both or neither. */
520
- function parseResolver(env) {
521
- const url = parseUrl(env, "NYLORUN_RESOLVER_URL", ["http:", "https:"]);
522
- const token = read(env, "NYLORUN_RESOLVER_TOKEN");
523
- if (url === undefined) {
524
- if (token !== undefined)
525
- throw new StackConfigError("NYLORUN_RESOLVER_TOKEN is set without NYLORUN_RESOLVER_URL: set the URL of your credential resolver");
526
- return undefined;
527
- }
528
- if (token === undefined)
529
- throw new StackConfigError("NYLORUN_RESOLVER_TOKEN is required with NYLORUN_RESOLVER_URL: the bearer the gateway presents to your credential resolver");
530
- if (/\s/.test(token))
531
- throw new StackConfigError("NYLORUN_RESOLVER_TOKEN cannot contain whitespace");
532
- return { url, token };
493
+ return { listen, token };
533
494
  }
495
+ /** The prefix of the credential resolver's variables (removed in protocol 10). */
496
+ const REMOVED_RESOLVER_PREFIX = "NYLORUN_RESOLVER_";
534
497
  /** `StackConfig.egress` from `NYLORUN_EGRESS_LISTEN_*`. CONNECT has no Host header to check. */
535
498
  function parseEgress(env) {
536
- const host = read(env, "NYLORUN_EGRESS_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
537
- if (/\s|\//.test(host))
538
- throw new StackConfigError(`NYLORUN_EGRESS_LISTEN_HOST is not an address: ${host}`);
539
- const rawPort = read(env, "NYLORUN_EGRESS_LISTEN_PORT");
540
- const port = rawPort === undefined ? DEFAULT_EGRESS_LISTEN_PORT : parsePort("NYLORUN_EGRESS_LISTEN_PORT", rawPort);
541
- return { listen: { host, port } };
499
+ return { listen: parseListener(env, "NYLORUN_EGRESS_", DEFAULT_EGRESS_LISTEN_PORT) };
542
500
  }
543
501
  /** `StackConfig.modelGate` from `NYLORUN_GATES_URL` and `NYLORUN_GATES_TOKEN`. */
544
502
  function parseModelGate(env) {
package/dist/index.d.ts CHANGED
@@ -4,7 +4,6 @@ export { parseIdentityFile, IdentityFileError, type TrustedIssuerConfig, } from
4
4
  export { createTrustedIssuers, type TrustedIssuers, type TrustedIssuersOptions, } from "./tenant/issuers.js";
5
5
  export type { TenantConfig, TenantHandle, TenantPaths, Logger, OpenTenantRuntime, } from "./tenant/types.js";
6
6
  export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
7
- export type { ResolverConfig } from "./vault/sources.js";
8
7
  export type { RuntimeAgent } from "./contracts.js";
9
8
  export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
10
9
  export type { RuntimeModelAdapter } from "./contracts.js";
@@ -12,18 +12,10 @@ import { KEYS_PATH } from "./contract.js";
12
12
  import { KEYS_OPERATIONS } from "./keys.js";
13
13
  /** Signing and vault writes are short; anything slower is a gateway in trouble. */
14
14
  const KEYS_TIMEOUT_MS = 30_000;
15
- /**
16
- * MCP OAuth connect calls the authorization server several times (discovery, registration, the
17
- * code exchange), each bounded by `guardedFetch`'s 30 s: these wait longer.
18
- */
19
- const SLOW_OPERATIONS = {
20
- startOAuth: 120_000,
21
- finishOAuth: 60_000,
22
- };
23
15
  export function httpKeys(options) {
24
16
  const where = new URL(options.url).origin;
25
17
  function call(operation, args) {
26
- const timeoutMs = Math.max(options.timeoutMs ?? KEYS_TIMEOUT_MS, SLOW_OPERATIONS[operation] ?? 0);
18
+ const timeoutMs = options.timeoutMs ?? KEYS_TIMEOUT_MS;
27
19
  const url = new URL(`${KEYS_PATH}/${operation}`, options.url);
28
20
  const send = url.protocol === "https:" ? httpsRequest : httpRequest;
29
21
  const payload = Buffer.from(JSON.stringify({ args }), "utf8");
@@ -1,7 +1,8 @@
1
- import type { CreateCredentialRequest, CredentialInfo, HostModelView, PutHostModelRequest, RotateCredentialRequest, SelectHostModelRequest, SigningKeyView } from "@nylorun/core/contracts";
1
+ import type { CreateCredentialRequest, CredentialInfo, HostModelView, McpPreview, McpPreviewRequest, PutHostModelRequest, RotateCredentialRequest, SelectHostModelRequest, SigningKeyView } from "@nylorun/core/contracts";
2
2
  import type { SessionStore } from "../store/types.js";
3
+ import type { OutboundPolicy } from "../tenant/outbound.js";
3
4
  import { type SigningKeys } from "../tenant/signing-keys.js";
4
- import type { FinishOAuthInput, StartOAuthInput, VaultService } from "../vault/service.js";
5
+ import type { VaultService } from "../vault/service.js";
5
6
  /** One JWT to sign: its `typ` and every claim, `iss`, `aud`, `sub`, `iat`, `exp` and `jti` included. */
6
7
  export interface SignRequest {
7
8
  readonly typ: string;
@@ -26,19 +27,14 @@ export interface Keys {
26
27
  }): Promise<SigningKeyView[]>;
27
28
  /** Creates the current and standby keys when the Tenant has none. */
28
29
  ensureSigningKeys(): Promise<void>;
29
- /** Starts an MCP OAuth connect into an installation vault (`VaultService.startOAuth`). */
30
- startOAuth(input: StartOAuthInput): Promise<{
31
- authorizeUrl: string;
32
- expiresAt: string;
33
- }>;
34
- /** Finishes one from its callback (`VaultService.finishOAuth`). */
35
- finishOAuth(input: FinishOAuthInput): Promise<{
36
- vaultId: string;
37
- credentialId: string;
38
- }>;
30
+ /**
31
+ * Lists a remote MCP server's tools with the installation vault's credential for its URL
32
+ * (R2b C12, `mcp/preview.ts`); never calls a tool.
33
+ */
34
+ previewMcp(body: McpPreviewRequest): Promise<McpPreview>;
39
35
  }
40
36
  /** The operations, by the name the HTTP route carries. */
41
- export declare const KEYS_OPERATIONS: readonly ["createCredential", "rotateCredential", "putHostModel", "selectHostModel", "sign", "rotateSigningKeys", "ensureSigningKeys", "startOAuth", "finishOAuth"];
37
+ export declare const KEYS_OPERATIONS: readonly ["createCredential", "rotateCredential", "putHostModel", "selectHostModel", "sign", "rotateSigningKeys", "ensureSigningKeys", "previewMcp"];
42
38
  export type KeysOperation = (typeof KEYS_OPERATIONS)[number];
43
39
  export interface InProcessKeysOptions {
44
40
  readonly store: SessionStore;
@@ -47,5 +43,7 @@ export interface InProcessKeysOptions {
47
43
  readonly signingKeys: SigningKeys;
48
44
  /** The vault key. */
49
45
  readonly kek: () => Buffer;
46
+ /** How a tool preview reaches the server (`TenantConfig.delivery`). Default: no limits. */
47
+ readonly policy?: OutboundPolicy;
50
48
  }
51
49
  export declare function inProcessKeys(options: InProcessKeysOptions): Keys;
package/dist/keys/keys.js CHANGED
@@ -5,15 +5,15 @@
5
5
  * links, run and host tokens, signing-key rotation) run here, whole, so the process that calls them never
6
6
  * holds the key or a private signing key.
7
7
  *
8
- * MCP OAuth connect (F9-D14) runs here too: discovery, registration, the code exchange and the
9
- * credential it seals, so the plaintext tokens, the PKCE verifier and any client secret never
10
- * reach the runtime container; core only routes the start and the callback.
8
+ * A tool preview (R2b C12, `previewMcp`) runs here too: it sends an installation vault's
9
+ * credential to the server it lists, so the process that reads the plaintext makes the request.
11
10
  *
12
11
  * Two implementations: `inProcessKeys` (embedding, the ephemeral Runtime, tests, and the gates
13
12
  * service itself, which serves the HTTP routes with it), and `httpKeys` (`client.ts`), the
14
13
  * runtime container's client of the `keys` service in the gateway.
15
14
  */
16
15
  import { SignJWT } from "jose";
16
+ import { previewMcpServer } from "../mcp/preview.js";
17
17
  import { signingKeyView } from "../tenant/signing-keys.js";
18
18
  /** The operations, by the name the HTTP route carries. */
19
19
  export const KEYS_OPERATIONS = [
@@ -24,8 +24,7 @@ export const KEYS_OPERATIONS = [
24
24
  "sign",
25
25
  "rotateSigningKeys",
26
26
  "ensureSigningKeys",
27
- "startOAuth",
28
- "finishOAuth",
27
+ "previewMcp",
29
28
  ];
30
29
  export function inProcessKeys(options) {
31
30
  const { store, vault, signingKeys, kek } = options;
@@ -34,8 +33,6 @@ export function inProcessKeys(options) {
34
33
  rotateCredential: (vaultId, id, body) => vault.rotateCredential(vaultId, id, body),
35
34
  putHostModel: (body) => vault.putHostModel(body),
36
35
  selectHostModel: (body) => vault.selectHostModel(body),
37
- startOAuth: (input) => vault.startOAuth(input),
38
- finishOAuth: (input) => vault.finishOAuth(input),
39
36
  async sign(request) {
40
37
  const key = kek();
41
38
  const row = await store.tx((t) => signingKeys.ensure(t, key));
@@ -54,5 +51,6 @@ export function inProcessKeys(options) {
54
51
  const key = kek();
55
52
  await store.tx((t) => signingKeys.ensure(t, key));
56
53
  },
54
+ previewMcp: (body) => previewMcpServer(body, { vault, policy: options.policy ?? {} }),
57
55
  };
58
56
  }