@nylorun/runtime 0.12.0-beta → 0.14.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 +361 -0
- package/README.md +69 -15
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +38 -8
- package/dist/adapters/replication/pgoutput.d.ts +16 -0
- package/dist/adapters/replication/pgoutput.js +169 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +1071 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +7825 -0
- package/dist/api/components.js +83 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +130 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +259 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +10 -0
- package/dist/api/http/routes/tenant.js +172 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +138 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +19 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/host/admin-api.d.ts +22 -0
- package/dist/host/admin-api.js +178 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +185 -0
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +31 -3
- package/dist/host/create-host.js +168 -327
- package/dist/host/http.d.ts +14 -3
- package/dist/host/http.js +64 -23
- package/dist/host/main.js +55 -4
- package/dist/host/stack-config.d.ts +20 -0
- package/dist/host/stack-config.js +55 -0
- package/dist/mcp/pool.d.ts +19 -1
- package/dist/mcp/pool.js +60 -10
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25696 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/event.d.ts +19 -0
- package/dist/store/event.js +49 -0
- package/dist/store/memory.d.ts +20 -8
- package/dist/store/memory.js +265 -100
- package/dist/store/postgres/connect.d.ts +13 -0
- package/dist/store/postgres/connect.js +5 -0
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
- package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
- package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
- package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
- package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
- package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
- package/dist/store/postgres/migrations/007_event_time.js +14 -0
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
- package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
- package/dist/store/postgres/migrations/index.d.ts +5 -1
- package/dist/store/postgres/migrations/index.js +22 -2
- package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
- package/dist/store/postgres/migrations/shared/index.js +92 -0
- package/dist/store/postgres/record.d.ts +11 -0
- package/dist/store/postgres/record.js +64 -0
- package/dist/store/postgres/store.js +337 -95
- package/dist/store/postgres/tenants.js +14 -0
- package/dist/store/types.d.ts +176 -81
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +29 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/memory.d.ts +69 -0
- package/dist/streams/relay/memory.js +176 -0
- package/dist/streams/relay/types.d.ts +77 -0
- package/dist/streams/relay/types.js +7 -0
- package/dist/streams/types.d.ts +66 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.js +32 -0
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +121 -129
- package/dist/tenant/context.d.ts +75 -18
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +105 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +476 -0
- package/dist/tenant/effects.d.ts +5 -0
- package/dist/tenant/effects.js +138 -48
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +6 -0
- package/dist/tenant/ephemeral.js +60 -36
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/module.d.ts +3 -0
- package/dist/tenant/module.js +6 -12
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/pool.d.ts +0 -2
- package/dist/tenant/pool.js +0 -18
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +10 -20
- package/dist/tenant/runtime.js +59 -42
- package/dist/tenant/scheduler.d.ts +7 -2
- package/dist/tenant/scheduler.js +7 -3
- package/dist/tenant/session-streams.d.ts +125 -0
- package/dist/tenant/session-streams.js +399 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +0 -3
- package/dist/tenant/status.js +19 -12
- package/dist/tenant/streams.d.ts +60 -67
- package/dist/tenant/streams.js +140 -208
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +178 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +29 -7
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +9 -3
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +57 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -8
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Admin API (`/v1/admin/**`): Tenants and the Host's status, for a caller with the admin
|
|
3
|
+
* key. The Host checks the listener, the protocol and the admin key first (`app.ts`); what
|
|
4
|
+
* gets here is authorized. Routes are declared once, for serving and for the OpenAPI document.
|
|
5
|
+
*/
|
|
6
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { CreateTenantRequestSchema } from "@nylorun/core/contracts";
|
|
9
|
+
import { AdminStatus as AdminStatusBody, AdminTenantList, AdminTenantStatus, CreateTenantRequest, HostShutdownResponse, ProtocolRejected, Rejected, TenantEnvelope, } from "../api/components.js";
|
|
10
|
+
import { serveRoute } from "../api/route.js";
|
|
11
|
+
import { TenantBusyError, TenantConflictError } from "../tenant/quarantine.js";
|
|
12
|
+
import { TenantNotFoundError, } from "../tenant/types.js";
|
|
13
|
+
import { jsonResponse, opaqueNotFoundResponse, readJsonBody, rejectedResponse, } from "./http.js";
|
|
14
|
+
const json = (schema, description) => ({
|
|
15
|
+
description,
|
|
16
|
+
content: { "application/json": { schema } },
|
|
17
|
+
});
|
|
18
|
+
const rejected = (description) => json(Rejected, description);
|
|
19
|
+
/** What every Admin route may answer before its own logic runs. */
|
|
20
|
+
const gated = {
|
|
21
|
+
404: rejected("Not found: an unknown admin key, the public listener, or an unknown Tenant"),
|
|
22
|
+
426: json(ProtocolRejected, "`Nylorun-Protocol` missing or unsupported"),
|
|
23
|
+
};
|
|
24
|
+
const tenantId = z.object({
|
|
25
|
+
tenantId: z.string().meta({ example: "tn_0123456789abcdefghjkmnpqrs" }),
|
|
26
|
+
});
|
|
27
|
+
function adminRoute(route) {
|
|
28
|
+
return {
|
|
29
|
+
...route,
|
|
30
|
+
tags: ["Admin"],
|
|
31
|
+
security: [{ adminKey: [] }],
|
|
32
|
+
responses: { ...gated, ...route.responses },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function createAdminApi(options) {
|
|
36
|
+
const { module } = options;
|
|
37
|
+
const api = new OpenAPIHono();
|
|
38
|
+
// HEAD is not a GET here: an Admin route answers only the methods it declares.
|
|
39
|
+
api.use(async (c, next) => {
|
|
40
|
+
if (c.req.method === "HEAD")
|
|
41
|
+
return notFound();
|
|
42
|
+
await next();
|
|
43
|
+
});
|
|
44
|
+
serveRoute(api, adminRoute({
|
|
45
|
+
method: "get",
|
|
46
|
+
path: "/v1/admin/tenants",
|
|
47
|
+
summary: "List Tenants",
|
|
48
|
+
responses: { 200: json(AdminTenantList, "Every Tenant on the Host") },
|
|
49
|
+
}), async () => jsonResponse(200, await module.list()));
|
|
50
|
+
serveRoute(api, adminRoute({
|
|
51
|
+
method: "post",
|
|
52
|
+
path: "/v1/admin/tenants",
|
|
53
|
+
summary: "Create a Tenant",
|
|
54
|
+
description: "Idempotent on `idempotencyKey`: a replay answers 200 with the same Tenant.",
|
|
55
|
+
request: {
|
|
56
|
+
body: {
|
|
57
|
+
required: true,
|
|
58
|
+
content: { "application/json": { schema: CreateTenantRequest } },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
responses: {
|
|
62
|
+
200: json(TenantEnvelope, "The Tenant, created earlier by the same request"),
|
|
63
|
+
201: json(TenantEnvelope, "The Tenant, created"),
|
|
64
|
+
400: rejected("Invalid JSON or request body (`invalid_request`)"),
|
|
65
|
+
409: rejected("Another Tenant has this id or idempotency key (`tenant_conflict`)"),
|
|
66
|
+
413: rejected("Request body over 1 MiB"),
|
|
67
|
+
},
|
|
68
|
+
}), async (c) => {
|
|
69
|
+
const body = CreateTenantRequestSchema.parse(await readJsonBody(c.req.raw));
|
|
70
|
+
try {
|
|
71
|
+
const result = await module.create({
|
|
72
|
+
tenantId: body.tenantId,
|
|
73
|
+
name: body.name,
|
|
74
|
+
principalId: body.principalId,
|
|
75
|
+
credentialHash: body.credentialHash,
|
|
76
|
+
idempotencyKey: body.idempotencyKey,
|
|
77
|
+
...(body.studioCredentialHash
|
|
78
|
+
? { studioCredentialHash: body.studioCredentialHash }
|
|
79
|
+
: {}),
|
|
80
|
+
...(body.derivedPrincipals?.length
|
|
81
|
+
? { derivedPrincipals: body.derivedPrincipals }
|
|
82
|
+
: {}),
|
|
83
|
+
});
|
|
84
|
+
return jsonResponse(result.created ? 201 : 200, result.envelope);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const code = error.code;
|
|
88
|
+
if (error instanceof TenantConflictError ||
|
|
89
|
+
error.name === "TenantConflictError" ||
|
|
90
|
+
code === "conflict" ||
|
|
91
|
+
code === "tenant_conflict")
|
|
92
|
+
return rejectedResponse(409, "tenant_conflict", error instanceof Error ? error.message : "Tenant conflict");
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
serveRoute(api, adminRoute({
|
|
97
|
+
method: "get",
|
|
98
|
+
path: "/v1/admin/tenants/{tenantId}",
|
|
99
|
+
summary: "Get a Tenant's status",
|
|
100
|
+
request: { params: tenantId },
|
|
101
|
+
responses: { 200: json(AdminTenantStatus, "The Tenant and its state") },
|
|
102
|
+
}), async (c) => {
|
|
103
|
+
const status = await module.status(c.req.param("tenantId"));
|
|
104
|
+
return status ? jsonResponse(200, status) : opaqueNotFoundResponse();
|
|
105
|
+
});
|
|
106
|
+
serveRoute(api, adminRoute({
|
|
107
|
+
method: "delete",
|
|
108
|
+
path: "/v1/admin/tenants/{tenantId}",
|
|
109
|
+
summary: "Delete a Tenant",
|
|
110
|
+
request: {
|
|
111
|
+
params: tenantId,
|
|
112
|
+
query: z.object({
|
|
113
|
+
activeWork: z.enum(["refuse", "drain", "cancel"]).optional().meta({
|
|
114
|
+
description: "What to do with turns in progress: refuse to delete (default), wait for them, or cancel them",
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
responses: {
|
|
119
|
+
204: { description: "Deleted" },
|
|
120
|
+
400: rejected("`activeWork` is not refuse, drain or cancel (`invalid_request`)"),
|
|
121
|
+
409: rejected("The Tenant has work in progress and `activeWork` is refuse (`active_work`)"),
|
|
122
|
+
},
|
|
123
|
+
}), async (c) => {
|
|
124
|
+
const activeWork = c.req.query("activeWork") ?? "refuse";
|
|
125
|
+
if (activeWork !== "refuse" && activeWork !== "drain" && activeWork !== "cancel")
|
|
126
|
+
return rejectedResponse(400, "invalid_request", "activeWork must be refuse, drain, or cancel");
|
|
127
|
+
try {
|
|
128
|
+
await module.delete(c.req.param("tenantId"), activeWork);
|
|
129
|
+
return new Response(null, { status: 204 });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof TenantNotFoundError)
|
|
133
|
+
return opaqueNotFoundResponse();
|
|
134
|
+
const code = error.code;
|
|
135
|
+
if (error instanceof TenantBusyError ||
|
|
136
|
+
error.name === "TenantBusyError" ||
|
|
137
|
+
code === "active_work" ||
|
|
138
|
+
code === "conflict")
|
|
139
|
+
return rejectedResponse(409, "active_work", error instanceof Error ? error.message : "Active work");
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// D12: `/v1/admin/status` and `/v1/admin/host` are one answer.
|
|
144
|
+
for (const path of ["/v1/admin/status", "/v1/admin/host"])
|
|
145
|
+
serveRoute(api, adminRoute({
|
|
146
|
+
method: "get",
|
|
147
|
+
path,
|
|
148
|
+
summary: path.endsWith("host") ? "Get the Host's status (alias)" : "Get the Host's status",
|
|
149
|
+
responses: { 200: json(AdminStatusBody, "Version, protocol, Tenants and the Host") },
|
|
150
|
+
}), async () => jsonResponse(200, await options.status()));
|
|
151
|
+
serveRoute(api, adminRoute({
|
|
152
|
+
method: "post",
|
|
153
|
+
path: "/v1/admin/host/shutdown",
|
|
154
|
+
summary: "Shut the Host down",
|
|
155
|
+
description: "Answers, then stops the Host. Served by a self-hosted Runtime only.",
|
|
156
|
+
responses: { 200: json(HostShutdownResponse, "The Host is stopping") },
|
|
157
|
+
}), (c) => {
|
|
158
|
+
c.env.outgoing.once("finish", () => options.shutdown());
|
|
159
|
+
return jsonResponse(200, { status: "shutting_down" });
|
|
160
|
+
});
|
|
161
|
+
serveRoute(api, adminRoute({
|
|
162
|
+
method: "get",
|
|
163
|
+
path: "/v1/admin/openapi.json",
|
|
164
|
+
summary: "Get this document",
|
|
165
|
+
responses: { 200: { description: "The Admin API's OpenAPI document" } },
|
|
166
|
+
}), () => options.document
|
|
167
|
+
? jsonResponse(200, options.document(), { "cache-control": "no-cache" })
|
|
168
|
+
: notFound());
|
|
169
|
+
api.notFound(() => notFound());
|
|
170
|
+
// The Host answers what a route throws (`app.ts`), as it did before Hono.
|
|
171
|
+
api.onError((error) => {
|
|
172
|
+
throw error;
|
|
173
|
+
});
|
|
174
|
+
return api;
|
|
175
|
+
}
|
|
176
|
+
function notFound() {
|
|
177
|
+
return rejectedResponse(404, "not_found", "Route not found");
|
|
178
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Logger, NodeBindings, TenantModule } from "../tenant/types.js";
|
|
3
|
+
import type { ListenerRole } from "./create-host.js";
|
|
4
|
+
export type HostBindings = NodeBindings & {
|
|
5
|
+
readonly role: ListenerRole;
|
|
6
|
+
};
|
|
7
|
+
export type HostEnv = {
|
|
8
|
+
Bindings: HostBindings;
|
|
9
|
+
Variables: {
|
|
10
|
+
/** The Tenant a request named, for the request log. */
|
|
11
|
+
tenantId?: string;
|
|
12
|
+
/** The status to log when it is not the answer's (a rejection after a stream started). */
|
|
13
|
+
status?: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export interface HostAppOptions {
|
|
17
|
+
module: TenantModule;
|
|
18
|
+
logger: Logger;
|
|
19
|
+
hostId: string;
|
|
20
|
+
adminKey: string;
|
|
21
|
+
coreVersion: string;
|
|
22
|
+
pid: number;
|
|
23
|
+
browserAccess: boolean;
|
|
24
|
+
readiness?: () => Promise<{
|
|
25
|
+
ok: boolean;
|
|
26
|
+
checks: Record<string, boolean>;
|
|
27
|
+
}>;
|
|
28
|
+
/** Every listener is listening. */
|
|
29
|
+
listening(): boolean;
|
|
30
|
+
closing(): boolean;
|
|
31
|
+
/** The Admin API (`admin-api.ts`), for a request whose admin key checked out. */
|
|
32
|
+
admin(request: Request, node: NodeBindings): Promise<Response>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createHostApp(options: HostAppOptions): Hono<HostEnv>;
|
package/dist/host/app.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
|
|
3
|
+
import { checkCompatibility, HOST_PROTOCOL, isTenantId, PROTOCOL_HEADER, PUBLISHABLE_KEY_HEADER, TENANT_HEADER, tenantOfPublishableKey, } from "@nylorun/core/compatibility";
|
|
4
|
+
import { RUNTIME_VERSION } from "../version.js";
|
|
5
|
+
import { findTenantRoute } from "../api/http/app.js";
|
|
6
|
+
import { tenantDocument } from "../api/openapi.js";
|
|
7
|
+
import { answerPreflight } from "./cors.js";
|
|
8
|
+
import { adminKeyMatches, headerValue, isJsonContentType, jsonResponse, opaqueNotFoundResponse, protocolRejectedResponse, readBearer, pathnameIsLogged, redactRoutePath, rejectedResponse, requestHasBody, } from "./http.js";
|
|
9
|
+
export function createHostApp(options) {
|
|
10
|
+
const { module, logger } = options;
|
|
11
|
+
const app = new Hono();
|
|
12
|
+
app.use(async (c, next) => {
|
|
13
|
+
const started = Date.now();
|
|
14
|
+
try {
|
|
15
|
+
await next();
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
const { incoming } = c.env;
|
|
19
|
+
if (pathnameIsLogged(incoming.url))
|
|
20
|
+
logger.info("request", {
|
|
21
|
+
status: statusOf(c),
|
|
22
|
+
tenantId: c.get("tenantId"),
|
|
23
|
+
durationMs: Date.now() - started,
|
|
24
|
+
path: redactRoutePath(pathnameOf(incoming)),
|
|
25
|
+
method: incoming.method,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
app.use(async (c, next) => {
|
|
30
|
+
const { incoming, outgoing, role } = c.env;
|
|
31
|
+
if (headerValue(incoming, "origin") !== undefined) {
|
|
32
|
+
const route = pathnameOf(incoming).split("/").filter(Boolean);
|
|
33
|
+
// Only Tenant routes, and only when the operator allows browsers; the Tenant then
|
|
34
|
+
// checks the publishable key and its origins before adding any CORS header.
|
|
35
|
+
const tenantRoute = route[0] === "v1" && route[1] !== "admin";
|
|
36
|
+
// The operator listener never serves browsers.
|
|
37
|
+
if (!options.browserAccess || role === "operator" || !tenantRoute)
|
|
38
|
+
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
|
+
}
|
|
44
|
+
await next();
|
|
45
|
+
});
|
|
46
|
+
app.use(async (c, next) => {
|
|
47
|
+
const { incoming } = c.env;
|
|
48
|
+
if (requestHasBody(incoming) && !isJsonContentType(headerValue(incoming, "content-type")))
|
|
49
|
+
return rejectedResponse(415, "unsupported_media_type", "Request bodies must use application/json");
|
|
50
|
+
await next();
|
|
51
|
+
});
|
|
52
|
+
app.all("*", async (c) => {
|
|
53
|
+
const { incoming, outgoing, role } = c.env;
|
|
54
|
+
const url = new URL(incoming.url ?? "/", "http://runtime.local");
|
|
55
|
+
const pathname = url.pathname;
|
|
56
|
+
if (pathname === "/health")
|
|
57
|
+
return jsonResponse(200, {
|
|
58
|
+
status: "ok",
|
|
59
|
+
service: "nylorun-runtime",
|
|
60
|
+
version: RUNTIME_VERSION,
|
|
61
|
+
protocol: {
|
|
62
|
+
min: HOST_PROTOCOL.min,
|
|
63
|
+
max: HOST_PROTOCOL.max,
|
|
64
|
+
features: [...HOST_PROTOCOL.features],
|
|
65
|
+
},
|
|
66
|
+
coreVersion: options.coreVersion,
|
|
67
|
+
hostId: options.hostId,
|
|
68
|
+
pid: options.pid,
|
|
69
|
+
});
|
|
70
|
+
if (pathname === "/ready") {
|
|
71
|
+
const listener = options.listening();
|
|
72
|
+
const discovery = module.started;
|
|
73
|
+
const infra = await options.readiness?.();
|
|
74
|
+
const ready = listener && discovery && !options.closing() && (infra?.ok ?? true);
|
|
75
|
+
return jsonResponse(ready ? 200 : 503, {
|
|
76
|
+
status: ready ? "ready" : "not_ready",
|
|
77
|
+
service: "nylorun-runtime",
|
|
78
|
+
checks: { listener, discovery, ...infra?.checks },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// This API's own description: public, as the npm package that ships it.
|
|
82
|
+
if (pathname === "/openapi.json" && incoming.method === "GET")
|
|
83
|
+
return jsonResponse(200, tenantDocument(), { "cache-control": "no-cache" });
|
|
84
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
85
|
+
if (segments[0] === "v1" && segments[1] === "admin") {
|
|
86
|
+
// A public listener has no admin routes: the same 404 as a wrong admin key.
|
|
87
|
+
if (role === "public")
|
|
88
|
+
return opaqueNotFoundResponse();
|
|
89
|
+
if (!protocolAccepted(headerValue(incoming, PROTOCOL_HEADER)))
|
|
90
|
+
return protocolRejectedResponse();
|
|
91
|
+
const token = readBearer(headerValue(incoming, "authorization"));
|
|
92
|
+
if (!adminKeyMatches(token, options.adminKey))
|
|
93
|
+
return opaqueNotFoundResponse();
|
|
94
|
+
return await options.admin(c.req.raw, { incoming, outgoing });
|
|
95
|
+
}
|
|
96
|
+
// Tenant routes: header pattern → protocol → resolve → the Tenant. The Tenant is named by
|
|
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);
|
|
99
|
+
const keyHeader = headerValue(incoming, PUBLISHABLE_KEY_HEADER);
|
|
100
|
+
const keyTenant = keyHeader === undefined ? undefined : tenantOfPublishableKey(keyHeader);
|
|
101
|
+
if (keyHeader !== undefined && keyTenant === undefined)
|
|
102
|
+
return invalid(`${PUBLISHABLE_KEY_HEADER} header is malformed`);
|
|
103
|
+
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);
|
|
113
|
+
if (!protocolAccepted(headerValue(incoming, PROTOCOL_HEADER)))
|
|
114
|
+
return protocolRejectedResponse();
|
|
115
|
+
const resolution = await module.resolve(tenantId);
|
|
116
|
+
if (resolution.kind !== "open") {
|
|
117
|
+
if (resolution.kind === "quarantined")
|
|
118
|
+
logger.warn("tenant_quarantined", {
|
|
119
|
+
tenantId,
|
|
120
|
+
code: resolution.quarantine.code,
|
|
121
|
+
repair: resolution.quarantine.repair,
|
|
122
|
+
});
|
|
123
|
+
return opaqueNotFoundResponse();
|
|
124
|
+
}
|
|
125
|
+
return await resolution.handle.fetch(c.req.raw, { incoming, outgoing });
|
|
126
|
+
});
|
|
127
|
+
app.onError((error, c) => {
|
|
128
|
+
const status = error.status;
|
|
129
|
+
let rejection;
|
|
130
|
+
if (typeof status === "number" && status >= 400 && status < 600) {
|
|
131
|
+
rejection = {
|
|
132
|
+
status,
|
|
133
|
+
code: status === 400 ? "invalid_request" : "request_rejected",
|
|
134
|
+
message: error.message,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
else if (error.name === "ZodError") {
|
|
138
|
+
rejection = { status: 400, code: "invalid_request", message: "Invalid request body" };
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
logger.error("request_failed", { error: error.message });
|
|
142
|
+
rejection = { status: 500, code: "internal_error", message: "Internal error" };
|
|
143
|
+
}
|
|
144
|
+
c.set("status", rejection.status);
|
|
145
|
+
const { outgoing } = c.env;
|
|
146
|
+
// Once a response has started, the rejection can only end it.
|
|
147
|
+
if (outgoing.headersSent) {
|
|
148
|
+
outgoing.end();
|
|
149
|
+
return RESPONSE_ALREADY_SENT;
|
|
150
|
+
}
|
|
151
|
+
return rejectedResponse(rejection.status, rejection.code, rejection.message);
|
|
152
|
+
});
|
|
153
|
+
return app;
|
|
154
|
+
}
|
|
155
|
+
/** A route a browser page may call, decided from the route alone (`api/http/define.ts`). */
|
|
156
|
+
function browserRoute(method, segments) {
|
|
157
|
+
return findTenantRoute(method, segments)?.browser === true;
|
|
158
|
+
}
|
|
159
|
+
/** The answer's status: a Response's, or the Node response's when it was written there. */
|
|
160
|
+
function statusOf(c) {
|
|
161
|
+
const logged = c.get("status");
|
|
162
|
+
if (logged !== undefined)
|
|
163
|
+
return logged;
|
|
164
|
+
if (c.res.headers.has(ALREADY_SENT))
|
|
165
|
+
return c.env.outgoing.statusCode || 200;
|
|
166
|
+
return c.res.status;
|
|
167
|
+
}
|
|
168
|
+
const ALREADY_SENT = "x-hono-already-sent";
|
|
169
|
+
function pathnameOf(incoming) {
|
|
170
|
+
return new URL(incoming.url ?? "/", "http://runtime.local").pathname;
|
|
171
|
+
}
|
|
172
|
+
function parseProtocolVersion(raw) {
|
|
173
|
+
if (raw === undefined)
|
|
174
|
+
return undefined;
|
|
175
|
+
const trimmed = raw.trim();
|
|
176
|
+
if (!/^\d+$/.test(trimmed))
|
|
177
|
+
return undefined;
|
|
178
|
+
return Number(trimmed);
|
|
179
|
+
}
|
|
180
|
+
function protocolAccepted(raw) {
|
|
181
|
+
const version = parseProtocolVersion(raw);
|
|
182
|
+
if (version === undefined)
|
|
183
|
+
return false;
|
|
184
|
+
return checkCompatibility({ version, required: [] }, HOST_PROTOCOL).ok;
|
|
185
|
+
}
|
|
@@ -14,4 +14,6 @@ export declare function configForFactory(options: {
|
|
|
14
14
|
mode?: TenantConfig["mode"];
|
|
15
15
|
/** Override model; default vault. Fixture/scripted require ephemeral/test mode. */
|
|
16
16
|
model?: TenantConfig["model"];
|
|
17
|
+
/** How Tenants may call Action endpoints (`StackConfig.delivery`). */
|
|
18
|
+
delivery?: TenantConfig["delivery"];
|
|
17
19
|
}): (id: string) => TenantConfig;
|
package/dist/host/config-for.js
CHANGED
|
@@ -16,6 +16,7 @@ export function configForFactory(options) {
|
|
|
16
16
|
paths: tenant,
|
|
17
17
|
sandbox: { backend: sandboxBackend },
|
|
18
18
|
model: options.model ?? { kind: "vault" },
|
|
19
|
+
...(options.delivery ? { delivery: options.delivery } : {}),
|
|
19
20
|
childEnv: tenantChildEnvironment(baseline, options.hostConfig, tenant),
|
|
20
21
|
logger: {
|
|
21
22
|
info: (message, fields) => options.logger.info(message, { tenantId: id, ...fields }),
|
package/dist/host/config.d.ts
CHANGED
|
@@ -4,6 +4,17 @@ 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
|
+
/**
|
|
14
|
+
* The operator listener's port: the Admin API moves there (on `host`, loopback only), and
|
|
15
|
+
* `port` serves the Tenant API alone. Absent: one port serves both.
|
|
16
|
+
*/
|
|
17
|
+
adminPort?: number;
|
|
7
18
|
proxy?: {
|
|
8
19
|
httpsProxy?: string;
|
|
9
20
|
noProxy?: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser access at the Host (Host feature `browser-access`). A preflight carries no
|
|
3
|
+
* credentials and no header values, so it cannot name a Tenant: the Host answers it from the
|
|
4
|
+
* route alone, permissively for browser routes, and the Tenant enforces the publishable key's
|
|
5
|
+
* origin allowlist on the actual request (`tenant/browser.ts`). A cached preflight therefore
|
|
6
|
+
* never widens access, and it grants no credentials.
|
|
7
|
+
*/
|
|
8
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
+
/** Request headers a browser may send to the Runtime. */
|
|
10
|
+
export declare const BROWSER_REQUEST_HEADERS: readonly ["authorization", "content-type", "nylorun-key", "nylorun-tenant", "nylorun-protocol", "last-event-id"];
|
|
11
|
+
/**
|
|
12
|
+
* Tenant routes a browser may call: agents (list), sessions (not the sandbox tool route),
|
|
13
|
+
* vaults, AG-UI and the JWKS. Tokens, access management, endpoints, actions and Tenant
|
|
14
|
+
* settings never are.
|
|
15
|
+
*/
|
|
16
|
+
/** The `Origin` header if it is a serialized origin (not `null`, not a path). */
|
|
17
|
+
export declare function browserOrigin(request: IncomingMessage): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Answers a CORS preflight: `204` with the allowed methods and headers for a browser route,
|
|
20
|
+
* `403` without any CORS header otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export declare function answerPreflight(request: IncomingMessage, response: ServerResponse, segments: readonly string[], isBrowserRoute: (method: string, segments: readonly string[]) => boolean): number;
|
|
23
|
+
/**
|
|
24
|
+
* CORS headers for an actual request from an allowed origin; set once with `setHeader`, so
|
|
25
|
+
* every response that follows (JSON, errors, SSE) carries them.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setCorsHeaders(response: ServerResponse, origin: string): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isSerializedOrigin } from "@nylorun/core/contracts";
|
|
2
|
+
/** Request headers a browser may send to the Runtime. */
|
|
3
|
+
export const BROWSER_REQUEST_HEADERS = [
|
|
4
|
+
"authorization",
|
|
5
|
+
"content-type",
|
|
6
|
+
"nylorun-key",
|
|
7
|
+
"nylorun-tenant",
|
|
8
|
+
"nylorun-protocol",
|
|
9
|
+
"last-event-id",
|
|
10
|
+
];
|
|
11
|
+
const BROWSER_METHODS = ["GET", "POST", "PUT", "DELETE"];
|
|
12
|
+
const PREFLIGHT_MAX_AGE_SECONDS = 600;
|
|
13
|
+
/**
|
|
14
|
+
* Tenant routes a browser may call: agents (list), sessions (not the sandbox tool route),
|
|
15
|
+
* vaults, AG-UI and the JWKS. Tokens, access management, endpoints, actions and Tenant
|
|
16
|
+
* settings never are.
|
|
17
|
+
*/
|
|
18
|
+
/** The `Origin` header if it is a serialized origin (not `null`, not a path). */
|
|
19
|
+
export function browserOrigin(request) {
|
|
20
|
+
const raw = request.headers.origin;
|
|
21
|
+
const origin = Array.isArray(raw) ? raw[0] : raw;
|
|
22
|
+
return origin !== undefined && isSerializedOrigin(origin) ? origin : undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Answers a CORS preflight: `204` with the allowed methods and headers for a browser route,
|
|
26
|
+
* `403` without any CORS header otherwise.
|
|
27
|
+
*/
|
|
28
|
+
export function answerPreflight(request, response, segments, isBrowserRoute) {
|
|
29
|
+
const origin = browserOrigin(request);
|
|
30
|
+
const method = String(request.headers["access-control-request-method"] ?? "").toUpperCase();
|
|
31
|
+
const requested = String(request.headers["access-control-request-headers"] ?? "")
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((name) => name.trim().toLowerCase())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
const allowed = origin !== undefined &&
|
|
36
|
+
BROWSER_METHODS.includes(method) &&
|
|
37
|
+
isBrowserRoute(method, segments) &&
|
|
38
|
+
requested.every((name) => BROWSER_REQUEST_HEADERS.includes(name));
|
|
39
|
+
if (!allowed) {
|
|
40
|
+
response.writeHead(403, { "content-length": "0" });
|
|
41
|
+
response.end();
|
|
42
|
+
return 403;
|
|
43
|
+
}
|
|
44
|
+
response.writeHead(204, {
|
|
45
|
+
"access-control-allow-origin": origin,
|
|
46
|
+
"access-control-allow-methods": BROWSER_METHODS.join(", "),
|
|
47
|
+
"access-control-allow-headers": requested.join(", "),
|
|
48
|
+
"access-control-max-age": String(PREFLIGHT_MAX_AGE_SECONDS),
|
|
49
|
+
vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
|
|
50
|
+
"content-length": "0",
|
|
51
|
+
});
|
|
52
|
+
response.end();
|
|
53
|
+
return 204;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* CORS headers for an actual request from an allowed origin; set once with `setHeader`, so
|
|
57
|
+
* every response that follows (JSON, errors, SSE) carries them.
|
|
58
|
+
*/
|
|
59
|
+
export function setCorsHeaders(response, origin) {
|
|
60
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
61
|
+
response.setHeader("vary", "Origin");
|
|
62
|
+
response.setHeader("access-control-expose-headers", "retry-after, www-authenticate");
|
|
63
|
+
response.setHeader("cache-control", "no-store");
|
|
64
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Logger, TenantModule } from "../tenant/types.js";
|
|
2
2
|
import type { HostConfigFile, HostCredentialsFile } from "./config.js";
|
|
3
3
|
import type { ContainerListen } from "./stack-config.js";
|
|
4
4
|
export interface CreateHostOptions {
|
|
@@ -23,6 +23,20 @@ export interface CreateHostOptions {
|
|
|
23
23
|
* mode is `http://0.0.0.0:4000`.
|
|
24
24
|
*/
|
|
25
25
|
publicUrl?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Browser access (Host feature `browser-access`): requests with an `Origin` may reach Tenant
|
|
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
|
|
35
|
+
* its own address, kept off the network that reaches the Tenant API. When set, the main
|
|
36
|
+
* listener is public and answers admin routes with the opaque 404. Absent: one listener
|
|
37
|
+
* serves everything, as before.
|
|
38
|
+
*/
|
|
39
|
+
operator?: OperatorListen;
|
|
26
40
|
/**
|
|
27
41
|
* Infrastructure readiness (`infra/readiness.ts`). `/ready` adds its checks
|
|
28
42
|
* and answers 503 while it reports not ok. Default: listener and discovery
|
|
@@ -44,13 +58,27 @@ export interface CreateHostOptions {
|
|
|
44
58
|
afterTenants?(): Promise<void>;
|
|
45
59
|
};
|
|
46
60
|
}
|
|
61
|
+
/** Where the operator listener binds, and the `Host` values it accepts. */
|
|
62
|
+
export interface OperatorListen {
|
|
63
|
+
host: string;
|
|
64
|
+
port: number;
|
|
65
|
+
/** Exact `Host` values (lowercase `name:port`); absent means the loopback forms of the port. */
|
|
66
|
+
allowedHosts?: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* What a listener serves. `combined`: everything (one port). `public`: the Tenant API, with
|
|
70
|
+
* browser access when enabled; admin routes are the opaque 404. `operator`: the Admin API and
|
|
71
|
+
* the Tenant API, never to browsers.
|
|
72
|
+
*/
|
|
73
|
+
export type ListenerRole = "combined" | "public" | "operator";
|
|
47
74
|
export interface HostServer {
|
|
48
75
|
listen(): Promise<void>;
|
|
49
76
|
close(): Promise<void>;
|
|
50
77
|
/** Settles once `close()` has finished, whatever called it. */
|
|
51
78
|
readonly closed: Promise<void>;
|
|
52
79
|
readonly url: string;
|
|
80
|
+
/** Where the Admin API answers: the operator listener, or `url` when there is one listener. */
|
|
81
|
+
readonly adminUrl: string;
|
|
53
82
|
}
|
|
54
|
-
|
|
55
|
-
export declare function adminKeyMatches(presented: string | undefined, adminKey: string): boolean;
|
|
83
|
+
export { adminKeyMatches } from "./http.js";
|
|
56
84
|
export declare function createHost(options: CreateHostOptions): HostServer;
|