@nylorun/runtime 0.16.0-beta → 0.17.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +39 -27
- package/dist/admin-openapi.json +307 -7
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +10 -12
- package/dist/api/components.d.ts +54 -116
- package/dist/api/components.js +8 -11
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/define.d.ts +9 -12
- package/dist/api/http/define.js +11 -26
- package/dist/api/http/routes/access.d.ts +3 -19
- package/dist/api/http/routes/access.js +12 -172
- package/dist/api/http/routes/actions.js +0 -1
- package/dist/api/http/routes/artifacts.js +1 -3
- package/dist/api/http/routes/me.d.ts +13 -0
- package/dist/api/http/routes/me.js +45 -0
- package/dist/api/http/routes/sandboxes.d.ts +2 -2
- package/dist/api/http/routes/sandboxes.js +1 -3
- package/dist/api/http/routes/sessions.js +6 -12
- package/dist/api/http/routes/vaults.d.ts +12 -3
- package/dist/api/http/routes/vaults.js +122 -54
- package/dist/api/openapi.js +4 -10
- package/dist/artifacts/links.js +6 -6
- package/dist/artifacts/service.js +1 -1
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/tenant-vaults.d.ts +18 -9
- package/dist/gates/tenant-vaults.js +17 -12
- package/dist/host/admin-api.d.ts +12 -5
- package/dist/host/admin-api.js +72 -6
- package/dist/host/app.d.ts +0 -1
- package/dist/host/app.js +13 -23
- package/dist/host/config-for.d.ts +4 -0
- package/dist/host/config-for.js +2 -0
- package/dist/host/config.d.ts +0 -6
- package/dist/host/create-host.d.ts +3 -10
- package/dist/host/create-host.js +4 -1
- package/dist/host/gates.js +6 -1
- package/dist/host/main.js +32 -10
- package/dist/host/stack-config.d.ts +11 -8
- package/dist/host/stack-config.js +23 -12
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/keys/client.js +9 -1
- package/dist/keys/keys.d.ts +12 -2
- package/dist/keys/keys.js +8 -0
- package/dist/mcp/connect.js +2 -1
- package/dist/mcp/pool.d.ts +1 -0
- package/dist/mcp/pool.js +1 -0
- package/dist/openapi.json +2359 -4096
- package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
- package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
- package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
- package/dist/store/postgres/schema.d.ts +194 -170
- package/dist/store/postgres/schema.js +46 -28
- package/dist/store/postgres/store.js +51 -100
- package/dist/store/postgres/tenant.d.ts +1 -1
- package/dist/store/postgres/tenant.js +2 -2
- package/dist/store/types.d.ts +24 -30
- package/dist/streams/control.d.ts +0 -5
- package/dist/streams/control.js +0 -8
- package/dist/streams/types.d.ts +2 -11
- package/dist/streams/types.js +1 -1
- package/dist/tenant/auth.d.ts +13 -9
- package/dist/tenant/auth.js +25 -32
- package/dist/tenant/commands.js +3 -7
- package/dist/tenant/context.d.ts +20 -22
- package/dist/tenant/delivery-token.js +4 -4
- package/dist/tenant/effects.d.ts +5 -1
- package/dist/tenant/effects.js +5 -16
- package/dist/tenant/ephemeral.d.ts +14 -2
- package/dist/tenant/ephemeral.js +8 -1
- package/dist/tenant/host-token.js +5 -5
- package/dist/tenant/identity-file.d.ts +75 -0
- package/dist/tenant/identity-file.js +202 -0
- package/dist/tenant/issuers.d.ts +48 -0
- package/dist/tenant/issuers.js +311 -0
- package/dist/tenant/jwt.js +3 -3
- package/dist/tenant/operator-keys.d.ts +18 -0
- package/dist/tenant/operator-keys.js +55 -0
- package/dist/tenant/outbound.d.ts +30 -0
- package/dist/tenant/outbound.js +142 -10
- package/dist/tenant/principals.d.ts +2 -11
- package/dist/tenant/principals.js +10 -30
- package/dist/tenant/run-token.js +3 -3
- package/dist/tenant/runtime.d.ts +4 -0
- package/dist/tenant/runtime.js +14 -1
- package/dist/tenant/sandboxes.d.ts +5 -5
- package/dist/tenant/sandboxes.js +5 -5
- package/dist/tenant/session-streams.d.ts +6 -10
- package/dist/tenant/session-streams.js +9 -27
- package/dist/tenant/sessions.d.ts +2 -2
- package/dist/tenant/sessions.js +1 -1
- package/dist/tenant/signing-keys.d.ts +2 -1
- package/dist/tenant/signing-keys.js +2 -2
- package/dist/tenant/streams.d.ts +2 -3
- package/dist/tenant/streams.js +1 -5
- package/dist/tenant/types.d.ts +21 -0
- package/dist/vault/crypto.d.ts +7 -0
- package/dist/vault/crypto.js +16 -0
- package/dist/vault/oauth.d.ts +55 -0
- package/dist/vault/oauth.js +186 -0
- package/dist/vault/service.d.ts +54 -7
- package/dist/vault/service.js +261 -17
- package/dist/vault/sources.d.ts +54 -0
- package/dist/vault/sources.js +185 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
- package/dist/host/cors.d.ts +0 -27
- package/dist/host/cors.js +0 -64
- package/dist/tenant/access-policy.d.ts +0 -30
- package/dist/tenant/access-policy.js +0 -52
- package/dist/tenant/browser.d.ts +0 -15
- package/dist/tenant/browser.js +0 -39
- package/dist/tenant/subject-limits.d.ts +0 -16
- package/dist/tenant/subject-limits.js +0 -45
- package/dist/tenant/tokens.d.ts +0 -14
- package/dist/tenant/tokens.js +0 -187
package/dist/host/app.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
|
|
3
|
-
import { checkCompatibility, HOST_PROTOCOL, PROTOCOL_HEADER,
|
|
3
|
+
import { checkCompatibility, HOST_PROTOCOL, PROTOCOL_HEADER, TENANT_HEADER, } from "@nylorun/core/compatibility";
|
|
4
4
|
import { RUNTIME_VERSION } from "../version.js";
|
|
5
5
|
import { findTenantRoute } from "../api/http/app.js";
|
|
6
6
|
import { tenantDocument } from "../api/openapi.js";
|
|
7
|
-
import { answerPreflight } from "./cors.js";
|
|
8
7
|
import { adminKeyMatches, headerValue, isJsonContentType, jsonResponse, opaqueNotFoundResponse, protocolRejectedResponse, readBearer, pathnameIsLogged, redactRoutePath, rejectedResponse, requestHasBody, } from "./http.js";
|
|
9
8
|
export function createHostApp(options) {
|
|
10
9
|
const { module, logger } = options;
|
|
@@ -27,20 +26,18 @@ export function createHostApp(options) {
|
|
|
27
26
|
}
|
|
28
27
|
});
|
|
29
28
|
app.use(async (c, next) => {
|
|
30
|
-
const { incoming,
|
|
29
|
+
const { incoming, role } = c.env;
|
|
31
30
|
if (headerValue(incoming, "origin") !== undefined) {
|
|
32
31
|
const route = pathnameOf(incoming).split("/").filter(Boolean);
|
|
33
|
-
// Only Tenant routes
|
|
34
|
-
//
|
|
32
|
+
// Only Tenant routes of the public listener; the Tenant then accepts only a trusted
|
|
33
|
+
// issuer's token from a browser. The operator listener never serves browsers.
|
|
35
34
|
const tenantRoute = route[0] === "v1" && route[1] !== "admin";
|
|
36
|
-
|
|
37
|
-
if (!options.browserAccess || role === "operator" || !tenantRoute)
|
|
35
|
+
if (role === "operator" || !tenantRoute)
|
|
38
36
|
return rejectedResponse(403, "origin_rejected", "Browser Origin headers are not accepted");
|
|
39
|
-
if (incoming.method === "OPTIONS") {
|
|
40
|
-
c.set("status", answerPreflight(incoming, outgoing, route, browserRoute));
|
|
41
|
-
return RESPONSE_ALREADY_SENT;
|
|
42
|
-
}
|
|
43
37
|
}
|
|
38
|
+
// CORS is the operator's proxy's: an OPTIONS request gets no CORS header.
|
|
39
|
+
if (incoming.method === "OPTIONS")
|
|
40
|
+
return new Response(null, { status: 204, headers: { allow: ALLOWED_METHODS } });
|
|
44
41
|
await next();
|
|
45
42
|
});
|
|
46
43
|
app.use(async (c, next) => {
|
|
@@ -96,11 +93,7 @@ export function createHostApp(options) {
|
|
|
96
93
|
return opaqueNotFoundResponse();
|
|
97
94
|
return await options.admin(c.req.raw, { incoming, outgoing });
|
|
98
95
|
}
|
|
99
|
-
// Tenant routes:
|
|
100
|
-
const keyHeader = headerValue(incoming, PUBLISHABLE_KEY_HEADER);
|
|
101
|
-
const keyTenant = keyHeader === undefined ? undefined : tenantOfPublishableKey(keyHeader);
|
|
102
|
-
if (keyHeader !== undefined && keyTenant === undefined)
|
|
103
|
-
return rejectedResponse(400, "invalid_request", `${PUBLISHABLE_KEY_HEADER} header is malformed`);
|
|
96
|
+
// Tenant routes: protocol → the Tenant → selection → the Tenant's routes.
|
|
104
97
|
const tenantHeader = headerValue(incoming, TENANT_HEADER);
|
|
105
98
|
const named = tenantHeader === undefined || tenantHeader.trim() === "" ? undefined : tenantHeader.trim();
|
|
106
99
|
const protocol = headerValue(incoming, PROTOCOL_HEADER);
|
|
@@ -119,9 +112,8 @@ export function createHostApp(options) {
|
|
|
119
112
|
}
|
|
120
113
|
const { handle } = resolution;
|
|
121
114
|
const tenantId = handle.envelope.id;
|
|
122
|
-
// A protocol 4 client names the Tenant
|
|
123
|
-
|
|
124
|
-
if ((named !== undefined && named !== tenantId) || (keyTenant !== undefined && keyTenant !== tenantId))
|
|
115
|
+
// A protocol 4 client names the Tenant: naming another reached the wrong installation.
|
|
116
|
+
if (named !== undefined && named !== tenantId)
|
|
125
117
|
return opaqueNotFoundResponse();
|
|
126
118
|
c.set("tenantId", tenantId);
|
|
127
119
|
return await handle.fetch(c.req.raw, { incoming, outgoing });
|
|
@@ -154,10 +146,8 @@ export function createHostApp(options) {
|
|
|
154
146
|
});
|
|
155
147
|
return app;
|
|
156
148
|
}
|
|
157
|
-
/**
|
|
158
|
-
|
|
159
|
-
return findTenantRoute(method, segments)?.browser === true;
|
|
160
|
-
}
|
|
149
|
+
/** The methods the Runtime serves, for an `OPTIONS` answer's `Allow`. */
|
|
150
|
+
const ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS";
|
|
161
151
|
/** The Tenant route a request names, if any; a malformed path names none. */
|
|
162
152
|
function tenantRouteOf(incoming) {
|
|
163
153
|
const segments = [];
|
|
@@ -16,4 +16,8 @@ export declare function configForFactory(options: {
|
|
|
16
16
|
model?: TenantConfig["model"];
|
|
17
17
|
/** How Tenants may call Action endpoints (`StackConfig.delivery`). */
|
|
18
18
|
delivery?: TenantConfig["delivery"];
|
|
19
|
+
/** The trusted issuers of the identity file (`NYLORUN_IDENTITY_FILE`). */
|
|
20
|
+
issuers?: TenantConfig["issuers"];
|
|
21
|
+
/** `NYLORUN_PUBLIC_URL`: the base of the MCP OAuth callback (F9 C2). */
|
|
22
|
+
publicUrl?: string;
|
|
19
23
|
}): (id: string) => TenantConfig;
|
package/dist/host/config-for.js
CHANGED
|
@@ -17,6 +17,8 @@ export function configForFactory(options) {
|
|
|
17
17
|
sandbox: { backend: sandboxBackend },
|
|
18
18
|
model: options.model ?? { kind: "vault" },
|
|
19
19
|
...(options.delivery ? { delivery: options.delivery } : {}),
|
|
20
|
+
...(options.issuers ? { issuers: options.issuers } : {}),
|
|
21
|
+
...(options.publicUrl ? { publicUrl: options.publicUrl } : {}),
|
|
20
22
|
childEnv: tenantChildEnvironment(baseline, options.hostConfig, tenant),
|
|
21
23
|
logger: {
|
|
22
24
|
info: (message, fields) => options.logger.info(message, { tenantId: id, ...fields }),
|
package/dist/host/config.d.ts
CHANGED
|
@@ -4,12 +4,6 @@ export interface HostConfigFile {
|
|
|
4
4
|
host: string;
|
|
5
5
|
port: number;
|
|
6
6
|
allowNonLoopback?: boolean;
|
|
7
|
-
/**
|
|
8
|
-
* Whether browser requests (an `Origin` with a publishable key) may reach Tenant routes.
|
|
9
|
-
* Default off for a Host started from host.json; container mode reads
|
|
10
|
-
* `NYLORUN_BROWSER_ACCESS` instead.
|
|
11
|
-
*/
|
|
12
|
-
browserAccess?: boolean;
|
|
13
7
|
/**
|
|
14
8
|
* The operator listener's port: the Admin API moves there (on `host`, loopback only), and
|
|
15
9
|
* `port` serves the Tenant API alone. Absent: one port serves both.
|
|
@@ -24,14 +24,7 @@ export interface CreateHostOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
publicUrl?: string;
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
* routes, where the publishable key's origin allowlist decides. Off by default: every
|
|
29
|
-
* `Origin` is `403 origin_rejected`, as before. Admin routes, `/health` and `/ready` refuse
|
|
30
|
-
* `Origin` either way.
|
|
31
|
-
*/
|
|
32
|
-
browserAccess?: boolean;
|
|
33
|
-
/**
|
|
34
|
-
* The operator listener: the Admin API (and the Tenant API, never with browser access) on
|
|
27
|
+
* The operator listener: the Admin API (and the Tenant API, never to browsers) on
|
|
35
28
|
* its own address, kept off the network that reaches the Tenant API. When set, the main
|
|
36
29
|
* listener is public and answers admin routes with the opaque 404. Absent: one listener
|
|
37
30
|
* serves everything, as before.
|
|
@@ -66,8 +59,8 @@ export interface OperatorListen {
|
|
|
66
59
|
allowedHosts?: readonly string[];
|
|
67
60
|
}
|
|
68
61
|
/**
|
|
69
|
-
* What a listener serves. `combined`: everything (one port). `public`: the Tenant API,
|
|
70
|
-
*
|
|
62
|
+
* What a listener serves. `combined`: everything (one port). `public`: the Tenant API, to
|
|
63
|
+
* servers and to browsers with a trusted issuer's token; admin routes are the opaque 404. `operator`: the Admin API and
|
|
71
64
|
* the Tenant API, never to browsers.
|
|
72
65
|
*/
|
|
73
66
|
export type ListenerRole = "combined" | "public" | "operator";
|
package/dist/host/create-host.js
CHANGED
|
@@ -47,6 +47,10 @@ export function createHost(options) {
|
|
|
47
47
|
status: adminStatusBody,
|
|
48
48
|
shutdown: () => void close(),
|
|
49
49
|
document: adminDocument,
|
|
50
|
+
operatorKeys: async () => {
|
|
51
|
+
const resolved = await module.resolve();
|
|
52
|
+
return resolved.kind === "open" ? resolved.handle.operatorKeys?.() : undefined;
|
|
53
|
+
},
|
|
50
54
|
});
|
|
51
55
|
const app = createHostApp({
|
|
52
56
|
module,
|
|
@@ -55,7 +59,6 @@ export function createHost(options) {
|
|
|
55
59
|
adminKey: credentials.adminKey,
|
|
56
60
|
coreVersion,
|
|
57
61
|
pid,
|
|
58
|
-
browserAccess: options.browserAccess === true,
|
|
59
62
|
...(options.readiness ? { readiness: options.readiness } : {}),
|
|
60
63
|
listening: () => Boolean(server?.listening) && (!operator || Boolean(operatorServer?.listening)),
|
|
61
64
|
closing: () => closing,
|
package/dist/host/gates.js
CHANGED
|
@@ -34,7 +34,12 @@ export async function startGates(options) {
|
|
|
34
34
|
if (!vaults) {
|
|
35
35
|
if (!database || options.hostRoot === undefined)
|
|
36
36
|
throw new Error("startGates needs the Postgres pool and the Host root");
|
|
37
|
-
vaults = createTenantVaults({
|
|
37
|
+
vaults = createTenantVaults({
|
|
38
|
+
sql: database,
|
|
39
|
+
hostRoot: options.hostRoot,
|
|
40
|
+
...(gates.resolver ? { resolver: gates.resolver } : {}),
|
|
41
|
+
...(options.delivery ? { delivery: options.delivery } : {}),
|
|
42
|
+
});
|
|
38
43
|
}
|
|
39
44
|
const blobs = options.blobs ??
|
|
40
45
|
(options.hostRoot !== undefined
|
package/dist/host/main.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Execution and Durable Streams from the endpoints; `createHostExecution`
|
|
10
10
|
* builds the process's execution; the Host serves the one Tenant its Postgres
|
|
11
11
|
* database holds (`NYLORUN_DATABASE_URL`, required), and creates it there on
|
|
12
|
-
* first start (`NYLORUN_TENANT_ID`, `NYLORUN_TENANT_NAME
|
|
13
|
-
* `
|
|
12
|
+
* first start (`NYLORUN_TENANT_ID`, `NYLORUN_TENANT_NAME`; tenancy.md §4), trusting the issuers of the identity file
|
|
13
|
+
* `NYLORUN_IDENTITY_FILE` names (Host feature `trusted-issuers`); with S2, a process running core
|
|
14
14
|
* runs the stream relay, which feeds the Tenant's streams from the record over
|
|
15
15
|
* logical replication (one process at a time holds the slot); `/ready` reports
|
|
16
16
|
* the Tenant and the infrastructure checks. With an Object store
|
|
@@ -29,6 +29,8 @@ import { homedir } from "node:os";
|
|
|
29
29
|
import { resolve } from "node:path";
|
|
30
30
|
import { hostPaths } from "../tenant/paths.js";
|
|
31
31
|
import { createTenantModule } from "../tenant/module.js";
|
|
32
|
+
import { parseIdentityFile } from "../tenant/identity-file.js";
|
|
33
|
+
import { createTrustedIssuers } from "../tenant/issuers.js";
|
|
32
34
|
import { hostPrincipals } from "../tenant/principals.js";
|
|
33
35
|
import { createPostgresTenantOpener } from "../tenant/store-pg.js";
|
|
34
36
|
import { openTenantRuntime } from "../tenant/runtime.js";
|
|
@@ -66,6 +68,21 @@ function coreVersion() {
|
|
|
66
68
|
function loadJson(path) {
|
|
67
69
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
68
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The trusted issuers of the identity file (`NYLORUN_IDENTITY_FILE`, Host feature
|
|
73
|
+
* `trusted-issuers`). A missing or malformed file stops the boot, naming the issuer and field;
|
|
74
|
+
* an issuer's JWKS is fetched only when a token needs it, so one that is down does not.
|
|
75
|
+
*/
|
|
76
|
+
function loadIssuers(path) {
|
|
77
|
+
let text;
|
|
78
|
+
try {
|
|
79
|
+
text = readFileSync(path, "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw new Error(`NYLORUN_IDENTITY_FILE names ${path}, which cannot be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
return createTrustedIssuers(parseIdentityFile(text, path));
|
|
85
|
+
}
|
|
69
86
|
function resolveHostRoot() {
|
|
70
87
|
const fromEnv = process.env.NYLORUN_HOME;
|
|
71
88
|
const root = fromEnv && fromEnv.length > 0 ? fromEnv : resolve(homedir(), ".nylorun");
|
|
@@ -124,6 +141,8 @@ async function runGates(stack) {
|
|
|
124
141
|
...(stack.packing ? { packing: stack.packing } : {}),
|
|
125
142
|
endpoints: describeEndpoints(stack.endpoints),
|
|
126
143
|
objectStore: stack.objectStore ? "s3" : "none",
|
|
144
|
+
// Never the resolver's URL or token: only whether one is set (F9 C1).
|
|
145
|
+
resolver: stack.gates?.resolver ? "configured" : "none",
|
|
127
146
|
});
|
|
128
147
|
const database = createDatabase(stack);
|
|
129
148
|
const servers = [];
|
|
@@ -195,7 +214,14 @@ export async function main() {
|
|
|
195
214
|
}
|
|
196
215
|
const config = loadJson(paths.config);
|
|
197
216
|
const credentials = loadJson(paths.credentials);
|
|
217
|
+
const identityFile = stack.tenant?.identityFile;
|
|
218
|
+
const issuers = identityFile ? loadIssuers(identityFile) : undefined;
|
|
198
219
|
const logger = createHostLogger();
|
|
220
|
+
if (issuers)
|
|
221
|
+
logger.info("trusted_issuers", {
|
|
222
|
+
file: identityFile,
|
|
223
|
+
issuers: issuers.issuers.map((issuer) => issuer.config.name),
|
|
224
|
+
});
|
|
199
225
|
if (stack.deprecatedRole)
|
|
200
226
|
logger.warn("deprecated_flag", {
|
|
201
227
|
flag: "--role",
|
|
@@ -225,6 +251,8 @@ export async function main() {
|
|
|
225
251
|
logger,
|
|
226
252
|
baseline,
|
|
227
253
|
...(stack.delivery ? { delivery: stack.delivery } : {}),
|
|
254
|
+
...(issuers ? { issuers } : {}),
|
|
255
|
+
...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
|
|
228
256
|
});
|
|
229
257
|
// The process's Durable Session Execution: Restate when its endpoints are set, else the
|
|
230
258
|
// in-process memory execution. An invocation that arrives while the Tenant opens waits for it.
|
|
@@ -290,7 +318,7 @@ export async function main() {
|
|
|
290
318
|
harnessImage: stack.sandboxes.harnessImage,
|
|
291
319
|
}
|
|
292
320
|
: undefined;
|
|
293
|
-
const tenantSettings = stack.tenant ?? { name: "default"
|
|
321
|
+
const tenantSettings = stack.tenant ?? { name: "default" };
|
|
294
322
|
const module = createTenantModule({
|
|
295
323
|
open: createPostgresTenantOpener({
|
|
296
324
|
hostRoot,
|
|
@@ -298,10 +326,7 @@ export async function main() {
|
|
|
298
326
|
create: {
|
|
299
327
|
...(tenantSettings.id ? { tenantId: tenantSettings.id } : {}),
|
|
300
328
|
name: tenantSettings.name,
|
|
301
|
-
principals: hostPrincipals({
|
|
302
|
-
adminKey: credentials.adminKey,
|
|
303
|
-
derived: tenantSettings.derivedPrincipals,
|
|
304
|
-
}),
|
|
329
|
+
principals: hostPrincipals({ adminKey: credentials.adminKey }),
|
|
305
330
|
},
|
|
306
331
|
configFor,
|
|
307
332
|
logger,
|
|
@@ -335,9 +360,6 @@ export async function main() {
|
|
|
335
360
|
coreVersion: coreVersion(),
|
|
336
361
|
...(stack.listen ? { listen: stack.listen } : {}),
|
|
337
362
|
...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
|
|
338
|
-
// Container mode (the local stack) allows browsers unless told not to; a Host started
|
|
339
|
-
// from host.json only when it says so. Without publishable keys nothing is reachable.
|
|
340
|
-
browserAccess: stack.browserAccess ?? (stack.listen ? true : config.browserAccess === true),
|
|
341
363
|
// The Admin API on its own listener: from the container environment, or from host.json
|
|
342
364
|
// (loopback, on the same host) when the Host runs outside a container.
|
|
343
365
|
...(stack.operator
|
|
@@ -54,6 +54,14 @@ 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
|
+
};
|
|
57
65
|
}
|
|
58
66
|
/** egress-gate's listener (`NYLORUN_EGRESS_LISTEN_HOST`, `NYLORUN_EGRESS_LISTEN_PORT`). */
|
|
59
67
|
export interface EgressConfig {
|
|
@@ -169,11 +177,6 @@ export interface StackConfig {
|
|
|
169
177
|
* means nothing outside the container.
|
|
170
178
|
*/
|
|
171
179
|
publicUrl?: string;
|
|
172
|
-
/**
|
|
173
|
-
* `NYLORUN_BROWSER_ACCESS` (`on` or `off`): whether browser requests may reach Tenant
|
|
174
|
-
* routes. Absent means the Host's default (on in container mode).
|
|
175
|
-
*/
|
|
176
|
-
browserAccess?: boolean;
|
|
177
180
|
/**
|
|
178
181
|
* The operator listener in container mode (`NYLORUN_ADMIN_LISTEN_PORT`, `…_HOST`,
|
|
179
182
|
* `…_ALLOWED_HOSTS`). Absent: one listener serves the Admin API and the Tenant API.
|
|
@@ -221,10 +224,10 @@ export interface TenantSettings {
|
|
|
221
224
|
/** `NYLORUN_TENANT_NAME`. Default `default`. */
|
|
222
225
|
name: string;
|
|
223
226
|
/**
|
|
224
|
-
* `
|
|
225
|
-
*
|
|
227
|
+
* `NYLORUN_IDENTITY_FILE`: the identity file listing the trusted issuers (Host feature
|
|
228
|
+
* `trusted-issuers`), read once at boot (`host/main.ts`). Absent: no issuer is trusted.
|
|
226
229
|
*/
|
|
227
|
-
|
|
230
|
+
identityFile?: string;
|
|
228
231
|
}
|
|
229
232
|
export declare class StackConfigError extends Error {
|
|
230
233
|
constructor(message: string);
|
|
@@ -15,6 +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
20
|
*
|
|
19
21
|
* `harness` (F6.2) runs alone: it holds the harness credential and nothing else. It connects to
|
|
20
22
|
* core's Harness API listener (`NYLORUN_HARNESS_URL`, `NYLORUN_HARNESS_TOKEN`), calls models and
|
|
@@ -34,9 +36,9 @@
|
|
|
34
36
|
* The Postgres, Restate and S2 endpoints are parsed and validated here;
|
|
35
37
|
* `infra/*` builds the clients from them. So is the Object store (`NYLORUN_OBJECT_STORE_*`),
|
|
36
38
|
* which every service may read; `host/main.ts` builds the `BlobStore` from it. So is who the Host's Tenant is when its database
|
|
37
|
-
* holds none yet (`NYLORUN_TENANT_ID`, `NYLORUN_TENANT_NAME
|
|
39
|
+
* holds none yet (`NYLORUN_TENANT_ID`, `NYLORUN_TENANT_NAME`).
|
|
38
40
|
*/
|
|
39
|
-
import {
|
|
41
|
+
import { isTenantId } from "@nylorun/core/compatibility";
|
|
40
42
|
import { blockedAddresses } from "../sandbox/pods/network-gate.js";
|
|
41
43
|
export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys", "egress", "harness"];
|
|
42
44
|
/**
|
|
@@ -318,9 +320,6 @@ export function parseStackConfig(env, argv) {
|
|
|
318
320
|
if (restateIdentityKeys)
|
|
319
321
|
endpoints.restateIdentityKeys = restateIdentityKeys;
|
|
320
322
|
const publicUrl = parseUrl(env, "NYLORUN_PUBLIC_URL", http)?.replace(/\/+$/, "");
|
|
321
|
-
const rawBrowser = read(env, "NYLORUN_BROWSER_ACCESS");
|
|
322
|
-
if (rawBrowser !== undefined && rawBrowser !== "on" && rawBrowser !== "off")
|
|
323
|
-
throw new StackConfigError(`NYLORUN_BROWSER_ACCESS must be on or off, not ${rawBrowser}`);
|
|
324
323
|
const harnessMode = servesApi ? parseHarnessMode(env) : undefined;
|
|
325
324
|
const sandboxes = servesApi ? parseSandboxes(env) : undefined;
|
|
326
325
|
// Pods connect to the Harness API listener even when the Tenant runs its own harness.
|
|
@@ -344,7 +343,6 @@ export function parseStackConfig(env, argv) {
|
|
|
344
343
|
...(listen ? { listen } : {}),
|
|
345
344
|
endpoints,
|
|
346
345
|
...(publicUrl ? { publicUrl } : {}),
|
|
347
|
-
...(rawBrowser === undefined ? {} : { browserAccess: rawBrowser === "on" }),
|
|
348
346
|
...(harnessMode ? { harnessMode } : {}),
|
|
349
347
|
...(harnessListener ? { harnessListener } : {}),
|
|
350
348
|
...(sandboxes ? { sandboxes } : {}),
|
|
@@ -380,15 +378,11 @@ function parseTenant(env) {
|
|
|
380
378
|
const id = read(env, "NYLORUN_TENANT_ID");
|
|
381
379
|
if (id !== undefined && !isTenantId(id))
|
|
382
380
|
throw new StackConfigError(`NYLORUN_TENANT_ID must be a Tenant id (tn_ and 26 Crockford characters), not ${id}`);
|
|
383
|
-
const
|
|
384
|
-
const derivedPrincipals = raw === undefined ? ["project"] : raw.split(",").map((entry) => entry.trim());
|
|
385
|
-
for (const principal of derivedPrincipals)
|
|
386
|
-
if (!DERIVED_PRINCIPAL_ID_PATTERN.test(principal) || principal === "studio")
|
|
387
|
-
throw new StackConfigError(`NYLORUN_DERIVED_PRINCIPALS has '${principal}': each entry must match ${DERIVED_PRINCIPAL_ID_PATTERN} and not be studio`);
|
|
381
|
+
const identityFile = read(env, "NYLORUN_IDENTITY_FILE");
|
|
388
382
|
return {
|
|
389
383
|
...(id ? { id } : {}),
|
|
390
384
|
name: read(env, "NYLORUN_TENANT_NAME") ?? "default",
|
|
391
|
-
|
|
385
|
+
...(identityFile ? { identityFile } : {}),
|
|
392
386
|
};
|
|
393
387
|
}
|
|
394
388
|
const GATES_TOKEN = /^[0-9a-f]{64,}$/i;
|
|
@@ -542,11 +536,28 @@ function parseGates(env) {
|
|
|
542
536
|
throw new StackConfigError("NYLORUN_GATES_TOKEN is required for --service gates: the token the loop presents (`nylorun start` sets it)");
|
|
543
537
|
if (!GATES_TOKEN.test(token))
|
|
544
538
|
throw new StackConfigError("NYLORUN_GATES_TOKEN must be at least 32 bytes as hex");
|
|
539
|
+
const resolver = parseResolver(env);
|
|
545
540
|
return {
|
|
546
541
|
listen: { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] },
|
|
547
542
|
token,
|
|
543
|
+
...(resolver ? { resolver } : {}),
|
|
548
544
|
};
|
|
549
545
|
}
|
|
546
|
+
/** `GatesConfig.resolver` from `NYLORUN_RESOLVER_URL` and `NYLORUN_RESOLVER_TOKEN`: both or neither. */
|
|
547
|
+
function parseResolver(env) {
|
|
548
|
+
const url = parseUrl(env, "NYLORUN_RESOLVER_URL", ["http:", "https:"]);
|
|
549
|
+
const token = read(env, "NYLORUN_RESOLVER_TOKEN");
|
|
550
|
+
if (url === undefined) {
|
|
551
|
+
if (token !== undefined)
|
|
552
|
+
throw new StackConfigError("NYLORUN_RESOLVER_TOKEN is set without NYLORUN_RESOLVER_URL: set the URL of your credential resolver");
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
if (token === undefined)
|
|
556
|
+
throw new StackConfigError("NYLORUN_RESOLVER_TOKEN is required with NYLORUN_RESOLVER_URL: the bearer the gateway presents to your credential resolver");
|
|
557
|
+
if (/\s/.test(token))
|
|
558
|
+
throw new StackConfigError("NYLORUN_RESOLVER_TOKEN cannot contain whitespace");
|
|
559
|
+
return { url, token };
|
|
560
|
+
}
|
|
550
561
|
/** `StackConfig.egress` from `NYLORUN_EGRESS_LISTEN_*`. CONNECT has no Host header to check. */
|
|
551
562
|
function parseEgress(env) {
|
|
552
563
|
const host = read(env, "NYLORUN_EGRESS_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { openTenantRuntime, type TenantOpenHooks, } from "./tenant/runtime.js";
|
|
2
2
|
export { startEphemeralRuntime, type StartEphemeralRuntimeOptions, type EphemeralRuntime, } from "./tenant/ephemeral.js";
|
|
3
|
+
export { parseIdentityFile, IdentityFileError, type TrustedIssuerConfig, } from "./tenant/identity-file.js";
|
|
4
|
+
export { createTrustedIssuers, type TrustedIssuers, type TrustedIssuersOptions, } from "./tenant/issuers.js";
|
|
3
5
|
export type { TenantConfig, TenantHandle, TenantPaths, Logger, OpenTenantRuntime, } from "./tenant/types.js";
|
|
4
6
|
export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
|
|
7
|
+
export type { ResolverConfig } from "./vault/sources.js";
|
|
5
8
|
export type { RuntimeAgent } from "./contracts.js";
|
|
6
9
|
export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
|
|
7
10
|
export type { RuntimeModelAdapter } from "./contracts.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { openTenantRuntime, } from "./tenant/runtime.js";
|
|
2
2
|
export { startEphemeralRuntime, } from "./tenant/ephemeral.js";
|
|
3
|
+
export { parseIdentityFile, IdentityFileError, } from "./tenant/identity-file.js";
|
|
4
|
+
export { createTrustedIssuers, } from "./tenant/issuers.js";
|
|
3
5
|
export { scriptedModel, gatewayModel } from "./core/provider.js";
|
|
4
6
|
export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
|
|
5
7
|
export { baselineEnvironment, hostProcessEnvironment, tenantChildEnvironment, } from "./host/environment.js";
|
package/dist/keys/client.js
CHANGED
|
@@ -12,10 +12,18 @@ 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
|
+
};
|
|
15
23
|
export function httpKeys(options) {
|
|
16
24
|
const where = new URL(options.url).origin;
|
|
17
|
-
const timeoutMs = options.timeoutMs ?? KEYS_TIMEOUT_MS;
|
|
18
25
|
function call(operation, args) {
|
|
26
|
+
const timeoutMs = Math.max(options.timeoutMs ?? KEYS_TIMEOUT_MS, SLOW_OPERATIONS[operation] ?? 0);
|
|
19
27
|
const url = new URL(`${KEYS_PATH}/${operation}`, options.url);
|
|
20
28
|
const send = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
21
29
|
const payload = Buffer.from(JSON.stringify({ args }), "utf8");
|
package/dist/keys/keys.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CreateCredentialRequest, CredentialInfo, HostModelView, PutHostModelRequest, RotateCredentialRequest, SelectHostModelRequest, SigningKeyView } from "@nylorun/core/contracts";
|
|
2
2
|
import type { SessionStore } from "../store/types.js";
|
|
3
3
|
import { type SigningKeys } from "../tenant/signing-keys.js";
|
|
4
|
-
import type { VaultService } from "../vault/service.js";
|
|
4
|
+
import type { FinishOAuthInput, StartOAuthInput, VaultService } from "../vault/service.js";
|
|
5
5
|
/** One JWT to sign: its `typ` and every claim, `iss`, `aud`, `sub`, `iat`, `exp` and `jti` included. */
|
|
6
6
|
export interface SignRequest {
|
|
7
7
|
readonly typ: string;
|
|
@@ -26,9 +26,19 @@ export interface Keys {
|
|
|
26
26
|
}): Promise<SigningKeyView[]>;
|
|
27
27
|
/** Creates the current and standby keys when the Tenant has none. */
|
|
28
28
|
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
|
+
}>;
|
|
29
39
|
}
|
|
30
40
|
/** The operations, by the name the HTTP route carries. */
|
|
31
|
-
export declare const KEYS_OPERATIONS: readonly ["createCredential", "rotateCredential", "putHostModel", "selectHostModel", "sign", "rotateSigningKeys", "ensureSigningKeys"];
|
|
41
|
+
export declare const KEYS_OPERATIONS: readonly ["createCredential", "rotateCredential", "putHostModel", "selectHostModel", "sign", "rotateSigningKeys", "ensureSigningKeys", "startOAuth", "finishOAuth"];
|
|
32
42
|
export type KeysOperation = (typeof KEYS_OPERATIONS)[number];
|
|
33
43
|
export interface InProcessKeysOptions {
|
|
34
44
|
readonly store: SessionStore;
|
package/dist/keys/keys.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* delivery 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.
|
|
11
|
+
*
|
|
8
12
|
* Two implementations: `inProcessKeys` (embedding, the ephemeral Runtime, tests, and the gates
|
|
9
13
|
* service itself, which serves the HTTP routes with it), and `httpKeys` (`client.ts`), the
|
|
10
14
|
* runtime container's client of the `keys` service in the gateway.
|
|
@@ -20,6 +24,8 @@ export const KEYS_OPERATIONS = [
|
|
|
20
24
|
"sign",
|
|
21
25
|
"rotateSigningKeys",
|
|
22
26
|
"ensureSigningKeys",
|
|
27
|
+
"startOAuth",
|
|
28
|
+
"finishOAuth",
|
|
23
29
|
];
|
|
24
30
|
export function inProcessKeys(options) {
|
|
25
31
|
const { store, vault, signingKeys, kek } = options;
|
|
@@ -28,6 +34,8 @@ export function inProcessKeys(options) {
|
|
|
28
34
|
rotateCredential: (vaultId, id, body) => vault.rotateCredential(vaultId, id, body),
|
|
29
35
|
putHostModel: (body) => vault.putHostModel(body),
|
|
30
36
|
selectHostModel: (body) => vault.selectHostModel(body),
|
|
37
|
+
startOAuth: (input) => vault.startOAuth(input),
|
|
38
|
+
finishOAuth: (input) => vault.finishOAuth(input),
|
|
31
39
|
async sign(request) {
|
|
32
40
|
const key = kek();
|
|
33
41
|
const row = await store.tx((t) => signingKeys.ensure(t, key));
|
package/dist/mcp/connect.js
CHANGED
|
@@ -162,7 +162,8 @@ function authorizedFetch(server, authorize) {
|
|
|
162
162
|
headers.set(key, value);
|
|
163
163
|
new Headers(init?.headers).forEach((value, key) => headers.set(key, value));
|
|
164
164
|
if (result.status === "authorized")
|
|
165
|
-
|
|
165
|
+
for (const [key, value] of Object.entries(result.headers))
|
|
166
|
+
headers.set(key, value);
|
|
166
167
|
return fetch(target, { ...init, headers, redirect: "error" });
|
|
167
168
|
};
|
|
168
169
|
}
|
package/dist/mcp/pool.d.ts
CHANGED