@nylorun/runtime 0.14.0-beta → 0.15.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 (198) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +75 -33
  3. package/dist/adapters/execution/restate.js +2 -2
  4. package/dist/adapters/replication/pgoutput.d.ts +5 -0
  5. package/dist/adapters/replication/pgoutput.js +4 -3
  6. package/dist/admin-openapi.json +184 -685
  7. package/dist/api/components.d.ts +308 -68
  8. package/dist/api/components.js +4 -5
  9. package/dist/api/gate/keys.d.ts +14 -0
  10. package/dist/api/gate/keys.js +59 -0
  11. package/dist/api/gate/routes.d.ts +34 -0
  12. package/dist/api/gate/routes.js +235 -0
  13. package/dist/api/http/define.js +0 -8
  14. package/dist/api/http/routes/access.js +25 -18
  15. package/dist/api/http/routes/tenant.d.ts +2 -1
  16. package/dist/api/http/routes/tenant.js +77 -5
  17. package/dist/api/http/routes/vaults.js +2 -2
  18. package/dist/api/openapi.js +4 -5
  19. package/dist/gates/contract.d.ts +51 -0
  20. package/dist/gates/contract.js +37 -0
  21. package/dist/gates/handler.d.ts +21 -0
  22. package/dist/gates/handler.js +55 -0
  23. package/dist/gates/http-client.d.ts +12 -0
  24. package/dist/gates/http-client.js +160 -0
  25. package/dist/gates/in-process.d.ts +32 -0
  26. package/dist/gates/in-process.js +42 -0
  27. package/dist/gates/inflight.d.ts +39 -0
  28. package/dist/gates/inflight.js +70 -0
  29. package/dist/gates/mcp-handler.d.ts +35 -0
  30. package/dist/gates/mcp-handler.js +142 -0
  31. package/dist/gates/meter.d.ts +24 -0
  32. package/dist/gates/meter.js +165 -0
  33. package/dist/gates/model-gate.d.ts +45 -0
  34. package/dist/gates/model-gate.js +1 -0
  35. package/dist/gates/tenant-vaults.d.ts +64 -0
  36. package/dist/gates/tenant-vaults.js +94 -0
  37. package/dist/gates/tool-calls.d.ts +51 -0
  38. package/dist/gates/tool-calls.js +64 -0
  39. package/dist/gates/tool-client.d.ts +12 -0
  40. package/dist/gates/tool-client.js +201 -0
  41. package/dist/gates/tool-contract.d.ts +116 -0
  42. package/dist/gates/tool-contract.js +64 -0
  43. package/dist/gates/tool-gate.d.ts +45 -0
  44. package/dist/gates/tool-gate.js +10 -0
  45. package/dist/host/admin-api.d.ts +7 -6
  46. package/dist/host/admin-api.js +9 -113
  47. package/dist/host/app.d.ts +1 -1
  48. package/dist/host/app.js +20 -24
  49. package/dist/host/config-for.d.ts +1 -1
  50. package/dist/host/config-for.js +2 -2
  51. package/dist/host/create-host.d.ts +5 -5
  52. package/dist/host/create-host.js +5 -15
  53. package/dist/host/execution.d.ts +20 -18
  54. package/dist/host/execution.js +2 -2
  55. package/dist/host/gates.d.ts +58 -0
  56. package/dist/host/gates.js +127 -0
  57. package/dist/host/http.d.ts +6 -1
  58. package/dist/host/http.js +17 -0
  59. package/dist/host/index.d.ts +1 -1
  60. package/dist/host/main.js +151 -71
  61. package/dist/host/stack-config.d.ts +71 -19
  62. package/dist/host/stack-config.js +192 -18
  63. package/dist/infra/database.d.ts +2 -2
  64. package/dist/infra/execution.d.ts +5 -5
  65. package/dist/infra/execution.js +8 -6
  66. package/dist/infra/index.d.ts +1 -1
  67. package/dist/infra/workers.d.ts +17 -17
  68. package/dist/infra/workers.js +9 -9
  69. package/dist/keys/client.d.ts +9 -0
  70. package/dist/keys/client.js +70 -0
  71. package/dist/keys/contract.d.ts +24 -0
  72. package/dist/keys/contract.js +12 -0
  73. package/dist/keys/keys.d.ts +41 -0
  74. package/dist/keys/keys.js +50 -0
  75. package/dist/mcp/connect.d.ts +43 -3
  76. package/dist/mcp/connect.js +12 -5
  77. package/dist/mcp/pool.d.ts +21 -3
  78. package/dist/mcp/pool.js +15 -2
  79. package/dist/openapi.json +887 -694
  80. package/dist/record/append.d.ts +51 -0
  81. package/dist/record/append.js +20 -0
  82. package/dist/{store/event.js → record/envelope.js} +1 -1
  83. package/dist/record/index.d.ts +11 -0
  84. package/dist/record/index.js +11 -0
  85. package/dist/store/postgres/connect.d.ts +15 -12
  86. package/dist/store/postgres/connect.js +24 -12
  87. package/dist/store/postgres/db.d.ts +16 -0
  88. package/dist/store/postgres/db.js +28 -0
  89. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  90. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  91. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  92. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  93. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  94. package/dist/store/postgres/migrate.d.ts +68 -0
  95. package/dist/store/postgres/migrate.js +183 -0
  96. package/dist/store/postgres/record-writer.d.ts +4 -0
  97. package/dist/store/postgres/record-writer.js +34 -0
  98. package/dist/store/postgres/record.d.ts +2 -8
  99. package/dist/store/postgres/record.js +50 -54
  100. package/dist/store/postgres/schema.d.ts +3096 -0
  101. package/dist/store/postgres/schema.js +363 -0
  102. package/dist/store/postgres/store.d.ts +2 -4
  103. package/dist/store/postgres/store.js +481 -567
  104. package/dist/store/postgres/tenant.d.ts +56 -0
  105. package/dist/store/postgres/tenant.js +154 -0
  106. package/dist/store/types.d.ts +43 -87
  107. package/dist/store/types.js +0 -1
  108. package/dist/streams/control.d.ts +1 -1
  109. package/dist/streams/control.js +6 -2
  110. package/dist/streams/relay/types.d.ts +2 -1
  111. package/dist/streams/relay/types.js +2 -1
  112. package/dist/streams/types.d.ts +5 -0
  113. package/dist/tenant/advance.d.ts +7 -2
  114. package/dist/tenant/advance.js +95 -16
  115. package/dist/tenant/cause.d.ts +25 -0
  116. package/dist/tenant/cause.js +45 -0
  117. package/dist/tenant/commands.js +4 -2
  118. package/dist/tenant/context.d.ts +26 -3
  119. package/dist/tenant/delivery-token.js +17 -18
  120. package/dist/tenant/delivery.d.ts +1 -1
  121. package/dist/tenant/delivery.js +18 -13
  122. package/dist/tenant/effects.d.ts +15 -1
  123. package/dist/tenant/effects.js +93 -17
  124. package/dist/tenant/ephemeral.d.ts +16 -7
  125. package/dist/tenant/ephemeral.js +45 -111
  126. package/dist/tenant/history.d.ts +64 -0
  127. package/dist/tenant/history.js +120 -0
  128. package/dist/tenant/model-setting.d.ts +2 -2
  129. package/dist/tenant/module.d.ts +14 -28
  130. package/dist/tenant/module.js +131 -271
  131. package/dist/tenant/paths.d.ts +11 -16
  132. package/dist/tenant/paths.js +24 -68
  133. package/dist/tenant/pool.d.ts +1 -1
  134. package/dist/tenant/pool.js +1 -1
  135. package/dist/tenant/principals.d.ts +23 -11
  136. package/dist/tenant/principals.js +61 -49
  137. package/dist/tenant/runtime.d.ts +19 -1
  138. package/dist/tenant/runtime.js +40 -26
  139. package/dist/tenant/scheduler.d.ts +4 -2
  140. package/dist/tenant/scheduler.js +4 -4
  141. package/dist/tenant/session-streams.d.ts +2 -0
  142. package/dist/tenant/session-streams.js +8 -3
  143. package/dist/tenant/status.d.ts +3 -0
  144. package/dist/tenant/status.js +1 -1
  145. package/dist/tenant/store-pg.d.ts +9 -6
  146. package/dist/tenant/store-pg.js +60 -125
  147. package/dist/tenant/streams.d.ts +9 -25
  148. package/dist/tenant/streams.js +7 -22
  149. package/dist/tenant/tokens.js +16 -14
  150. package/dist/tenant/types.d.ts +41 -92
  151. package/dist/tenant/types.js +4 -15
  152. package/dist/tenant/worker.js +4 -4
  153. package/dist/vault/host-model.d.ts +31 -0
  154. package/dist/vault/host-model.js +57 -0
  155. package/dist/vault/service.d.ts +26 -15
  156. package/dist/vault/service.js +70 -109
  157. package/dist/version.d.ts +1 -1
  158. package/dist/version.js +1 -1
  159. package/package.json +9 -5
  160. package/dist/store/memory.d.ts +0 -69
  161. package/dist/store/memory.js +0 -810
  162. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  163. package/dist/store/postgres/migrations/001_initial.js +0 -196
  164. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  165. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  166. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
  167. package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
  168. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
  169. package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
  170. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
  171. package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
  172. package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
  173. package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
  174. package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
  175. package/dist/store/postgres/migrations/007_event_time.js +0 -14
  176. package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
  177. package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
  178. package/dist/store/postgres/migrations/index.d.ts +0 -53
  179. package/dist/store/postgres/migrations/index.js +0 -96
  180. package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
  181. package/dist/store/postgres/migrations/shared/index.js +0 -92
  182. package/dist/store/postgres/names.d.ts +0 -11
  183. package/dist/store/postgres/names.js +0 -42
  184. package/dist/store/postgres/tenants.d.ts +0 -90
  185. package/dist/store/postgres/tenants.js +0 -228
  186. package/dist/streams/relay/memory.d.ts +0 -69
  187. package/dist/streams/relay/memory.js +0 -176
  188. package/dist/tenant/envelope.d.ts +0 -7
  189. package/dist/tenant/envelope.js +0 -22
  190. package/dist/tenant/quarantine-error.d.ts +0 -6
  191. package/dist/tenant/quarantine-error.js +0 -9
  192. package/dist/tenant/quarantine.d.ts +0 -27
  193. package/dist/tenant/quarantine.js +0 -72
  194. package/dist/tenant/store-memory.d.ts +0 -16
  195. package/dist/tenant/store-memory.js +0 -111
  196. /package/dist/{store → record}/cursor.d.ts +0 -0
  197. /package/dist/{store → record}/cursor.js +0 -0
  198. /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
