@nylorun/runtime 0.9.0-beta → 0.11.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +110 -37
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/media.d.ts +2 -2
  6. package/dist/adapters/media.js +2 -2
  7. package/dist/adapters/observe.d.ts +2 -1
  8. package/dist/adapters/observe.js +2 -3
  9. package/dist/adapters/sandbox/virtual.js +1 -1
  10. package/dist/adapters/streams/s2.d.ts +18 -0
  11. package/dist/adapters/streams/s2.js +361 -0
  12. package/dist/assets.d.ts +2 -2
  13. package/dist/assets.js +5 -3
  14. package/dist/core/executors.d.ts +15 -3
  15. package/dist/core/executors.js +40 -5
  16. package/dist/core/flow-host.d.ts +225 -0
  17. package/dist/core/flow-host.js +518 -0
  18. package/dist/core/limits.d.ts +40 -0
  19. package/dist/core/limits.js +76 -0
  20. package/dist/core/runtime.d.ts +5 -90
  21. package/dist/core/runtime.js +3 -1447
  22. package/dist/core/sandbox-routes.d.ts +40 -0
  23. package/dist/core/sandbox-routes.js +106 -0
  24. package/dist/core/turn-manifest.d.ts +50 -0
  25. package/dist/core/turn-manifest.js +66 -0
  26. package/dist/execution/memory.d.ts +54 -0
  27. package/dist/execution/memory.js +217 -0
  28. package/dist/execution/types.d.ts +125 -0
  29. package/dist/execution/types.js +52 -0
  30. package/dist/host/config-for.d.ts +17 -0
  31. package/dist/host/config-for.js +27 -0
  32. package/dist/host/config.d.ts +16 -0
  33. package/dist/host/create-host.d.ts +56 -0
  34. package/dist/host/create-host.js +420 -0
  35. package/dist/host/environment.d.ts +13 -0
  36. package/dist/host/environment.js +53 -0
  37. package/dist/host/execution.d.ts +61 -0
  38. package/dist/host/execution.js +69 -0
  39. package/dist/host/http.d.ts +39 -0
  40. package/dist/host/http.js +142 -0
  41. package/dist/host/index.d.ts +8 -0
  42. package/dist/host/index.js +7 -0
  43. package/dist/host/logger.d.ts +3 -0
  44. package/dist/host/logger.js +18 -0
  45. package/dist/host/main.d.ts +3 -0
  46. package/dist/host/main.js +201 -0
  47. package/dist/host/stack-config.d.ts +72 -0
  48. package/dist/host/stack-config.js +193 -0
  49. package/dist/index.d.ts +7 -2
  50. package/dist/index.js +4 -2
  51. package/dist/infra/database.d.ts +16 -0
  52. package/dist/infra/database.js +31 -0
  53. package/dist/infra/execution.d.ts +39 -0
  54. package/dist/infra/execution.js +68 -0
  55. package/dist/infra/index.d.ts +42 -0
  56. package/dist/infra/index.js +56 -0
  57. package/dist/infra/readiness.d.ts +33 -0
  58. package/dist/infra/readiness.js +53 -0
  59. package/dist/infra/streams.d.ts +17 -0
  60. package/dist/infra/streams.js +33 -0
  61. package/dist/infra/workers.d.ts +39 -0
  62. package/dist/infra/workers.js +31 -0
  63. package/dist/mcp/connect.d.ts +1 -0
  64. package/dist/mcp/connect.js +2 -0
  65. package/dist/mcp/pool.d.ts +3 -1
  66. package/dist/mcp/pool.js +2 -1
  67. package/dist/mcp/stdio.d.ts +2 -0
  68. package/dist/mcp/stdio.js +3 -1
  69. package/dist/model/auth-store.d.ts +1 -1
  70. package/dist/model/auth-store.js +2 -2
  71. package/dist/model/models.d.ts +4 -0
  72. package/dist/model/models.js +22 -26
  73. package/dist/model/pi-model.d.ts +2 -2
  74. package/dist/model/pi-model.js +6 -6
  75. package/dist/model/settings.d.ts +5 -1
  76. package/dist/model/settings.js +6 -2
  77. package/dist/sandbox/manager.d.ts +26 -8
  78. package/dist/sandbox/manager.js +95 -47
  79. package/dist/sandbox/select.d.ts +5 -4
  80. package/dist/sandbox/select.js +18 -13
  81. package/dist/sandbox/share.d.ts +23 -0
  82. package/dist/sandbox/share.js +58 -0
  83. package/dist/sandbox/types.d.ts +2 -2
  84. package/dist/store/canonical.d.ts +5 -0
  85. package/dist/store/canonical.js +17 -0
  86. package/dist/store/cursor.d.ts +9 -0
  87. package/dist/store/cursor.js +18 -0
  88. package/dist/store/memory.d.ts +57 -0
  89. package/dist/store/memory.js +643 -0
  90. package/dist/store/ownership.d.ts +19 -0
  91. package/dist/store/ownership.js +33 -0
  92. package/dist/store/postgres/connect.d.ts +25 -0
  93. package/dist/store/postgres/connect.js +25 -0
  94. package/dist/store/postgres/locking.d.ts +16 -0
  95. package/dist/store/postgres/locking.js +22 -0
  96. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  97. package/dist/store/postgres/migrations/001_initial.js +196 -0
  98. package/dist/store/postgres/migrations/index.d.ts +49 -0
  99. package/dist/store/postgres/migrations/index.js +75 -0
  100. package/dist/store/postgres/names.d.ts +11 -0
  101. package/dist/store/postgres/names.js +42 -0
  102. package/dist/store/postgres/store.d.ts +11 -0
  103. package/dist/store/postgres/store.js +790 -0
  104. package/dist/store/postgres/tenants.d.ts +90 -0
  105. package/dist/store/postgres/tenants.js +207 -0
  106. package/dist/store/types.d.ts +440 -0
  107. package/dist/store/types.js +10 -0
  108. package/dist/streams/basin.d.ts +23 -0
  109. package/dist/streams/basin.js +51 -0
  110. package/dist/streams/memory.d.ts +25 -0
  111. package/dist/streams/memory.js +124 -0
  112. package/dist/streams/relay.d.ts +37 -0
  113. package/dist/streams/relay.js +254 -0
  114. package/dist/streams/types.d.ts +115 -0
  115. package/dist/streams/types.js +86 -0
  116. package/dist/tenant/actions.d.ts +153 -0
  117. package/dist/tenant/actions.js +191 -0
  118. package/dist/tenant/advance.d.ts +15 -0
  119. package/dist/tenant/advance.js +439 -0
  120. package/dist/tenant/auth.d.ts +11 -0
  121. package/dist/tenant/auth.js +34 -0
  122. package/dist/tenant/commands.d.ts +3 -0
  123. package/dist/tenant/commands.js +384 -0
  124. package/dist/tenant/context.d.ts +140 -0
  125. package/dist/tenant/context.js +40 -0
  126. package/dist/tenant/effects.d.ts +20 -0
  127. package/dist/tenant/effects.js +539 -0
  128. package/dist/tenant/envelope.d.ts +7 -0
  129. package/dist/tenant/envelope.js +22 -0
  130. package/dist/tenant/ephemeral.d.ts +42 -0
  131. package/dist/tenant/ephemeral.js +224 -0
  132. package/dist/tenant/http.d.ts +32 -0
  133. package/dist/tenant/http.js +51 -0
  134. package/dist/tenant/live.d.ts +92 -0
  135. package/dist/tenant/live.js +290 -0
  136. package/dist/tenant/logger.d.ts +9 -0
  137. package/dist/tenant/logger.js +25 -0
  138. package/dist/tenant/model-setting.d.ts +16 -0
  139. package/dist/tenant/model-setting.js +12 -0
  140. package/dist/tenant/module.d.ts +38 -0
  141. package/dist/tenant/module.js +319 -0
  142. package/dist/tenant/paths.d.ts +28 -0
  143. package/dist/tenant/paths.js +91 -0
  144. package/dist/tenant/pool.d.ts +10 -0
  145. package/dist/tenant/pool.js +54 -0
  146. package/dist/tenant/principals.d.ts +13 -0
  147. package/dist/tenant/principals.js +37 -0
  148. package/dist/tenant/quarantine-error.d.ts +6 -0
  149. package/dist/tenant/quarantine-error.js +9 -0
  150. package/dist/tenant/quarantine.d.ts +27 -0
  151. package/dist/tenant/quarantine.js +72 -0
  152. package/dist/tenant/reset.d.ts +22 -0
  153. package/dist/tenant/reset.js +81 -0
  154. package/dist/tenant/routes-tenant.d.ts +12 -0
  155. package/dist/tenant/routes-tenant.js +126 -0
  156. package/dist/tenant/routes.d.ts +9 -0
  157. package/dist/tenant/routes.js +142 -0
  158. package/dist/tenant/runtime.d.ts +81 -0
  159. package/dist/tenant/runtime.js +298 -0
  160. package/dist/tenant/scheduler.d.ts +25 -0
  161. package/dist/tenant/scheduler.js +84 -0
  162. package/dist/tenant/session.d.ts +60 -0
  163. package/dist/tenant/session.js +94 -0
  164. package/dist/tenant/sessions.d.ts +34 -0
  165. package/dist/tenant/sessions.js +149 -0
  166. package/dist/tenant/status.d.ts +37 -0
  167. package/dist/tenant/status.js +130 -0
  168. package/dist/tenant/store-memory.d.ts +16 -0
  169. package/dist/tenant/store-memory.js +111 -0
  170. package/dist/tenant/store-pg.d.ts +15 -0
  171. package/dist/tenant/store-pg.js +140 -0
  172. package/dist/tenant/streams.d.ts +126 -0
  173. package/dist/tenant/streams.js +419 -0
  174. package/dist/tenant/sweep.d.ts +15 -0
  175. package/dist/tenant/sweep.js +122 -0
  176. package/dist/tenant/types.d.ts +205 -0
  177. package/dist/tenant/types.js +23 -0
  178. package/dist/tenant/worker.d.ts +84 -0
  179. package/dist/tenant/worker.js +189 -0
  180. package/dist/vault/kek.d.ts +5 -2
  181. package/dist/vault/kek.js +7 -10
  182. package/dist/vault/service.d.ts +37 -31
  183. package/dist/vault/service.js +439 -359
  184. package/dist/version.d.ts +1 -1
  185. package/dist/version.js +1 -1
  186. package/package.json +14 -11
  187. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  188. package/dist/adapters/sandbox/microsandbox.js +0 -238
  189. package/dist/core/main.js +0 -35
  190. package/dist/core/store.d.ts +0 -30
  191. package/dist/core/store.js +0 -202
  192. /package/dist/{core/main.d.ts → host/config.js} +0 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Session and definition resources: `PUT/GET /v1/agents`, `PUT /v1/sessions/:id` (create or
3
+ * re-attach vaults), the session list and the session view. Reads use the store's typed
4
+ * queries; `PUT /v1/sessions/:id` runs in one transaction with the vault attachment checks.
5
+ */
6
+ import { DefinitionDocumentSchema, } from "@nylorun/core/contracts";
7
+ import { hashManifest } from "@nylorun/core/compatibility";
8
+ import { aggregateWaits, isWorkflowManifest } from "../core/flow-host.js";
9
+ import { canonical } from "../store/canonical.js";
10
+ import { newStreamIncarnation } from "../streams/types.js";
11
+ import { validateSandboxAttach } from "../core/sandbox-routes.js";
12
+ import { sandboxLookup } from "./context.js";
13
+ import { fail } from "./http.js";
14
+ /** A session's creation identity: everything but the request id and vault attachments. */
15
+ const sessionIdentity = (value) => {
16
+ const { requestId: _requestId, vaultIds: _vaultIds, credentialSelections: _selections, ...body } = value ?? {};
17
+ return canonical(body);
18
+ };
19
+ export async function listDefinitions(ctx) {
20
+ const definitions = await ctx.store.tx((t) => t.listDefinitions());
21
+ return {
22
+ agents: definitions.map((d) => ({
23
+ agentId: d.manifest.id,
24
+ manifest: d.manifest,
25
+ manifestHash: d.manifestHash,
26
+ implementationVersion: d.implementationVersion,
27
+ })),
28
+ };
29
+ }
30
+ export async function putDefinition(ctx, agentId, body) {
31
+ if (body.manifest.id !== agentId)
32
+ fail(400, "Agent id mismatch");
33
+ DefinitionDocumentSchema.parse(body.manifest);
34
+ const definition = {
35
+ ...body,
36
+ manifestHash: hashManifest(body.manifest),
37
+ };
38
+ await ctx.store.tx((t) => t.put("definitions", agentId, definition));
39
+ return {
40
+ agentId,
41
+ manifestHash: definition.manifestHash,
42
+ implementationVersion: body.implementationVersion,
43
+ };
44
+ }
45
+ export async function listSessions(ctx, agentId) {
46
+ const sessions = await ctx.store.tx((t) => t.listSessions(agentId === null ? {} : { agentId }));
47
+ return {
48
+ sessions: sessions.map((s) => ({
49
+ id: s.id,
50
+ agentId: s.agentId,
51
+ ownerUserId: s.ownerUserId,
52
+ status: s.status,
53
+ activeTurnId: s.activeTurnId,
54
+ })),
55
+ };
56
+ }
57
+ /** Create a session from its definition, or re-attach vaults to an identical one. */
58
+ export function putSession(ctx, id, body) {
59
+ const vaultIds = body.vaultIds ?? [];
60
+ const credentialSelections = body.credentialSelections ?? [];
61
+ return ctx.store.tx(async (t) => {
62
+ const prior = await t.lockSession(id);
63
+ const definition = prior === undefined || body.sandbox
64
+ ? (await t.get("definitions", body.agentId)) ??
65
+ fail(404, "Definition not found")
66
+ : undefined;
67
+ const sandboxOwnerId = body.sandbox
68
+ ? validateSandboxAttach(body, (definition ?? prior).manifest, await sandboxLookup(t, body.sandbox.session))
69
+ : undefined;
70
+ await ctx.vault.assertAttachment(t, body.ownerUserId, vaultIds, credentialSelections);
71
+ if (prior) {
72
+ if (sessionIdentity(prior.creation) !== sessionIdentity(body))
73
+ fail(409, "Session already exists with different creation parameters");
74
+ prior.vaultIds = vaultIds;
75
+ prior.credentialSelections = credentialSelections;
76
+ prior.creation = body;
77
+ if (sandboxOwnerId !== undefined)
78
+ prior.sandboxOwnerId = sandboxOwnerId;
79
+ await t.put("sessions", id, prior);
80
+ await ctx.vault.recordAttachment(t, id, vaultIds);
81
+ return prior;
82
+ }
83
+ const created = {
84
+ id,
85
+ agentId: body.agentId,
86
+ ownerUserId: body.ownerUserId,
87
+ manifest: definition.manifest,
88
+ manifestHash: definition.manifestHash,
89
+ implementationVersion: definition.implementationVersion,
90
+ info: body.info,
91
+ status: "idle",
92
+ activeTurnId: null,
93
+ creation: body,
94
+ vaultIds,
95
+ credentialSelections,
96
+ pluginRoots: definition.pluginRoots ?? {},
97
+ ...(sandboxOwnerId !== undefined ? { sandboxOwnerId } : {}),
98
+ streamIncarnation: newStreamIncarnation(),
99
+ };
100
+ await t.put("sessions", id, created);
101
+ await ctx.vault.recordAttachment(t, id, vaultIds);
102
+ return created;
103
+ });
104
+ }
105
+ /** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
106
+ export async function sessionView(t, s) {
107
+ let waits = Array.isArray(s.waits)
108
+ ? s.waits.map((call) => ({
109
+ invocationId: call.invocationId,
110
+ interaction: call.interaction,
111
+ wait: call.wait,
112
+ status: call.status,
113
+ }))
114
+ : s.waits;
115
+ if (isWorkflowManifest(s.manifest)) {
116
+ const aggregated = await aggregateWaits({ t, workflowSessionId: s.id });
117
+ if (aggregated.length > 0)
118
+ waits = aggregated;
119
+ }
120
+ const actions = await t.actionsForSession(s.id, {
121
+ statuses: ["pending", "claimed", "uncertain"],
122
+ });
123
+ const uncertain = await t.effectsForSession(s.id, {
124
+ statuses: ["uncertain"],
125
+ });
126
+ return {
127
+ id: s.id,
128
+ agentId: s.agentId,
129
+ ownerUserId: s.ownerUserId,
130
+ manifestHash: s.manifestHash,
131
+ implementationVersion: s.implementationVersion,
132
+ status: s.status,
133
+ activeTurnId: s.activeTurnId,
134
+ vaultIds: s.vaultIds ?? [],
135
+ credentialSelections: s.credentialSelections ?? [],
136
+ sandboxOwnerId: s.sandboxOwnerId ?? null,
137
+ mcpSnapshot: s.mcpSnapshot ?? null,
138
+ mcpDiagnostics: s.mcpDiagnostics ?? [],
139
+ waits,
140
+ error: s.error,
141
+ actions,
142
+ uncertainEffects: uncertain.map((e) => ({
143
+ effectId: e.request.effectId,
144
+ turnId: e.request.turnId,
145
+ kind: e.request.kind,
146
+ error: e.error,
147
+ })),
148
+ };
149
+ }
@@ -0,0 +1,37 @@
1
+ import type { SeedTenantConfigRequest, SeedTenantConfigResponse, TenantStatus } from "@nylorun/core/contracts";
2
+ import type { SessionStore } from "../store/types.js";
3
+ import type { ExecutorRegistry } from "../core/executors.js";
4
+ import type { VaultService } from "../vault/service.js";
5
+ import type { SandboxManager } from "../sandbox/manager.js";
6
+ import type { TenantConfig } from "./types.js";
7
+ import type { TenantEnvelope } from "@nylorun/core/contracts";
8
+ import type { StuckInvocation } from "../execution/types.js";
9
+ import type { StreamsStatus } from "./streams.js";
10
+ export interface TenantStatusContext {
11
+ envelope: TenantEnvelope;
12
+ config: TenantConfig;
13
+ store: SessionStore;
14
+ registry: ExecutorRegistry;
15
+ vault: VaultService;
16
+ sandbox: SandboxManager;
17
+ closing: boolean;
18
+ modelConfigured: boolean;
19
+ executorStreams: Map<string, Set<unknown>>;
20
+ /**
21
+ * This Tenant's execution invocations that need an operator (`TenantExecution`). Absent
22
+ * when the execution cannot report them.
23
+ */
24
+ stuckInvocations?: () => Promise<StuckInvocation[]>;
25
+ /** This Tenant's Durable Streams status (`streamsStatus` in `streams.ts`). */
26
+ streamsStatus?: () => Promise<StreamsStatus>;
27
+ }
28
+ /** Build `TenantStatusSchema` with secrets redacted (A14). */
29
+ export declare function buildTenantStatus(ctx: TenantStatusContext): Promise<TenantStatus>;
30
+ /**
31
+ * Insert-if-absent Tenant configuration seed (A18).
32
+ * Response lists field names only; never secret values.
33
+ */
34
+ export declare function seedTenantConfig(ctx: {
35
+ store: SessionStore;
36
+ vault: VaultService;
37
+ }, body: SeedTenantConfigRequest): Promise<SeedTenantConfigResponse>;
@@ -0,0 +1,130 @@
1
+ import { FIXTURE_MODEL_SETTING, seedFixtureModel } from "./model-setting.js";
2
+ /** How long status waits for the execution to list stuck invocations. */
3
+ const STUCK_TIMEOUT_MS = 5000;
4
+ /** `TenantStatus.execution`: paused and backing-off invocations, or why they are unknown. */
5
+ async function executionStatus(stuckInvocations) {
6
+ let timer;
7
+ try {
8
+ const stuck = await Promise.race([
9
+ stuckInvocations(),
10
+ new Promise((_, reject) => {
11
+ timer = setTimeout(() => reject(new Error("Timed out listing stuck invocations")), STUCK_TIMEOUT_MS);
12
+ }),
13
+ ]);
14
+ return {
15
+ stuckInvocations: stuck.map((invocation) => ({
16
+ id: invocation.id,
17
+ status: invocation.status,
18
+ service: invocation.service,
19
+ handler: invocation.handler,
20
+ key: invocation.key,
21
+ retryCount: invocation.retryCount,
22
+ ...(invocation.lastFailure !== undefined
23
+ ? { lastFailure: invocation.lastFailure }
24
+ : {}),
25
+ ...(invocation.modifiedAt !== undefined
26
+ ? { modifiedAt: invocation.modifiedAt }
27
+ : {}),
28
+ })),
29
+ };
30
+ }
31
+ catch (error) {
32
+ return {
33
+ stuckInvocations: [],
34
+ error: error instanceof Error ? error.message : String(error),
35
+ };
36
+ }
37
+ finally {
38
+ clearTimeout(timer);
39
+ }
40
+ }
41
+ /** Build `TenantStatusSchema` with secrets redacted (A14). */
42
+ export async function buildTenantStatus(ctx) {
43
+ const health = await ctx.store.health();
44
+ const { counts, definitions } = await ctx.store.tx(async (t) => ({
45
+ counts: await t.counts(),
46
+ definitions: await t.listDefinitions(),
47
+ }));
48
+ const [modelView, sandboxReport, execution, streams] = await Promise.all([
49
+ ctx.vault.getHostModel(),
50
+ ctx.sandbox.report(),
51
+ ctx.stuckInvocations ? executionStatus(ctx.stuckInvocations) : undefined,
52
+ ctx.streamsStatus?.(),
53
+ ]);
54
+ const definitionIds = new Set(definitions
55
+ .map((d) => d.manifest?.id)
56
+ .filter((id) => typeof id === "string"));
57
+ const executorIds = new Set(ctx.registry.list().map((e) => e.agentId));
58
+ const agentIds = new Set([...definitionIds, ...executorIds]);
59
+ const agents = [...agentIds].sort().map((agentId) => {
60
+ const record = ctx.registry.get(agentId);
61
+ return {
62
+ agentId,
63
+ registered: definitionIds.has(agentId),
64
+ connected: !!record &&
65
+ (ctx.executorStreams.get(record.tokenHash)?.size ?? 0) > 0,
66
+ };
67
+ });
68
+ return {
69
+ tenant: ctx.envelope,
70
+ path: ctx.config.paths.root,
71
+ checks: {
72
+ store: health.schemaVersion > 0,
73
+ scheduler: !ctx.closing,
74
+ model: ctx.modelConfigured || modelView.configured,
75
+ executors: true,
76
+ schema: health.ok,
77
+ },
78
+ model: modelView,
79
+ agents,
80
+ counts: {
81
+ sessions: counts.sessions,
82
+ runningSessions: counts.runningSessions,
83
+ pendingActions: counts.pendingActions,
84
+ uncertainEffects: counts.uncertainEffects,
85
+ },
86
+ sandbox: {
87
+ backend: sandboxReport.backend,
88
+ retained: counts.sandboxes,
89
+ },
90
+ ...(execution ? { execution } : {}),
91
+ ...(streams ? { streams } : {}),
92
+ };
93
+ }
94
+ /**
95
+ * Insert-if-absent Tenant configuration seed (A18).
96
+ * Response lists field names only; never secret values.
97
+ */
98
+ export async function seedTenantConfig(ctx, body) {
99
+ const applied = [];
100
+ const kept = [];
101
+ const backend = body.sandbox?.backend;
102
+ if (backend) {
103
+ const inserted = await ctx.store.tx(async (t) => {
104
+ if ((await t.getSetting("sandbox.backend")) !== undefined)
105
+ return false;
106
+ await t.putSetting("sandbox.backend", backend);
107
+ return true;
108
+ });
109
+ (inserted ? applied : kept).push("sandbox.backend");
110
+ }
111
+ if (body.fixtureModel) {
112
+ const inserted = await ctx.store.tx((t) => seedFixtureModel(t));
113
+ (inserted ? applied : kept).push(FIXTURE_MODEL_SETTING);
114
+ }
115
+ if (body.model) {
116
+ const current = await ctx.vault.getHostModel();
117
+ if (current.configured) {
118
+ kept.push("model");
119
+ }
120
+ else {
121
+ await ctx.vault.putHostModel({
122
+ requestId: body.requestId,
123
+ idempotencyKey: `seed-model:${body.requestId}`,
124
+ ...body.model,
125
+ });
126
+ applied.push("model");
127
+ }
128
+ }
129
+ return { applied, kept };
130
+ }
@@ -0,0 +1,16 @@
1
+ import type { SessionStore } from "../store/types.js";
2
+ import { type Logger, type OpenTenantRuntime, type TenantConfig, type TenantStore } from "./types.js";
3
+ export interface MemoryTenantStoreOptions {
4
+ /** When set, each Tenant's directory under this Host root lives as long as the Tenant. */
5
+ hostRoot?: string;
6
+ openRuntime: OpenTenantRuntime;
7
+ configFor: (tenantId: string) => TenantConfig;
8
+ logger?: Logger;
9
+ }
10
+ export type MemoryTenantStore = TenantStore & {
11
+ /** Test helper: current Tenant ids. */
12
+ ids(): string[];
13
+ /** Test helper: a Session Store on a Tenant's data, as another process would open it. */
14
+ sessionStore(id: string): SessionStore;
15
+ };
16
+ export declare function createMemoryTenantStore(options: MemoryTenantStoreOptions): MemoryTenantStore;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * An in-memory Tenant store for tests: each Tenant is a `MemoryStoreData` (its Session
3
+ * Store's data) and an envelope. It behaves like the Postgres store (`store-pg.ts`): the
4
+ * Tenant directory under the Host root, when one is given, is created with the Tenant and
5
+ * removed with it, and `open` hands the Tenant Runtime a fresh Session Store on the Tenant's
6
+ * data. Not a supported profile.
7
+ */
8
+ import { mkdirSync, rmSync } from "node:fs";
9
+ import { isTenantId } from "@nylorun/core/compatibility";
10
+ import { MEMORY_SCHEMA_VERSION, MemorySessionStore, MemoryStoreData, } from "../store/memory.js";
11
+ import { quarantine } from "./quarantine.js";
12
+ import { parseEnvelope } from "./envelope.js";
13
+ import { tenantPaths } from "./paths.js";
14
+ import { bootstrapPrincipal, bootstrapPrincipalMatches } from "./principals.js";
15
+ import { TenantNotFoundError, } from "./types.js";
16
+ export function createMemoryTenantStore(options) {
17
+ const records = new Map();
18
+ function sessionStore(id, data) {
19
+ return new MemorySessionStore({
20
+ tenantId: id,
21
+ onError: (error) => options.logger?.error("post-commit step failed", {
22
+ message: error instanceof Error ? error.message : String(error),
23
+ }),
24
+ }, data);
25
+ }
26
+ function makeDirectory(id) {
27
+ if (!options.hostRoot)
28
+ return;
29
+ const p = tenantPaths(options.hostRoot, id);
30
+ mkdirSync(p.root, { recursive: true, mode: 0o700 });
31
+ for (const dir of [p.home, p.tmp, p.sandboxes, p.pluginData, p.logs])
32
+ mkdirSync(dir, { recursive: true });
33
+ }
34
+ function removeDirectory(id) {
35
+ if (options.hostRoot)
36
+ rmSync(tenantPaths(options.hostRoot, id).root, {
37
+ recursive: true,
38
+ force: true,
39
+ });
40
+ }
41
+ async function withStore(id, data, fn) {
42
+ const store = sessionStore(id, data);
43
+ try {
44
+ return await fn(store);
45
+ }
46
+ finally {
47
+ await store.close();
48
+ }
49
+ }
50
+ return {
51
+ ids() {
52
+ return [...records.keys()];
53
+ },
54
+ sessionStore(id) {
55
+ const record = records.get(id);
56
+ if (!record)
57
+ throw new TenantNotFoundError(id);
58
+ return sessionStore(id, record.data);
59
+ },
60
+ async enumerate() {
61
+ return [...records.keys()].filter(isTenantId).sort();
62
+ },
63
+ async readEnvelope(id) {
64
+ const record = records.get(id);
65
+ if (!record)
66
+ throw new TenantNotFoundError(id);
67
+ return parseEnvelope(record.envelope, id);
68
+ },
69
+ async create(envelope, bootstrap) {
70
+ const parsed = parseEnvelope({ ...envelope, schemaVersion: MEMORY_SCHEMA_VERSION }, envelope.id);
71
+ if (records.has(parsed.id))
72
+ return "exists";
73
+ makeDirectory(parsed.id);
74
+ const data = new MemoryStoreData();
75
+ await withStore(parsed.id, data, (store) => store.tx((t) => bootstrapPrincipal(t, bootstrap)));
76
+ records.set(parsed.id, { envelope: parsed, data });
77
+ return "created";
78
+ },
79
+ async bootstrapMatches(id, bootstrap) {
80
+ const record = records.get(id);
81
+ if (!record)
82
+ return false;
83
+ return withStore(id, record.data, (store) => store.tx((t) => bootstrapPrincipalMatches(t, bootstrap)));
84
+ },
85
+ async open(id) {
86
+ const record = records.get(id);
87
+ if (!record)
88
+ throw new TenantNotFoundError(id);
89
+ if (record.envelope.schemaVersion > MEMORY_SCHEMA_VERSION)
90
+ throw quarantine("schema-too-new", `Tenant schema version ${record.envelope.schemaVersion} is newer than this Runtime's ${MEMORY_SCHEMA_VERSION}`, { tenantId: id });
91
+ makeDirectory(id);
92
+ const store = sessionStore(id, record.data);
93
+ try {
94
+ return await options.openRuntime({ ...options.configFor(id), tenantId: id }, { store, envelope: record.envelope });
95
+ }
96
+ catch (error) {
97
+ await store.close().catch(() => undefined);
98
+ throw error;
99
+ }
100
+ },
101
+ async trash(id) {
102
+ records.delete(id);
103
+ removeDirectory(id);
104
+ },
105
+ async removePartial(id) {
106
+ if (records.has(id))
107
+ return;
108
+ removeDirectory(id);
109
+ },
110
+ };
111
+ }
@@ -0,0 +1,15 @@
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 {
5
+ hostRoot: string;
6
+ /** The Host's pool. The store never ends it. */
7
+ sql: PostgresClient;
8
+ /** Opens the Tenant Runtime on a store the catalog opened; the Runtime then owns it. */
9
+ openRuntime: OpenTenantRuntime;
10
+ configFor: (tenantId: string) => TenantConfig;
11
+ logger?: Logger;
12
+ /** Tests only: the migrations this Runtime knows. */
13
+ migrations?: readonly Migration[];
14
+ }
15
+ export declare function createPostgresTenantStore(options: PostgresTenantStoreOptions): TenantStore;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The Tenant store over the Postgres Tenant catalog (architecture §8.1, §8.2).
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.
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.
19
+ */
20
+ import { mkdirSync, rmSync } from "node:fs";
21
+ import { tenantSchemaName } from "../store/postgres/names.js";
22
+ import { createPostgresTenantCatalog } from "../store/postgres/tenants.js";
23
+ 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) {
44
+ return cause instanceof TenantUnavailableError
45
+ ? cause
46
+ : new TenantUnavailableError(tenantId, { cause });
47
+ }
48
+ function quarantined(reason, tenantId) {
49
+ return new QuarantineError({ ...reason, repair: repairFor(reason.code, tenantId) });
50
+ }
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) {
67
+ try {
68
+ return await catalog.readEnvelope(id);
69
+ }
70
+ 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";
92
+ throw error;
93
+ }
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
+ },
139
+ };
140
+ }