@nylorun/runtime 0.10.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 (190) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +53 -51
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/sandbox/virtual.js +1 -1
  6. package/dist/adapters/streams/s2.d.ts +18 -0
  7. package/dist/adapters/streams/s2.js +361 -0
  8. package/dist/core/executors.d.ts +4 -1
  9. package/dist/core/executors.js +20 -1
  10. package/dist/core/flow-host.d.ts +93 -132
  11. package/dist/core/flow-host.js +282 -547
  12. package/dist/core/sandbox-routes.d.ts +8 -3
  13. package/dist/core/sandbox-routes.js +10 -10
  14. package/dist/execution/memory.d.ts +54 -0
  15. package/dist/execution/memory.js +217 -0
  16. package/dist/execution/types.d.ts +125 -0
  17. package/dist/execution/types.js +52 -0
  18. package/dist/host/config-for.d.ts +2 -1
  19. package/dist/host/config-for.js +3 -21
  20. package/dist/host/config.d.ts +0 -8
  21. package/dist/host/create-host.d.ts +36 -1
  22. package/dist/host/create-host.js +48 -24
  23. package/dist/host/execution.d.ts +61 -0
  24. package/dist/host/execution.js +69 -0
  25. package/dist/host/http.d.ts +4 -0
  26. package/dist/host/http.js +6 -0
  27. package/dist/host/index.d.ts +3 -1
  28. package/dist/host/index.js +2 -0
  29. package/dist/host/main.js +106 -47
  30. package/dist/host/stack-config.d.ts +72 -0
  31. package/dist/host/stack-config.js +193 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/infra/database.d.ts +16 -0
  34. package/dist/infra/database.js +31 -0
  35. package/dist/infra/execution.d.ts +39 -0
  36. package/dist/infra/execution.js +68 -0
  37. package/dist/infra/index.d.ts +42 -0
  38. package/dist/infra/index.js +56 -0
  39. package/dist/infra/readiness.d.ts +33 -0
  40. package/dist/infra/readiness.js +53 -0
  41. package/dist/infra/streams.d.ts +17 -0
  42. package/dist/infra/streams.js +33 -0
  43. package/dist/infra/workers.d.ts +39 -0
  44. package/dist/infra/workers.js +31 -0
  45. package/dist/model/pi-model.d.ts +2 -2
  46. package/dist/model/pi-model.js +5 -5
  47. package/dist/sandbox/manager.d.ts +26 -8
  48. package/dist/sandbox/manager.js +95 -47
  49. package/dist/sandbox/select.d.ts +1 -1
  50. package/dist/sandbox/select.js +4 -5
  51. package/dist/sandbox/types.d.ts +2 -2
  52. package/dist/store/canonical.d.ts +5 -0
  53. package/dist/store/canonical.js +17 -0
  54. package/dist/store/cursor.d.ts +9 -0
  55. package/dist/store/cursor.js +18 -0
  56. package/dist/store/memory.d.ts +57 -0
  57. package/dist/store/memory.js +643 -0
  58. package/dist/store/ownership.d.ts +19 -0
  59. package/dist/store/ownership.js +33 -0
  60. package/dist/store/postgres/connect.d.ts +25 -0
  61. package/dist/store/postgres/connect.js +25 -0
  62. package/dist/store/postgres/locking.d.ts +16 -0
  63. package/dist/store/postgres/locking.js +22 -0
  64. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  65. package/dist/store/postgres/migrations/001_initial.js +196 -0
  66. package/dist/store/postgres/migrations/index.d.ts +49 -0
  67. package/dist/store/postgres/migrations/index.js +75 -0
  68. package/dist/store/postgres/names.d.ts +11 -0
  69. package/dist/store/postgres/names.js +42 -0
  70. package/dist/store/postgres/store.d.ts +11 -0
  71. package/dist/store/postgres/store.js +790 -0
  72. package/dist/store/postgres/tenants.d.ts +90 -0
  73. package/dist/store/postgres/tenants.js +207 -0
  74. package/dist/store/types.d.ts +440 -0
  75. package/dist/store/types.js +10 -0
  76. package/dist/streams/basin.d.ts +23 -0
  77. package/dist/streams/basin.js +51 -0
  78. package/dist/streams/memory.d.ts +25 -0
  79. package/dist/streams/memory.js +124 -0
  80. package/dist/streams/relay.d.ts +37 -0
  81. package/dist/streams/relay.js +254 -0
  82. package/dist/streams/types.d.ts +115 -0
  83. package/dist/streams/types.js +86 -0
  84. package/dist/tenant/actions.d.ts +153 -0
  85. package/dist/tenant/actions.js +191 -0
  86. package/dist/tenant/advance.d.ts +15 -0
  87. package/dist/tenant/advance.js +439 -0
  88. package/dist/tenant/auth.d.ts +11 -0
  89. package/dist/tenant/auth.js +34 -0
  90. package/dist/tenant/commands.d.ts +3 -0
  91. package/dist/tenant/commands.js +384 -0
  92. package/dist/tenant/context.d.ts +140 -0
  93. package/dist/tenant/context.js +40 -0
  94. package/dist/tenant/effects.d.ts +20 -0
  95. package/dist/tenant/effects.js +539 -0
  96. package/dist/tenant/envelope.d.ts +0 -3
  97. package/dist/tenant/envelope.js +2 -34
  98. package/dist/tenant/ephemeral.d.ts +15 -4
  99. package/dist/tenant/ephemeral.js +106 -38
  100. package/dist/tenant/http.d.ts +32 -0
  101. package/dist/tenant/http.js +51 -0
  102. package/dist/tenant/live.d.ts +92 -0
  103. package/dist/tenant/live.js +290 -0
  104. package/dist/tenant/model-setting.d.ts +16 -0
  105. package/dist/tenant/model-setting.js +12 -0
  106. package/dist/tenant/module.d.ts +29 -8
  107. package/dist/tenant/module.js +162 -158
  108. package/dist/tenant/paths.d.ts +12 -8
  109. package/dist/tenant/paths.js +38 -12
  110. package/dist/tenant/principals.d.ts +9 -13
  111. package/dist/tenant/principals.js +31 -38
  112. package/dist/tenant/quarantine-error.d.ts +1 -1
  113. package/dist/tenant/quarantine-error.js +2 -8
  114. package/dist/tenant/quarantine.d.ts +0 -8
  115. package/dist/tenant/quarantine.js +8 -30
  116. package/dist/tenant/reset.d.ts +7 -6
  117. package/dist/tenant/reset.js +7 -51
  118. package/dist/tenant/routes-tenant.d.ts +12 -0
  119. package/dist/tenant/routes-tenant.js +126 -0
  120. package/dist/tenant/routes.d.ts +9 -0
  121. package/dist/tenant/routes.js +142 -0
  122. package/dist/tenant/runtime.d.ts +59 -58
  123. package/dist/tenant/runtime.js +243 -2201
  124. package/dist/tenant/scheduler.d.ts +25 -0
  125. package/dist/tenant/scheduler.js +84 -0
  126. package/dist/tenant/session.d.ts +60 -0
  127. package/dist/tenant/session.js +94 -0
  128. package/dist/tenant/sessions.d.ts +34 -0
  129. package/dist/tenant/sessions.js +149 -0
  130. package/dist/tenant/status.d.ts +13 -4
  131. package/dist/tenant/status.js +76 -41
  132. package/dist/tenant/store-memory.d.ts +9 -12
  133. package/dist/tenant/store-memory.js +79 -68
  134. package/dist/tenant/store-pg.d.ts +15 -0
  135. package/dist/tenant/store-pg.js +140 -0
  136. package/dist/tenant/streams.d.ts +126 -0
  137. package/dist/tenant/streams.js +419 -0
  138. package/dist/tenant/sweep.d.ts +15 -0
  139. package/dist/tenant/sweep.js +122 -0
  140. package/dist/tenant/types.d.ts +95 -15
  141. package/dist/tenant/types.js +23 -1
  142. package/dist/tenant/worker.d.ts +84 -0
  143. package/dist/tenant/worker.js +189 -0
  144. package/dist/vault/service.d.ts +37 -31
  145. package/dist/vault/service.js +439 -359
  146. package/dist/version.d.ts +1 -1
  147. package/dist/version.js +1 -1
  148. package/package.json +13 -13
  149. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  150. package/dist/adapters/sandbox/microsandbox.js +0 -238
  151. package/dist/core/store.d.ts +0 -32
  152. package/dist/core/store.js +0 -143
  153. package/dist/launcher/commands.d.ts +0 -24
  154. package/dist/launcher/commands.js +0 -260
  155. package/dist/launcher/ensure-config.d.ts +0 -16
  156. package/dist/launcher/ensure-config.js +0 -79
  157. package/dist/launcher/errors.d.ts +0 -12
  158. package/dist/launcher/errors.js +0 -22
  159. package/dist/launcher/host-config.d.ts +0 -17
  160. package/dist/launcher/host-config.js +0 -64
  161. package/dist/launcher/host-state.d.ts +0 -17
  162. package/dist/launcher/host-state.js +0 -61
  163. package/dist/launcher/lifecycle.d.ts +0 -66
  164. package/dist/launcher/lifecycle.js +0 -407
  165. package/dist/launcher/locks.d.ts +0 -26
  166. package/dist/launcher/locks.js +0 -117
  167. package/dist/launcher/logs.d.ts +0 -18
  168. package/dist/launcher/logs.js +0 -117
  169. package/dist/launcher/main.d.ts +0 -2
  170. package/dist/launcher/main.js +0 -66
  171. package/dist/launcher/output.d.ts +0 -11
  172. package/dist/launcher/output.js +0 -68
  173. package/dist/launcher/paths.d.ts +0 -20
  174. package/dist/launcher/paths.js +0 -36
  175. package/dist/launcher/probe.d.ts +0 -23
  176. package/dist/launcher/probe.js +0 -72
  177. package/dist/launcher/protocol.d.ts +0 -62
  178. package/dist/launcher/protocol.js +0 -1
  179. package/dist/launcher/schema-guard.d.ts +0 -6
  180. package/dist/launcher/schema-guard.js +0 -36
  181. package/dist/launcher/spawn.d.ts +0 -22
  182. package/dist/launcher/spawn.js +0 -85
  183. package/dist/launcher/status.d.ts +0 -6
  184. package/dist/launcher/status.js +0 -65
  185. package/dist/tenant/migration.d.ts +0 -16
  186. package/dist/tenant/migration.js +0 -116
  187. package/dist/tenant/schema.d.ts +0 -23
  188. package/dist/tenant/schema.js +0 -151
  189. package/dist/tenant/store-fs.d.ts +0 -16
  190. package/dist/tenant/store-fs.js +0 -205
