@nylorun/runtime 0.9.0-beta → 0.11.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 (192) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +110 -37
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/media.d.ts +2 -2
  6. package/dist/adapters/media.js +2 -2
  7. package/dist/adapters/observe.d.ts +2 -1
  8. package/dist/adapters/observe.js +2 -3
  9. package/dist/adapters/sandbox/virtual.js +1 -1
  10. package/dist/adapters/streams/s2.d.ts +18 -0
  11. package/dist/adapters/streams/s2.js +361 -0
  12. package/dist/assets.d.ts +2 -2
  13. package/dist/assets.js +5 -3
  14. package/dist/core/executors.d.ts +15 -3
  15. package/dist/core/executors.js +40 -5
  16. package/dist/core/flow-host.d.ts +225 -0
  17. package/dist/core/flow-host.js +518 -0
  18. package/dist/core/limits.d.ts +40 -0
  19. package/dist/core/limits.js +76 -0
  20. package/dist/core/runtime.d.ts +5 -90
  21. package/dist/core/runtime.js +3 -1447
  22. package/dist/core/sandbox-routes.d.ts +40 -0
  23. package/dist/core/sandbox-routes.js +106 -0
  24. package/dist/core/turn-manifest.d.ts +50 -0
  25. package/dist/core/turn-manifest.js +66 -0
  26. package/dist/execution/memory.d.ts +54 -0
  27. package/dist/execution/memory.js +217 -0
  28. package/dist/execution/types.d.ts +125 -0
  29. package/dist/execution/types.js +52 -0
  30. package/dist/host/config-for.d.ts +17 -0
  31. package/dist/host/config-for.js +27 -0
  32. package/dist/host/config.d.ts +16 -0
  33. package/dist/host/create-host.d.ts +56 -0
  34. package/dist/host/create-host.js +420 -0
  35. package/dist/host/environment.d.ts +13 -0
  36. package/dist/host/environment.js +53 -0
  37. package/dist/host/execution.d.ts +61 -0
  38. package/dist/host/execution.js +69 -0
  39. package/dist/host/http.d.ts +39 -0
  40. package/dist/host/http.js +142 -0
  41. package/dist/host/index.d.ts +8 -0
  42. package/dist/host/index.js +7 -0
  43. package/dist/host/logger.d.ts +3 -0
  44. package/dist/host/logger.js +18 -0
  45. package/dist/host/main.d.ts +3 -0
  46. package/dist/host/main.js +201 -0
  47. package/dist/host/stack-config.d.ts +72 -0
  48. package/dist/host/stack-config.js +193 -0
  49. package/dist/index.d.ts +7 -2
  50. package/dist/index.js +4 -2
  51. package/dist/infra/database.d.ts +16 -0
  52. package/dist/infra/database.js +31 -0
  53. package/dist/infra/execution.d.ts +39 -0
  54. package/dist/infra/execution.js +68 -0
  55. package/dist/infra/index.d.ts +42 -0
  56. package/dist/infra/index.js +56 -0
  57. package/dist/infra/readiness.d.ts +33 -0
  58. package/dist/infra/readiness.js +53 -0
  59. package/dist/infra/streams.d.ts +17 -0
  60. package/dist/infra/streams.js +33 -0
  61. package/dist/infra/workers.d.ts +39 -0
  62. package/dist/infra/workers.js +31 -0
  63. package/dist/mcp/connect.d.ts +1 -0
  64. package/dist/mcp/connect.js +2 -0
  65. package/dist/mcp/pool.d.ts +3 -1
  66. package/dist/mcp/pool.js +2 -1
  67. package/dist/mcp/stdio.d.ts +2 -0
  68. package/dist/mcp/stdio.js +3 -1
  69. package/dist/model/auth-store.d.ts +1 -1
  70. package/dist/model/auth-store.js +2 -2
  71. package/dist/model/models.d.ts +4 -0
  72. package/dist/model/models.js +22 -26
  73. package/dist/model/pi-model.d.ts +2 -2
  74. package/dist/model/pi-model.js +6 -6
  75. package/dist/model/settings.d.ts +5 -1
  76. package/dist/model/settings.js +6 -2
  77. package/dist/sandbox/manager.d.ts +26 -8
  78. package/dist/sandbox/manager.js +95 -47
  79. package/dist/sandbox/select.d.ts +5 -4
  80. package/dist/sandbox/select.js +18 -13
  81. package/dist/sandbox/share.d.ts +23 -0
  82. package/dist/sandbox/share.js +58 -0
  83. package/dist/sandbox/types.d.ts +2 -2
  84. package/dist/store/canonical.d.ts +5 -0
  85. package/dist/store/canonical.js +17 -0
  86. package/dist/store/cursor.d.ts +9 -0
  87. package/dist/store/cursor.js +18 -0
  88. package/dist/store/memory.d.ts +57 -0
  89. package/dist/store/memory.js +643 -0
  90. package/dist/store/ownership.d.ts +19 -0
  91. package/dist/store/ownership.js +33 -0
  92. package/dist/store/postgres/connect.d.ts +25 -0
  93. package/dist/store/postgres/connect.js +25 -0
  94. package/dist/store/postgres/locking.d.ts +16 -0
  95. package/dist/store/postgres/locking.js +22 -0
  96. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  97. package/dist/store/postgres/migrations/001_initial.js +196 -0
  98. package/dist/store/postgres/migrations/index.d.ts +49 -0
  99. package/dist/store/postgres/migrations/index.js +75 -0
  100. package/dist/store/postgres/names.d.ts +11 -0
  101. package/dist/store/postgres/names.js +42 -0
  102. package/dist/store/postgres/store.d.ts +11 -0
  103. package/dist/store/postgres/store.js +790 -0
  104. package/dist/store/postgres/tenants.d.ts +90 -0
  105. package/dist/store/postgres/tenants.js +207 -0
  106. package/dist/store/types.d.ts +440 -0
  107. package/dist/store/types.js +10 -0
  108. package/dist/streams/basin.d.ts +23 -0
  109. package/dist/streams/basin.js +51 -0
  110. package/dist/streams/memory.d.ts +25 -0
  111. package/dist/streams/memory.js +124 -0
  112. package/dist/streams/relay.d.ts +37 -0
  113. package/dist/streams/relay.js +254 -0
  114. package/dist/streams/types.d.ts +115 -0
  115. package/dist/streams/types.js +86 -0
  116. package/dist/tenant/actions.d.ts +153 -0
  117. package/dist/tenant/actions.js +191 -0
  118. package/dist/tenant/advance.d.ts +15 -0
  119. package/dist/tenant/advance.js +439 -0
  120. package/dist/tenant/auth.d.ts +11 -0
  121. package/dist/tenant/auth.js +34 -0
  122. package/dist/tenant/commands.d.ts +3 -0
  123. package/dist/tenant/commands.js +384 -0
  124. package/dist/tenant/context.d.ts +140 -0
  125. package/dist/tenant/context.js +40 -0
  126. package/dist/tenant/effects.d.ts +20 -0
  127. package/dist/tenant/effects.js +539 -0
  128. package/dist/tenant/envelope.d.ts +7 -0
  129. package/dist/tenant/envelope.js +22 -0
  130. package/dist/tenant/ephemeral.d.ts +42 -0
  131. package/dist/tenant/ephemeral.js +224 -0
  132. package/dist/tenant/http.d.ts +32 -0
  133. package/dist/tenant/http.js +51 -0
  134. package/dist/tenant/live.d.ts +92 -0
  135. package/dist/tenant/live.js +290 -0
  136. package/dist/tenant/logger.d.ts +9 -0
  137. package/dist/tenant/logger.js +25 -0
  138. package/dist/tenant/model-setting.d.ts +16 -0
  139. package/dist/tenant/model-setting.js +12 -0
  140. package/dist/tenant/module.d.ts +38 -0
  141. package/dist/tenant/module.js +319 -0
  142. package/dist/tenant/paths.d.ts +28 -0
  143. package/dist/tenant/paths.js +91 -0
  144. package/dist/tenant/pool.d.ts +10 -0
  145. package/dist/tenant/pool.js +54 -0
  146. package/dist/tenant/principals.d.ts +13 -0
  147. package/dist/tenant/principals.js +37 -0
  148. package/dist/tenant/quarantine-error.d.ts +6 -0
  149. package/dist/tenant/quarantine-error.js +9 -0
  150. package/dist/tenant/quarantine.d.ts +27 -0
  151. package/dist/tenant/quarantine.js +72 -0
  152. package/dist/tenant/reset.d.ts +22 -0
  153. package/dist/tenant/reset.js +81 -0
  154. package/dist/tenant/routes-tenant.d.ts +12 -0
  155. package/dist/tenant/routes-tenant.js +126 -0
  156. package/dist/tenant/routes.d.ts +9 -0
  157. package/dist/tenant/routes.js +142 -0
  158. package/dist/tenant/runtime.d.ts +81 -0
  159. package/dist/tenant/runtime.js +298 -0
  160. package/dist/tenant/scheduler.d.ts +25 -0
  161. package/dist/tenant/scheduler.js +84 -0
  162. package/dist/tenant/session.d.ts +60 -0
  163. package/dist/tenant/session.js +94 -0
  164. package/dist/tenant/sessions.d.ts +34 -0
  165. package/dist/tenant/sessions.js +149 -0
  166. package/dist/tenant/status.d.ts +37 -0
  167. package/dist/tenant/status.js +130 -0
  168. package/dist/tenant/store-memory.d.ts +16 -0
  169. package/dist/tenant/store-memory.js +111 -0
  170. package/dist/tenant/store-pg.d.ts +15 -0
  171. package/dist/tenant/store-pg.js +140 -0
  172. package/dist/tenant/streams.d.ts +126 -0
  173. package/dist/tenant/streams.js +419 -0
  174. package/dist/tenant/sweep.d.ts +15 -0
  175. package/dist/tenant/sweep.js +122 -0
  176. package/dist/tenant/types.d.ts +205 -0
  177. package/dist/tenant/types.js +23 -0
  178. package/dist/tenant/worker.d.ts +84 -0
  179. package/dist/tenant/worker.js +189 -0
  180. package/dist/vault/kek.d.ts +5 -2
  181. package/dist/vault/kek.js +7 -10
  182. package/dist/vault/service.d.ts +37 -31
  183. package/dist/vault/service.js +439 -359
  184. package/dist/version.d.ts +1 -1
  185. package/dist/version.js +1 -1
  186. package/package.json +14 -11
  187. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  188. package/dist/adapters/sandbox/microsandbox.js +0 -238
  189. package/dist/core/main.js +0 -35
  190. package/dist/core/store.d.ts +0 -30
  191. package/dist/core/store.js +0 -202
  192. /package/dist/{core/main.d.ts → host/config.js} +0 -0
