@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.
- package/CHANGELOG.md +86 -0
- package/README.md +19 -12
- package/dist/adapters/execution/restate.js +26 -1
- package/dist/api/components.d.ts +155 -36
- package/dist/api/components.js +3 -3
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/routes/mcp.d.ts +9 -0
- package/dist/api/http/routes/mcp.js +32 -0
- package/dist/api/http/routes/vaults.d.ts +1 -9
- package/dist/api/http/routes/vaults.js +3 -103
- package/dist/api/openapi.js +6 -3
- package/dist/artifacts/media-types.d.ts +2 -0
- package/dist/artifacts/media-types.js +5 -0
- package/dist/core/flow-host.d.ts +2 -3
- package/dist/core/flow-host.js +10 -7
- package/dist/execution/types.d.ts +13 -5
- package/dist/execution/types.js +13 -5
- package/dist/gates/http-tool.d.ts +7 -0
- package/dist/gates/http-tool.js +34 -3
- package/dist/gates/mcp-handler.d.ts +4 -1
- package/dist/gates/mcp-handler.js +64 -8
- package/dist/gates/tenant-vaults.d.ts +9 -15
- package/dist/gates/tenant-vaults.js +9 -17
- package/dist/gates/tool-client.js +36 -17
- package/dist/gates/tool-contract.d.ts +19 -3
- package/dist/gates/tool-contract.js +9 -3
- package/dist/gates/tool-gate.d.ts +2 -2
- package/dist/harness/calls.d.ts +2 -0
- package/dist/harness/calls.js +13 -3
- package/dist/harness-api/record.d.ts +7 -6
- package/dist/harness-api/record.js +99 -17
- package/dist/harness-api/server.js +35 -27
- package/dist/harness-api/start.js +2 -1
- package/dist/host/config-for.d.ts +0 -2
- package/dist/host/config-for.js +0 -1
- package/dist/host/create-host.d.ts +1 -1
- package/dist/host/create-host.js +2 -1
- package/dist/host/gates.js +0 -1
- package/dist/host/http.d.ts +0 -1
- package/dist/host/http.js +0 -6
- package/dist/host/main.js +13 -3
- package/dist/host/stack-config.d.ts +7 -8
- package/dist/host/stack-config.js +50 -92
- package/dist/index.d.ts +0 -1
- package/dist/keys/client.js +1 -9
- package/dist/keys/keys.d.ts +11 -13
- package/dist/keys/keys.js +5 -7
- package/dist/management-openapi.json +315 -345
- package/dist/mcp/connect.d.ts +159 -13
- package/dist/mcp/connect.js +538 -55
- package/dist/mcp/pool.d.ts +11 -2
- package/dist/mcp/pool.js +74 -27
- package/dist/mcp/preview.d.ts +35 -0
- package/dist/mcp/preview.js +215 -0
- package/dist/mcp/snapshot.d.ts +79 -5
- package/dist/mcp/snapshot.js +145 -27
- package/dist/model/catalog.d.ts +14 -0
- package/dist/model/catalog.js +14 -1
- package/dist/model/pi-model.js +36 -2
- package/dist/openapi.json +362 -123
- package/dist/redact.d.ts +19 -1
- package/dist/redact.js +42 -13
- package/dist/sandbox/session-sandbox.d.ts +7 -0
- package/dist/sandbox/session-sandbox.js +89 -28
- package/dist/store/postgres/drizzle/0016_mcp_oauth_removed.sql +5 -0
- package/dist/store/postgres/drizzle/0017_wake_outbox.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
- package/dist/store/postgres/schema.d.ts +105 -261
- package/dist/store/postgres/schema.js +18 -36
- package/dist/store/postgres/store.js +54 -21
- package/dist/store/postgres/tenant.d.ts +14 -0
- package/dist/store/postgres/tenant.js +22 -2
- package/dist/store/types.d.ts +48 -13
- package/dist/tenant/advance.js +1 -4
- package/dist/tenant/artifact-tool.d.ts +9 -1
- package/dist/tenant/artifact-tool.js +113 -9
- package/dist/tenant/commands.js +5 -5
- package/dist/tenant/context.d.ts +15 -13
- package/dist/tenant/effects.d.ts +2 -2
- package/dist/tenant/effects.js +20 -5
- package/dist/tenant/ephemeral.d.ts +0 -6
- package/dist/tenant/ephemeral.js +0 -1
- package/dist/tenant/outbound.d.ts +14 -5
- package/dist/tenant/outbound.js +21 -9
- package/dist/tenant/runtime.js +11 -15
- package/dist/tenant/sessions.js +20 -3
- package/dist/tenant/store-pg.js +9 -0
- package/dist/tenant/sweep.d.ts +14 -0
- package/dist/tenant/sweep.js +69 -10
- package/dist/tenant/tool-results.d.ts +57 -0
- package/dist/tenant/tool-results.js +298 -0
- package/dist/tenant/tool-search.d.ts +43 -0
- package/dist/tenant/tool-search.js +100 -0
- package/dist/tenant/transcript.d.ts +108 -0
- package/dist/tenant/transcript.js +13 -0
- package/dist/tenant/types.d.ts +0 -14
- package/dist/vault/crypto.d.ts +0 -7
- package/dist/vault/crypto.js +0 -16
- package/dist/vault/headers.d.ts +17 -0
- package/dist/vault/headers.js +61 -0
- package/dist/vault/service.d.ts +11 -68
- package/dist/vault/service.js +83 -438
- package/dist/vault/sources.d.ts +20 -48
- package/dist/vault/sources.js +25 -189
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/vault/oauth.d.ts +0 -55
- 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) =>
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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;
|
package/dist/host/config-for.js
CHANGED
|
@@ -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
|
|
4
|
+
import { type ContainerListen } from "./stack-config.js";
|
|
5
5
|
export interface CreateHostOptions {
|
|
6
6
|
hostRoot: string;
|
|
7
7
|
module: TenantModule;
|
package/dist/host/create-host.js
CHANGED
|
@@ -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 {
|
|
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;
|
package/dist/host/gates.js
CHANGED
package/dist/host/http.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
202
|
-
const
|
|
203
|
-
const
|
|
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(
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/dist/keys/client.js
CHANGED
|
@@ -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 =
|
|
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");
|
package/dist/keys/keys.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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", "
|
|
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
|
-
*
|
|
9
|
-
* credential it
|
|
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
|
-
"
|
|
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
|
}
|