@nylorun/runtime 0.14.0-beta → 0.15.1-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +82 -40
  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 +185 -686
  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 +6 -7
  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 +888 -695
  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,14 +1,9 @@
1
- import { existsSync, mkdirSync, readdirSync, realpathSync, renameSync, } from "node:fs";
1
+ import { existsSync, mkdirSync, realpathSync } from "node:fs";
2
2
  import { join, resolve, sep } from "node:path";
3
- import { isTenantId } from "@nylorun/core/compatibility";
4
- function ensureDir(path) {
5
- mkdirSync(path, { recursive: true });
6
- return realpathSync(path);
7
- }
8
3
  function assertContained(parent, child) {
9
4
  const root = parent.endsWith(sep) ? parent : parent + sep;
10
5
  if (child !== parent && !child.startsWith(root)) {
11
- throw new Error(`Path ${child} escapes host tenants root ${parent}`);
6
+ throw new Error(`Path ${child} escapes the Host root ${parent}`);
12
7
  }
13
8
  }
14
9
  /** Host layout under NYLORUN_HOME / ~/.nylorun. */
@@ -20,72 +15,33 @@ export function hostPaths(hostRoot) {
20
15
  credentials: join(root, "host-credentials.json"),
21
16
  home: join(root, "home"),
22
17
  tmp: join(root, "tmp"),
23
- tenants: join(root, "tenants"),
24
- trash: join(root, "trash"),
18
+ tenant: join(root, "tenant"),
25
19
  };
26
20
  }
27
21
  /**
28
- * Absolute Tenant directory paths under `<hostRoot>/tenants/<tenantId>/`.
29
- * Validates the id and asserts containment after realpath.
22
+ * Absolute paths of the Tenant directory, `<hostRoot>/tenant/`: a Host serves one Tenant
23
+ * (tenancy.md §1), so the directory has no id segment. Asserts that it stays inside the Host
24
+ * root after realpath.
30
25
  *
31
- * The directory holds what stays on the Host (the Tenant's data is its Postgres schema):
32
- * `vault-kek`, `home/`, `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
26
+ * The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
27
+ * `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`. The vault key is the one exception:
28
+ * it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
29
+ * mounts only into the gateway (F4.2), never into the runtime container.
33
30
  */