@@ -1,17 +1,13 @@
1
- import type { DatabaseSync } from "node:sqlite";
1
+ import type { Tx } from "../store/types.js";
2
2
  import type { BootstrapPrincipal } from "./types.js";
3
- export interface PrincipalRow {
4
- id: string;
5
- role: "application";
6
- tokenHash: string;
7
- idempotencyKey: string | null;
8
- createdAt: string;
9
- }
3
+ export type { PrincipalRow } from "../store/types.js";
4
+ /** Principal id of the Studio key derived from the admin key. */
5
+ export declare const STUDIO_PRINCIPAL_ID = "studio";
10
6
  /**
11
- * Writes the application principal and idempotency key in one transaction.
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.
12
9
  * Exported for WS-B's `TenantStore.create` (A3).
13
10
  */
14
- export declare function bootstrapPrincipal(db: DatabaseSync, bootstrap: BootstrapPrincipal): void;
15
- export declare function findPrincipalByTokenHash(db: DatabaseSync, tokenHash: string): PrincipalRow | undefined;
16
- export declare function applicationTokenHashes(db: DatabaseSync): readonly string[];
17
- export declare function isApplicationTokenHash(db: DatabaseSync, tokenHash: string): boolean;
11
+ export declare function bootstrapPrincipal(t: Tx, bootstrap: BootstrapPrincipal, now?: Date): Promise<void>;
12
+ /** Whether the stored bootstrap (and Studio) principals equal `bootstrap`. */
13
+ export declare function bootstrapPrincipalMatches(t: Tx, bootstrap: BootstrapPrincipal): Promise<boolean>;
@@ -1,44 +1,37 @@
1
+ /** Principal id of the Studio key derived from the admin key. */
2
+ export const STUDIO_PRINCIPAL_ID = "studio";
1
3
  /**
2
- * Writes the application principal and idempotency key in one transaction.
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.
3
6
  * Exported for WS-B's `TenantStore.create` (A3).
4
7
  */
