@nylorun/runtime 0.19.0-beta → 0.20.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +12 -7
  3. package/dist/adapters/streams/s2.js +5 -11
  4. package/dist/api/components.d.ts +171 -169
  5. package/dist/api/http/define.d.ts +3 -2
  6. package/dist/api/http/define.js +10 -11
  7. package/dist/api/openapi.js +45 -22
  8. package/dist/execution/types.d.ts +1 -1
  9. package/dist/host/app.d.ts +3 -0
  10. package/dist/host/app.js +33 -4
  11. package/dist/host/create-host.d.ts +6 -0
  12. package/dist/host/create-host.js +1 -0
  13. package/dist/host/main.js +11 -4
  14. package/dist/host/stack-config.js +1 -1
  15. package/dist/infra/index.d.ts +2 -1
  16. package/dist/infra/index.js +0 -1
  17. package/dist/infra/readiness.d.ts +6 -6
  18. package/dist/infra/readiness.js +2 -5
  19. package/dist/management-openapi.json +430 -107
  20. package/dist/openapi.json +761 -630
  21. package/dist/store/postgres/connect.d.ts +10 -0
  22. package/dist/store/postgres/connect.js +30 -3
  23. package/dist/store/postgres/control.d.ts +14 -0
  24. package/dist/store/postgres/control.js +187 -0
  25. package/dist/store/postgres/drizzle/0015_control_signals.sql +11 -0
  26. package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
  27. package/dist/store/postgres/schema.d.ts +120 -0
  28. package/dist/store/postgres/schema.js +20 -0
  29. package/dist/store/postgres/store.js +15 -0
  30. package/dist/store/types.d.ts +40 -0
  31. package/dist/streams/types.d.ts +6 -29
  32. package/dist/streams/types.js +5 -4
  33. package/dist/tenant/auth.d.ts +16 -6
  34. package/dist/tenant/auth.js +41 -17
  35. package/dist/tenant/commands.js +10 -6
  36. package/dist/tenant/context.d.ts +4 -0
  37. package/dist/tenant/control.d.ts +26 -0
  38. package/dist/tenant/control.js +35 -0
  39. package/dist/tenant/ephemeral.js +1 -0
  40. package/dist/tenant/http.d.ts +4 -2
  41. package/dist/tenant/http.js +4 -2
  42. package/dist/tenant/identity-file.d.ts +16 -12
  43. package/dist/tenant/identity-file.js +41 -29
  44. package/dist/tenant/issuers.d.ts +4 -3
  45. package/dist/tenant/issuers.js +22 -16
  46. package/dist/tenant/reset.d.ts +2 -2
  47. package/dist/tenant/reset.js +8 -2
  48. package/dist/tenant/resource-server.d.ts +56 -0
  49. package/dist/tenant/resource-server.js +62 -0
  50. package/dist/tenant/runtime.d.ts +1 -1
  51. package/dist/tenant/runtime.js +7 -1
  52. package/dist/tenant/scheduler.d.ts +1 -1
  53. package/dist/tenant/scheduler.js +1 -1
  54. package/dist/tenant/streams.d.ts +16 -22
  55. package/dist/tenant/streams.js +7 -87
  56. package/dist/tenant/sweep.js +9 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +3 -3
  60. package/dist/streams/control.d.ts +0 -13
  61. package/dist/streams/control.js +0 -25
@@ -27,7 +27,8 @@ export interface RouteAccess {
27
27
  /** The subject scopes that reach the route: any one of them, none (`never`), or all (`any`). */
28
28
  readonly scopes: readonly SubjectScope[] | "never" | "any";
29
29
  /**
30
- * Public data: a request with no credential at all (no `Authorization`) is served too. A credential that is sent is still checked, so a wrong one stays the opaque 404.
30
+ * Public data: a request with no credential at all (no `Authorization`) is served too. A
31
+ * credential that is sent is still checked, so a wrong one is `401 credential_invalid`.
31
32
  */
32
33
  readonly anonymous?: boolean;
33
34
  /**
@@ -49,7 +50,7 @@ export declare function authenticateCaller(c: Context<TenantEnv>,
49
50
  anonymous?: boolean,
50
51
  /** The keys the route takes; a Runtime API route's by default. */
51
52
  keys?: KeyAccess): Promise<AuthScope>;