@@ -1,140 +1,75 @@
1
1
  /**
2
- * The Tenant store over the Postgres Tenant catalog (architecture §8.1, §8.2).
2
+ * Opens the Host's Tenant on its Postgres database (tenancy.md §4–§5, plan P9).
3
3
  *
4
- * A Tenant exists when its schema `tenant_<id>` exists (`store/postgres/tenants.ts`); the
5
- * envelope is a row in it, so this store keeps no registry. What stays on the Host root is
6
- * the Tenant directory `tenants/<id>/`: the vault key (`vault-kek`), `plugin-data/`, `logs/`,
7
- * and the private `home/`, `tmp/` and `sandboxes/`. It is created with the Tenant and removed
8
- * with it; nothing in it says whether the Tenant exists.
4
+ * The database holds one Tenant (`store/postgres/tenant.ts`): opening migrates it, creates
5
+ * the Tenant on first start (from the Host's configuration: id, name, Studio and derived
6
+ * principals), and opens the Tenant Runtime on its Session Store. What stays on the Host root
7
+ * is the Tenant directory `tenant/`: the vault key (`vault-kek`), `plugin-data/`, `logs/`,
8
+ * and the private `home/`, `tmp/` and `sandboxes/`.
9
9
  *
10
- * - `create` makes the directory, then creates, migrates and bootstraps the schema in one
11
- * transaction. A schema that already exists is `exists`; the module compares its bootstrap.
12
- * - `open` migrates an older schema forward and opens the Tenant Runtime on the schema's
13
- * Session Store. A missing schema is `TenantNotFoundError`; a schema newer than this
14
- * Runtime, a failed migration, an unreadable envelope or vault ciphertext without its key
15
- * throws that quarantine. A failure outside the Tenant (Postgres unreachable, its sweep
16
- * not armed) is `TenantUnavailableError`, which the module does not cache.
17
- * - `trash` drops the schema and removes the directory. There is no `trash/` copy: the key
18
- * is useless without the schema.
10
+ * - A failure in the Tenant (an old layout, a database newer than this Runtime, a failed
11
+ * migration, an unreadable Tenant row, vault ciphertext without its key) rejects with that
12
+ * `TenantOpenError`; the Host reports it and fails readiness.
13
+ * - A failure outside the Tenant (Postgres unreachable, its sweep not armed in Restate) is
14
+ * `TenantUnavailableError`; the Tenant module retries.
19
15
  */