34
- export function tenantPaths(hostRoot, tenantId) {
35
- if (!isTenantId(tenantId)) {
36
- throw new Error(`Invalid tenant id: ${String(tenantId)}`);
37
- }
38
- const host = hostPaths(hostRoot);
39
- const tenantsRoot = ensureDir(host.tenants);
40
- const root = join(tenantsRoot, tenantId);
41
- // Containment before the Tenant directory exists: resolve under real tenants root.
42
- const resolvedRoot = resolve(tenantsRoot, tenantId);
43
- assertContained(tenantsRoot, resolvedRoot);
44
- if (existsSync(root)) {
45
- assertContained(tenantsRoot, realpathSync(root));
46
- }
31
+ export function tenantPaths(hostRoot) {
32
+ mkdirSync(hostRoot, { recursive: true });
33
+ const host = realpathSync(hostRoot);
34
+ const root = join(host, "tenant");
35
+ if (existsSync(root))
36
+ assertContained(host, realpathSync(root));
47
37
  return {
48
- root: resolvedRoot,
49
- kek: join(resolvedRoot, "vault-kek"),
50
- home: join(resolvedRoot, "home"),
51
- tmp: join(resolvedRoot, "tmp"),
52
- sandboxes: join(resolvedRoot, "sandboxes"),
53
- pluginData: join(resolvedRoot, "plugin-data"),
54
- logs: join(resolvedRoot, "logs"),
55
- log: join(resolvedRoot, "logs", "tenant.log"),
38
+ root,
39
+ kek: join(host, "keys", "vault-kek"),
40
+ home: join(root, "home"),
41
+ tmp: join(root, "tmp"),
42
+ sandboxes: join(root, "sandboxes"),
43
+ pluginData: join(root, "plugin-data"),
44
+ logs: join(root, "logs"),
45
+ log: join(root, "logs", "tenant.log"),
56
46
  };
57
47
  }
58
- /** The database file of a Tenant from before the Postgres Session Store. */
59
- const SQLITE_DATABASE = "tenant.sqlite";
60
- /**
61
- * Moves every Tenant directory that holds a `tenant.sqlite` to `trash/`, logging each.
62
- *
63
- * Those are beta Tenants from the SQLite Runtime. They are not migrated (recreate them); a
64
- * Postgres Tenant's directory never holds that file. Moving them at Host start keeps a new
65
- * Postgres Tenant with the same id from sharing the old directory. Returns the moved ids.
66
- */
67
- export function trashSqliteTenants(hostRoot, logger) {
68
- const host = hostPaths(hostRoot);
69
- if (!existsSync(host.tenants))
70
- return [];
71
- const moved = [];
72
- const stamp = new Date().toISOString().replaceAll(":", "-");
73
- for (const entry of readdirSync(host.tenants, { withFileTypes: true })) {
74
- if (!entry.isDirectory())
75
- continue;
76
- const directory = join(host.tenants, entry.name);
77
- if (!existsSync(join(directory, SQLITE_DATABASE)))
78
- continue;
79
- mkdirSync(host.trash, { recursive: true });
80
- const destination = join(host.trash, `${entry.name}-sqlite-${stamp}`);
81
- renameSync(directory, destination);
82
- moved.push(entry.name);
83
- logger.warn("sqlite_tenant_moved_to_trash", {
84
- tenant: entry.name,
85
- from: directory,
86
- to: destination,
87
- reason: "SQLite Tenants are not migrated to the Postgres Session Store; recreate the Tenant",
88
- });
89
- }
90
- return moved;
91
- }
@@ -3,6 +3,6 @@ export declare class TimeoutError extends Error {
3
3
  }
4
4
  /**
5
5
  * Race a promise against a timeout. On timeout the late result is handed to
6
- * `onLate` (Risk R6: mark quarantined and close a late handle).
6
+ * `onLate` (Risk R6: the module closes a handle that opened too late).
7
7
  */
8
8
  export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onLate?: (late: Promise<T>) => void): Promise<T>;
@@ -6,7 +6,7 @@ export class TimeoutError extends Error {
6
6
  }
7
7
  /**
8
8
  * Race a promise against a timeout. On timeout the late result is handed to
9
- * `onLate` (Risk R6: mark quarantined and close a late handle).
9
+ * `onLate` (Risk R6: the module closes a handle that opened too late).
10
10
  */