52
- /** No route: authenticated first, so an unknown credential stays the opaque 404. */
53
+ /** No route: authenticated first, so only a known credential learns there is no such route. */
53
54
  export declare function routeNotFound(c: Context<TenantEnv>): Promise<Response>;
54
55
  /** Which keys a route's credentials take. */
55
56
  export declare function keyAccess(access: RouteAccess): KeyAccess;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { PROTOCOL_VERSION } from "@nylorun/core/compatibility";
3
- import { authenticate, requireScopes } from "../../tenant/auth.js";
3
+ import { authenticate, challengeMetadata, requireScopes, } from "../../tenant/auth.js";
4
4
  import { fail } from "../../tenant/http.js";
5
5
  import { ProtocolRejected, Rejected } from "../components.js";
6
6
  const SCHEMES = {
@@ -38,7 +38,7 @@ keys) {
38
38
  return { kind: "anonymous" };
39
39
  return await authenticate(tenant, incoming, keys);
40
40
  }
41
- /** No route: authenticated first, so an unknown credential stays the opaque 404. */
41
+ /** No route: authenticated first, so only a known credential learns there is no such route. */
42
42
  export async function routeNotFound(c) {
43
43
  // Any known key, of either API, learns only that there is no such route.
44
44
  await authenticateCaller(c, false, { application: true, management: true });
@@ -64,8 +64,9 @@ export function declaredRoute(method, segments) {
64
64
  }
65
65
  function authenticated(access) {
66
66
  return async (c, next) => {
67
- const scope = await authenticateCaller(c, access.anonymous === true, keyAccess(access));
68
- requireScopes(scope, access.scopes);
67
+ const keys = keyAccess(access);
68
+ const scope = await authenticateCaller(c, access.anonymous === true, keys);
69
+ requireScopes(scope, access.scopes, challengeMetadata(c.env.tenant, c.env.incoming, keys));
69
70
  c.set("scope", scope);
70
71
  await next();
71
72
  };
@@ -99,15 +100,13 @@ export function tenantRoute(api, access, route, handler) {
99
100
  request: { ...route.request, headers },
100
101
  responses: {
101
102
  400: rejected("Invalid headers, path, body or cursor"),
102
- ...(takes("token")
103
- ? {
104
- 401: rejected("The token expired (`token_expired`), or its issuer's keys cannot be fetched now (`issuer_unavailable`)"),
105
- }
106
- : {}),
103
+ 401: rejected(takes("token")
104
+ ? "No credential (`credential_required`), one the Tenant does not accept (`credential_invalid`), a token that expired (`token_expired`), or a token whose issuer's keys cannot be fetched now (`issuer_unavailable`). `WWW-Authenticate: Bearer` names the protected resource metadata (`resource_metadata`)"
105
+ : "No credential (`credential_required`), or one the Tenant does not accept (`credential_invalid`), with a `WWW-Authenticate: Bearer` challenge"),
107
106
  403: rejected(takes("management")
108
107
  ? "Not allowed for this credential, a key of the other API (`key_role_mismatch`), a key from a browser (`origin_rejected`) or a management key acting for a subject"
109
- : "Not allowed for this credential or subject scope, a key of the other API (`key_role_mismatch`), or an application key from a browser (`origin_rejected`)"),
110
- 404: rejected("Not found, or a credential the Tenant does not know (`not_found`)"),
108
+ : "Not allowed for this credential or subject scope (`scope_required`; a token also gets `WWW-Authenticate: Bearer error=\"insufficient_scope\"` and the scope), a key of the other API (`key_role_mismatch`), or an application key from a browser (`origin_rejected`)"),
109
+ 404: rejected("Not found (`not_found`)"),
111
110
  426: { description: "`Nylorun-Protocol` missing or unsupported", content: { "application/json": { schema: ProtocolRejected } } },
112
111
  503: rejected("The Tenant's storage or streams are unavailable"),
113
112
  ...route.responses,
@@ -12,6 +12,7 @@
12
12
  import { OpenAPIRegistry, OpenApiGeneratorV32, } from "@asteasolutions/zod-to-openapi";
13
13
  import { HOST_PROTOCOL } from "@nylorun/core/compatibility";
14
14
  import { HealthResponseSchema, ReadyResponseSchema } from "@nylorun/core/contracts";
15
+ import { z } from "zod";
15
16
  import { RUNTIME_VERSION } from "../version.js";
16
17
  import { tenantApi } from "./http/app.js";
17
18
  const PROTOCOL = {
@@ -25,7 +26,22 @@ const bearer = (description, bearerFormat) => ({
25
26
  ...(bearerFormat ? { bearerFormat } : {}),
26
27
  description,
27
28
  });
28
- /** `/health`, `/ready` and `/openapi.json`: what every listener answers without a key. */
29
+ /** RFC 9728 protected resource metadata, as `tenant/resource-server.ts` builds it. */
30
+ const ProtectedResourceMetadataSchema = z
31
+ .object({
32
+ resource: z.string().meta({ description: "The Runtime API: `NYLORUN_PUBLIC_URL`, else the origin the request reached" }),
33
+ authorization_servers: z
34
+ .array(z.string())
35
+ .meta({ description: "The identity file's issuers, in its order: a client usually signs in at the first" }),
36
+ scopes_supported: z.array(z.string()).meta({ description: "The scopes the issuers' tokens may carry, `studio` aside" }),
37
+ bearer_methods_supported: z.array(z.literal("header")),
38
+ resource_name: z.string(),
39
+ })
40
+ .meta({ id: "ProtectedResourceMetadata" });
41
+ /**
42
+ * `/health`, `/ready`, the documents and the OAuth metadata: what every listener answers
43
+ * without a key. The `/openapi.json` alias is served but left out of the documents.
44
+ */
29
45
  function hostRoutes() {
30
46
  const registry = new OpenAPIRegistry();
31
47
  const route = (config) => registry.registerPath(config);
@@ -58,17 +74,24 @@ function hostRoutes() {
58
74
  });
59
75
  route({
60
76
  method: "get",
61
- path: "/openapi/runtime.json",
77
+ path: "/.well-known/oauth-protected-resource",
62
78
  tags: ["Host"],
63
- summary: "Get the Runtime API's document",
64
- description: "This document. `/openapi.json` is the same.",
65
- responses: { 200: { description: "The Runtime API's OpenAPI document" } },
79
+ summary: "Get the protected resource metadata",
80
+ description: "OAuth 2.0 Protected Resource Metadata (RFC 9728): the identity providers whose access tokens the Runtime API accepts (the identity file's trusted issuers) and their scopes. Nylorun is never the authorization server: a client signs in at one of `authorization_servers` and presents its token as `Authorization: Bearer`. A `401` from the Runtime API names this document in `WWW-Authenticate` (`resource_metadata`). Needs no key or `Nylorun-Protocol`, and accepts an `Origin`.",
81
+ responses: {
82
+ 200: {
83
+ description: "The metadata",
84
+ content: { "application/json": { schema: ProtectedResourceMetadataSchema } },
85
+ },
86
+ 404: { description: "The Runtime trusts no identity provider (no identity file)" },
87
+ },
66
88
  });
67
89
  route({
68
90
  method: "get",
69
- path: "/openapi.json",
91
+ path: "/openapi/runtime.json",
70
92
  tags: ["Host"],
71
- summary: "Get the Runtime API's document (alias)",
93
+ summary: "Get the Runtime API's document",
94
+ description: "This document. `/openapi.json` is the same.",
72
95
  responses: { 200: { description: "The Runtime API's OpenAPI document" } },
73
96
  });
74
97
  route({
@@ -86,19 +109,6 @@ function tenantSchemes(registry) {
86
109
  registry.registerComponent("securitySchemes", "issuerToken", bearer("A JWT from a trusted issuer of the Host's identity file (Host feature `trusted-issuers`): one person, with the issuer's scopes, agents and sandbox grants, until it expires. Accepted from servers and browsers alike; CORS is the operator's proxy's.", "JWT"));
87
110
  }
88
111
  const RUNTIME_TAGS = [
89
- {
90
- name: "Runtime",
91
- group: "Get started",
92
- description: "Check a Runtime is up and speaks your protocol, ask who your credential is, and read the public keys of the tokens it signs. `/health`, `/ready`, the JWKS and the documents need no key.",
93
- operations: [
94
- "GET /health",
95
- "GET /ready",
96
- "GET /v1/me",
97
- "GET /v1/access/jwks",
98
- "GET /openapi/runtime.json",
99
- "GET /openapi.json",
100
- ],
101
- },
102
112
  {
103
113
  name: "Agents",
104
114
  group: "Agents",
@@ -147,7 +157,7 @@ const RUNTIME_TAGS = [
147
157
  },
148
158
  {
149
159
  name: "Sandboxes",
150
- group: "Sandboxes & artifacts",
160
+ group: "Sandboxes",
151
161
  description: "Isolated machines an agent works in: create or find one, watch its lifecycle, stop, reset or delete it. In `@nylorun/agents`.",
152
162
  operations: [
153
163
  "PUT /v1/sandboxes/{sandboxId}",
@@ -161,7 +171,7 @@ const RUNTIME_TAGS = [
161
171
  },
162
172
  {
163
173
  name: "Artifacts",
164
- group: "Sandboxes & artifacts",
174
+ group: "Artifacts",
165
175
  description: "Files and folders agents and people produce, versioned, with short-lived capability links to share them. In `@nylorun/agents`.",
166
176
  operations: [
167
177
  "POST /v1/artifacts",
@@ -178,6 +188,19 @@ const RUNTIME_TAGS = [
178
188
  "DELETE /v1/artifacts/{artifactId}",
179
189
  ],
180
190
  },
191
+ {
192
+ name: "Service",
193
+ group: "Service",
194
+ description: "Check a Runtime is up and speaks your protocol, ask who your credential is, find the identity providers it takes tokens from, and read the public keys of the tokens it signs. `/health`, `/ready`, the protected resource metadata, the JWKS and the documents need no key.",
195
+ operations: [
196
+ "GET /health",
197
+ "GET /ready",
198
+ "GET /v1/me",
199
+ "GET /.well-known/oauth-protected-resource",
200
+ "GET /v1/access/jwks",
201
+ "GET /openapi/runtime.json",
202
+ ],
203
+ },
181
204
  ];
182
205
  const MANAGEMENT_TAGS = [
183
206
  {
@@ -91,7 +91,7 @@ export interface SandboxResult {
91
91
  export interface WorkerHandlers {
92
92
  /**
93
93
  * Advances one session (§10.5). `signal` aborts when the Worker stops or the
94
- * session is cancelled through the control stream. Throw only on
94
+ * session is cancelled through the control bus. Throw only on
95
95
  * infrastructure errors.
96
96
  */
97
97
  advance(tenantId: string, sessionId: string, signal: AbortSignal): Promise<AdvanceResult>;
@@ -1,5 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import type { Logger, NodeBindings, TenantModule } from "../tenant/types.js";
3
+ import { type ResourceServerConfig } from "../tenant/resource-server.js";
3
4
  export type HostEnv = {
4
5
  Bindings: NodeBindings;
5
6
  Variables: {
@@ -19,6 +20,8 @@ export interface HostAppOptions {
19
20
  ok: boolean;
20
21
  checks: Record<string, boolean>;
21
22
  }>;
23
+ /** The public URL and trusted issuers the OAuth metadata and challenges name. */
24
+ resourceServer?: ResourceServerConfig;
22
25
  /** The listener is listening. */
23
26
  listening(): boolean;
24
27
  closing(): boolean;
package/dist/host/app.js CHANGED
@@ -3,10 +3,13 @@ import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
3
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
+ import { keyAccess } from "../api/http/define.js";
7
+ import { bearerChallenge, PROTECTED_RESOURCE_PATH, protectedResourceMetadata, protectedResourceMetadataUrl, } from "../tenant/resource-server.js";
6
8
  import { managementDocument, runtimeDocument } from "../api/openapi.js";
7
9
  import { headerValue, isJsonContentType, jsonResponse, opaqueNotFoundResponse, protocolRejectedResponse, pathnameIsLogged, redactRoutePath, rejectedResponse, requestHasBody, } from "./http.js";
8
10
  export function createHostApp(options) {
9
11
  const { module, logger } = options;
12
+ const resourceServer = options.resourceServer ?? {};
10
13
  const app = new Hono();
11
14
  app.use(async (c, next) => {
12
15
  const started = Date.now();
@@ -28,9 +31,10 @@ export function createHostApp(options) {
28
31
  app.use(async (c, next) => {
29
32
  const { incoming } = c.env;
30
33
  if (headerValue(incoming, "origin") !== undefined) {
31
- // Only Tenant routes; the Tenant then accepts only a trusted issuer's token from a
32
- // browser.
33
- if (pathnameOf(incoming).split("/").filter(Boolean)[0] !== "v1")
34
+ // Only Tenant routes, where the Tenant then accepts only a trusted issuer's token from a
35
+ // browser, and the public OAuth metadata a browser client reads before it signs in.
36
+ if (pathnameOf(incoming).split("/").filter(Boolean)[0] !== "v1" &&
37
+ !isProtectedResourcePath(pathnameOf(incoming)))
34
38
  return rejectedResponse(403, "origin_rejected", "Browser Origin headers are not accepted");
35
39
  }
36
40
  // CORS is the operator's proxy's: an OPTIONS request gets no CORS header.
@@ -83,13 +87,35 @@ export function createHostApp(options) {
83
87
  return jsonResponse(200, runtimeDocument(), { "cache-control": "no-cache" });
84
88
  if (pathname === "/openapi/management.json" && incoming.method === "GET")
85
89
  return jsonResponse(200, managementDocument(), { "cache-control": "no-cache" });
90
+ // RFC 9728: where the Runtime API's tokens come from. None without trusted issuers.
91
+ if (isProtectedResourcePath(pathname) && incoming.method === "GET") {
92
+ const metadata = protectedResourceMetadata(resourceServer, incoming);
93
+ return metadata
94
+ ? jsonResponse(200, metadata, { "cache-control": "no-cache" })
95
+ : rejectedResponse(404, "not_found", "This Runtime trusts no identity provider");
96
+ }
86
97
  // Tenant routes: protocol → the Tenant → selection → the Tenant's routes.
87
98
  const tenantHeader = headerValue(incoming, TENANT_HEADER);
88
99
  const named = tenantHeader === undefined || tenantHeader.trim() === "" ? undefined : tenantHeader.trim();
89
100
  const protocol = headerValue(incoming, PROTOCOL_HEADER);
101
+ // A generic OAuth client sends neither header: answer it the challenge, not the 426.
102
+ const route = tenantRouteOf(incoming);
103
+ if (protocol === undefined &&
104
+ headerValue(incoming, "authorization") === undefined &&
105
+ route !== undefined &&
106
+ route.anonymous !== true) {
107
+ const metadata = keyAccess(route).application
108
+ ? protectedResourceMetadataUrl(resourceServer, incoming)
109
+ : undefined;
110
+ return jsonResponse(401, {
111
+ status: "rejected",
112
+ code: "credential_required",
113
+ message: "A bearer credential is required",
114
+ }, { "www-authenticate": bearerChallenge({ resourceMetadata: metadata }) });
115
+ }
90
116
  // A capability link is opened without the header; when one is sent, it is checked.
91
117
  if (!protocolAccepted(protocol) &&
92
- !(protocol === undefined && tenantRouteOf(incoming)?.unversioned === true))
118
+ !(protocol === undefined && route?.unversioned === true))
93
119
  return protocolRejectedResponse();
94
120
  const resolution = await module.resolve();
95
121
  if (resolution.kind !== "open") {
@@ -138,6 +164,9 @@ export function createHostApp(options) {
138
164
  }
139
165
  /** The methods the Runtime serves, for an `OPTIONS` answer's `Allow`. */
140
166
  const ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS";
167
+ function isProtectedResourcePath(pathname) {
168
+ return pathname === PROTECTED_RESOURCE_PATH || pathname.startsWith(`${PROTECTED_RESOURCE_PATH}/`);
169
+ }
141
170
  /** The Tenant route a request names, if any; a malformed path names none. */
142
171
  function tenantRouteOf(incoming) {
143
172
  const segments = [];
@@ -1,3 +1,4 @@
1
+ import type { ResourceServerConfig } from "../tenant/resource-server.js";
1
2
  import type { Logger, TenantModule } from "../tenant/types.js";
2
3
  import type { HostConfigFile } from "./config.js";
3
4
  import type { ContainerListen } from "./stack-config.js";
@@ -25,6 +26,11 @@ export interface CreateHostOptions {
25
26
  ok: boolean;
26
27
  checks: Record<string, boolean>;
27
28
  }>;
29
+ /**
30
+ * The public URL and trusted issuers behind `/.well-known/oauth-protected-resource` and the
31
+ * challenge a request without credential or protocol gets (protocol 9). Absent: no metadata.
32
+ */
33
+ resourceServer?: ResourceServerConfig;
28
34
  /**
29
35
  * Shutdown steps around closing the Tenant. `close()` runs them whatever asked for it
30
36
  * (SIGTERM in `host/main.ts`): the listener stops, then
@@ -20,6 +20,7 @@ export function createHost(options) {
20
20
  coreVersion,
21
21
  pid,
22
22
  ...(options.readiness ? { readiness: options.readiness } : {}),
23
+ ...(options.resourceServer ? { resourceServer: options.resourceServer } : {}),
23
24
  listening: () => Boolean(server?.listening),
24
25
  closing: () => closing,
25
26
  });
package/dist/host/main.js CHANGED
@@ -71,9 +71,10 @@ function loadJson(path) {
71
71
  /**
72
72
  * The trusted issuers of the identity file (`NYLORUN_IDENTITY_FILE`, Host feature
73
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.
74
+ * a key the file does not define is logged and ignored. An issuer's JWKS is fetched only when a
75
+ * token needs it, so one that is down does not stop the boot.
75
76
  */
76
- function loadIssuers(path) {
77
+ function loadIssuers(path, logger) {
77
78
  let text;
78
79
  try {
79
80
  text = readFileSync(path, "utf8");
@@ -81,7 +82,9 @@ function loadIssuers(path) {
81
82
  catch (error) {
82
83
  throw new Error(`NYLORUN_IDENTITY_FILE names ${path}, which cannot be read: ${error instanceof Error ? error.message : String(error)}`);
83
84
  }
84
- return createTrustedIssuers(parseIdentityFile(text, path));
85
+ return createTrustedIssuers(parseIdentityFile(text, path, {
86
+ warn: (message) => logger.warn("identity_file_key_ignored", { message }),
87
+ }));
85
88
  }
86
89
  /** The bootstrap management key: 64 hex characters, surrounding whitespace ignored. */
87
90
  function loadManagementKey(path) {
@@ -224,8 +227,8 @@ export async function main() {
224
227
  const config = loadJson(paths.config);
225
228
  const credentials = loadJson(paths.credentials);
226
229
  const identityFile = stack.tenant?.identityFile;
227
- const issuers = identityFile ? loadIssuers(identityFile) : undefined;
228
230
  const logger = createHostLogger();
231
+ const issuers = identityFile ? loadIssuers(identityFile, logger) : undefined;
229
232
  if (issuers)
230
233
  logger.info("trusted_issuers", {
231
234
  file: identityFile,
@@ -362,6 +365,10 @@ export async function main() {
362
365
  coreVersion: coreVersion(),
363
366
  ...(stack.listen ? { listen: stack.listen } : {}),
364
367
  ...(infra.readiness ? { readiness: infra.readiness } : {}),
368
+ resourceServer: {
369
+ ...(issuers ? { issuers } : {}),
370
+ ...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
371
+ },
365
372
  // SIGTERM closes the Host this way: stop the Worker and the relay, close the Tenant, then end the infrastructure clients.
366
373
  shutdown: {
367
374
  beforeTenants: async () => {
@@ -66,7 +66,7 @@ export const DEFAULT_SERVICES = new Set([
66
66
  ]);
67
67
  /** Services of the blueprint this release doesn't have yet. */
68
68
  const LATER_SERVICES = [
69
- "sandboxd",
69
+ "sandboxes",
70
70
  ];
71
71
  const ROLE_SERVICES = {
72
72
  api: ["core"],
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The Host's infrastructure clients, built from the stack configuration: the
3
3
  * Postgres pool, Durable Session Execution and Durable Streams, each only when
4
- * its endpoints are configured, plus the readiness that covers them.
4
+ * its endpoints are configured, plus the readiness that covers Postgres and
5
+ * Restate (not S2: `readiness.ts`).
5
6
  *
6
7
  * Building a client connects to nothing: the pool connects on first query,
7
8
  * Restate and S2 on first call. Nothing here starts a Worker (see
@@ -20,7 +20,6 @@ export function createInfra(config, options = {}) {
20
20
  const probes = infraProbes({
21
21
  ...(database ? { database } : {}),
22
22
  ...(execution ? { execution } : {}),
23
- ...(streams ? { streams } : {}),
24
23
  });
25
24
  const readiness = Object.keys(probes).length > 0
26
25
  ? logTransitions(createReadiness(probes), options.logger)
@@ -1,20 +1,21 @@
1
1
  /**
2
2
  * Aggregate readiness of the Host's infrastructure (architecture §13, §14.3):
3
- * the Runtime's `/ready` covers Postgres, Restate and S2. s2-lite's image has
4
- * no health check of its own, so this is the only place its health shows.
3
+ * the Runtime's `/ready` covers Postgres and Restate. S2 is left out (D48): it only
4
+ * serves API listeners (history, SSE, AG-UI, A2A), so an outage degrades those reads
5
+ * and never makes the Runtime unready. Its health shows in the Tenant's status
6
+ * (`GET /v1/tenant`, `streams.reachable`).
5
7
  *
6
8
  * Every probe runs on each call, in parallel, bounded by one timeout. A probe
7
9
  * that throws or outlives the timeout marks its check `false`. Only check
8
10
  * names and booleans are reported; the errors stay in the report for logs.
9
11
  */
10
12
  import type { DurableExecution } from "../execution/types.js";
11
- import type { DurableStreams } from "../streams/types.js";
12
13
  import { type PostgresClient } from "./database.js";
13
14
  /** Resolves when the dependency answers; rejects otherwise. Must honour `signal`. */
14
15
  export type Probe = (signal: AbortSignal) => Promise<void>;
15
16
  export interface ReadinessReport {
16
17
  ok: boolean;
17
- /** One entry per configured dependency, e.g. `{ postgres, restate, s2 }`. */
18
+ /** One entry per configured dependency, e.g. `{ postgres, restate }`. */
18
19
  checks: Record<string, boolean>;
19
20
  /** Why each failing check failed. Not for unauthenticated responses. */
20
21
  errors: Record<string, string>;
@@ -25,9 +26,8 @@ export declare const READINESS_TIMEOUT_MS = 2000;
25
26
  export declare function createReadiness(probes: Readonly<Record<string, Probe>>, options?: {
26
27
  timeoutMs?: number;
27
28
  }): Readiness;
28
- /** The standard checks for whichever infrastructure the Host was built with. */
29
+ /** The standard checks for whichever infrastructure the Host was built with. Never S2 (D48). */
29
30
  export declare function infraProbes(infra: {
30
31
  database?: PostgresClient;
31
32
  execution?: DurableExecution;
32
- streams?: DurableStreams;
33
33
  }): Record<string, Probe>;
@@ -1,6 +1,5 @@
1
1
  import { probeDatabase } from "./database.js";
2
2
  import { probeExecution } from "./execution.js";
3
- import { probeStreams } from "./streams.js";
4
3
  /** Default bound on one readiness pass. Compose probes `/ready` every 2 s with a 5 s timeout. */
5
4
  export const READINESS_TIMEOUT_MS = 2000;
6
5
  export function createReadiness(probes, options = {}) {
@@ -18,16 +17,14 @@ export function createReadiness(probes, options = {}) {
18
17
  return { ok: Object.values(checks).every(Boolean), checks, errors };
19
18
  };
20
19
  }
21
- /** The standard checks for whichever infrastructure the Host was built with. */
20
+ /** The standard checks for whichever infrastructure the Host was built with. Never S2 (D48). */
22
21
  export function infraProbes(infra) {
23
22
  const probes = {};
24
- const { database, execution, streams } = infra;
23
+ const { database, execution } = infra;
25
24
  if (database)
26
25
  probes.postgres = (signal) => probeDatabase(database, signal);
27
26
  if (execution)
28
27
  probes.restate = (signal) => probeExecution(execution, signal);
29
- if (streams)
30
- probes.s2 = (signal) => probeStreams(streams, signal);
31
28
  return probes;
32
29
  }
33
30
  /** Runs one probe under a timeout; returns its failure message, or undefined when ready. */