@nylorun/runtime 0.14.0-beta → 0.15.1-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +82 -40
- package/dist/adapters/execution/restate.js +2 -2
- package/dist/adapters/replication/pgoutput.d.ts +5 -0
- package/dist/adapters/replication/pgoutput.js +4 -3
- package/dist/admin-openapi.json +185 -686
- package/dist/api/components.d.ts +308 -68
- package/dist/api/components.js +4 -5
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/define.js +0 -8
- package/dist/api/http/routes/access.js +25 -18
- package/dist/api/http/routes/tenant.d.ts +2 -1
- package/dist/api/http/routes/tenant.js +77 -5
- package/dist/api/http/routes/vaults.js +2 -2
- package/dist/api/openapi.js +6 -7
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +7 -6
- package/dist/host/admin-api.js +9 -113
- package/dist/host/app.d.ts +1 -1
- package/dist/host/app.js +20 -24
- package/dist/host/config-for.d.ts +1 -1
- package/dist/host/config-for.js +2 -2
- package/dist/host/create-host.d.ts +5 -5
- package/dist/host/create-host.js +5 -15
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +6 -1
- package/dist/host/http.js +17 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +151 -71
- package/dist/host/stack-config.d.ts +71 -19
- package/dist/host/stack-config.js +192 -18
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +21 -3
- package/dist/mcp/pool.js +15 -2
- package/dist/openapi.json +888 -695
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/{store/event.js → record/envelope.js} +1 -1
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/store/postgres/connect.d.ts +15 -12
- package/dist/store/postgres/connect.js +24 -12
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +2 -8
- package/dist/store/postgres/record.js +50 -54
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +481 -567
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +43 -87
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +1 -1
- package/dist/streams/control.js +6 -2
- package/dist/streams/relay/types.d.ts +2 -1
- package/dist/streams/relay/types.js +2 -1
- package/dist/streams/types.d.ts +5 -0
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +95 -16
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.js +4 -2
- package/dist/tenant/context.d.ts +26 -3
- package/dist/tenant/delivery-token.js +17 -18
- package/dist/tenant/delivery.d.ts +1 -1
- package/dist/tenant/delivery.js +18 -13
- package/dist/tenant/effects.d.ts +15 -1
- package/dist/tenant/effects.js +93 -17
- package/dist/tenant/ephemeral.d.ts +16 -7
- package/dist/tenant/ephemeral.js +45 -111
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +14 -28
- package/dist/tenant/module.js +131 -271
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -1
- package/dist/tenant/pool.js +1 -1
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/runtime.d.ts +19 -1
- package/dist/tenant/runtime.js +40 -26
- package/dist/tenant/scheduler.d.ts +4 -2
- package/dist/tenant/scheduler.js +4 -4
- package/dist/tenant/session-streams.d.ts +2 -0
- package/dist/tenant/session-streams.js +8 -3
- package/dist/tenant/status.d.ts +3 -0
- package/dist/tenant/status.js +1 -1
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +9 -25
- package/dist/tenant/streams.js +7 -22
- package/dist/tenant/tokens.js +16 -14
- package/dist/tenant/types.d.ts +41 -92
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.js +4 -4
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +26 -15
- package/dist/vault/service.js +70 -109
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -5
- package/dist/store/memory.d.ts +0 -69
- package/dist/store/memory.js +0 -810
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
- package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
- package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
- package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
- package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
- package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
- package/dist/store/postgres/migrations/007_event_time.js +0 -14
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
- package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
- package/dist/store/postgres/migrations/index.d.ts +0 -53
- package/dist/store/postgres/migrations/index.js +0 -96
- package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
- package/dist/store/postgres/migrations/shared/index.js +0 -92
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -228
- package/dist/streams/relay/memory.d.ts +0 -69
- package/dist/streams/relay/memory.js +0 -176
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
- /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
package/dist/host/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
|
|
3
|
-
import { checkCompatibility, HOST_PROTOCOL,
|
|
3
|
+
import { checkCompatibility, HOST_PROTOCOL, PROTOCOL_HEADER, PUBLISHABLE_KEY_HEADER, TENANT_HEADER, tenantOfPublishableKey, } 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";
|
|
@@ -69,13 +69,13 @@ export function createHostApp(options) {
|
|
|
69
69
|
});
|
|
70
70
|
if (pathname === "/ready") {
|
|
71
71
|
const listener = options.listening();
|
|
72
|
-
const
|
|
72
|
+
const tenant = module.ready;
|
|
73
73
|
const infra = await options.readiness?.();
|
|
74
|
-
const ready = listener &&
|
|
74
|
+
const ready = listener && tenant && !options.closing() && (infra?.ok ?? true);
|
|
75
75
|
return jsonResponse(ready ? 200 : 503, {
|
|
76
76
|
status: ready ? "ready" : "not_ready",
|
|
77
77
|
service: "nylorun-runtime",
|
|
78
|
-
checks: { listener,
|
|
78
|
+
checks: { listener, tenant, ...infra?.checks },
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
// This API's own description: public, as the npm package that ships it.
|
|
@@ -93,36 +93,32 @@ export function createHostApp(options) {
|
|
|
93
93
|
return opaqueNotFoundResponse();
|
|
94
94
|
return await options.admin(c.req.raw, { incoming, outgoing });
|
|
95
95
|
}
|
|
96
|
-
// Tenant routes:
|
|
97
|
-
// `Nylorun-Tenant`, by the publishable key in `Nylorun-Key`, or by both when they agree.
|
|
98
|
-
const invalid = (message) => rejectedResponse(400, "invalid_request", message);
|
|
96
|
+
// Tenant routes: key pattern → protocol → the Tenant → selection → the Tenant's routes.
|
|
99
97
|
const keyHeader = headerValue(incoming, PUBLISHABLE_KEY_HEADER);
|
|
100
98
|
const keyTenant = keyHeader === undefined ? undefined : tenantOfPublishableKey(keyHeader);
|
|
101
99
|
if (keyHeader !== undefined && keyTenant === undefined)
|
|
102
|
-
return
|
|
100
|
+
return rejectedResponse(400, "invalid_request", `${PUBLISHABLE_KEY_HEADER} header is malformed`);
|
|
103
101
|
const tenantHeader = headerValue(incoming, TENANT_HEADER);
|
|
104
|
-
const named = tenantHeader === undefined || tenantHeader.trim() === "" ? undefined : tenantHeader;
|
|
105
|
-
if (named === undefined && keyTenant === undefined)
|
|
106
|
-
return invalid(`${TENANT_HEADER} header is required`);
|
|
107
|
-
if (named !== undefined && !isTenantId(named))
|
|
108
|
-
return invalid(`${TENANT_HEADER} header is malformed`);
|
|
109
|
-
if (named !== undefined && keyTenant !== undefined && named !== keyTenant)
|
|
110
|
-
return invalid(`${PUBLISHABLE_KEY_HEADER} and ${TENANT_HEADER} name different Tenants`);
|
|
111
|
-
const tenantId = (named ?? keyTenant);
|
|
112
|
-
c.set("tenantId", tenantId);
|
|
102
|
+
const named = tenantHeader === undefined || tenantHeader.trim() === "" ? undefined : tenantHeader.trim();
|
|
113
103
|
if (!protocolAccepted(headerValue(incoming, PROTOCOL_HEADER)))
|
|
114
104
|
return protocolRejectedResponse();
|
|
115
|
-
const resolution = await module.resolve(
|
|
105
|
+
const resolution = await module.resolve();
|
|
116
106
|
if (resolution.kind !== "open") {
|
|
117
|
-
if (resolution.
|
|
118
|
-
logger.warn("
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
repair: resolution.quarantine.repair,
|
|
107
|
+
if (resolution.cause)
|
|
108
|
+
logger.warn("tenant_unavailable", {
|
|
109
|
+
code: resolution.cause.code,
|
|
110
|
+
repair: resolution.cause.repair,
|
|
122
111
|
});
|
|
123
112
|
return opaqueNotFoundResponse();
|
|
124
113
|
}
|
|
125
|
-
|
|
114
|
+
const { handle } = resolution;
|
|
115
|
+
const tenantId = handle.envelope.id;
|
|
116
|
+
// A protocol 4 client names the Tenant; a publishable key carries one. Either naming
|
|
117
|
+
// another Tenant (or a malformed header) reached the wrong installation.
|
|
118
|
+
if ((named !== undefined && named !== tenantId) || (keyTenant !== undefined && keyTenant !== tenantId))
|
|
119
|
+
return opaqueNotFoundResponse();
|
|
120
|
+
c.set("tenantId", tenantId);
|
|
121
|
+
return await handle.fetch(c.req.raw, { incoming, outgoing });
|
|
126
122
|
});
|
|
127
123
|
app.onError((error, c) => {
|
|
128
124
|
const status = error.status;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { HostConfigFile } from "./config.js";
|
|
2
2
|
import type { Logger, TenantConfig } from "../tenant/types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Builds
|
|
4
|
+
* Builds the TenantConfig of the Host's Tenant for `openTenantRuntime`.
|
|
5
5
|
* The sandbox backend here is the Host default; a Tenant's seeded `sandbox.backend`
|
|
6
6
|
* setting overrides it when the Tenant opens (I1, A18).
|
|
7
7
|
*/
|
package/dist/host/config-for.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { tenantPaths } from "../tenant/paths.js";
|
|
2
2
|
import { tenantChildEnvironment } from "./environment.js";
|
|
3
3
|
/**
|
|
4
|
-
* Builds
|
|
4
|
+
* Builds the TenantConfig of the Host's Tenant for `openTenantRuntime`.
|
|
5
5
|
* The sandbox backend here is the Host default; a Tenant's seeded `sandbox.backend`
|
|
6
6
|
* setting overrides it when the Tenant opens (I1, A18).
|
|
7
7
|
*/
|
|
8
8
|
export function configForFactory(options) {
|
|
9
9
|
const { baseline } = options;
|
|
10
10
|
return (id) => {
|
|
11
|
-
const tenant = tenantPaths(options.hostRoot
|
|
11
|
+
const tenant = tenantPaths(options.hostRoot);
|
|
12
12
|
const sandboxBackend = "auto";
|
|
13
13
|
return {
|
|
14
14
|
tenantId: id,
|
|
@@ -39,18 +39,18 @@ export interface CreateHostOptions {
|
|
|
39
39
|
operator?: OperatorListen;
|
|
40
40
|
/**
|
|
41
41
|
* Infrastructure readiness (`infra/readiness.ts`). `/ready` adds its checks
|
|
42
|
-
* and answers 503 while it reports not ok. Default: listener and
|
|
43
|
-
* only.
|
|
42
|
+
* and answers 503 while it reports not ok. Default: the listener and the
|
|
43
|
+
* Tenant only.
|
|
44
44
|
*/
|
|
45
45
|
readiness?: () => Promise<{
|
|
46
46
|
ok: boolean;
|
|
47
47
|
checks: Record<string, boolean>;
|
|
48
48
|
}>;
|
|
49
49
|
/**
|
|
50
|
-
* Shutdown steps around closing the
|
|
50
|
+
* Shutdown steps around closing the Tenant. `close()` runs them whatever asked for it
|
|
51
51
|
* (SIGTERM in `host/main.ts`, `POST /v1/admin/host/shutdown`): the listener stops, then
|
|
52
|
-
* `beforeTenants` (stop the Worker so no advance starts on
|
|
53
|
-
*
|
|
52
|
+
* `beforeTenants` (stop the Worker so no advance starts on the closing Tenant), the Tenant
|
|
53
|
+
* closes, then `afterTenants` (end the infrastructure clients). A failing step is logged
|
|
54
54
|
* and shutdown goes on.
|
|
55
55
|
*/
|
|
56
56
|
shutdown?: {
|
package/dist/host/create-host.js
CHANGED
|
@@ -5,7 +5,7 @@ import { AdminStatusSchema } from "@nylorun/core/contracts";
|
|
|
5
5
|
import { adminDocument } from "../api/openapi.js";
|
|
6
6
|
import { createAdminApi } from "./admin-api.js";
|
|
7
7
|
import { createHostApp } from "./app.js";
|
|
8
|
-
import {
|
|
8
|
+
import { bindListener as bind, EXIT_NON_LOOPBACK, headerValue, HostListenError, isAllowedRequestHost, isLoopbackHost, pathnameIsLogged, redactRoutePath, rejectedResponse, sendRejected, } from "./http.js";
|
|
9
9
|
import { RUNTIME_VERSION } from "../version.js";
|
|
10
10
|
export { adminKeyMatches } from "./http.js";
|
|
11
11
|
export function createHost(options) {
|
|
@@ -25,7 +25,6 @@ export function createHost(options) {
|
|
|
25
25
|
let closing = false;
|
|
26
26
|
let closePromise;
|
|
27
27
|
const adminStatusBody = async () => {
|
|
28
|
-
const tenants = await module.list();
|
|
29
28
|
const aggregate = await module.summarize();
|
|
30
29
|
return AdminStatusSchema.parse({
|
|
31
30
|
service: "nylorun-runtime",
|
|
@@ -35,7 +34,7 @@ export function createHost(options) {
|
|
|
35
34
|
max: HOST_PROTOCOL.max,
|
|
36
35
|
features: [...HOST_PROTOCOL.features],
|
|
37
36
|
},
|
|
38
|
-
|
|
37
|
+
tenant: module.tenant(),
|
|
39
38
|
aggregate,
|
|
40
39
|
host: {
|
|
41
40
|
hostId: config.hostId,
|
|
@@ -45,7 +44,6 @@ export function createHost(options) {
|
|
|
45
44
|
});
|
|
46
45
|
};
|
|
47
46
|
const adminApi = createAdminApi({
|
|
48
|
-
module,
|
|
49
47
|
status: adminStatusBody,
|
|
50
48
|
shutdown: () => void close(),
|
|
51
49
|
document: adminDocument,
|
|
@@ -114,17 +112,7 @@ export function createHost(options) {
|
|
|
114
112
|
};
|
|
115
113
|
};
|
|
116
114
|
/** Binds `listening`; once bound, a listener error is logged rather than lost. */
|
|
117
|
-
const bindListener = (listening, port, host) =>
|
|
118
|
-
const failed = (error) => reject(error.code === "EADDRINUSE"
|
|
119
|
-
? new HostListenError(`Port ${port} on ${host} is already in use`, EXIT_PORT_IN_USE, error)
|
|
120
|
-
: error);
|
|
121
|
-
listening.once("error", failed);
|
|
122
|
-
listening.listen(port, host, () => {
|
|
123
|
-
listening.off("error", failed);
|
|
124
|
-
listening.on("error", (error) => logger.error("listener_error", { error: error.message }));
|
|
125
|
-
resolve();
|
|
126
|
-
});
|
|
127
|
-
});
|
|
115
|
+
const bindListener = (listening, port, host) => bind(listening, port, host, (error) => logger.error("listener_error", { error: error.message }));
|
|
128
116
|
async function listen() {
|
|
129
117
|
if (server)
|
|
130
118
|
throw new Error("Already listening");
|
|
@@ -159,6 +147,8 @@ export function createHost(options) {
|
|
|
159
147
|
: operator.port;
|
|
160
148
|
adminUrl = `http://${operator.host}:${operatorPort}`;
|
|
161
149
|
}
|
|
150
|
+
// The Tenant opens once the listener is bound: `/ready` and `/v1/admin/status` answer
|
|
151
|
+
// while it opens, and say why when it cannot.
|
|
162
152
|
await module.start();
|
|
163
153
|
logger.info("host_listening", {
|
|
164
154
|
hostId: config.hostId,
|
package/dist/host/execution.d.ts
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Host's Durable Session Execution (architecture §6, §12.3, §14.8): one per process,
|
|
3
|
-
*
|
|
3
|
+
* for the Tenant the Host serves.
|
|
4
4
|
*
|
|
5
|
-
* - `tenantExecution` is what the Host passes to
|
|
5
|
+
* - `tenantExecution` is what the Host passes to its Tenant when it opens it
|
|
6
6
|
* (`TenantOpenHooks.execution`): the execution, the `TenantWorkers` registry its handlers
|
|
7
|
-
* dispatch through (with `resolve`
|
|
8
|
-
*
|
|
9
|
-
* - `start` serves the Worker endpoint
|
|
10
|
-
* nothing for
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - `
|
|
7
|
+
* dispatch through (with `resolve` waiting for the Tenant when an invocation arrives while
|
|
8
|
+
* it opens), and the Tenant status hook for stuck invocations.
|
|
9
|
+
* - `start` serves the Worker endpoint when the process runs loop (`infra/workers.ts`), and does
|
|
10
|
+
* nothing for a core-only process, whose wakes, timers and sweep arming go through Restate's
|
|
11
|
+
* ingress.
|
|
12
|
+
* - `armAll` arms the listed Tenants' sweeps (tests, recovery). Arming is idempotent; opening
|
|
13
|
+
* the Tenant arms its own at every start, which is what recovers from lost Restate state:
|
|
14
|
+
* the sweep re-wakes `runnable` sessions without a live owner (§14.8).
|
|
15
|
+
* - `disarm` stops a Tenant's sweep.
|
|
15
16
|
*
|
|
16
|
-
* **Order at startup.** Call `start` before opening
|
|
17
|
+
* **Order at startup.** Call `start` before opening the Tenant. Opening the Tenant arms its
|
|
17
18
|
* sweep through Restate's ingress, which answers 404 until a Worker has registered the
|
|
18
|
-
* services, so the open would fail.
|
|
19
|
+
* services, so the open would fail. A core-only process relies on a Worker having registered
|
|
19
20
|
* them first.
|
|
20
21
|
*
|
|
21
22
|
* **One Worker URL.** `start` registers the Worker endpoint (`NYLORUN_WORKER_URL`) with
|
|
22
23
|
* Restate once. Restate sends new invocations to the latest registered deployment, so
|
|
23
24
|
* several Workers that each register their own URL do not share load: the last one to start
|
|
24
|
-
* takes every new invocation. The
|
|
25
|
-
* `--
|
|
25
|
+
* takes every new invocation. The local stack runs a single `runtime` container with
|
|
26
|
+
* `--service core,loop`. A deployment with several Worker processes (Helm, Cloud) must advertise one
|
|
26
27
|
* load-balanced `NYLORUN_WORKER_URL` shared by all of them.
|
|
27
28
|
*/
|
|
28
29
|
import type { DurableExecution } from "../execution/types.js";
|
|
29
30
|
import type { Logger } from "../tenant/types.js";
|
|
30
31
|
import { type TenantExecution, type TenantWorker } from "../tenant/worker.js";
|
|
31
|
-
import type {
|
|
32
|
+
import type { RuntimeServices } from "./stack-config.js";
|
|
32
33
|
export interface CreateHostExecutionOptions {
|
|
33
34
|
execution: DurableExecution;
|
|
34
|
-
|
|
35
|
+
/** The services this process runs: it serves the Worker endpoint when they include loop. */
|
|
36
|
+
services: RuntimeServices;
|
|
35
37
|
/** Finds or opens a Tenant an invocation arrived for that is not open on this process. */
|
|
36
38
|
resolve: (tenantId: string) => Promise<TenantWorker | undefined>;
|
|
37
39
|
logger?: Logger;
|
|
@@ -44,9 +46,9 @@ export interface CreateHostExecutionOptions {
|
|
|
44
46
|
advanceGraceMs?: number;
|
|
45
47
|
}
|
|
46
48
|
export interface HostExecution {
|
|
47
|
-
/** Pass to
|
|
49
|
+
/** Pass to the Tenant the Host opens (`TenantOpenHooks.execution`). */
|
|
48
50
|
readonly tenantExecution: TenantExecution;
|
|
49
|
-
/** Serves the Worker endpoint
|
|
51
|
+
/** Serves the Worker endpoint when the process runs loop. Idempotent. */
|
|
50
52
|
start(): Promise<void>;
|
|
51
53
|
/** Stops serving: aborts running advances and waits for them. Idempotent. */
|
|
52
54
|
stop(): Promise<void>;
|
package/dist/host/execution.js
CHANGED
|
@@ -3,7 +3,7 @@ import { TenantWorkers, } from "../tenant/worker.js";
|
|
|
3
3
|
/** How many sweep arms `armAll` sends at once. */
|
|
4
4
|
const ARM_CONCURRENCY = 16;
|
|
5
5
|
export function createHostExecution(options) {
|
|
6
|
-
const { execution,
|
|
6
|
+
const { execution, services, logger } = options;
|
|
7
7
|
const workers = new TenantWorkers({
|
|
8
8
|
resolve: options.resolve,
|
|
9
9
|
...(options.advanceDeadlineMs !== undefined
|
|
@@ -27,7 +27,7 @@ export function createHostExecution(options) {
|
|
|
27
27
|
return {
|
|
28
28
|
tenantExecution,
|
|
29
29
|
async start() {
|
|
30
|
-
started ??= startWorker({
|
|
30
|
+
started ??= startWorker({ services, execution, handlers: workers.handlers });
|
|
31
31
|
try {
|
|
32
32
|
await started;
|
|
33
33
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gates service's process (blueprint §15, §19): one listener serving the Model Gate's and
|
|
3
|
+
* the Tool Gate's routes (`api/gate/routes.ts`), behind the same `Host` check as the Runtime's
|
|
4
|
+
* listeners.
|
|
5
|
+
* It opens no Tenant runtime, no Restate endpoint and no stream relay, and runs no migration;
|
|
6
|
+
* it needs only the Postgres pool and the Host's tenant directory (read-only).
|
|
7
|
+
*
|
|
8
|
+
* Nothing is written while a model call runs, so the server's request timeout sits above the
|
|
9
|
+
* gate's longest call. Closing stops accepting calls, lets running ones finish for up to
|
|
10
|
+
* `drainMs`, then aborts them (their providers' requests with them).
|
|
11
|
+
*/
|
|
12
|
+
import { type Server } from "node:http";
|
|
13
|
+
import { type InflightCallsOptions } from "../gates/inflight.js";
|
|
14
|
+
import type { openMcpServer } from "../mcp/connect.js";
|
|
15
|
+
import type { OutboundPolicy } from "../tenant/outbound.js";
|
|
16
|
+
import type { ModelCallSettings } from "../gates/model-gate.js";
|
|
17
|
+
import { type TenantVaults } from "../gates/tenant-vaults.js";
|
|
18
|
+
import type { PostgresClient } from "../store/postgres/connect.js";
|
|
19
|
+
import type { Logger } from "../tenant/types.js";
|
|
20
|
+
import type { GatesConfig } from "./stack-config.js";
|
|
21
|
+
/** Above the gate's 600 s provider request timeout and the loop's 630 s client timeout. */
|
|
22
|
+
export declare const GATES_REQUEST_TIMEOUT_MS = 660000;
|
|
23
|
+
export interface StartGatesOptions {
|
|
24
|
+
readonly gates: GatesConfig;
|
|
25
|
+
readonly logger: Logger;
|
|
26
|
+
/** The Host root, for the Tenant's vault key. Required unless `vaults` is given. */
|
|
27
|
+
readonly hostRoot?: string;
|
|
28
|
+
/** The pool for the Tenant's vault and readiness. Required unless `vaults` is given. */
|
|
29
|
+
readonly database?: PostgresClient;
|
|
30
|
+
/** Replaces the Postgres-backed Tenant vault (tests). */
|
|
31
|
+
readonly vaults?: TenantVaults;
|
|
32
|
+
/** Retries and timeouts of model calls (tests). */
|
|
33
|
+
readonly settings?: ModelCallSettings;
|
|
34
|
+
/** Largest model call body (tests). */
|
|
35
|
+
readonly maxBodyBytes?: number;
|
|
36
|
+
/** How long `close` lets running calls finish. Default 10 s. */
|
|
37
|
+
readonly drainMs?: number;
|
|
38
|
+
/** How long keyed outcomes are kept, and how many (tests). */
|
|
39
|
+
readonly inflight?: InflightCallsOptions;
|
|
40
|
+
/** How the gate may call Action endpoints (`NYLORUN_ENDPOINT_*`). Default: no limits. */
|
|
41
|
+
readonly delivery?: OutboundPolicy;
|
|
42
|
+
/** How long an MCP connection may sit unused. Default `GATE_MCP_IDLE_MS`. */
|
|
43
|
+
readonly mcpIdleMs?: number;
|
|
44
|
+
/** Tests replace how a remote MCP server is opened. */
|
|
45
|
+
readonly openMcp?: typeof openMcpServer;
|
|
46
|
+
/**
|
|
47
|
+
* Serve the keys service too (`--service gates,keys`, F4.2): vault writes and token signing
|
|
48
|
+
* with the Tenant's vault key. The gateway is then not ready until the key file is there.
|
|
49
|
+
*/
|
|
50
|
+
readonly keys?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface GatesServer {
|
|
53
|
+
/** Where the gate listens, e.g. `http://0.0.0.0:4100`. */
|
|
54
|
+
readonly url: string;
|
|
55
|
+
readonly server: Server;
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export declare function startGates(options: StartGatesOptions): Promise<GatesServer>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gates service's process (blueprint §15, §19): one listener serving the Model Gate's and
|
|
3
|
+
* the Tool Gate's routes (`api/gate/routes.ts`), behind the same `Host` check as the Runtime's
|
|
4
|
+
* listeners.
|
|
5
|
+
* It opens no Tenant runtime, no Restate endpoint and no stream relay, and runs no migration;
|
|
6
|
+
* it needs only the Postgres pool and the Host's tenant directory (read-only).
|
|
7
|
+
*
|
|
8
|
+
* Nothing is written while a model call runs, so the server's request timeout sits above the
|
|
9
|
+
* gate's longest call. Closing stops accepting calls, lets running ones finish for up to
|
|
10
|
+
* `drainMs`, then aborts them (their providers' requests with them).
|
|
11
|
+
*/
|
|
12
|
+
import { createServer } from "node:http";
|
|
13
|
+
import { getRequestListener } from "@hono/node-server";
|
|
14
|
+
import { createGatesApp } from "../api/gate/routes.js";
|
|
15
|
+
import { createModelCallHandler } from "../gates/handler.js";
|
|
16
|
+
import { createInflightCalls } from "../gates/inflight.js";
|
|
17
|
+
import { createMcpHandler, GATE_MCP_IDLE_MS } from "../gates/mcp-handler.js";
|
|
18
|
+
import { createToolCalls } from "../gates/tool-calls.js";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import { tenantPaths } from "../tenant/paths.js";
|
|
21
|
+
import { createTenantVaults } from "../gates/tenant-vaults.js";
|
|
22
|
+
import { probeDatabase } from "../infra/database.js";
|
|
23
|
+
import { bindListener, headerValue, isAllowedRequestHost, sendRejected } from "./http.js";
|
|
24
|
+
/** Above the gate's 600 s provider request timeout and the loop's 630 s client timeout. */
|
|
25
|
+
export const GATES_REQUEST_TIMEOUT_MS = 660_000;
|
|
26
|
+
export async function startGates(options) {
|
|
27
|
+
const { gates, logger, database } = options;
|
|
28
|
+
let vaults = options.vaults;
|
|
29
|
+
if (!vaults) {
|
|
30
|
+
if (!database || options.hostRoot === undefined)
|
|
31
|
+
throw new Error("startGates needs the Postgres pool and the Host root");
|
|
32
|
+
vaults = createTenantVaults({ sql: database, hostRoot: options.hostRoot });
|
|
33
|
+
}
|
|
34
|
+
const inflight = createInflightCalls(options.inflight);
|
|
35
|
+
const mcpIdleMs = options.mcpIdleMs ?? GATE_MCP_IDLE_MS;
|
|
36
|
+
const mcp = createMcpHandler({
|
|
37
|
+
vaults,
|
|
38
|
+
logger,
|
|
39
|
+
idleMs: mcpIdleMs,
|
|
40
|
+
...(options.openMcp ? { open: options.openMcp } : {}),
|
|
41
|
+
});
|
|
42
|
+
const toolCalls = createToolCalls({
|
|
43
|
+
vaults,
|
|
44
|
+
mcp,
|
|
45
|
+
logger,
|
|
46
|
+
...(options.inflight ? { inflight: options.inflight } : {}),
|
|
47
|
+
});
|
|
48
|
+
const sweep = setInterval(() => void mcp.sweep(), Math.max(1_000, Math.min(mcpIdleMs, 60_000)));
|
|
49
|
+
sweep.unref();
|
|
50
|
+
const prune = setInterval(() => void toolCalls.prune(), 60 * 60_000);
|
|
51
|
+
prune.unref();
|
|
52
|
+
const app = createGatesApp({
|
|
53
|
+
token: gates.token,
|
|
54
|
+
inflight,
|
|
55
|
+
modelGate: createModelCallHandler({
|
|
56
|
+
vaults,
|
|
57
|
+
logger,
|
|
58
|
+
...(options.settings ? { settings: options.settings } : {}),
|
|
59
|
+
}),
|
|
60
|
+
ready: async () => {
|
|
61
|
+
// The keys service needs the vault key, which `nylorun start` writes; it never creates one.
|
|
62
|
+
if (options.keys && options.hostRoot !== undefined && !existsSync(tenantPaths(options.hostRoot).kek))
|
|
63
|
+
return false;
|
|
64
|
+
if (!database)
|
|
65
|
+
return true;
|
|
66
|
+
try {
|
|
67
|
+
await probeDatabase(database, AbortSignal.timeout(2000));
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
...(options.maxBodyBytes !== undefined ? { maxBodyBytes: options.maxBodyBytes } : {}),
|
|
75
|
+
mcp,
|
|
76
|
+
toolCalls,
|
|
77
|
+
delivery: options.delivery ?? {},
|
|
78
|
+
logger,
|
|
79
|
+
...(options.keys ? { keys: async () => (await vaults.open()).keys() } : {}),
|
|
80
|
+
});
|
|
81
|
+
const listener = getRequestListener(app.fetch);
|
|
82
|
+
const inFlight = new Set();
|
|
83
|
+
// The configured Host values, plus the loopback forms of the port actually bound (probes
|
|
84
|
+
// from inside the container; port 0 in tests).
|
|
85
|
+
let allowedHosts = gates.listen.allowedHosts;
|
|
86
|
+
const server = createServer((req, res) => {
|
|
87
|
+
if (!isAllowedRequestHost(headerValue(req, "host"), {
|
|
88
|
+
port: gates.listen.port,
|
|
89
|
+
host: gates.listen.host,
|
|
90
|
+
allowedHosts,
|
|
91
|
+
}))
|
|
92
|
+
return sendRejected(res, 421, "host_rejected", "Host header is not an allowed address of the gateway");
|
|
93
|
+
inFlight.add(res);
|
|
94
|
+
res.once("close", () => inFlight.delete(res));
|
|
95
|
+
void listener(req, res);
|
|
96
|
+
});
|
|
97
|
+
server.requestTimeout = GATES_REQUEST_TIMEOUT_MS;
|
|
98
|
+
await bindListener(server, gates.listen.port, gates.listen.host, (error) => logger.error("listener_error", { error: error.message }));
|
|
99
|
+
const port = server.address().port;
|
|
100
|
+
allowedHosts = [
|
|
101
|
+
...new Set([...allowedHosts, `localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`]),
|
|
102
|
+
];
|
|
103
|
+
const url = `http://${gates.listen.host.includes(":") ? `[${gates.listen.host}]` : gates.listen.host}:${port}`;
|
|
104
|
+
let closing;
|
|
105
|
+
return {
|
|
106
|
+
url,
|
|
107
|
+
server,
|
|
108
|
+
close() {
|
|
109
|
+
closing ??= (async () => {
|
|
110
|
+
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
111
|
+
server.closeIdleConnections();
|
|
112
|
+
const deadline = Date.now() + (options.drainMs ?? 10_000);
|
|
113
|
+
while (inFlight.size > 0 && Date.now() < deadline)
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
115
|
+
// Closing a running call's connection aborts an unkeyed one; keyed calls stop here.
|
|
116
|
+
server.closeAllConnections();
|
|
117
|
+
inflight.close();
|
|
118
|
+
toolCalls.close();
|
|
119
|
+
clearInterval(sweep);
|
|
120
|
+
clearInterval(prune);
|
|
121
|
+
await mcp.closeAll();
|
|
122
|
+
await closed;
|
|
123
|
+
})();
|
|
124
|
+
return closing;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
package/dist/host/http.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
1
|
+
import type { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
2
2
|
import { jsonResponse } from "../api/http/respond.js";
|
|
3
3
|
import { type ErrorCode, type ProtocolRange } from "@nylorun/core/compatibility";
|
|
4
4
|
export declare const OPAQUE_NOT_FOUND: {
|
|
@@ -13,6 +13,11 @@ export declare class HostListenError extends Error {
|
|
|
13
13
|
readonly cause?: unknown;
|
|
14
14
|
constructor(message: string, exitCode: number, cause?: unknown);
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Binds `listening` on `host:port`. A port in use rejects with `HostListenError` (exit 98);
|
|
18
|
+
* once bound, a listener error goes to `onError` rather than being lost.
|
|
19
|
+
*/
|
|
20
|
+
export declare function bindListener(listening: Server, port: number, host: string, onError: (error: Error) => void): Promise<void>;
|
|
16
21
|
export declare function sendJson(response: ServerResponse, status: number, body: unknown): void;
|
|
17
22
|
/** `sendJson` as a Response, byte for byte (with any extra headers): the Host's answers. */
|
|
18
23
|
export { jsonResponse };
|
package/dist/host/http.js
CHANGED
|
@@ -18,6 +18,23 @@ export class HostListenError extends Error {
|
|
|
18
18
|
this.name = "HostListenError";
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Binds `listening` on `host:port`. A port in use rejects with `HostListenError` (exit 98);
|
|
23
|
+
* once bound, a listener error goes to `onError` rather than being lost.
|
|
24
|
+
*/
|
|
25
|
+
export function bindListener(listening, port, host, onError) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const failed = (error) => reject(error.code === "EADDRINUSE"
|
|
28
|
+
? new HostListenError(`Port ${port} on ${host} is already in use`, EXIT_PORT_IN_USE, error)
|
|
29
|
+
: error);
|
|
30
|
+
listening.once("error", failed);
|
|
31
|
+
listening.listen(port, host, () => {
|
|
32
|
+
listening.off("error", failed);
|
|
33
|
+
listening.on("error", onError);
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
21
38
|
export function sendJson(response, status, body) {
|
|
22
39
|
const payload = JSON.stringify(body);
|
|
23
40
|
// No Access-Control-* headers (D§11).
|
package/dist/host/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export { createHost, adminKeyMatches, type CreateHostOptions, type HostServer }
|
|
|
4
4
|
export { createHostLogger } from "./logger.js";
|
|
5
5
|
export { configForFactory } from "./config-for.js";
|
|
6
6
|
export { createHostExecution, type CreateHostExecutionOptions, type HostExecution, } from "./execution.js";
|
|
7
|
-
export { parseStackConfig, StackConfigError, type ContainerListen, type RuntimeRole, type StackConfig, type StackEndpoints, } from "./stack-config.js";
|
|
7
|
+
export { parseStackConfig, StackConfigError, type ContainerListen, type RuntimeRole, type RuntimeService, type RuntimeServices, type StackConfig, type StackEndpoints, } from "./stack-config.js";
|
|
8
8
|
export { EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK, HostListenError, OPAQUE_NOT_FOUND, } from "./http.js";
|