11
11
  export async function withTimeout(promise, timeoutMs, onLate) {
12
12
  let timer;
@@ -1,16 +1,28 @@
1
- import type { Tx } from "../store/types.js";
2
- import type { BootstrapPrincipal } from "./types.js";
1
+ import type { InitialPrincipal } from "../store/postgres/tenant.js";
3
2
  export type { PrincipalRow } from "../store/types.js";
4
3
  /** Principal id of the Studio key derived from the admin key. */
5
4
  export declare const STUDIO_PRINCIPAL_ID = "studio";
5
+ /** The derived principal Projects use (`PROJECT_PRINCIPAL_ID` in `@nylorun/admin`). */
6
+ export declare const PROJECT_PRINCIPAL_ID = "project";
7
+ /** The derived principals a Host registers when not told otherwise. */
8
+ export declare const DEFAULT_DERIVED_PRINCIPALS: readonly string[];
9
+ /** The Studio key of `tenantId`: HMAC-SHA256 of the admin key. */
10
+ export declare function deriveStudioKey(adminKey: string, tenantId: string): string;
11
+ /** The key of derived principal `principalId` on `tenantId`: HMAC-SHA256 of the admin key. */
12
+ export declare function deriveTenantKey(adminKey: string, tenantId: string, principalId: string): string;
13
+ /** Whether `id` may name a derived principal: the client's name, never `studio`. */
14
+ export declare function isDerivedPrincipalId(id: string): boolean;
6
15
  /**
7
- * Writes the application principal and idempotency key in the caller's transaction, plus
8
- * principal `studio` when the bootstrap carries a Studio credential hash.
9
- * Exported for WS-B's `TenantStore.create` (A3).
16
+ * The principals a Host's Tenant is created with: `studio`, each derived principal, and an
17
+ * application principal when a key is given (the ephemeral Runtime). Keys are hashed here.
10
18
  */
11
- export declare function bootstrapPrincipal(t: Tx, bootstrap: BootstrapPrincipal, now?: Date): Promise<void>;
12
- /**
13
- * Whether the stored bootstrap, Studio and derived principals equal `bootstrap`. A retried
14
- * create names the same derived principals, so each must be stored with its hash.
15
- */
16
- export declare function bootstrapPrincipalMatches(t: Tx, bootstrap: BootstrapPrincipal): Promise<boolean>;
19
+ export declare function hostPrincipals(options: {
20
+ adminKey: string;
21
+ derived?: readonly string[];
22
+ application?: {
23
+ principalId: string;
24
+ key: string;
25
+ };
26
+ /** Registers `studio` with this hash instead of the admin key's derivation. */
27
+ studioCredentialHash?: string;
28
+ }): (tenantId: string) => InitialPrincipal[];
@@ -1,56 +1,68 @@
1
+ /**
2
+ * The principals the Host gives its Tenant when it creates it (tenancy.md §4, plan P9): the
3
+ * Studio principal and the derived principals, whose keys the admin key derives, so the
4
+ * clients holding the admin key (Studio, `nylorun start`, Babai) store none. The Tenant keeps
5
+ * only their hashes.
6
+ *
7
+ * The derivations are the ones `@nylorun/admin` computes on the client side
8
+ * (`admin/src/derived-credentials.ts`: `deriveStudioToken`, `deriveTenantKey`); both sides
9
+ * must stay byte for byte the same.
10
+ */
11
+ import { createHmac } from "node:crypto";
12
+ import { DERIVED_PRINCIPAL_ID_PATTERN } from "@nylorun/core/compatibility";
13
+ import { hashToken } from "../core/bearer.js";
1
14
  /** Principal id of the Studio key derived from the admin key. */
2
15
  export const STUDIO_PRINCIPAL_ID = "studio";
16
+ /** The derived principal Projects use (`PROJECT_PRINCIPAL_ID` in `@nylorun/admin`). */
17
+ export const PROJECT_PRINCIPAL_ID = "project";
18
+ /** The derived principals a Host registers when not told otherwise. */
19
+ export const DEFAULT_DERIVED_PRINCIPALS = [PROJECT_PRINCIPAL_ID];
20
+ /** The Studio key of `tenantId`: HMAC-SHA256 of the admin key. */
21
+ export function deriveStudioKey(adminKey, tenantId) {
22
+ return hmac(adminKey, ["nylorun/studio/v1", tenantId]);
23
+ }
24
+ /** The key of derived principal `principalId` on `tenantId`: HMAC-SHA256 of the admin key. */
25
+ export function deriveTenantKey(adminKey, tenantId, principalId) {
26
+ return hmac(adminKey, ["nylorun/principal/v1", principalId, tenantId]);
27
+ }
28
+ /** Whether `id` may name a derived principal: the client's name, never `studio`. */
29
+ export function isDerivedPrincipalId(id) {
30
+ return DERIVED_PRINCIPAL_ID_PATTERN.test(id) && id !== STUDIO_PRINCIPAL_ID;
31
+ }
3
32
  /**
4
- * Writes the application principal and idempotency key in the caller's transaction, plus
5
- * principal `studio` when the bootstrap carries a Studio credential hash.
6
- * Exported for WS-B's `TenantStore.create` (A3).
33
+ * The principals a Host's Tenant is created with: `studio`, each derived principal, and an
34
+ * application principal when a key is given (the ephemeral Runtime). Keys are hashed here.
7
35
  */
8
- export async function bootstrapPrincipal(t, bootstrap, now = new Date()) {
9
- const createdAt = now.toISOString();
10
- await t.insertPrincipal({
11
- id: bootstrap.principalId,
12
- role: "application",
13
- tokenHash: bootstrap.credentialHash,
14
- idempotencyKey: bootstrap.idempotencyKey,
15
- createdAt,
16
- });
17
- if (bootstrap.studioCredentialHash)
18
- await t.insertPrincipal({
36
+ export function hostPrincipals(options) {
37
+ const derived = options.derived ?? DEFAULT_DERIVED_PRINCIPALS;
38
+ for (const id of derived)
39
+ if (!isDerivedPrincipalId(id))
40
+ throw new Error(`Derived principal id '${id}' must match ${DERIVED_PRINCIPAL_ID_PATTERN} and not be '${STUDIO_PRINCIPAL_ID}'`);
41
+ return (tenantId) => [
42
+ ...(options.application
43
+ ? [
44
+ {
45
+ id: options.application.principalId,
46
+ credentialHash: hashToken(options.application.key),
47
+ },
48
+ ]
49
+ : []),
50
+ {
19
51
  id: STUDIO_PRINCIPAL_ID,
20
- role: "application",
21
- tokenHash: bootstrap.studioCredentialHash,
22
- idempotencyKey: null,
23
- createdAt,
24
- });
25
- for (const principal of bootstrap.derivedPrincipals ?? [])
26
- await t.insertPrincipal({
27
- id: principal.id,
28
- role: "application",
29
- tokenHash: principal.credentialHash,
30
- idempotencyKey: null,
31
- createdAt,
32
- });
52
+ credentialHash: options.studioCredentialHash ??
53
+ hashToken(deriveStudioKey(options.adminKey, tenantId)),
54
+ },
55
+ ...[...new Set(derived)].map((id) => ({
56
+ id,
57
+ credentialHash: hashToken(deriveTenantKey(options.adminKey, tenantId, id)),
58
+ })),
59
+ ];
33
60
  }
34
- /**
35
- * Whether the stored bootstrap, Studio and derived principals equal `bootstrap`. A retried
36
- * create names the same derived principals, so each must be stored with its hash.
37
- */
38
- export async function bootstrapPrincipalMatches(t, bootstrap) {
39
- const row = await t.principalById(bootstrap.principalId);
40
- if (!row ||
41
- row.role !== "application" ||
42
- row.tokenHash !== bootstrap.credentialHash ||
43
- row.idempotencyKey !== bootstrap.idempotencyKey)
44
- return false;
45
- const studio = await t.principalById(STUDIO_PRINCIPAL_ID);
46
- if ((studio?.role === "application" ? studio.tokenHash : undefined) !==
47
- bootstrap.studioCredentialHash)
48
- return false;
49
- for (const principal of bootstrap.derivedPrincipals ?? []) {
50
- const stored = await t.principalById(principal.id);
51
- if (stored?.role !== "application" ||
52
- stored.tokenHash !== principal.credentialHash)
53
- return false;
54
- }
55
- return true;
61
+ /** HMAC-SHA256 (hex) of `parts` joined by NUL bytes, keyed with the admin key. */
62
+ function hmac(adminKey, parts) {
63
+ const message = Buffer.concat(parts.flatMap((part, index) => [
64
+ ...(index > 0 ? [Buffer.from([0])] : []),
65
+ Buffer.from(part, "utf8"),
66
+ ]));
67
+ return createHmac("sha256", Buffer.from(adminKey, "utf8")).update(message).digest("hex");
56
68
  }
@@ -1,13 +1,31 @@
1
1
  import type { TenantEnvelope } from "@nylorun/core/contracts";
2
2
  import { type ModelProvider } from "../core/provider.js";
3
+ import type { ModelGate } from "../gates/model-gate.js";
3
4
  import { type AuthorizeResult } from "../vault/service.js";
4
5
  import type { DurableStreams } from "../streams/types.js";
5
6
  import type { NodeBindings, OpenedTenant, TenantConfig, TenantHandle, TenantSummary } from "./types.js";
6
7
  import { type StreamsStatus } from "./streams.js";
7
8
  import { type TenantExecution, type TenantWorker } from "./worker.js";
9
+ import { type ToolGate } from "../gates/tool-gate.js";
10
+ import { type Keys } from "../keys/keys.js";
8
11
  /** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
9
12
  export type TenantOpenHooks = {
10
13
  modelProvider?: ModelProvider;
14
+ /**
15
+ * Serves the Tenant's vault-backed model calls (the gates service's client). Without one,
16
+ * the Tenant calls the model in this process.
17
+ */
18
+ modelGate?: ModelGate;
19
+ /**
20
+ * Serves the Tenant's remote MCP servers and Action deliveries (the gates service's client).
21
+ * Without one, the Tenant opens them and POSTs deliveries in this process.
22
+ */
23
+ toolGate?: ToolGate;
24
+ /**
25
+ * Runs vault writes and token signing (the `keys` service's client, F4.2). With one, this
26
+ * process never reads, creates or holds the vault key. Without one, it does them here.
27
+ */
28
+ keys?: Keys;
11
29
  vaultKek?: Buffer | string | null;
12
30
  /** When true, create the KEK file on first vault write (tests / new Tenants). */
13
31
  createKekIfMissing?: boolean;
@@ -26,7 +44,7 @@ export type TenantOpenHooks = {
26
44
  */
27
45
  streams?: DurableStreams;
28
46
  /**
29
- * The Host's stream relay feeds `streams` from the record for every Tenant. Without it the
47
+ * The Host's stream relay feeds `streams` from the record. Without it the
30
48
  * Tenant relays its own commits.
31
49
  */
32
50
  hostRelay?: boolean;
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import { resolveFlowLimits } from "../core/limits.js";
3
- import { scriptedModel, gatewayModel, toolFixtureModel, } from "../core/provider.js";
3
+ import { scriptedModel, toolFixtureModel, } from "../core/provider.js";
4
+ import { tenantModelGate } from "../gates/in-process.js";
4
5
  import { createKekFile, readVaultKek } from "../vault/kek.js";
5
6
  import { SigningKeys } from "./signing-keys.js";
6
7
  import { VaultService } from "../vault/service.js";
@@ -8,7 +9,7 @@ import { McpPool } from "../mcp/pool.js";
8
9
  import { SandboxManager } from "../sandbox/manager.js";
9
10
  import { defaultSandboxBackends } from "../sandbox/select.js";
10
11
  import { MemoryExecution } from "../execution/memory.js";
11
- import { QuarantineError } from "./quarantine-error.js";
12
+ import { openError } from "./cause.js";
12
13
  import { MemoryStreams } from "../streams/memory.js";
13
14
  import { createSessionStreams, endAllStreams, } from "./session-streams.js";
14
15
  import { closeStreams, streamsStatus, wireStreams, } from "./streams.js";
@@ -18,6 +19,8 @@ import { sweep } from "./sweep.js";
18
19
  import { deliverAction } from "./delivery.js";
19
20
  import { TenantWorkers, WORKER_ID, } from "./worker.js";
20
21
  import { authorize } from "./effects.js";
22
+ import { inProcessToolGate } from "../gates/tool-gate.js";
23
+ import { inProcessKeys } from "../keys/keys.js";
21
24
  import { tenantApi } from "../api/http/app.js";
22
25
  /** Default ownership lease of an advance; the heartbeat renews it every third. */
23
26
  const DEFAULT_OWNER_LEASE_MS = 30_000;
@@ -67,20 +70,28 @@ export class TenantRuntime {
67
70
  let wired;
68
71
  let detach;
69
72
  try {
70
- let kek = readVaultKek({
71
- vaultKek: hooks.vaultKek,
72
- vaultKekPath: paths.kek,
73
- });
74
- const sealed = await store.tx(async (t) => (await t.countCredentials()) + (await t.countSigningKeys()));
75
- if (sealed > 0 && !kek) {
76
- throw new QuarantineError("kek-missing", "Vault key-encryption key is missing for ciphertext (vault credentials or signing keys) in this Tenant", "restore the vault-kek file in the Tenant directory");
73
+ // With the keys service (F4.2) the key lives in the gateway: this process never reads,
74
+ // creates or holds it, and the gateway reports a missing key on its readiness.
75
+ let kek = hooks.keys
76
+ ? undefined
77
+ : readVaultKek({
78
+ vaultKek: hooks.vaultKek,
79
+ vaultKekPath: paths.kek,
80
+ });
81
+ if (!hooks.keys) {
82
+ const sealed = await store.tx(async (t) => (await t.countCredentials()) + (await t.countSigningKeys()));
83
+ if (sealed > 0 && !kek) {
84
+ throw openError("kek-missing", "Vault key-encryption key is missing for ciphertext (vault credentials or signing keys) in this Tenant");
85
+ }
77
86
  }
78
87
  const createKekIfMissing = hooks.createKekIfMissing !== false;
79
88
  const ensureKek = () => {
89
+ if (hooks.keys)
90
+ throw new Error("The vault key lives in the gateway's keys service: this process never reads it");
80
91
  if (kek)
81
92
  return kek;
82
93
  if (!createKekIfMissing)
83
- throw new QuarantineError("kek-missing", "Vault key-encryption key is required", "restore the vault-kek file in the Tenant directory");
94
+ throw openError("kek-missing", "Vault key-encryption key is required");
84
95
  kek = createKekFile(paths.kek);
85
96
  return kek;
86
97
  };
@@ -94,10 +105,14 @@ export class TenantRuntime {
94
105
  });
95
106
  // `ctx` is assigned below; these callbacks only run once the Tenant is open.
96
107
  let ctx;
108
+ const toolGate = hooks.toolGate ?? inProcessToolGate(config.delivery ?? {});
109
+ const signingKeys = new SigningKeys({ tenantId: config.tenantId, kek: ensureKek });
110
+ const keys = hooks.keys ?? inProcessKeys({ store: opened, vault, signingKeys, kek: ensureKek });
97
111
  const mcp = new McpPool({
98
112
  pluginData: paths.pluginData,
99
113
  childEnv: config.childEnv,
100
114
  authorize: (sessionId, request) => authorize(ctx, sessionId, request),
115
+ ...(toolGate.openMcp ? { openRemote: (server) => toolGate.openMcp(server) } : {}),
101
116
  });
102
117
  const ephemeral = config.mode === "ephemeral";
103
118
  const sandbox = new SandboxManager({
@@ -126,22 +141,18 @@ export class TenantRuntime {
126
141
  modelProvider = scriptedModel(config.model.output);
127
142
  else if (config.model.kind === "fixture")
128
143
  modelProvider = toolFixtureModel();
129
- else if (config.model.kind === "gateway") {
130
- const gateway = config.model;
131
- modelProvider = async (effect, signal) => {
132
- const secret = await vault.readHostModel();
133
- const token = typeof secret?.credential.key === "string"
134
- ? secret.credential.key
135
- : "";
136
- return gatewayModel({
137
- url: gateway.url,
138
- model: gateway.model,
139
- token,
140
- })(effect, signal);
141
- };
142
- }
143
144
  else
144
145
  modelProvider = scriptedModel();
146
+ // With the gates service the loop never reads a model credential: only a Runtime
147
+ // without one (embedding, the ephemeral Runtime, tests) reads it here.
148
+ const modelGate = hooks.modelGate ??
149
+ tenantModelGate({
150
+ store: opened,
151
+ kek: ensureKek,
152
+ root: paths.home,
153
+ logger: config.logger,
154
+ ...(config.modelCall ? { settings: config.modelCall } : {}),
155
+ });
145
156
  const sessionStreams = createSessionStreams();
146
157
  const local = hooks.execution
147
158
  ? undefined
@@ -166,11 +177,14 @@ export class TenantRuntime {
166
177
  flowLimits,
167
178
  modelProvider,
168
179
  useVaultModel,
180
+ modelGate,
181
+ toolGate,
169
182
  closing: false,
170
183
  closed: false,
171
184
  work: createWorkState(),
172
185
  sessionStreams,
173
- signingKeys: new SigningKeys({ tenantId: config.tenantId, kek: ensureKek }),
186
+ signingKeys,
187
+ keys,
174
188
  workerId: hooks.workerId ?? WORKER_ID,
175
189
  ownerLeaseMs: config.ownerLeaseMs ?? DEFAULT_OWNER_LEASE_MS,
176
190
  wake: async (sessionId, wake) => {
@@ -178,7 +192,7 @@ export class TenantRuntime {
178
192
  return;
179
193
  await execution.wake(config.tenantId, sessionId, wake);
180
194
  },
181
- abortLocal: (sessionId) => abortLocal(ctx, sessionId),
195
+ abortLocal: (sessionId, turnId) => abortLocal(ctx, sessionId, "cancel", turnId),
182
196
  deliver: async (actionId) => {
183
197
  if (ctx.closing || ctx.closed)
184
198
  return;
@@ -3,6 +3,8 @@ import { type AdvanceAbortKind } from "./worker.js";
3
3
  export interface WorkState {
4
4
  /** Advances running on this process, by session id. */
5
5
  readonly running: Map<string, AbortController>;
6
+ /** The turn each running advance belongs to, by session id (`null`: none). */
7
+ readonly runningTurns: Map<string, string | null>;
6
8
  /** Deliveries to Action endpoints in flight on this process, by their Action's session id. */
7
9
  readonly deliveries: Map<string, Set<AbortController>>;
8
10
  /** When each Action last reported a failed delivery (`action.delivery_failed` throttling). */
@@ -12,9 +14,9 @@ export declare function createWorkState(): WorkState;
12
14
  /**
13
15
  * Abort the advance of `id` if it runs on this process, and its deliveries to Action endpoints.
14
16
  * Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
15
- * elsewhere.
17
+ * elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
16
18
  */
17
- export declare function abortLocal(ctx: TenantContext, id: string, kind?: AdvanceAbortKind): void;
19
+ export declare function abortLocal(ctx: TenantContext, id: string, kind?: AdvanceAbortKind, turnId?: string): void;
18
20
  /** Reset: abort every advance running on this process; their sessions are being cleared. */
19
21
  export declare function clearWork(ctx: TenantContext): void;
20
22
  /** Stop new advances; cancel active turns if asked; wait for running advances until the timeout. */
@@ -13,7 +13,7 @@ import { randomUUID } from "node:crypto";
13
13
  import { command } from "./commands.js";
14
14
  import { AdvanceAbort } from "./worker.js";
15
15
  export function createWorkState() {
16
- return { running: new Map(), deliveries: new Map(), deliveryNotices: new Map() };
16
+ return { running: new Map(), runningTurns: new Map(), deliveries: new Map(), deliveryNotices: new Map() };
17
17
  }
18
18
  const MESSAGES = {
19
19
  cancel: "Turn cancelled",
@@ -28,11 +28,11 @@ function abortWith(controller, kind) {
28
28
  /**
29
29
  * Abort the advance of `id` if it runs on this process, and its deliveries to Action endpoints.
30
30
  * Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
31
- * elsewhere.
31
+ * elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
32
32
  */
33
- export function abortLocal(ctx, id, kind = "cancel") {
33
+ export function abortLocal(ctx, id, kind = "cancel", turnId) {
34
34
  const controller = ctx.work.running.get(id);
35
- if (controller)
35
+ if (controller && (turnId === undefined || ctx.work.runningTurns.get(id) === turnId))
36
36
  abortWith(controller, kind);
37
37
  // A cancel also closes the session's deliveries, which aborts the tools' `ctx.signal`.
38
38
  for (const delivery of ctx.work.deliveries.get(id) ?? [])
@@ -105,6 +105,8 @@ export declare function observeSession(ctx: TenantContext, sessionId: string, cu
105
105
  holder?: StreamHolder;
106
106
  signal: AbortSignal;
107
107
  }): AsyncGenerator<LiveEvent>;
108
+ /** Internal events (`transcript.updated`) are recorded and streamed but never served. */
109
+ export declare function served(event: LiveEvent): boolean;
108
110
  /**
109
111
  * Ends every feed in this process whose session is gone or whose Tenant moved to a new basin
110
112
  * generation, and moves this process to the Tenant's current generation. Run on a
@@ -1,4 +1,4 @@
1
- import { decodeCursor, encodeCursor } from "../store/cursor.js";
1
+ import { decodeCursor, encodeCursor } from "../record/index.js";
2
2
  import { basinOf } from "../streams/basin.js";
3
3
  import { sessionStream, } from "../streams/types.js";
4
4
  import { fail } from "./http.js";
@@ -88,7 +88,7 @@ export async function readHistory(ctx, sessionId, cursor, agent) {
88
88
  try {
89
89
  for await (const record of streams.read(ref.basin, ref.stream, from, { follow: false })) {
90
90
  last = record.seq;
91
- if (agent === undefined || belongsTo(record.body, agent))
91
+ if (served(record.body) && (agent === undefined || belongsTo(record.body, agent)))
92
92
  items.push(record.body);
93
93
  }
94
94
  }
@@ -255,7 +255,12 @@ function deliver(observer, record) {
255
255
  if (record.seq < observer.next)
256
256
  return;
257
257
  observer.next = record.seq + 1;
258
- observer.sink.write(record.body);
258
+ if (served(record.body))
259
+ observer.sink.write(record.body);
260
+ }
261
+ /** Internal events (`transcript.updated`) are recorded and streamed but never served. */
262
+ export function served(event) {
263
+ return event.visibility !== "internal";
259
264
  }
260
265
  /**
261
266
  * Follows the feed's stream from `feed.next` for the current `feed.read`, retrying failed
@@ -6,6 +6,7 @@ import type { TenantConfig } from "./types.js";
6
6
  import type { TenantEnvelope } from "@nylorun/core/contracts";
7
7
  import type { StuckInvocation } from "../execution/types.js";
8
8
  import type { StreamsStatus } from "./streams.js";
9
+ import type { Keys } from "../keys/keys.js";
9
10
  export interface TenantStatusContext {
10
11
  envelope: TenantEnvelope;
11
12
  config: TenantConfig;
@@ -31,4 +32,6 @@ export declare function buildTenantStatus(ctx: TenantStatusContext): Promise<Ten
31
32
  export declare function seedTenantConfig(ctx: {
32
33
  store: SessionStore;
33
34
  vault: VaultService;
35
+ /** Seals the seeded model credential (F4.2). */
36
+ keys: Pick<Keys, "putHostModel">;
34
37
  }, body: SeedTenantConfigRequest): Promise<SeedTenantConfigResponse>;
@@ -125,7 +125,7 @@ export async function seedTenantConfig(ctx, body) {
125
125
  kept.push("model");
126
126
  }
127
127
  else {
128
- await ctx.vault.putHostModel({
128
+ await ctx.keys.putHostModel({
129
129
  requestId: body.requestId,
130
130
  idempotencyKey: `seed-model:${body.requestId}`,
131
131
  ...body.model,
@@ -1,15 +1,18 @@
1
1
  import type { PostgresClient } from "../store/postgres/connect.js";
2
- import type { Migration } from "../store/postgres/migrations/index.js";
3
- import { type Logger, type OpenTenantRuntime, type TenantConfig, type TenantStore } from "./types.js";
4
- export interface PostgresTenantStoreOptions {
2
+ import type { Migration } from "../store/postgres/migrate.js";
3
+ import { type TenantCreation } from "../store/postgres/tenant.js";
4
+ import { type Logger, type OpenTenantRuntime, type TenantConfig, type TenantOpener } from "./types.js";
5
+ export interface PostgresTenantOptions {
5
6
  hostRoot: string;
6
- /** The Host's pool. The store never ends it. */
7
+ /** The pool on the Tenant's database. The Tenant never ends it. */
7
8
  sql: PostgresClient;
8
- /** Opens the Tenant Runtime on a store the catalog opened; the Runtime then owns it. */
9
+ /** Who the Tenant is when the database holds none yet. */
10
+ create: TenantCreation;
11
+ /** Opens the Tenant Runtime on the opened store; the Runtime then owns it. */
9
12
  openRuntime: OpenTenantRuntime;
10
13
  configFor: (tenantId: string) => TenantConfig;
11
14
  logger?: Logger;
12
15
  /** Tests only: the migrations this Runtime knows. */
13
16
  migrations?: readonly Migration[];
14
17
  }
15
- export declare function createPostgresTenantStore(options: PostgresTenantStoreOptions): TenantStore;
18
+ export declare function createPostgresTenantOpener(options: PostgresTenantOptions): TenantOpener;