20
- import { mkdirSync, rmSync } from "node:fs";
21
- import { tenantSchemaName } from "../store/postgres/names.js";
22
- import { createPostgresTenantCatalog } from "../store/postgres/tenants.js";
16
+ import { mkdirSync } from "node:fs";
17
+ import { openTenantDatabase, } from "../store/postgres/tenant.js";
18
+ import { TenantOpenError } from "./cause.js";
23
19
  import { tenantPaths } from "./paths.js";
24
- import { QuarantineError, TenantConflictError } from "./quarantine.js";
25
- import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
26
- /** Repair instructions for a Postgres Tenant, naming its schema. */
27
- function repairFor(code, tenantId) {
28
- const status = `nylo tenant status ${tenantId}`;
29
- const schema = tenantSchemaName(tenantId);
30
- switch (code) {
31
- case "schema-too-new":
32
- return `${status} — schema ${schema} was migrated by a newer Runtime; run that version or newer`;
33
- case "migration-failed":
34
- return `${status} — inspect the Runtime log for the failed migration of ${schema}, fix it, then restart the Runtime`;
35
- case "envelope-invalid":
36
- return `${status} — the tenant table of schema ${schema} is missing or invalid; restore the schema from a Postgres backup`;
37
- case "kek-missing":
38
- return `${status} — restore vault-kek under the Tenant directory (ciphertext cannot be opened without it)`;
39
- default:
40
- return `${status} — inspect the Runtime log and Postgres, then restart the Runtime`;
41
- }
42
- }
43
- function unavailable(tenantId, cause) {
20
+ import { TenantUnavailableError, } from "./types.js";
21
+ function unavailable(cause) {
44
22
  return cause instanceof TenantUnavailableError
45
23
  ? cause
46
- : new TenantUnavailableError(tenantId, { cause });
47
- }
48
- function quarantined(reason, tenantId) {
49
- return new QuarantineError({ ...reason, repair: repairFor(reason.code, tenantId) });
24
+ : new TenantUnavailableError({ cause });
50
25
  }
51
- export function createPostgresTenantStore(options) {
52
- const catalog = createPostgresTenantCatalog({
53
- sql: options.sql,
54
- ...(options.migrations ? { migrations: options.migrations } : {}),
55
- onError: (error) => options.logger?.error("post-commit step failed", {
56
- message: error instanceof Error ? error.message : String(error),
57
- }),
58
- });
59
- const paths = (id) => tenantPaths(options.hostRoot, id);
60
- function makeDirectory(id) {
61
- const p = paths(id);
62
- mkdirSync(p.root, { recursive: true, mode: 0o700 });
63
- for (const dir of [p.home, p.tmp, p.sandboxes, p.pluginData, p.logs])
64
- mkdirSync(dir, { recursive: true });
65
- }
66
- async function readEnvelope(id) {
26
+ export function createPostgresTenantOpener(options) {
27
+ const { logger } = options;
28
+ return async () => {
29
+ const opened = await openTenantDatabase({
30
+ sql: options.sql,
31
+ create: options.create,
32
+ ...(options.migrations ? { migrations: options.migrations } : {}),
33
+ onError: (error) => logger?.error("post-commit step failed", {
34
+ message: error instanceof Error ? error.message : String(error),
35
+ }),
36
+ }).catch((error) => {
37
+ if (error instanceof TenantOpenError)
38
+ throw error;
39
+ throw unavailable(error);
40
+ });
41
+ const { store, envelope } = opened;
42
+ const tenantId = envelope.id;
43
+ if (opened.created)
44
+ logger?.info("tenant created", { tenantId, name: envelope.name });
45
+ else if (options.create.tenantId !== undefined && options.create.tenantId !== tenantId)
46
+ logger?.warn("tenant id configured differs", {
47
+ tenantId,
48
+ configured: options.create.tenantId,
49
+ message: "The database already holds a Tenant; the configured id applies only to a new database",
50
+ });
51
+ if (opened.migrated.from !== opened.migrated.to)
52
+ logger?.info("tenant schema migrated", {
53
+ tenantId,
54
+ from: opened.migrated.from,
55
+ to: opened.migrated.to,
56
+ });
67
57
  try {
68
- return await catalog.readEnvelope(id);
58
+ const config = options.configFor(tenantId);
59
+ const paths = tenantPaths(options.hostRoot);
60
+ mkdirSync(paths.root, { recursive: true, mode: 0o700 });
61
+ return await options.openRuntime({ ...config, tenantId }, { store, envelope });
69
62
  }
70
63
  catch (error) {
71
- if (!(error instanceof QuarantineError))
72
- throw unavailable(id, error);
73
- // Dropped since it was listed (deleted by another process).
74
- if (!(await catalog.tenantExists(id).catch(() => true)))
75
- throw new TenantNotFoundError(id);
76
- throw quarantined(error.toQuarantine(), id);
77
- }
78
- }
79
- return {
80
- enumerate: () => catalog.listTenantIds(),
81
- readEnvelope,
82
- async create(envelope, bootstrap) {
83
- makeDirectory(envelope.id);
84
- try {
85
- const result = await catalog.createTenant({ envelope, principals: bootstrap });
86
- return result.status;
87
- }
88
- catch (error) {
89
- // An existing Tenant with other bootstrap material; the module reports the conflict.
90
- if (error instanceof TenantConflictError)
91
- return "exists";
64
+ await store.close().catch(() => undefined);
65
+ if (error instanceof TenantOpenError) {
66
+ // The Tenant's own failure (`kek-missing`): report which Tenant it is.
67
+ error.envelope ??= envelope;
92
68
  throw error;
93
69
  }
94
- },
95
- bootstrapMatches: (id, bootstrap) => catalog.bootstrapMatches(id, bootstrap),
96
- async open(id) {
97
- const result = await catalog
98
- .openTenant(id)
99
- .catch((error) => {
100
- throw unavailable(id, error);
101
- });
102
- if (result.status === "not-found")
103
- throw new TenantNotFoundError(id);
104
- if (result.status === "quarantined")
105
- throw quarantined(result.reason, id);
106
- if (result.migrated.from !== result.migrated.to)
107
- options.logger?.info("tenant schema migrated", {
108
- tenantId: id,
109
- from: result.migrated.from,
110
- to: result.migrated.to,
111
- });
112
- try {
113
- makeDirectory(id);
114
- const config = options.configFor(id);
115
- return await options.openRuntime({ ...config, tenantId: id }, { store: result.store, envelope: result.envelope });
116
- }
117
- catch (error) {
118
- await result.store.close().catch(() => undefined);
119
- // The Runtime's own quarantines (`kek-missing`) get this store's repair wording.
120
- const reason = error?.quarantine;
121
- if (reason)
122
- throw quarantined(reason, id);
123
- if (error instanceof QuarantineError)
124
- throw error;
125
- // Anything else failed outside the Tenant (Postgres, arming its sweep in Restate):
126
- // the next use tries again rather than quarantining it.
127
- throw unavailable(id, error);
128
- }
129
- },
130
- async trash(id) {
131
- await catalog.deleteTenant(id);
132
- rmSync(paths(id).root, { recursive: true, force: true });
133
- },
134
- async removePartial(id) {
135
- if (await catalog.tenantExists(id))
136
- return;
137
- rmSync(paths(id).root, { recursive: true, force: true });
138
- },
70
+ // Anything else failed outside the Tenant (Postgres, arming its sweep in Restate): the
71
+ // next use tries again.
72
+ throw unavailable(error);
73
+ }
139
74
  };
140
75
  }
@@ -4,16 +4,15 @@
4
4
  *
5
5
  * - **Record.** Every event is written to the record in its transaction (`Tx.event`). The
6
6
  * stream relay (`streams/relay/`) feeds the Tenant's basin from it. On a Host with S2, one
7
- * process-wide relay reads every Tenant's record over logical replication (`host/main.ts`);
7
+ * process-wide relay reads the record over logical replication (`host/main.ts`);
8
8
  * otherwise (tests, a Host without S2) this module runs a relay of the Tenant's own commits,
9
9
  * which reconciles the record with the streams when it starts.
10
10
  * - **Basin generations.** The Tenant's session streams live in its current basin generation
11
11
  * (`streams/basin.ts`). A reset moves it to the next one (`tenantReset`): the old basin gets
12
12
  * a `sessions.reset` signal, every process moves its readers, and the old basin is deleted
13
13
  * after a grace period (again when the Tenant opens, until it is gone).
14
- * - **Basin.** The current basin is created with the Tenant (`createTenantStreams`) and
15
- * checked when it opens. A missing basin (S2 down at creation, a crash in between) is
16
- * repaired on first use: a failed `ensureTenant` retries in the background with backoff,
14
+ * - **Basin.** The current basin is created when the Tenant opens. A missing basin (S2 down
15
+ * then, or deleted since) is repaired on first use: a failed `ensureTenant` retries in the background with backoff,
17
16
  * and `streamsStatus` reports the basin's state.
18
17
  * - **Readers.** History and session SSE read the session's stream
19
18
  * (`tenant/session-streams.ts`). This module runs one `tenant/control` reader per Tenant per
@@ -35,7 +34,7 @@ export interface WireStreamsOptions {
35
34
  /** Close `streams` with the wiring (streams created for this Tenant alone). */
36
35
  ownsStreams?: boolean;
37
36
  /**
38
- * The Host runs the stream relay for every Tenant (logical replication). Otherwise the
37
+ * The Host runs the stream relay for the Tenant (logical replication). Otherwise the
39
38
  * Tenant relays its own commits.
40
39
  */
41
40
  hostRelay?: boolean;
@@ -73,12 +72,12 @@ export interface StreamsWiring {
73
72
  */
74
73
  export declare function wireStreams(ctx: TenantContext, options: WireStreamsOptions): Promise<StreamsWiring>;
75
74
  /**
76
- * Appends `session.cancel` for `sessionId` to `tenant/control`, so the process running its
77
- * advance aborts it. Call it from `t.afterCommit` after a cancel commits. A lost signal costs
78
- * latency only (the advance checks the Session Store before every effect), so failures are
79
- * logged, never thrown.
75
+ * Appends `session.cancel` for `sessionId` and its cancelled turn to `tenant/control`, so the
76
+ * process running that turn's advance aborts it. Call it from `t.afterCommit` after a cancel
77
+ * commits. A lost signal costs latency only (the advance checks the Session Store before every
78
+ * effect), so failures are logged, never thrown.
80
79
  */
81
- export declare function signalSessionCancel(ctx: TenantContext, sessionId: string): void;
80
+ export declare function signalSessionCancel(ctx: TenantContext, sessionId: string, turnId: string | null): void;
82
81
  /**
83
82
  * After a reset deleted the Tenant's sessions and moved it to a new basin generation: tells
84
83
  * every process with the Tenant open (`sessions.reset` on the old basin's control stream),
@@ -100,20 +99,5 @@ export interface StreamsStatus {
100
99
  export declare function streamsStatus(ctx: TenantContext, options?: {
101
100
  probeTimeoutMs?: number;
102
101
  }): Promise<StreamsStatus>;
103
- /**
104
- * Creates a new Tenant's basin. Call it when the Tenant is created, after its schema commits;
105
- * retries transient failures, then throws. A failure need not fail the creation: opening the
106
- * Tenant repairs a missing basin.
107
- */
108
- export declare function createTenantStreams(streams: DurableStreams, tenantId: string, options?: {
109
- attempts?: number;
110
- }): Promise<void>;
111
- /**
112
- * Deletes every basin of a Tenant and every stream in them. Call it when the Tenant is
113
- * deleted. Idempotent; retries transient failures, then throws.
114
- */
115
- export declare function deleteTenantStreams(streams: DurableStreams, tenantId: string, options?: {
116
- attempts?: number;
117
- }): Promise<void>;
118
102
  /** Close: stops the readers and relay; the observers were ended by `endAllStreams`. */
119
103
  export declare function closeStreams(ctx: TenantContext): Promise<void>;
@@ -58,7 +58,7 @@ export async function wireStreams(ctx, options) {
58
58
  const startControl = () => follow(streams, currentBasin(ctx), CONTROL_STREAM, AbortSignal.any([stop.signal, control.signal]), (body) => {
59
59
  const signal = body;
60
60
  if (signal?.type === "session.cancel" && typeof signal.sessionId === "string")
61
- ctx.abortLocal(signal.sessionId);
61
+ ctx.abortLocal(signal.sessionId, typeof signal.turnId === "string" ? signal.turnId : undefined);
62
62
  else if (signal?.type === "sessions.reset")
63
63
  void checkSessionStreams(ctx).catch(report("session stream check failed"));
64
64
  else if (signal?.type === "subject.revoked" &&
@@ -157,16 +157,16 @@ function commitSource(store, tenantId) {
157
157
  };
158
158
  }
159
159
  /**
160
- * Appends `session.cancel` for `sessionId` to `tenant/control`, so the process running its
161
- * advance aborts it. Call it from `t.afterCommit` after a cancel commits. A lost signal costs
162
- * latency only (the advance checks the Session Store before every effect), so failures are
163
- * logged, never thrown.
160
+ * Appends `session.cancel` for `sessionId` and its cancelled turn to `tenant/control`, so the
161
+ * process running that turn's advance aborts it. Call it from `t.afterCommit` after a cancel
162
+ * commits. A lost signal costs latency only (the advance checks the Session Store before every
163
+ * effect), so failures are logged, never thrown.
164
164
  */
165
- export function signalSessionCancel(ctx, sessionId) {
165
+ export function signalSessionCancel(ctx, sessionId, turnId) {
166
166
  const streams = ctx.sessionStreams.wiring?.streams;
167
167
  if (!streams)
168
168
  return;
169
- void signalCancel(streams, currentBasin(ctx), sessionId).catch((error) => ctx.config.logger.warn("cancel signal failed", {
169
+ void signalCancel(streams, currentBasin(ctx), sessionId, turnId ?? undefined).catch((error) => ctx.config.logger.warn("cancel signal failed", {
170
170
  sessionId,
171
171
  message: messageOf(error),
172
172
  }));
@@ -209,21 +209,6 @@ async function probe(streams, timeoutMs) {
209
209
  return false;
210
210
  }
211
211
  }
212
- /**
213
- * Creates a new Tenant's basin. Call it when the Tenant is created, after its schema commits;
214
- * retries transient failures, then throws. A failure need not fail the creation: opening the
215
- * Tenant repairs a missing basin.
216
- */
217
- export async function createTenantStreams(streams, tenantId, options = {}) {
218
- await withRetries(() => streams.ensureTenant(tenantId), options.attempts ?? 3);
219
- }
220
- /**
221
- * Deletes every basin of a Tenant and every stream in them. Call it when the Tenant is
222
- * deleted. Idempotent; retries transient failures, then throws.
223
- */
224
- export async function deleteTenantStreams(streams, tenantId, options = {}) {
225
- await withRetries(() => streams.deleteTenant(tenantId, { allGenerations: true }), options.attempts ?? 5);
226
- }
227
212
  /** Close: stops the readers and relay; the observers were ended by `endAllStreams`. */
228
213
  export async function closeStreams(ctx) {
229
214
  await ctx.sessionStreams.wiring?.close();
@@ -8,7 +8,7 @@
8
8
  * `401 token_expired`, so clients always react the same way: get a new token.
9
9
  */
10
10
  import { randomUUID } from "node:crypto";
11
- import { errors, jwtVerify, SignJWT } from "jose";
11
+ import { errors, jwtVerify } from "jose";
12
12
  import { isSubject, SUBJECT_TOKEN_AUDIENCE, SUBJECT_TOKEN_TYPE, subjectTokenIssuer, TOKEN_SCOPES, TOKEN_TTL_MIN_SECONDS, } from "@nylorun/core/contracts";
13
13
  import { agentAllowed, readPolicy, resolveRole } from "./access-policy.js";
14
14
  import { fail, failOpaque, HttpError } from "./http.js";
@@ -29,9 +29,8 @@ function rejected(ctx, reason) {
29
29
  return failOpaque();
30
30
  }
31
31
  export async function mintToken(ctx, body) {
32
- const kek = ctx.signingKeys.kek();
33
32
  const tenantId = ctx.config.tenantId;
34
- const { access, epoch, row, ttl } = await ctx.store.tx(async (t) => {
33
+ const { access, epoch, ttl } = await ctx.store.tx(async (t) => {
35
34
  const policy = await readPolicy(t);
36
35
  const role = Object.hasOwn(policy.roles, body.role)
37
36
  ? policy.roles[body.role]
@@ -61,11 +60,9 @@ export async function mintToken(ctx, body) {
61
60
  return {
62
61
  access: resolveRole(policy, body.role, scopes, body.agents),
63
62
  epoch: await t.subjectEpoch(body.subject),
64
- row: await ctx.signingKeys.ensure(t, kek),
65
63
  ttl,
66
64
  };
67
65
  });
68
- const signer = await ctx.signingKeys.privateKey(row, kek);
69
66
  const iat = Math.floor(Date.now() / 1000);
70
67
  const jti = randomUUID();
71
68
  const scopes = [...access.scopes];
@@ -76,15 +73,20 @@ export async function mintToken(ctx, body) {
76
73
  ...(body.agents ? { agt: [...body.agents] } : {}),
77
74
  epc: epoch,
78
75
  };
79
- const token = await new SignJWT(claims)
80
- .setProtectedHeader({ alg: "ES256", typ: SUBJECT_TOKEN_TYPE, kid: signer.id })
81
- .setIssuer(subjectTokenIssuer(tenantId))
82
- .setAudience(SUBJECT_TOKEN_AUDIENCE)
83
- .setSubject(body.subject)
84
- .setIssuedAt(iat)
85
- .setExpirationTime(iat + ttl)
86
- .setJti(jti)
87
- .sign(signer.key);
76
+ // Signed by the keys service (F4.2): this process never holds the private key.
77
+ const { token, keyId } = await ctx.keys.sign({
78
+ typ: SUBJECT_TOKEN_TYPE,
79
+ claims: {
80
+ ...claims,
81
+ iss: subjectTokenIssuer(tenantId),
82
+ aud: SUBJECT_TOKEN_AUDIENCE,
83
+ sub: body.subject,
84
+ iat,
85
+ exp: iat + ttl,
86
+ jti,
87
+ },
88
+ });
89
+ const signer = { id: keyId };
88
90
  ctx.config.logger.info("subject token minted", {
89
91
  jti,
90
92
  kid: signer.id,
@@ -1,14 +1,15 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { AdminTenant, AdminTenantStatus, HostAggregate, TenantEnvelope } from "@nylorun/core/contracts";
2
+ import type { HostAggregate, HostTenant, TenantEnvelope } from "@nylorun/core/contracts";
3
3
  import type { FlowLimits } from "../core/limits.js";
4
4
  import type { SessionStore } from "../store/types.js";
5
5
  import type { SandboxBackend } from "../sandbox/types.js";
6
+ import type { TenantCause } from "./cause.js";
6
7
  import type { OutboundPolicy } from "./outbound.js";
7
8
  import type { TenantWorker } from "./worker.js";
8
9
  export type TenantMode = "shared" | "ephemeral" | "test";
9
10
  /**
10
- * The Tenant directory on the Host root. The Tenant's data lives in its store (a Postgres
11
- * schema); this holds what stays on the Host: the vault key, plugin data, logs, and the
11
+ * The Tenant directory on the Host root (`<Host root>/tenant/`). The Tenant's data lives in
12
+ * its database; this holds what stays on the Host: the vault key, plugin data, logs, and the
12
13
  * private home, tmp and sandbox directories.
13
14
  */
14
15
  export interface TenantPaths {
@@ -23,10 +24,6 @@ export interface TenantPaths {
23
24
  }
24
25
  export type TenantModelConfig = {
25
26
  kind: "vault";
26
- } | {
27
- kind: "gateway";
28
- url: string;
29
- model: string;
30
27
  } | {
31
28
  kind: "fixture";
32
29
  } | {
@@ -52,7 +49,7 @@ export interface TenantConfig {
52
49
  ms?: number;
53
50
  };
54
51
  /** Retries and timeouts for model calls (Model Calls §5, §6). Defaults in `piModel`. */
55
- modelCall?: import("../model/pi-model.js").ModelCallSettings;
52
+ modelCall?: import("../gates/model-gate.js").ModelCallSettings;
56
53
  childEnv: Readonly<Record<string, string>>;
57
54
  /** Action claim lease. Default 30 s. */
58
55
  /** How the Runtime may call Action endpoints (Host settings). Default: http and private addresses allowed. */
@@ -111,115 +108,67 @@ export interface TenantHandle {
111
108
  drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
112
109
  close(): Promise<void>;
113
110
  }
111
+ /**
112
+ * The Host's Tenant for a request: open, or unavailable. `cause` says why when opening it
113
+ * failed; without one the Host is starting or closing. Either way the request gets the opaque
114
+ * 404.
115
+ */
114
116
  export type TenantResolution = {
115
117
  kind: "open";
116
118
  handle: TenantHandle;
117
119
  } | {
118
- kind: "not-found";
119
- } | {
120
- kind: "quarantined";
121
- quarantine: Quarantine;
120
+ kind: "unavailable";
121
+ cause?: TenantCause;
122
122
  };
123
- export interface Quarantine {
124
- code: "kek-missing" | "corrupt" | "schema-too-new" | "migration-failed" | "envelope-invalid" | "open-timeout" | "open-failed";
125
- message: string;
126
- repair: string;
127
- }
128
- export interface BootstrapPrincipal {
129
- principalId: string;
130
- credentialHash: string;
131
- idempotencyKey: string;
132
- /** When set, also registers application principal `studio` with this hash. */
133
- studioCredentialHash?: string;
134
- /** Application principals whose keys the admin key derives (feature `derived-principals`). */
135
- derivedPrincipals?: readonly DerivedPrincipal[];
136
- }
137
- export interface DerivedPrincipal {
138
- id: string;
139
- credentialHash: string;
140
- }
141
123
  /**
142
- * The deep module (§8). HTTP, CLI and tests use only this.
124
+ * The deep module (§8): the one Tenant a Host serves (tenancy.md §5). HTTP, the Worker and
125
+ * tests use only this.
143
126
  *
144
- * Tenants open on demand (architecture §8.2): the first `resolve`, `status`, `worker` or
145
- * `delete` naming a Tenant opens it (bounded by the open timeout) and caches the handle or
146
- * the quarantine; `start` only marks discovery done, so a Host with many Tenants opens none
147
- * at startup.
127
+ * `start` opens the Tenant; the Host has it open from readiness to shutdown. There is no
128
+ * Tenant list, no opening on demand of other Tenants and no quarantine cache: when opening
129
+ * fails for a reason in the Tenant (a `TenantCause`), the Host fails readiness and reports
130
+ * the cause until it is restarted. A failure outside the Tenant (`TenantUnavailableError`:
131
+ * Postgres unreachable) is retried in the background and by the next request.
148
132
  */
149
133
  export interface TenantModule {
150
- /** Marks discovery done (`/ready`). Opens nothing. */
134
+ /** Opens the Tenant. Never rejects: a failure is recorded and reported by `tenant()`. */
151
135
  start(): Promise<void>;
152
- readonly started: boolean;
153
- /** Opens the Tenant on first use; a missing Tenant is `not-found`. */
154
- resolve(id: string): Promise<TenantResolution>;
136
+ /** The Tenant is open (`/ready`). */
137
+ readonly ready: boolean;
155
138
  /**
156
- * The Tenant's Worker handlers, opening it on demand, for `TenantWorkers.resolve`.
157
- * Undefined when the Tenant does not exist, is quarantined or is being deleted, or the
158
- * module is closed.
139
+ * The open Tenant, waiting for an open in progress. Rejects with `TenantUnavailableError`
140
+ * when it cannot be reached for a reason outside it.
159
141
  */
160
- worker(id: string): Promise<TenantWorker | undefined>;
161
- create(input: {
162
- tenantId: string;
163
- name: string;
164
- } & BootstrapPrincipal): Promise<{
165
- envelope: TenantEnvelope;
166
- created: boolean;
167
- }>;
142
+ resolve(): Promise<TenantResolution>;
168
143
  /**
169
- * Every Tenant the store holds, without opening any. A Tenant not opened yet is listed
170
- * `open` when its envelope reads; opening it may still quarantine it.
144
+ * The Tenant's Worker handlers when `id` is the open Tenant's, for `TenantWorkers.resolve`.
145
+ * Undefined otherwise, or once the module is closed.
171
146
  */
172
- list(): Promise<readonly AdminTenant[]>;
173
- status(id: string): Promise<AdminTenantStatus | undefined>;
174
- delete(id: string, activeWork: "refuse" | "drain" | "cancel"): Promise<void>;
147
+ worker(id: string): Promise<TenantWorker | undefined>;
148
+ /** The Tenant as `/v1/admin/status` reports it. */
149
+ tenant(): HostTenant;
175
150
  summarize(): Promise<HostAggregate>;
176
151
  close(): Promise<void>;
177
152
  }
178
153
  /**
179
- * Storage adapter behind the module (§8): a Postgres schema per Tenant (`store-pg.ts`), and
180
- * in-memory for tests (`store-memory.ts`).
154
+ * Opens the Host's Tenant (`store-pg.ts`): migrates its database, creates the Tenant when it
155
+ * has none, and opens the Tenant Runtime. Rejects with a `TenantOpenError` (`cause.ts`) when
156
+ * the Tenant itself cannot be opened, and with `TenantUnavailableError` when something
157
+ * outside it failed.
181
158
  */
182
- export interface TenantStore {
183
- /** Ids of every Tenant the store holds. */
184
- enumerate(): Promise<readonly string[]>;
185
- /** Throws a quarantine error when unreadable, `TenantNotFoundError` when gone. */
186
- readEnvelope(id: string): Promise<TenantEnvelope>;
187
- /**
188
- * Creates the Tenant, or returns `exists` when it already exists (whatever its bootstrap
189
- * material; the module compares it with `bootstrapMatches`). The stored envelope's
190
- * `schemaVersion` is the store's own; read it back with `readEnvelope`.
191
- */
192
- create(envelope: TenantEnvelope, bootstrap: BootstrapPrincipal): Promise<"created" | "exists">;
193
- bootstrapMatches(id: string, bootstrap: BootstrapPrincipal): Promise<boolean>;
194
- /**
195
- * Opens the Tenant Runtime; runs migration. Throws `TenantNotFoundError` when the Tenant
196
- * does not exist, `TenantUnavailableError` when something outside the Tenant failed, and
197
- * any other error (a quarantine error, ideally) when the Tenant itself cannot be opened.
198
- */
199
- open(id: string): Promise<TenantHandle>;
200
- /** Removes the Tenant: its data and its Tenant directory. */
201
- trash(id: string, now: Date): Promise<void>;
202
- /** Removes what a failed `create` left, never an existing Tenant. */
203
- removePartial(id: string): Promise<void>;
204
- }
159
+ export type TenantOpener = () => Promise<TenantHandle>;
205
160
  /**
206
- * Thrown by a store when a Tenant cannot be reached for a reason outside it (the database
207
- * or Durable Session Execution is unavailable). The module does not quarantine it: the next
208
- * use tries again. The Host answers 503.
161
+ * Thrown when the Tenant cannot be reached for a reason outside it (the database or Durable
162
+ * Session Execution is unavailable). Not a cause: the next use tries again. The Host answers
163
+ * 503.
209
164
  */
210
165
  export declare class TenantUnavailableError extends Error {
211
- readonly tenantId: string;
212
166
  readonly status = 503;
213
- constructor(tenantId: string, options?: {
167
+ constructor(options?: {
214
168
  cause?: unknown;
215
169
  });
216
170
  }
217
- /** Thrown by `TenantStore.open` for a Tenant that does not exist. */
218
- export declare class TenantNotFoundError extends Error {
219
- readonly tenantId: string;
220
- constructor(tenantId: string);
221
- }
222
- /** What a Tenant store hands the Tenant Runtime it opens. */
171
+ /** What the Tenant opener hands the Tenant Runtime it opens. */
223
172
  export interface OpenedTenant {
224
173
  /**
225
174
  * The Tenant's opened Session Store. The Tenant Runtime owns it from here on and closes it
@@ -229,5 +178,5 @@ export interface OpenedTenant {
229
178
  /** The Tenant envelope as its store reports it. */
230
179
  envelope: TenantEnvelope;
231
180
  }
232
- /** Opens the Tenant Runtime on a store the Tenant store opened (injected, so tests can fake it). */
181
+ /** Opens the Tenant Runtime on an opened store (injected, so tests can fake it). */
233
182
  export type OpenTenantRuntime = (config: TenantConfig, opened: OpenedTenant) => Promise<TenantHandle>;
@@ -1,23 +1,12 @@
1
1
  /**
2
- * Thrown by a store when a Tenant cannot be reached for a reason outside it (the database
3
- * or Durable Session Execution is unavailable). The module does not quarantine it: the next
4
- * use tries again. The Host answers 503.
2
+ * Thrown when the Tenant cannot be reached for a reason outside it (the database or Durable
3
+ * Session Execution is unavailable). Not a cause: the next use tries again. The Host answers
4
+ * 503.
5
5
  */
6
6
  export class TenantUnavailableError extends Error {
7
- tenantId;
8
7
  status = 503;
9
- constructor(tenantId, options) {
8
+ constructor(options) {
10
9
  super("Tenant is temporarily unavailable", options);
11
- this.tenantId = tenantId;
12
10
  this.name = "TenantUnavailableError";
13
11
  }
14
12
  }
15
- /** Thrown by `TenantStore.open` for a Tenant that does not exist. */
16
- export class TenantNotFoundError extends Error {
17
- tenantId;
18
- constructor(tenantId) {
19
- super(`Tenant ${tenantId} not found`);
20
- this.tenantId = tenantId;
21
- this.name = "TenantNotFoundError";
22
- }
23
- }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * The Worker side of Durable Session Execution (architecture §12.3): what a `DurableExecution`
3
- * calls for one Tenant, and the registry that lets one execution serve every Tenant open on
4
- * this process.
3
+ * calls for the Tenant, and the registry its handlers dispatch through by Tenant id.
5
4
  *
6
5
  * - `TenantWorker` is one open Tenant's handlers: `advance(sessionId, signal)` (§10.5, in
7
6
  * `advance.ts`), `sweep()` (the Tenant sweep, in `sweep.ts`) and, optionally,
@@ -9,12 +8,13 @@
9
8
  * - `TenantWorkers` is the registry. Its `handlers` are the `WorkerHandlers` an execution is
10
9
  * started with; they dispatch by `tenantId` to the registered `TenantWorker`. A Tenant
11
10
  * registers when it opens and unregisters when it closes. A call for a Tenant that is not
12
- * open here asks the optional `resolve` hook (the Host can open it on demand); without one,
11
+ * open here asks the optional `resolve` hook (the Host's, which waits for its Tenant to
12
+ * open, and knows no other); without one,
13
13
  * the advance and the delivery return `done` and the sweep does nothing. Nothing is lost:
14
14
  * the Tenant's sweep re-wakes its orphaned sessions once it is open again, and an Action
15
15
  * that was not delivered stays pending.
16
16
  * - `TenantExecution` pairs an execution with its registry. The Host creates one per process
17
- * (`host/execution.ts`) and passes it to every Tenant it opens; a Tenant opened without one
17
+ * (`host/execution.ts`) and passes it to its Tenant; a Tenant opened without one
18
18
  * (tests, ephemeral) gets its own in-process `MemoryExecution`.
19
19
  *
20
20
  * ## Advance deadline