@@ -0,0 +1,9 @@
1
+ /** Typed quarantine raised while opening a Tenant Runtime (A7). */
2
+ export class QuarantineError extends Error {
3
+ quarantine;
4
+ constructor(code, message, repair = "nylo tenant status") {
5
+ super(message);
6
+ this.name = "QuarantineError";
7
+ this.quarantine = { code, message, repair };
8
+ }
9
+ }
@@ -0,0 +1,27 @@
1
+ import type { Quarantine } from "./types.js";
2
+ /** Typed quarantine failure thrown by stores and migration. */
3
+ export declare class QuarantineError extends Error implements Quarantine {
4
+ readonly code: Quarantine["code"];
5
+ readonly repair: string;
6
+ constructor(fields: Quarantine);
7
+ toQuarantine(): Quarantine;
8
+ }
9
+ export declare function isQuarantineError(error: unknown): error is QuarantineError;
10
+ export declare function asQuarantine(error: unknown): Quarantine | undefined;
11
+ /** CLI-facing repair strings for every quarantine code (B9). */
12
+ export declare function repairFor(code: Quarantine["code"], detail?: {
13
+ tenantId?: string;
14
+ }): string;
15
+ export declare function quarantine(code: Quarantine["code"], message: string, detail?: {
16
+ tenantId?: string;
17
+ }): QuarantineError;
18
+ /** Conflict when create bootstrap material does not match an existing Tenant (HTTP 409). */
19
+ export declare class TenantConflictError extends Error {
20
+ readonly statusCode: 409;
21
+ constructor(message?: string);
22
+ }
23
+ /** Delete refused because the Tenant still has live work (HTTP 409). */
24
+ export declare class TenantBusyError extends Error {
25
+ readonly statusCode: 409;
26
+ constructor(message?: string);
27
+ }
@@ -0,0 +1,72 @@
1
+ /** Typed quarantine failure thrown by stores and migration. */
2
+ export class QuarantineError extends Error {
3
+ code;
4
+ repair;
5
+ constructor(fields) {
6
+ super(fields.message);
7
+ this.name = "QuarantineError";
8
+ this.code = fields.code;
9
+ this.repair = fields.repair;
10
+ }
11
+ toQuarantine() {
12
+ return { code: this.code, message: this.message, repair: this.repair };
13
+ }
14
+ }
15
+ export function isQuarantineError(error) {
16
+ return error instanceof QuarantineError;
17
+ }
18
+ export function asQuarantine(error) {
19
+ if (isQuarantineError(error))
20
+ return error.toQuarantine();
21
+ if (error &&
22
+ typeof error === "object" &&
23
+ "code" in error &&
24
+ "message" in error &&
25
+ "repair" in error) {
26
+ return error;
27
+ }
28
+ return undefined;
29
+ }
30
+ /** CLI-facing repair strings for every quarantine code (B9). */
31
+ export function repairFor(code, detail = {}) {
32
+ const id = detail.tenantId ? ` ${detail.tenantId}` : "";
33
+ switch (code) {
34
+ case "kek-missing":
35
+ return `nylo tenant status${id} — restore vault-kek for this Tenant (ciphertext cannot be opened without it)`;
36
+ case "corrupt":
37
+ return `nylo tenant status${id} — restore the Tenant from backup`;
38
+ case "schema-too-new":
39
+ return `nylo tenant status${id} — the Tenant was migrated by a newer Runtime; run that version or newer`;
40
+ case "migration-failed":
41
+ return `nylo tenant status${id} — inspect the Runtime log for the failed migration, fix it, then restart the Runtime`;
42
+ case "envelope-invalid":
43
+ return `nylo tenant status${id} — the Tenant envelope is missing or invalid; restore the Tenant from backup`;
44
+ case "open-timeout":
45
+ return `nylo tenant status${id} — open timed out; inspect the Runtime log and Postgres, then retry`;
46
+ case "open-failed":
47
+ return `nylo tenant status${id} — inspect Tenant logs and repair before the Host retries open`;
48
+ }
49
+ }
50
+ export function quarantine(code, message, detail = {}) {
51
+ return new QuarantineError({
52
+ code,
53
+ message,
54
+ repair: repairFor(code, detail),
55
+ });
56
+ }
57
+ /** Conflict when create bootstrap material does not match an existing Tenant (HTTP 409). */
58
+ export class TenantConflictError extends Error {
59
+ statusCode = 409;
60
+ constructor(message = "Tenant already exists with different bootstrap material") {
61
+ super(message);
62
+ this.name = "TenantConflictError";
63
+ }
64
+ }
65
+ /** Delete refused because the Tenant still has live work (HTTP 409). */
66
+ export class TenantBusyError extends Error {
67
+ statusCode = 409;
68
+ constructor(message = "Tenant has live work; pass activeWork=drain|cancel or retry later") {
69
+ super(message);
70
+ this.name = "TenantBusyError";
71
+ }
72
+ }
@@ -0,0 +1,22 @@
1
+ import type { ResetScope, SessionStore } from "../store/types.js";
2
+ import type { ExecutorRegistry } from "../core/executors.js";
3
+ import type { SandboxManager } from "../sandbox/manager.js";
4
+ import type { TenantPaths } from "./types.js";
5
+ export type { ResetScope } from "../store/types.js";
6
+ export interface ResetTenantContext {
7
+ store: SessionStore;
8
+ registry: ExecutorRegistry;
9
+ sandbox: SandboxManager;
10
+ paths: TenantPaths;
11
+ /** Clear in-memory session observers after the store wipe. */
12
+ clearSessionState: () => void;
13
+ /** Clear executor SSE streams when registrations are removed. */
14
+ clearExecutorStreams: () => void;
15
+ }
16
+ /**
17
+ * Reset Tenant durable state for `scope` (A17).
18
+ * Caller must have already drained (`activeWork`). Deletes store state in one
19
+ * transaction (`Tx.reset`), then does the filesystem rename+delete for sandboxes /
20
+ * log as needed. The host vault (model credentials), principals and settings stay.
21
+ */
22
+ export declare function resetTenant(ctx: ResetTenantContext, scope: ResetScope): Promise<void>;
@@ -0,0 +1,81 @@
1
+ import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Rename `dir` to a sibling, delete the sibling, recreate an empty `dir`.
5
+ * Failure before rename leaves the original intact; after rename the empty
6
+ * target is restored so the Tenant layout stays valid (A17).
7
+ */
8
+ function replaceDirectory(dir) {
9
+ mkdirSync(dir, { recursive: true });
10
+ const sibling = `${dir}.resetting`;
11
+ if (existsSync(sibling))
12
+ rmSync(sibling, { recursive: true, force: true });
13
+ renameSync(dir, sibling);
14
+ try {
15
+ mkdirSync(dir, { recursive: true });
16
+ rmSync(sibling, { recursive: true, force: true });
17
+ }
18
+ catch (error) {
19
+ // Best-effort restore if recreation failed.
20
+ if (!existsSync(dir) && existsSync(sibling)) {
21
+ try {
22
+ renameSync(sibling, dir);
23
+ }
24
+ catch {
25
+ /* leave sibling for operator inspection */
26
+ }
27
+ }
28
+ throw error;
29
+ }
30
+ }
31
+ /**
32
+ * Reset Tenant durable state for `scope` (A17).
33
+ * Caller must have already drained (`activeWork`). Deletes store state in one
34
+ * transaction (`Tx.reset`), then does the filesystem rename+delete for sandboxes /
35
+ * log as needed. The host vault (model credentials), principals and settings stay.
36
+ */
37
+ export async function resetTenant(ctx, scope) {
38
+ const clearSessions = scope === "sessions" || scope === "all";
39
+ const clearSandboxes = scope === "sandboxes" || scope === "all";
40
+ const clearAll = scope === "all";
41
+ if (clearSandboxes) {
42
+ await ctx.sandbox.reconcile(() => false);
43
+ }
44
+ await ctx.store.tx((t) => t.reset(scope));
45
+ if (clearAll)
46
+ for (const row of ctx.registry.list())
47
+ ctx.registry.remove(row.agentId);
48
+ if (clearSessions)
49
+ ctx.clearSessionState();
50
+ if (clearAll)
51
+ ctx.clearExecutorStreams();
52
+ if (clearSandboxes)
53
+ replaceDirectory(ctx.paths.sandboxes);
54
+ if (clearAll) {
55
+ mkdirSync(ctx.paths.logs, { recursive: true });
56
+ const logSibling = join(ctx.paths.logs, "tenant.log.resetting");
57
+ if (existsSync(ctx.paths.log)) {
58
+ if (existsSync(logSibling))
59
+ rmSync(logSibling, { force: true });
60
+ renameSync(ctx.paths.log, logSibling);
61
+ try {
62
+ writeFileSync(ctx.paths.log, "");
63
+ rmSync(logSibling, { force: true });
64
+ }
65
+ catch (error) {
66
+ if (!existsSync(ctx.paths.log) && existsSync(logSibling)) {
67
+ try {
68
+ renameSync(logSibling, ctx.paths.log);
69
+ }
70
+ catch {
71
+ /* leave sibling */
72
+ }
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+ else {
78
+ writeFileSync(ctx.paths.log, "");
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Tenant administration and vault routes: `/v1/tenant` (status, reset, config seed, host
3
+ * model and providers, sandbox report) and `/v1/vaults` (vaults and credentials).
4
+ * A reset that deletes sessions abandons their streams (a session created again gets a new
5
+ * incarnation) and collects them afterwards; it never deletes the Tenant's basin.
6
+ *
7
+ * Later waves: Wave 2 / X changes what reset clears once work leaves process memory.
8
+ */
9
+ import type { IncomingMessage } from "node:http";
10
+ import type { AuthScope, TenantContext } from "./context.js";
11
+ export declare function dispatchTenant(ctx: TenantContext, scope: AuthScope, method: string | undefined, path: string[], request: IncomingMessage): Promise<unknown>;
12
+ export declare function dispatchVault(ctx: TenantContext, scope: AuthScope, method: string | undefined, path: string[], url: URL, request: IncomingMessage): Promise<unknown>;
@@ -0,0 +1,126 @@
1
+ import { CreateCredentialRequestSchema, CreateVaultRequestSchema, PutHostModelRequestSchema, ResetTenantRequestSchema, RotateCredentialRequestSchema, SeedTenantConfigRequestSchema, SelectHostModelRequestSchema, } from "@nylorun/core/contracts";
2
+ import { hostModelCatalog } from "../model/catalog.js";
3
+ import { resetTenant } from "./reset.js";
4
+ import { buildTenantStatus, seedTenantConfig } from "./status.js";
5
+ import { fail, readBody } from "./http.js";
6
+ import { clearExecutorStreams, clearObservers } from "./live.js";
7
+ import { requestStreamCollection, sessionStreamsAbandoned, streamsStatus, } from "./streams.js";
8
+ import { clearWork, drain } from "./scheduler.js";
9
+ import { usesFixtureModel } from "./model-setting.js";
10
+ export async function dispatchTenant(ctx, scope, method, path, request) {
11
+ const { vault } = ctx;
12
+ if (scope.kind !== "application") {
13
+ await vault.reject(path.join("/"));
14
+ fail(403, "Application credential required");
15
+ }
16
+ if (path.length === 2 && method === "GET")
17
+ return await buildTenantStatus({
18
+ envelope: ctx.envelope,
19
+ config: ctx.config,
20
+ store: ctx.store,
21
+ registry: ctx.registry,
22
+ vault,
23
+ sandbox: ctx.sandbox,
24
+ closing: ctx.closing || ctx.closed,
25
+ modelConfigured: !ctx.useVaultModel ||
26
+ (await ctx.store.tx((t) => usesFixtureModel(t))) ||
27
+ (await vault.getHostModel()).configured,
28
+ executorStreams: ctx.live.executorStreams,
29
+ ...(ctx.stuckInvocations
30
+ ? { stuckInvocations: ctx.stuckInvocations }
31
+ : {}),
32
+ streamsStatus: () => streamsStatus(ctx),
33
+ });
34
+ if (path[2] === "reset" && path.length === 3 && method === "POST") {
35
+ const body = ResetTenantRequestSchema.parse(await readBody(request));
36
+ await drain(ctx, body.activeWork, 30_000);
37
+ // The deleted sessions' streams are abandoned: a session created again with the same id,
38
+ // during or after the reset, gets a new incarnation and starts at sequence 0. The pending
39
+ // collection is recorded first, so a crash before it runs leaves it to the sweep.
40
+ const sessionsReset = body.scope !== "sandboxes";
41
+ if (sessionsReset)
42
+ await requestStreamCollection(ctx);
43
+ await resetTenant({
44
+ store: ctx.store,
45
+ registry: ctx.registry,
46
+ sandbox: ctx.sandbox,
47
+ paths: ctx.config.paths,
48
+ clearSessionState: () => {
49
+ clearWork(ctx);
50
+ clearObservers(ctx.live);
51
+ },
52
+ clearExecutorStreams: () => clearExecutorStreams(ctx.live),
53
+ }, body.scope);
54
+ if (sessionsReset)
55
+ sessionStreamsAbandoned(ctx);
56
+ // Reset leaves the Tenant open for new work.
57
+ ctx.closing = false;
58
+ return { ok: true };
59
+ }
60
+ if (path[2] === "config" &&
61
+ path[3] === "seed" &&
62
+ path.length === 4 &&
63
+ method === "PUT") {
64
+ const body = SeedTenantConfigRequestSchema.parse(await readBody(request));
65
+ return await seedTenantConfig({ store: ctx.store, vault }, body);
66
+ }
67
+ if (path[2] === "models" && path.length === 3 && method === "GET")
68
+ return hostModelCatalog();
69
+ if (path[2] === "sandbox" && path.length === 3 && method === "GET")
70
+ return ctx.sandbox.report();
71
+ if (path[2] === "providers" && path.length === 3 && method === "GET")
72
+ return vault.listHostProviders();
73
+ if (path[2] === "model" && path.length === 3 && method === "GET")
74
+ return vault.getHostModel();
75
+ if (path[2] === "model" && path.length === 3 && method === "PUT")
76
+ return vault.putHostModel(PutHostModelRequestSchema.parse(await readBody(request)));
77
+ if (path[2] === "model" &&
78
+ path[3] === "selection" &&
79
+ path.length === 4 &&
80
+ method === "PUT")
81
+ return vault.selectHostModel(SelectHostModelRequestSchema.parse(await readBody(request)));
82
+ fail(404, "Route not found");
83
+ }
84
+ export async function dispatchVault(ctx, scope, method, path, url, request) {
85
+ const { vault } = ctx;
86
+ if (scope.kind !== "application") {
87
+ await vault.reject(path.join("/"));
88
+ fail(403, "Application credential required");
89
+ }
90
+ if (path.length === 2 && method === "POST") {
91
+ const body = CreateVaultRequestSchema.parse(await readBody(request));
92
+ return vault.createVault(body);
93
+ }
94
+ if (path.length === 2 && method === "GET") {
95
+ const ownerUserId = url.searchParams.get("ownerUserId") ??
96
+ fail(400, "ownerUserId is required");
97
+ return { vaults: await vault.listVaults(ownerUserId) };
98
+ }
99
+ const vaultId = path[2];
100
+ if (!vaultId)
101
+ fail(404, "Vault not found");
102
+ if (path.length === 3 && method === "GET")
103
+ return vault.getVault(vaultId);
104
+ if (path.length === 3 && method === "DELETE")
105
+ return vault.deleteVault(vaultId);
106
+ if (path[3] !== "credentials")
107
+ fail(404, "Route not found");
108
+ if (path.length === 4 && method === "POST") {
109
+ const body = CreateCredentialRequestSchema.parse(await readBody(request));
110
+ return vault.createCredential(vaultId, body);
111
+ }
112
+ if (path.length === 4 && method === "GET")
113
+ return { credentials: await vault.listCredentials(vaultId) };
114
+ const credentialId = path[4];
115
+ if (!credentialId)
116
+ fail(404, "Credential not found");
117
+ if (path.length === 5 && method === "GET")
118
+ return vault.getCredential(vaultId, credentialId);
119
+ if (path.length === 5 && method === "POST") {
120
+ const body = RotateCredentialRequestSchema.parse(await readBody(request));
121
+ return vault.rotateCredential(vaultId, credentialId, body);
122
+ }
123
+ if (path.length === 5 && method === "DELETE")
124
+ return vault.deleteCredential(vaultId, credentialId);
125
+ fail(404, "Route not found");
126
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The Tenant HTTP router: authenticates the bearer token, matches `/v1/...` routes and
3
+ * delegates to the service modules (commands, actions, sessions, live, routes-tenant). It
4
+ * maps typed errors to responses and owns no business logic of its own. History, SSE and
5
+ * executor work streams read Durable Streams (`live.ts`).
6
+ */
7
+ import type { IncomingMessage, ServerResponse } from "node:http";
8
+ import { type TenantContext } from "./context.js";
9
+ export declare function handle(ctx: TenantContext, request: IncomingMessage, response: ServerResponse, _url?: URL): Promise<void>;
@@ -0,0 +1,142 @@
1
+ import { PutAgentRequestSchema, PutSessionRequestSchema, RegisterExecutorsRequestSchema, SessionCommandSchema, } from "@nylorun/core/contracts";
2
+ import { VaultError } from "../vault/error.js";
3
+ import { SandboxRouteError, handleSessionSandboxTool, } from "../core/sandbox-routes.js";
4
+ import { loadSession, sessionOf } from "./context.js";
5
+ import { HttpError, OpaqueAuthError, fail, readBody, requestAborted, } from "./http.js";
6
+ import { authenticate, requireApplication } from "./auth.js";
7
+ import { command } from "./commands.js";
8
+ import { actionSandboxTool, deleteExecutor, listExecutors, listPendingActions, registerExecutors, sandboxRouteDeps, updateAction, } from "./actions.js";
9
+ import { listDefinitions, listSessions, putDefinition, putSession, sessionView, } from "./sessions.js";
10
+ import { readHistory, requestCursor, streamExecutorWork, streamSessionEvents, } from "./live.js";
11
+ import { dispatchTenant, dispatchVault } from "./routes-tenant.js";
12
+ export async function handle(ctx, request, response, _url) {
13
+ const json = (value, status = 200) => {
14
+ const payload = JSON.stringify(value);
15
+ response.writeHead(status, {
16
+ "content-type": "application/json",
17
+ "content-length": Buffer.byteLength(payload),
18
+ });
19
+ response.end(payload);
20
+ };
21
+ try {
22
+ const url = _url ?? new URL(request.url ?? "/", "http://runtime");
23
+ const path = url.pathname
24
+ .split("/")
25
+ .filter(Boolean)
26
+ .map(decodeURIComponent);
27
+ const method = request.method;
28
+ const scope = await authenticate(ctx, request);
29
+ if (path[0] !== "v1")
30
+ fail(404, "Route not found");
31
+ if (path[1] === "executors" && path[2] === "connect" && method === "GET") {
32
+ if (scope.kind !== "executor")
33
+ return fail(403, "Executor credential required");
34
+ streamExecutorWork(ctx.live, request, response, scope.executor.tokenHash);
35
+ return;
36
+ }
37
+ if (path[1] === "actions") {
38
+ if (scope.kind !== "executor")
39
+ return fail(403, "Executor credential required");
40
+ if (path.length === 2 && method === "GET")
41
+ return json(await listPendingActions(ctx, scope.executor));
42
+ const actionId = path[2];
43
+ if (!actionId)
44
+ fail(404, "Action not found");
45
+ if (method === "POST" &&
46
+ path[3] === "sandbox" &&
47
+ path[4] &&
48
+ path.length === 5)
49
+ return json(await actionSandboxTool(ctx, scope, actionId, path[4], request));
50
+ const body = await readBody(request);
51
+ return json(await updateAction(ctx, scope, actionId, method, path[3], body));
52
+ }
53
+ if (path[1] === "sessions" &&
54
+ path[2] &&
55
+ path[3] === "commands" &&
56
+ method === "POST")
57
+ return json(await command(ctx, path[2], SessionCommandSchema.parse(await readBody(request)), scope));
58
+ if (path[1] === "vaults")
59
+ return json(await dispatchVault(ctx, scope, method, path, url, request));
60
+ if (path[1] === "tenant")
61
+ return json(await dispatchTenant(ctx, scope, method, path, request));
62
+ const principalId = requireApplication(scope);
63
+ if (path[1] === "executors" && path.length === 2 && method === "GET")
64
+ return json(listExecutors(ctx));
65
+ // The length guard matters: the connect branch above only matches GET, so without it a
66
+ // PUT to /v1/executors/connect would register an agent literally named "connect".
67
+ if (path[1] === "executors" && path.length === 2 && method === "PUT")
68
+ return json(await registerExecutors(ctx, principalId, RegisterExecutorsRequestSchema.parse(await readBody(request))));
69
+ if (path[1] === "executors" &&
70
+ path.length === 3 &&
71
+ path[2] &&
72
+ method === "DELETE")
73
+ return json(await deleteExecutor(ctx, path[2]));
74
+ if (path[1] === "agents" && path.length === 2 && method === "GET")
75
+ return json(await listDefinitions(ctx));
76
+ if (path[1] === "sessions" && path.length === 2 && method === "GET")
77
+ return json(await listSessions(ctx, url.searchParams.get("agentId")));
78
+ if (path[1] === "agents" &&
79
+ path[2] &&
80
+ path.length === 3 &&
81
+ method === "PUT")
82
+ return json(await putDefinition(ctx, path[2], PutAgentRequestSchema.parse(await readBody(request))));
83
+ if (path[1] === "sessions" && path[2]) {
84
+ const id = path[2];
85
+ if (method === "PUT" && path.length === 3) {
86
+ const body = PutSessionRequestSchema.parse(await readBody(request));
87
+ const session = await putSession(ctx, id, body);
88
+ return json(await ctx.store.tx((t) => sessionView(t, session)));
89
+ }
90
+ if (method === "GET" && path.length === 3)
91
+ return json(await ctx.store.tx(async (t) => sessionView(t, await sessionOf(t, id))));
92
+ // Every other session route needs the session to exist.
93
+ await loadSession(ctx, id);
94
+ const cursor = requestCursor(request, url);
95
+ if (method === "GET" && path[3] === "items")
96
+ return json(await readHistory(ctx, id, cursor, url.searchParams.get("agent") ?? undefined));
97
+ if (method === "GET" && path[3] === "events") {
98
+ await streamSessionEvents(ctx, request, response, id, cursor);
99
+ return;
100
+ }
101
+ if (method === "POST" &&
102
+ path[3] === "sandbox" &&
103
+ path[4] &&
104
+ path.length === 5) {
105
+ requireApplication(scope);
106
+ try {
107
+ const outcome = await handleSessionSandboxTool(sandboxRouteDeps(ctx), id, path[4], await readBody(request), requestAborted(request));
108
+ return json(outcome);
109
+ }
110
+ catch (error) {
111
+ if (error instanceof SandboxRouteError)
112
+ fail(error.status, error.message);
113
+ throw error;
114
+ }
115
+ }
116
+ }
117
+ fail(404, "Route not found");
118
+ }
119
+ catch (error) {
120
+ if (response.headersSent) {
121
+ response.end();
122
+ return;
123
+ }
124
+ if (error instanceof OpaqueAuthError) {
125
+ json(error.body, error.status);
126
+ return;
127
+ }
128
+ const status = error instanceof HttpError ||
129
+ error instanceof VaultError ||
130
+ error instanceof SandboxRouteError
131
+ ? error.status
132
+ : error?.name === "ZodError" ||
133
+ error?.message === "Invalid cursor"
134
+ ? 400
135
+ : 500;
136
+ json({
137
+ status: "rejected",
138
+ code: status === 500 ? "internal_error" : "request_rejected",
139
+ message: status === 500 ? "Runtime request failed" : error.message,
140
+ }, status);
141
+ }
142
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Composition root of an open Tenant: validates the config, opens the Session Store and the
3
+ * vault key, builds the services and the `TenantContext`, wires the seams, registers the
4
+ * Tenant's Worker handlers and arms its sweep, and implements `TenantHandle` (handle,
5
+ * summary, drain, close) by delegating to the Tenant modules.
6
+ *
7
+ * Seams wired here: `wireStreams()` connects the store's commits to Durable Streams (the
8
+ * relay, and the history, SSE, work and control readers); `wake` goes to the
9
+ * `DurableExecution`, whose handlers (`worker.ts`) call `advance` and `sweep`; `abortLocal`
10
+ * aborts an advance running on this process. The sweep also drains the outbox.
11
+ *
12
+ * Execution: the Host passes one `TenantExecution` for every Tenant it opens
13
+ * (`TenantOpenHooks.execution`); without one, the Tenant runs its own in-process
14
+ * `MemoryExecution`. No lock file: several processes may open a Tenant, and ownership of each
15
+ * session (§10.6) keeps its advances apart.
16
+ */
17
+ import type { ServerResponse, IncomingMessage } from "node:http";
18
+ import type { TenantEnvelope } from "@nylorun/core/contracts";
19
+ import { type ModelProvider } from "../core/provider.js";
20
+ import { type AuthorizeResult } from "../vault/service.js";
21
+ import type { DurableStreams } from "../streams/types.js";
22
+ import type { OpenedTenant, TenantConfig, TenantHandle, TenantSummary } from "./types.js";
23
+ import { type StreamsStatus } from "./streams.js";
24
+ import { type TenantExecution, type TenantWorker } from "./worker.js";
25
+ /** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
26
+ export type TenantOpenHooks = {
27
+ modelProvider?: ModelProvider;
28
+ vaultKek?: Buffer | string | null;
29
+ /** When true, create the KEK file on first vault write (tests / new Tenants). */
30
+ createKekIfMissing?: boolean;
31
+ /**
32
+ * The Host's Durable Session Execution and the registry its handlers dispatch through.
33
+ * The Host starts it; the Tenant registers its worker and arms its sweep. Without one,
34
+ * the Tenant starts and stops its own in-process `MemoryExecution`.
35
+ */
36
+ execution?: TenantExecution;
37
+ /** The Worker id written as session owner. Defaults to this process's `WORKER_ID`. */
38
+ workerId?: string;
39
+ /**
40
+ * Durable Streams for this Tenant, owned by the caller (the Host's S2 streams). Without
41
+ * them the Tenant creates its own in-process `MemoryStreams`, whose history does not
42
+ * survive a restart (tests, and a Host without S2).
43
+ */
44
+ streams?: DurableStreams;
45
+ } & OpenedTenant;
46
+ export declare class TenantRuntime implements TenantHandle {
47
+ private readonly ctx;
48
+ readonly envelope: TenantEnvelope;
49
+ /** This Tenant's Worker handlers, as registered with the execution. */
50
+ readonly worker: TenantWorker;
51
+ private readonly detach;
52
+ /** How long close waits for running advances: the execution's advance grace period. */
53
+ private readonly closeGraceMs;
54
+ private constructor();
55
+ static open(config: TenantConfig, hooks: TenantOpenHooks): Promise<TenantRuntime>;
56
+ handle(request: IncomingMessage, response: ServerResponse, url?: URL): Promise<void>;
57
+ authorize(sessionId: string, request: {
58
+ url: string;
59
+ serverName?: string;
60
+ }): Promise<AuthorizeResult>;
61
+ summary(): Promise<TenantSummary>;
62
+ drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
63
+ /**
64
+ * Aborts the advance of `sessionId` if it runs on this process. Cancel calls it after
65
+ * committing `cancelled`; the control stream calls it for cancels made elsewhere.
66
+ */
67
+ abortLocal(sessionId: string): void;
68
+ /**
69
+ * The Durable Streams seam's status for Tenant status and readiness: S2 reachability, the
70
+ * basin, outbox depth and relay lag (`streamsStatus` in `tenant/streams.ts`).
71
+ */
72
+ streamsStatus(): Promise<StreamsStatus>;
73
+ /** Adds a callback to the Tenant sweep. Returns a function that removes it. */
74
+ onSweep(hook: () => Promise<void>): () => void;
75
+ close(): Promise<void>;
76
+ }
77
+ /**
78
+ * Open a Tenant Runtime from an explicit `TenantConfig`. No listen() (A1).
79
+ * Implements `OpenTenantRuntime`.
80
+ */
81
+ export declare function openTenantRuntime(config: TenantConfig, hooks: TenantOpenHooks): Promise<TenantHandle>;