5
- export function bootstrapPrincipal(db, bootstrap) {
6
- const now = new Date().toISOString();
7
- db.exec("BEGIN IMMEDIATE");
8
- try {
9
- db.prepare(`INSERT INTO principals(id, role, token_hash, idempotency_key, created_at)
10
- VALUES(?, 'application', ?, ?, ?)`).run(bootstrap.principalId, bootstrap.credentialHash, bootstrap.idempotencyKey, now);
11
- db.exec("COMMIT");
12
- }
13
- catch (error) {
14
- db.exec("ROLLBACK");
15
- throw error;
16
- }
17
- }
18
- export function findPrincipalByTokenHash(db, tokenHash) {
19
- const row = db
20
- .prepare(`SELECT id, role, token_hash, idempotency_key, created_at
21
- FROM principals WHERE token_hash = ?`)
22
- .get(tokenHash);
23
- if (!row)
24
- return undefined;
25
- return {
26
- id: String(row.id),
8
+ export async function bootstrapPrincipal(t, bootstrap, now = new Date()) {
9
+ const createdAt = now.toISOString();
10
+ await t.insertPrincipal({
11
+ id: bootstrap.principalId,
27
12
  role: "application",
28
- tokenHash: String(row.token_hash),
29
- idempotencyKey: row.idempotency_key == null ? null : String(row.idempotency_key),
30
- createdAt: String(row.created_at),
31
- };
32
- }
33
- export function applicationTokenHashes(db) {
34
- return db
35
- .prepare(`SELECT token_hash FROM principals WHERE role = 'application'`)
36
- .all()
37
- .map((row) => String(row.token_hash));
13
+ tokenHash: bootstrap.credentialHash,
14
+ idempotencyKey: bootstrap.idempotencyKey,
15
+ createdAt,
16
+ });
17
+ if (bootstrap.studioCredentialHash)
18
+ await t.insertPrincipal({
19
+ id: STUDIO_PRINCIPAL_ID,
20
+ role: "application",
21
+ tokenHash: bootstrap.studioCredentialHash,
22
+ idempotencyKey: null,
23
+ createdAt,
24
+ });
38
25
  }
39
- export function isApplicationTokenHash(db, tokenHash) {
40
- const row = db
41
- .prepare(`SELECT 1 AS ok FROM principals WHERE role = 'application' AND token_hash = ?`)
42
- .get(tokenHash);
43
- return !!row;
26
+ /** Whether the stored bootstrap (and Studio) principals equal `bootstrap`. */
27
+ export async function bootstrapPrincipalMatches(t, bootstrap) {
28
+ const row = await t.principalById(bootstrap.principalId);
29
+ if (!row ||
30
+ row.role !== "application" ||
31
+ row.tokenHash !== bootstrap.credentialHash ||
32
+ row.idempotencyKey !== bootstrap.idempotencyKey)
33
+ return false;
34
+ const studio = await t.principalById(STUDIO_PRINCIPAL_ID);
35
+ return ((studio?.role === "application" ? studio.tokenHash : undefined) ===
36
+ bootstrap.studioCredentialHash);
44
37
  }
@@ -2,5 +2,5 @@ import type { Quarantine } from "./types.js";
2
2
  /** Typed quarantine raised while opening a Tenant Runtime (A7). */
3
3
  export declare class QuarantineError extends Error {
4
4
  readonly quarantine: Quarantine;
5
- constructor(code: Quarantine["code"], message: string, repair?: string, extra?: Pick<Quarantine, "lockPath" | "lockPid">);
5
+ constructor(code: Quarantine["code"], message: string, repair?: string);
6
6
  }
@@ -1,15 +1,9 @@
1
1
  /** Typed quarantine raised while opening a Tenant Runtime (A7). */
2
2
  export class QuarantineError extends Error {
3
3
  quarantine;
4
- constructor(code, message, repair = "nylorun tenant status", extra) {
4
+ constructor(code, message, repair = "nylo tenant status") {
5
5
  super(message);
6
6
  this.name = "QuarantineError";
7
- this.quarantine = {
8
- code,
9
- message,
10
- repair,
11
- ...(extra?.lockPath === undefined ? {} : { lockPath: extra.lockPath }),
12
- ...(extra?.lockPid === undefined ? {} : { lockPid: extra.lockPid }),
13
- };
7
+ this.quarantine = { code, message, repair };
14
8
  }
15
9
  }
@@ -3,8 +3,6 @@ import type { Quarantine } from "./types.js";
3
3
  export declare class QuarantineError extends Error implements Quarantine {
4
4
  readonly code: Quarantine["code"];
5
5
  readonly repair: string;
6
- readonly lockPath?: string;
7
- readonly lockPid?: number;
8
6
  constructor(fields: Quarantine);
9
7
  toQuarantine(): Quarantine;
10
8
  }
@@ -13,15 +11,9 @@ export declare function asQuarantine(error: unknown): Quarantine | undefined;
13
11
  /** CLI-facing repair strings for every quarantine code (B9). */
14
12
  export declare function repairFor(code: Quarantine["code"], detail?: {
15
13
  tenantId?: string;
16
- lockPath?: string;
17
- lockPid?: number;
18
- migrationPath?: string;
19
14
  }): string;
20
15
  export declare function quarantine(code: Quarantine["code"], message: string, detail?: {
21
16
  tenantId?: string;
22
- lockPath?: string;
23
- lockPid?: number;
24
- migrationPath?: string;
25
17
  }): QuarantineError;
26
18
  /** Conflict when create bootstrap material does not match an existing Tenant (HTTP 409). */
27
19
  export declare class TenantConflictError extends Error {
@@ -2,26 +2,14 @@
2
2
  export class QuarantineError extends Error {
3
3
  code;
4
4
  repair;
5
- lockPath;
6
- lockPid;
7
5
  constructor(fields) {
8
6
  super(fields.message);
9
7
  this.name = "QuarantineError";
10
8
  this.code = fields.code;
11
9
  this.repair = fields.repair;
12
- if (fields.lockPath !== undefined)
13
- this.lockPath = fields.lockPath;
14
- if (fields.lockPid !== undefined)
15
- this.lockPid = fields.lockPid;
16
10
  }
17
11
  toQuarantine() {
18
- return {
19
- code: this.code,
20
- message: this.message,
21
- repair: this.repair,
22
- ...(this.lockPath !== undefined ? { lockPath: this.lockPath } : {}),
23
- ...(this.lockPid !== undefined ? { lockPid: this.lockPid } : {}),
24
- };
12
+ return { code: this.code, message: this.message, repair: this.repair };
25
13
  }
26
14
  }
27
15
  export function isQuarantineError(error) {
@@ -43,28 +31,20 @@ export function asQuarantine(error) {
43
31
  export function repairFor(code, detail = {}) {
44
32
  const id = detail.tenantId ? ` ${detail.tenantId}` : "";
45
33
  switch (code) {
46
- case "locked":
47
- return (`nylorun tenant status${id}` +
48
- (detail.lockPath !== undefined && detail.lockPid !== undefined
49
- ? ` — stop pid ${detail.lockPid} holding ${detail.lockPath}, or remove a stale lock`
50
- : " — resolve the .runtime-lock conflict"));
51
34
  case "kek-missing":
52
- return `nylorun tenant status${id} — restore vault-kek for this Tenant (ciphertext cannot be opened without it)`;
35
+ return `nylo tenant status${id} — restore vault-kek for this Tenant (ciphertext cannot be opened without it)`;
53
36
  case "corrupt":
54
- return `nylorun tenant status${id} — repair or restore the Tenant directory from backup`;
37
+ return `nylo tenant status${id} — restore the Tenant from backup`;
55
38
  case "schema-too-new":
56
- return `nylorun runtime restart` +
57
- `${id ? ` — Tenant${id} needs a newer Host` : " — Host schema is older than this Tenant"}`;
39
+ return `nylo tenant status${id} — the Tenant was migrated by a newer Runtime; run that version or newer`;
58
40
  case "migration-failed":
59
- return detail.migrationPath
60
- ? `restore ${detail.migrationPath} over the Tenant files, then nylorun tenant status${id}`
61
- : `nylorun tenant status${id} — restore from .migration/ snapshot, then retry`;
41
+ return `nylo tenant status${id} — inspect the Runtime log for the failed migration, fix it, then restart the Runtime`;
62
42
  case "envelope-invalid":
63
- return `nylorun tenant status${id} — fix or replace tenant.json so its id matches the directory name`;
43
+ return `nylo tenant status${id} — the Tenant envelope is missing or invalid; restore the Tenant from backup`;
64
44
  case "open-timeout":
65
- return `nylorun tenant status${id} — open timed out; inspect locks and SQLite, then retry`;
45
+ return `nylo tenant status${id} — open timed out; inspect the Runtime log and Postgres, then retry`;
66
46
  case "open-failed":
67
- return `nylorun tenant status${id} — inspect Tenant logs and repair before the Host retries open`;
47
+ return `nylo tenant status${id} — inspect Tenant logs and repair before the Host retries open`;
68
48
  }
69
49
  }
70
50
  export function quarantine(code, message, detail = {}) {
@@ -72,8 +52,6 @@ export function quarantine(code, message, detail = {}) {
72
52
  code,
73
53
  message,
74
54
  repair: repairFor(code, detail),
75
- ...(detail.lockPath !== undefined ? { lockPath: detail.lockPath } : {}),
76
- ...(detail.lockPid !== undefined ? { lockPid: detail.lockPid } : {}),
77
55
  });
78
56
  }
79
57
  /** Conflict when create bootstrap material does not match an existing Tenant (HTTP 409). */
@@ -1,21 +1,22 @@
1
- import type { Store } from "../core/store.js";
1
+ import type { ResetScope, SessionStore } from "../store/types.js";
2
2
  import type { ExecutorRegistry } from "../core/executors.js";
3
3
  import type { SandboxManager } from "../sandbox/manager.js";
4
4
  import type { TenantPaths } from "./types.js";
5
- export type ResetScope = "sessions" | "sandboxes" | "all";
5
+ export type { ResetScope } from "../store/types.js";
6
6
  export interface ResetTenantContext {
7
- store: Store;
7
+ store: SessionStore;
8
8
  registry: ExecutorRegistry;
9
9
  sandbox: SandboxManager;
10
10
  paths: TenantPaths;
11
- /** Clear in-memory session observers after DB wipe. */
11
+ /** Clear in-memory session observers after the store wipe. */
12
12
  clearSessionState: () => void;
13
13
  /** Clear executor SSE streams when registrations are removed. */
14
14
  clearExecutorStreams: () => void;
15
15
  }
16
16
  /**
17
17
  * Reset Tenant durable state for `scope` (A17).
18
- * Caller must have already drained (`activeWork`). Runs SQLite work in one
19
- * transaction, then filesystem rename+delete for sandboxes / log as needed.
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.
20
21
  */
21
22
  export declare function resetTenant(ctx: ResetTenantContext, scope: ResetScope): Promise<void>;
@@ -1,12 +1,5 @@
1
1
  import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { join } from "node:path";
3
- const SESSION_TABLES = [
4
- "sessions",
5
- "commands",
6
- "checkpoints",
7
- "effects",
8
- "actions",
9
- ];
10
3
  /**
11
4
  * Rename `dir` to a sibling, delete the sibling, recreate an empty `dir`.
12
5
  * Failure before rename leaves the original intact; after rename the empty
@@ -35,41 +28,11 @@ function replaceDirectory(dir) {
35
28
  throw error;
36
29
  }
37
30
  }
38
- function clearSessionTables(store) {
39
- for (const table of SESSION_TABLES) {
40
- store.db.prepare(`DELETE FROM ${table}`).run();
41
- }
42
- store.db.prepare("DELETE FROM events").run();
43
- }
44
- function clearSandboxRows(store) {
45
- store.db.prepare("DELETE FROM sandboxes").run();
46
- }
47
- function clearDefinitions(store) {
48
- store.db.prepare("DELETE FROM definitions").run();
49
- }
50
- function clearExecutors(store, registry) {
51
- for (const row of registry.list())
52
- registry.remove(row.agentId);
53
- store.db.prepare("DELETE FROM executors").run();
54
- }
55
- /** Delete user-scoped vaults; keep the host vault (model credentials). */
56
- function clearUserVaults(store) {
57
- const userVaults = store.db
58
- .prepare(`SELECT id FROM vaults WHERE scope != 'host'`)
59
- .all();
60
- for (const vault of userVaults) {
61
- store.db
62
- .prepare(`DELETE FROM vault_credentials WHERE vault_id=?`)
63
- .run(vault.id);
64
- store.db.prepare(`DELETE FROM vaults WHERE id=?`).run(vault.id);
65
- }
66
- // Drop audit / idempotency rows that referenced user vaults only when all.
67
- // Keep host-model idempotency so seed replay stays stable.
68
- }
69
31
  /**
70
32
  * Reset Tenant durable state for `scope` (A17).
71
- * Caller must have already drained (`activeWork`). Runs SQLite work in one
72
- * transaction, then filesystem rename+delete for sandboxes / log as needed.
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.
73
36
  */
74
37
  export async function resetTenant(ctx, scope) {
75
38
  const clearSessions = scope === "sessions" || scope === "all";
@@ -78,17 +41,10 @@ export async function resetTenant(ctx, scope) {
78
41
  if (clearSandboxes) {
79
42
  await ctx.sandbox.reconcile(() => false);
80
43
  }
81
- ctx.store.tx(() => {
82
- if (clearSessions)
83
- clearSessionTables(ctx.store);
84
- if (clearSandboxes)
85
- clearSandboxRows(ctx.store);
86
- if (clearAll) {
87
- clearDefinitions(ctx.store);
88
- clearExecutors(ctx.store, ctx.registry);
89
- clearUserVaults(ctx.store);
90
- }
91
- });
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);
92
48
  if (clearSessions)
93
49
  ctx.clearSessionState();
94
50
  if (clearAll)
@@ -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
+ }