@nylorun/runtime 0.17.0-beta → 0.19.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 (223) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +88 -58
  3. package/dist/adapters/execution/restate.d.ts +1 -3
  4. package/dist/adapters/execution/restate.js +4 -31
  5. package/dist/adapters/replication/pgoutput.js +1 -1
  6. package/dist/adapters/sandbox/local.js +1 -1
  7. package/dist/adapters/sandbox/virtual.js +1 -1
  8. package/dist/api/ag-ui/routes.js +1 -1
  9. package/dist/api/ag-ui/translate.d.ts +1 -3
  10. package/dist/api/ag-ui/translate.js +2 -17
  11. package/dist/api/components.d.ts +4871 -2614
  12. package/dist/api/components.js +12 -11
  13. package/dist/api/gate/routes.d.ts +7 -8
  14. package/dist/api/gate/routes.js +58 -52
  15. package/dist/api/http/app.d.ts +6 -0
  16. package/dist/api/http/app.js +18 -14
  17. package/dist/api/http/define.d.ts +14 -2
  18. package/dist/api/http/define.js +34 -15
  19. package/dist/api/http/routes/access.d.ts +3 -3
  20. package/dist/api/http/routes/access.js +18 -17
  21. package/dist/api/http/routes/artifacts.js +2 -4
  22. package/dist/api/http/routes/files.d.ts +8 -0
  23. package/dist/api/http/routes/files.js +77 -0
  24. package/dist/api/http/routes/keys.d.ts +10 -0
  25. package/dist/api/http/routes/keys.js +64 -0
  26. package/dist/api/http/routes/me.js +5 -2
  27. package/dist/api/http/routes/reads.d.ts +66 -0
  28. package/dist/api/http/routes/reads.js +127 -0
  29. package/dist/api/http/routes/sandboxes.js +7 -4
  30. package/dist/api/http/routes/sessions.js +25 -8
  31. package/dist/api/http/routes/tenant.js +15 -18
  32. package/dist/api/http/routes/vaults.d.ts +10 -9
  33. package/dist/api/http/routes/vaults.js +27 -23
  34. package/dist/api/openapi.d.ts +10 -11
  35. package/dist/api/openapi.js +291 -61
  36. package/dist/artifacts/links.js +1 -1
  37. package/dist/core/flow-host.d.ts +44 -23
  38. package/dist/core/flow-host.js +88 -106
  39. package/dist/core/provider.d.ts +4 -1
  40. package/dist/core/provider.js +6 -1
  41. package/dist/core/sandbox-routes.d.ts +0 -9
  42. package/dist/core/sandbox-routes.js +2 -21
  43. package/dist/execution/memory.d.ts +4 -5
  44. package/dist/execution/memory.js +11 -25
  45. package/dist/execution/types.d.ts +3 -33
  46. package/dist/execution/types.js +2 -12
  47. package/dist/gates/http-tool.d.ts +86 -0
  48. package/dist/gates/http-tool.js +197 -0
  49. package/dist/gates/mcp-handler.d.ts +3 -0
  50. package/dist/gates/mcp-handler.js +1 -3
  51. package/dist/gates/meter.d.ts +2 -2
  52. package/dist/gates/meter.js +6 -0
  53. package/dist/gates/tenant-vaults.d.ts +5 -9
  54. package/dist/gates/tool-calls.d.ts +7 -15
  55. package/dist/gates/tool-calls.js +4 -4
  56. package/dist/gates/tool-client.d.ts +1 -1
  57. package/dist/gates/tool-client.js +23 -47
  58. package/dist/gates/tool-contract.d.ts +27 -39
  59. package/dist/gates/tool-contract.js +30 -21
  60. package/dist/gates/tool-gate.d.ts +25 -24
  61. package/dist/gates/tool-gate.js +11 -7
  62. package/dist/harness/calls.d.ts +10 -4
  63. package/dist/harness/calls.js +15 -6
  64. package/dist/harness/executors.d.ts +1 -1
  65. package/dist/harness/executors.js +17 -12
  66. package/dist/harness/main.d.ts +2 -2
  67. package/dist/harness/main.js +5 -13
  68. package/dist/harness/service.d.ts +4 -5
  69. package/dist/harness/service.js +9 -2
  70. package/dist/harness-api/in-process.js +2 -2
  71. package/dist/harness-api/record.d.ts +6 -3
  72. package/dist/harness-api/record.js +65 -72
  73. package/dist/harness-api/server.d.ts +1 -9
  74. package/dist/harness-api/server.js +19 -17
  75. package/dist/harness-api/start.js +1 -5
  76. package/dist/harness-api/workspace.js +2 -3
  77. package/dist/harness-api/ws-server.js +1 -1
  78. package/dist/host/app.d.ts +2 -9
  79. package/dist/host/app.js +15 -25
  80. package/dist/host/config-for.d.ts +1 -5
  81. package/dist/host/config-for.js +0 -3
  82. package/dist/host/config.d.ts +0 -5
  83. package/dist/host/create-host.d.ts +3 -33
  84. package/dist/host/create-host.js +18 -94
  85. package/dist/host/environment.d.ts +0 -3
  86. package/dist/host/environment.js +0 -9
  87. package/dist/host/gates.d.ts +4 -1
  88. package/dist/host/gates.js +3 -2
  89. package/dist/host/http.d.ts +1 -2
  90. package/dist/host/http.js +1 -7
  91. package/dist/host/index.d.ts +2 -2
  92. package/dist/host/index.js +2 -2
  93. package/dist/host/main.js +27 -34
  94. package/dist/host/operate.d.ts +29 -0
  95. package/dist/host/operate.js +223 -0
  96. package/dist/host/stack-config.d.ts +12 -12
  97. package/dist/host/stack-config.js +5 -32
  98. package/dist/index.d.ts +1 -1
  99. package/dist/index.js +1 -1
  100. package/dist/infra/workers.d.ts +1 -1
  101. package/dist/keys/keys.js +2 -2
  102. package/dist/management-openapi.json +6491 -0
  103. package/dist/mcp/connect.d.ts +8 -4
  104. package/dist/mcp/connect.js +6 -26
  105. package/dist/mcp/pool.d.ts +5 -9
  106. package/dist/mcp/pool.js +8 -19
  107. package/dist/mcp/snapshot.d.ts +8 -2
  108. package/dist/mcp/snapshot.js +29 -2
  109. package/dist/media.d.ts +1 -1
  110. package/dist/media.js +1 -1
  111. package/dist/model/pi-model.js +3 -0
  112. package/dist/openapi.json +4700 -12350
  113. package/dist/reads/access.d.ts +4 -0
  114. package/dist/reads/access.js +12 -0
  115. package/dist/reads/cursor.d.ts +11 -0
  116. package/dist/reads/cursor.js +80 -0
  117. package/dist/reads/types.d.ts +25 -0
  118. package/dist/reads/types.js +1 -0
  119. package/dist/sandbox/manager.d.ts +16 -0
  120. package/dist/sandbox/manager.js +40 -0
  121. package/dist/sandbox/session-sandbox.js +13 -3
  122. package/dist/sandbox/skills.d.ts +13 -0
  123. package/dist/sandbox/skills.js +46 -0
  124. package/dist/sandbox/types.d.ts +2 -2
  125. package/dist/store/ownership.d.ts +9 -0
  126. package/dist/store/ownership.js +13 -0
  127. package/dist/store/postgres/connect.d.ts +2 -0
  128. package/dist/store/postgres/connect.js +29 -0
  129. package/dist/store/postgres/drizzle/0011_key_roles.sql +3 -0
  130. package/dist/store/postgres/drizzle/0012_session_reads.sql +8 -0
  131. package/dist/store/postgres/drizzle/0013_definition_files.sql +16 -0
  132. package/dist/store/postgres/drizzle/0014_action_endpoints_removed.sql +2 -0
  133. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  134. package/dist/store/postgres/reads.d.ts +3 -0
  135. package/dist/store/postgres/reads.js +272 -0
  136. package/dist/store/postgres/schema.d.ts +294 -489
  137. package/dist/store/postgres/schema.js +41 -76
  138. package/dist/store/postgres/store.js +51 -154
  139. package/dist/store/postgres/tenant.d.ts +34 -2
  140. package/dist/store/postgres/tenant.js +16 -32
  141. package/dist/store/types.d.ts +27 -96
  142. package/dist/store/types.js +0 -1
  143. package/dist/streams/control.d.ts +0 -2
  144. package/dist/streams/control.js +0 -5
  145. package/dist/streams/types.d.ts +1 -10
  146. package/dist/tenant/advance.d.ts +3 -3
  147. package/dist/tenant/advance.js +10 -7
  148. package/dist/tenant/auth.d.ts +14 -9
  149. package/dist/tenant/auth.js +24 -28
  150. package/dist/tenant/cause.d.ts +3 -3
  151. package/dist/tenant/cause.js +1 -1
  152. package/dist/tenant/commands.d.ts +2 -18
  153. package/dist/tenant/commands.js +35 -172
  154. package/dist/tenant/context.d.ts +14 -24
  155. package/dist/tenant/definition-files.d.ts +24 -0
  156. package/dist/tenant/definition-files.js +103 -0
  157. package/dist/tenant/effects.d.ts +18 -20
  158. package/dist/tenant/effects.js +56 -119
  159. package/dist/tenant/ephemeral.d.ts +7 -6
  160. package/dist/tenant/ephemeral.js +3 -11
  161. package/dist/tenant/host-token.js +1 -1
  162. package/dist/tenant/jwt.js +1 -1
  163. package/dist/tenant/module.d.ts +1 -4
  164. package/dist/tenant/module.js +3 -12
  165. package/dist/tenant/operator-keys.d.ts +34 -8
  166. package/dist/tenant/operator-keys.js +50 -18
  167. package/dist/tenant/outbound.d.ts +10 -1
  168. package/dist/tenant/outbound.js +44 -27
  169. package/dist/tenant/paths.d.ts +1 -1
  170. package/dist/tenant/paths.js +1 -3
  171. package/dist/tenant/principals.d.ts +11 -2
  172. package/dist/tenant/principals.js +27 -8
  173. package/dist/tenant/run-token.js +3 -3
  174. package/dist/tenant/runtime.d.ts +4 -6
  175. package/dist/tenant/runtime.js +19 -18
  176. package/dist/tenant/sandboxes.d.ts +4 -1
  177. package/dist/tenant/sandboxes.js +11 -0
  178. package/dist/tenant/scheduler.d.ts +1 -5
  179. package/dist/tenant/scheduler.js +2 -5
  180. package/dist/tenant/session-streams.d.ts +2 -1
  181. package/dist/tenant/session-streams.js +39 -5
  182. package/dist/tenant/session.d.ts +8 -41
  183. package/dist/tenant/session.js +0 -37
  184. package/dist/tenant/sessions.js +16 -8
  185. package/dist/tenant/signing-keys.d.ts +3 -3
  186. package/dist/tenant/signing-keys.js +5 -5
  187. package/dist/tenant/skill-tool.d.ts +20 -0
  188. package/dist/tenant/skill-tool.js +72 -0
  189. package/dist/tenant/slim.d.ts +1 -1
  190. package/dist/tenant/slim.js +1 -1
  191. package/dist/tenant/status.js +7 -12
  192. package/dist/tenant/store-pg.d.ts +15 -0
  193. package/dist/tenant/store-pg.js +7 -3
  194. package/dist/tenant/streams.d.ts +3 -10
  195. package/dist/tenant/streams.js +1 -24
  196. package/dist/tenant/sweep.js +0 -5
  197. package/dist/tenant/transcript.d.ts +2 -2
  198. package/dist/tenant/transcript.js +2 -2
  199. package/dist/tenant/types.d.ts +15 -22
  200. package/dist/tenant/worker.d.ts +1 -3
  201. package/dist/tenant/worker.js +3 -8
  202. package/dist/vault/sources.d.ts +7 -1
  203. package/dist/vault/sources.js +22 -11
  204. package/dist/version.d.ts +1 -1
  205. package/dist/version.js +1 -1
  206. package/package.json +4 -4
  207. package/dist/admin-openapi.json +0 -904
  208. package/dist/api/http/routes/actions.d.ts +0 -8
  209. package/dist/api/http/routes/actions.js +0 -75
  210. package/dist/api/http/routes/endpoints.d.ts +0 -7
  211. package/dist/api/http/routes/endpoints.js +0 -100
  212. package/dist/host/admin-api.d.ts +0 -30
  213. package/dist/host/admin-api.js +0 -140
  214. package/dist/mcp/stdio.d.ts +0 -17
  215. package/dist/mcp/stdio.js +0 -82
  216. package/dist/tenant/actions.d.ts +0 -14
  217. package/dist/tenant/actions.js +0 -29
  218. package/dist/tenant/delivery-token.d.ts +0 -41
  219. package/dist/tenant/delivery-token.js +0 -104
  220. package/dist/tenant/delivery.d.ts +0 -62
  221. package/dist/tenant/delivery.js +0 -481
  222. package/dist/tenant/endpoints.d.ts +0 -67
  223. package/dist/tenant/endpoints.js +0 -90
@@ -0,0 +1,4 @@
1
+ import type { AuthScope, TenantContext } from "../tenant/context.js";
2
+ import type { ReadAccess } from "./types.js";
3
+ export declare function readAccess(scope: AuthScope): ReadAccess;
4
+ export declare function readStoreOf(ctx: TenantContext): import("./types.js").ReadStore;
@@ -0,0 +1,12 @@
1
+ import { accessOf } from "../tenant/auth.js";
2
+ import { fail } from "../tenant/http.js";
3
+ export function readAccess(scope) {
4
+ const access = accessOf(scope);
5
+ return {
6
+ ...(access ? { owner: access.owner } : {}),
7
+ ...(access?.agents ? { agents: [...access.agents] } : {}),
8
+ };
9
+ }
10
+ export function readStoreOf(ctx) {
11
+ return ctx.reads ?? fail(503, "Session reads are unavailable", { code: "reads_unavailable" });
12
+ }
@@ -0,0 +1,11 @@
1
+ /** Cursors carry position, not authority. Authorization is reapplied at every read. */
2
+ export declare function readCursor(tenant: string, route: string, filters: Record<string, unknown>): {
3
+ encode: (key: (string | null)[]) => string;
4
+ decode(value: string | undefined): (string | null)[] | undefined;
5
+ };
6
+ /** Page cursors also resume SSE. SSE accepts the filter captured by the history page. */
7
+ export declare function historyCursor(tenant: string, sessionId: string, agent?: string): {
8
+ encode: (eventCursor: string) => string;
9
+ decode(value: string | undefined): string | undefined;
10
+ };
11
+ export declare function historyResume(tenant: string, sessionId: string, value: string | undefined): string | undefined;
@@ -0,0 +1,80 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { decodeCursor } from "../record/index.js";
4
+ import { fail } from "../tenant/http.js";
5
+ const schema = z
6
+ .object({
7
+ version: z.literal(1),
8
+ binding: z.string(),
9
+ key: z.array(z.string().nullable()).max(3),
10
+ })
11
+ .strict();
12
+ /** Cursors carry position, not authority. Authorization is reapplied at every read. */
13
+ export function readCursor(tenant, route, filters) {
14
+ const binding = createHash("sha256")
15
+ .update(JSON.stringify([
16
+ tenant,
17
+ route,
18
+ Object.entries(filters).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
19
+ ]))
20
+ .digest("hex");
21
+ return {
22
+ encode: (key) => Buffer.from(JSON.stringify({ version: 1, binding, key })).toString("base64url"),
23
+ decode(value) {
24
+ if (value === undefined)
25
+ return undefined;
26
+ if (value.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(value))
27
+ return fail(400, "Invalid cursor", { code: "cursor_invalid" });
28
+ let parsed;
29
+ try {
30
+ parsed = schema.safeParse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
31
+ }
32
+ catch {
33
+ return fail(400, "Invalid cursor", { code: "cursor_invalid" });
34
+ }
35
+ if (!parsed.success)
36
+ return fail(400, "Invalid cursor", { code: "cursor_invalid" });
37
+ if (parsed.data.binding !== binding)
38
+ return fail(400, "Cursor belongs to another query", { code: "cursor_mismatch" });
39
+ return parsed.data.key;
40
+ },
41
+ };
42
+ }
43
+ /** Page cursors also resume SSE. SSE accepts the filter captured by the history page. */
44
+ export function historyCursor(tenant, sessionId, agent) {
45
+ const codec = readCursor(tenant, "history", { sessionId, agent });
46
+ return {
47
+ encode: (eventCursor) => "h1." + codec.encode([eventCursor, agent ?? null]),
48
+ decode(value) {
49
+ if (value === undefined)
50
+ return undefined;
51
+ if (!value.startsWith("h1."))
52
+ return fail(400, "Invalid history page cursor", { code: "cursor_invalid" });
53
+ const key = codec.decode(value.slice(3));
54
+ if (!key || key.length !== 2 || key[0] === null || key[1] !== (agent ?? null))
55
+ return fail(400, "Invalid history page cursor", { code: "cursor_invalid" });
56
+ try {
57
+ if (!Number.isSafeInteger(decodeCursor(sessionId, key[0])))
58
+ throw new Error("Invalid cursor");
59
+ }
60
+ catch {
61
+ return fail(400, "Invalid history page cursor", { code: "cursor_invalid" });
62
+ }
63
+ return key[0];
64
+ },
65
+ };
66
+ }
67
+ export function historyResume(tenant, sessionId, value) {
68
+ if (!value?.startsWith("h1."))
69
+ return value;
70
+ let parsed;
71
+ try {
72
+ parsed = schema.safeParse(JSON.parse(Buffer.from(value.slice(3), "base64url").toString("utf8")));
73
+ }
74
+ catch {
75
+ return fail(400, "Invalid history cursor", { code: "cursor_invalid" });
76
+ }
77
+ if (!parsed.success || parsed.data.key.length !== 2)
78
+ return fail(400, "Invalid history cursor", { code: "cursor_invalid" });
79
+ return historyCursor(tenant, sessionId, parsed.data.key[1] ?? undefined).decode(value);
80
+ }
@@ -0,0 +1,25 @@
1
+ import type { ModelCallExportPage, ModelCallsPage, SandboxPage, SessionManifestView, SessionPage, SessionListItem, SessionUsageTotals } from "@nylorun/core/contracts";
2
+ export type ReadAccess = {
3
+ owner?: string;
4
+ agents?: readonly string[];
5
+ };
6
+ export type SessionFilters = {
7
+ agentId?: string;
8
+ status?: SessionListItem["status"];
9
+ sandboxId?: string;
10
+ ownerUserId?: string;
11
+ };
12
+ export type PageOptions = {
13
+ limit: number;
14
+ cursor?: string;
15
+ };
16
+ /** Public projections only. No execution-state methods or mutable transactions. */
17
+ export interface ReadStore {
18
+ sessions(filters: SessionFilters, page: PageOptions, access: ReadAccess): Promise<SessionPage>;
19
+ manifest(id: string, access: ReadAccess): Promise<SessionManifestView>;
20
+ usage(id: string, turnId: string | undefined, access: ReadAccess): Promise<SessionUsageTotals>;
21
+ modelCalls(id: string, turnId: string | undefined, page: PageOptions, access: ReadAccess): Promise<ModelCallsPage>;
22
+ exportModel(after: string | undefined, limit: number): Promise<ModelCallExportPage>;
23
+ sandboxes(labels: Record<string, string>, page: PageOptions, access: ReadAccess, grants?: readonly string[]): Promise<SandboxPage>;
24
+ close(): Promise<void>;
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -11,6 +11,10 @@
11
11
  * past their timeout and marks records of compute this process no longer holds as stopped
12
12
  * (after a restart); `reconcile` removes workspaces whose owner is gone. `sweep` does both, the
13
13
  * reconcile once per process, as the Tenant sweep does in core's process.
14
+ *
15
+ * A session's skills (track R2 M4) are mounted on its workspace, read-only under
16
+ * `/skills/<name>/`, before the first call that opens it (`skills.ts`); their bytes come
17
+ * through the `definitionFile` port.
14
18
  */
15
19
  import type { EventPayload } from "@nylorun/core/contracts";
16
20
  import { type AgentManifest, type CapabilityManifest, type SandboxToolName } from "@nylorun/core/define";
@@ -61,6 +65,12 @@ export interface SandboxManagerOptions {
61
65
  readonly preference: string | undefined;
62
66
  /** Delete sandboxes on close (the Runtime's store does not outlive the process). */
63
67
  readonly ephemeral: boolean;
68
+ /**
69
+ * A definition file's bytes by `sha256:<hex>`, for the skills a session's workspace mounts:
70
+ * the Tenant's Object store in core's process, core's answer in a harness (for the run that
71
+ * made the call, `session.claim`). Without it no skills are mounted.
72
+ */
73
+ readonly definitionFile?: (sha256: string, session: SandboxSessionRef) => Promise<Uint8Array>;
64
74
  /** Writes a session event (in core, in its own transaction; in a harness, as a claim). */
65
75
  readonly emit: <T extends SandboxEventType>(sessionId: string, turnId: string | null, type: T, payload: EventPayload<T>, meta: SandboxEventMeta) => void | Promise<void>;
66
76
  }
@@ -135,6 +145,12 @@ export declare class SandboxManager {
135
145
  private prepare;
136
146
  private execute;
137
147
  private executeOpen;
148
+ /**
149
+ * Mounts the session's skills the workspace does not hold as they are: on a handle just opened
150
+ * all of them, later only those another session brought or changed. A failed outcome when
151
+ * one cannot be mounted; nothing is marked mounted then, so the next call tries again.
152
+ */
153
+ private mountSkills;
138
154
  private open;
139
155
  private stop;
140
156
  private record;
@@ -2,6 +2,7 @@ import { SANDBOX_WORKSPACE, } from "@nylorun/core/define";
2
2
  import { sandboxWorkspaceKey, sessionWorkspaceKey, workspacePrefix, } from "./records.js";
3
3
  import { DEFAULT_SANDBOX_CPUS, DEFAULT_SANDBOX_IMAGE, describeNetwork, idleMsOf, memoryMiBOf, resolveNetwork, } from "./policy.js";
4
4
  import { parseSandboxPreference, reportSelection, selectSandboxBackend, } from "./select.js";
5
+ import { mountSkill, sandboxSkillsOf, skillSignature } from "./skills.js";
5
6
  import { resolveSandboxPath, runSandboxTool, } from "./tools.js";
6
7
  import { SandboxFileTooLargeError, } from "./types.js";
7
8
  export { sandboxCapabilityOf } from "./capability.js";
@@ -191,6 +192,9 @@ export class SandboxManager {
191
192
  };
192
193
  }
193
194
  }
195
+ const unmounted = await this.mountSkills(live, live.handle, session, signal);
196
+ if (unmounted)
197
+ return unmounted;
194
198
  return await use(live.handle, live);
195
199
  }
196
200
  finally {
@@ -236,6 +240,7 @@ export class SandboxManager {
236
240
  active: 0,
237
241
  lastUsedAt: Date.now(),
238
242
  tail: Promise.resolve(),
243
+ skills: new Map(),
239
244
  };
240
245
  this.live.set(key, live);
241
246
  }
@@ -272,6 +277,9 @@ export class SandboxManager {
272
277
  };
273
278
  }
274
279
  }
280
+ const unmounted = await this.mountSkills(live, live.handle, session, signal);
281
+ if (unmounted)
282
+ return unmounted;
275
283
  const started = Date.now();
276
284
  let report = {};
277
285
  try {
@@ -300,6 +308,36 @@ export class SandboxManager {
300
308
  };
301
309
  }
302
310
  }
311
+ /**
312
+ * Mounts the session's skills the workspace does not hold as they are: on a handle just opened
313
+ * all of them, later only those another session brought or changed. A failed outcome when
314
+ * one cannot be mounted; nothing is marked mounted then, so the next call tries again.
315
+ */
316
+ async mountSkills(live, handle, session, signal) {
317
+ const read = this.options.definitionFile;
318
+ if (!read)
319
+ return undefined;
320
+ for (const [name, files] of sandboxSkillsOf(session.manifest)) {
321
+ const signature = skillSignature(files);
322
+ if (live.skills.get(name) === signature)
323
+ continue;
324
+ try {
325
+ await mountSkill(handle, name, files, (sha256) => read(sha256, session), signal);
326
+ }
327
+ catch (error) {
328
+ if (signal.aborted)
329
+ throw error;
330
+ live.skills.delete(name);
331
+ return {
332
+ kind: "failed",
333
+ code: "sandbox.skills_unavailable",
334
+ message: `The files of skill '${name}' could not be put in the sandbox: ${error instanceof Error ? error.message : String(error)}`,
335
+ };
336
+ }
337
+ live.skills.set(name, signature);
338
+ }
339
+ return undefined;
340
+ }
303
341
  async open(live, session, spec) {
304
342
  const existing = await this.options.records.get(spec.key);
305
343
  const payload = {
@@ -311,6 +349,8 @@ export class SandboxManager {
311
349
  const creating = await this.record(spec.key, live, session.id, spec.image, "creating", existing);
312
350
  await this.options.emit(session.id, session.activeTurnId, "sandbox.state", { state: "creating", ...payload, ...(existing ? { reattach: true } : {}) }, { record: creating, ...claimOf(session) });
313
351
  const handle = await live.backend.open(spec);
352
+ // A handle opened afresh holds no skills this manager knows of.
353
+ live.skills.clear();
314
354
  const running = await this.record(spec.key, live, session.id, spec.image, "running", existing);
315
355
  await this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
316
356
  state: "running",
@@ -2,7 +2,8 @@
2
2
  * Gives a session the sandbox chosen when it was opened. An agent session's pinned manifest is
3
3
  * its definition plus the `nylorun.sandbox` capability (the six built-in tools, the sandbox
4
4
  * instructions and the resolved spec) and the `nylorun.artifacts` capability (`save_artifact`),
5
- * also added to each agent it uses as a tool, so the tree shares one spec. The definition in the
5
+ * also added to each agent it uses as a tool, so the tree shares one spec. The sandbox's
6
+ * instructions name the agent's skills' folders (`/skills/<name>/`). The definition in the
6
7
  * registry never changes.
7
8
  */
8
9
  import { AgentManifestSchema } from "@nylorun/core/contracts";
@@ -14,6 +15,10 @@ function isAgentManifest(value) {
14
15
  value.kind !== "workflow" &&
15
16
  Array.isArray(value.capabilities));
16
17
  }
18
+ /** The names of the agent's own skills. */
19
+ function skillNamesOf(manifest) {
20
+ return manifest.capabilities.flatMap((capability) => Object.keys(capability.skills ?? {})).sort();
21
+ }
17
22
  function addCapability(manifest, spec) {
18
23
  const capabilities = manifest.capabilities.map((capability) => capability.tools === undefined
19
24
  ? capability
@@ -31,8 +36,13 @@ function addCapability(manifest, spec) {
31
36
  });
32
37
  return {
33
38
  ...manifest,
34
- // With a sandbox comes `save_artifact`, so files the agent makes reach the user (F8.1).
35
- capabilities: [...capabilities, sandboxCapabilityManifest(spec), artifactsCapabilityManifest()],
39
+ // With a sandbox comes `save_artifact`, so files the agent makes reach the user (F8.1). The
40
+ // sandbox's instructions name where the agent's skills are (R2 M4).
41
+ capabilities: [
42
+ ...capabilities,
43
+ sandboxCapabilityManifest(spec, { skills: skillNamesOf(manifest) }),
44
+ artifactsCapabilityManifest(),
45
+ ],
36
46
  };
37
47
  }
38
48
  /** The agent's manifest with the sandbox capability added, or why it cannot take one. */
@@ -0,0 +1,13 @@
1
+ import { type AgentManifest } from "@nylorun/core/define";
2
+ import type { SandboxHandle } from "./types.js";
3
+ /** A skill's files, by path in its folder, as `sha256:<hex>`. */
4
+ export type SkillFiles = Readonly<Record<string, string>>;
5
+ /** The skills of the agent and the agents it uses as tools; the first of a name wins. */
6
+ export declare function sandboxSkillsOf(manifest: AgentManifest | undefined, into?: Map<string, Readonly<Record<string, string>>>): Map<string, SkillFiles>;
7
+ /** What a mounted skill holds, to tell whether a workspace has these files already. */
8
+ export declare function skillSignature(files: SkillFiles): string;
9
+ /**
10
+ * Writes one skill's files to `/skills/<name>/` of the sandbox, replacing what was there, and
11
+ * makes them read-only. `read` gives a file's bytes by `sha256:<hex>`.
12
+ */
13
+ export declare function mountSkill(handle: SandboxHandle, name: string, files: SkillFiles, read: (sha256: string) => Promise<Uint8Array>, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A sandbox's skills (track R2 M4): each skill of the session's agent, and of the agents it uses
3
+ * as tools (they share its sandbox), gets its files under `/skills/<name>/`, read-only, so the
4
+ * model can read them and run their scripts with bash. The SandboxManager mounts them on a
5
+ * workspace before the first call of a session's run that opens it, and again when a session
6
+ * with other skills uses it; the bytes come from the Tenant's definition files.
7
+ */
8
+ import { posix } from "node:path";
9
+ import { SKILLS_MOUNT } from "@nylorun/core/define";
10
+ import { canonical } from "../store/canonical.js";
11
+ import { quote } from "./tools.js";
12
+ /** The skills of the agent and the agents it uses as tools; the first of a name wins. */
13
+ export function sandboxSkillsOf(manifest, into = new Map()) {
14
+ for (const capability of manifest?.capabilities ?? []) {
15
+ for (const skill of Object.values(capability.skills ?? {}))
16
+ if (!into.has(skill.name))
17
+ into.set(skill.name, skill.files);
18
+ for (const tool of capability.tools ?? [])
19
+ if (tool.agent !== undefined && !("kind" in tool.agent))
20
+ sandboxSkillsOf(tool.agent, into);
21
+ }
22
+ return into;
23
+ }
24
+ /** What a mounted skill holds, to tell whether a workspace has these files already. */
25
+ export function skillSignature(files) {
26
+ return canonical(files);
27
+ }
28
+ /**
29
+ * Writes one skill's files to `/skills/<name>/` of the sandbox, replacing what was there, and
30
+ * makes them read-only. `read` gives a file's bytes by `sha256:<hex>`.
31
+ */
32
+ export async function mountSkill(handle, name, files, read, signal) {
33
+ const root = `${SKILLS_MOUNT}/${name}`;
34
+ const exec = async (command) => {
35
+ const result = await handle.exec({ command, cwd: "/", timeoutMs: 60_000 }, signal);
36
+ if (result.exitCode !== 0)
37
+ throw new Error(`${command.slice(0, 120)} failed: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
38
+ };
39
+ const directories = new Set([root]);
40
+ for (const path of Object.keys(files))
41
+ directories.add(posix.dirname(posix.join(root, path)));
42
+ await exec(`if [ -e ${quote(root)} ]; then chmod -R u+w ${quote(root)} && rm -rf ${quote(root)}; fi && mkdir -p ${[...directories].map(quote).join(" ")}`);
43
+ for (const [path, sha256] of Object.entries(files))
44
+ await handle.writeFile(posix.join(root, path), await read(sha256));
45
+ await exec(`chmod -R a-w ${quote(root)}`);
46
+ }
@@ -64,8 +64,8 @@ export interface SandboxHandle {
64
64
  * directory. Stops after `maxEntries + 1` files (`truncated`). For the turn-end export (F8.2).
65
65
  */
66
66
  listFiles?(dir: string, maxEntries: number): Promise<SandboxListing | undefined>;
67
- /** Parent directories must already exist. */
68
- writeFile(path: string, content: string): Promise<void>;
67
+ /** Text is written as UTF-8, bytes as they are. Parent directories must already exist. */
68
+ writeFile(path: string, content: string | Uint8Array): Promise<void>;
69
69
  /** Release compute; files persist. */
70
70
  stop(): Promise<void>;
71
71
  }
@@ -17,3 +17,12 @@ export declare function isOwnershipLost(error: unknown): error is OwnershipLostE
17
17
  * before `fn` runs, so nothing is written.
18
18
  */
19
19
  export declare function ownedTx<T, S extends SessionDoc = SessionDoc>(store: SessionStore, sessionId: string, epoch: number, fn: (t: Tx, session: StoredSession<S>) => Promise<T>): Promise<T>;
20
+ /**
21
+ * `putPrincipal` on an id that holds another role (protocol 8): rotating a key never changes
22
+ * what it reaches, so an application key and a management key cannot share a name.
23
+ */
24
+ export declare class PrincipalRoleConflict extends Error {
25
+ readonly id: string;
26
+ readonly code = "principal.role_conflict";
27
+ constructor(id: string);
28
+ }
@@ -31,3 +31,16 @@ export function isOwnershipLost(error) {
31
31
  export function ownedTx(store, sessionId, epoch, fn) {
32
32
  return store.tx(async (t) => fn(t, await t.assertEpoch(sessionId, epoch)));
33
33
  }
34
+ /**
35
+ * `putPrincipal` on an id that holds another role (protocol 8): rotating a key never changes
36
+ * what it reaches, so an application key and a management key cannot share a name.
37
+ */
38
+ export class PrincipalRoleConflict extends Error {
39
+ id;
40
+ code = "principal.role_conflict";
41
+ constructor(id) {
42
+ super(`Key ${id} holds another role`);
43
+ this.id = id;
44
+ this.name = "PrincipalRoleConflict";
45
+ }
46
+ }
@@ -39,3 +39,5 @@ export declare function createPostgresClient(url: string, options?: PostgresClie
39
39
  * needs `wal_level = logical` (a restart) and a role allowed to replicate.
40
40
  */
41
41
  export declare function assertLogicalReplication(sql: Sql): Promise<void>;
42
+ /** Separate, bounded pool for read projections; inherits the Tenant connection credentials. */
43
+ export declare function createPostgresReadClient(source: PostgresClient): PostgresClient;
@@ -40,3 +40,32 @@ export async function assertLogicalReplication(sql) {
40
40
  if (!row.replicates)
41
41
  throw new Error("The Runtime's Postgres role cannot replicate: grant it REPLICATION (ALTER ROLE … REPLICATION). See DEPLOYMENT.md.");
42
42
  }
43
+ /** Separate, bounded pool for read projections; inherits the Tenant connection credentials. */
44
+ export function createPostgresReadClient(source) {
45
+ const options = {
46
+ host: source.options.host.length === 1
47
+ ? source.options.host[0]
48
+ : source.options.host
49
+ .map((host, i) => `${host}:${source.options.port[i] ?? source.options.port[0]}`)
50
+ .join(","),
51
+ port: source.options.port[0],
52
+ database: source.options.database,
53
+ username: source.options.user,
54
+ pass: source.options.pass ?? "",
55
+ ssl: source.options.ssl,
56
+ path: source.options.path,
57
+ max: 4,
58
+ idle_timeout: 30,
59
+ connect_timeout: 2,
60
+ onnotice() { },
61
+ connection: {
62
+ ...source.options.connection,
63
+ application_name: "nylorun-reads",
64
+ statement_timeout: 2000,
65
+ idle_in_transaction_session_timeout: 2000,
66
+ default_transaction_read_only: true,
67
+ },
68
+ };
69
+ const pool = postgres(options);
70
+ return pool;
71
+ }
@@ -0,0 +1,3 @@
1
+ -- Key roles (protocol 8): Studio's derived key reaches both the Runtime API and the
2
+ -- Management API. Every other principal stays an application key.
3
+ UPDATE "nylorun"."principals" SET "role" = 'studio' WHERE "id" = 'studio';
@@ -0,0 +1,8 @@
1
+ ALTER TABLE "nylorun"."model_usage" ADD COLUMN "tokens_reported" boolean;--> statement-breakpoint
2
+ ALTER TABLE "nylorun"."model_usage" ADD COLUMN "cost_known" boolean;--> statement-breakpoint
3
+ ALTER TABLE "nylorun"."model_usage" ADD COLUMN "txid" "xid8" DEFAULT pg_current_xact_id() NOT NULL;--> statement-breakpoint
4
+ ALTER TABLE "nylorun"."sessions" ADD COLUMN "created_at" timestamp with time zone;--> statement-breakpoint
5
+ ALTER TABLE "nylorun"."sessions" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
6
+ CREATE INDEX "model_usage_session" ON "nylorun"."model_usage" USING btree ("session_id","created_at","id");--> statement-breakpoint
7
+ CREATE INDEX "model_usage_export" ON "nylorun"."model_usage" USING btree ("txid","id");--> statement-breakpoint
8
+ CREATE INDEX "sessions_created" ON "nylorun"."sessions" USING btree ("created_at" DESC NULLS LAST,"id" DESC NULLS FIRST);
@@ -0,0 +1,16 @@
1
+ CREATE TABLE "nylorun"."definition_file_uses" (
2
+ "agent_id" text COLLATE "C" NOT NULL,
3
+ "manifest_hash" text COLLATE "C" NOT NULL,
4
+ "sha256" text COLLATE "C" NOT NULL,
5
+ CONSTRAINT "definition_file_uses_pkey" PRIMARY KEY("agent_id","manifest_hash","sha256")
6
+ );
7
+ --> statement-breakpoint
8
+ CREATE TABLE "nylorun"."definition_files" (
9
+ "sha256" text COLLATE "C" PRIMARY KEY NOT NULL,
10
+ "kind" text NOT NULL,
11
+ "size" bigint NOT NULL,
12
+ "content_type" text,
13
+ "created_at" text COLLATE "C" NOT NULL
14
+ );
15
+ --> statement-breakpoint
16
+ CREATE INDEX "definition_file_uses_sha256" ON "nylorun"."definition_file_uses" USING btree ("sha256");
@@ -0,0 +1,2 @@
1
+ DROP TABLE "nylorun"."actions" CASCADE;--> statement-breakpoint
2
+ DROP TABLE "nylorun"."endpoints" CASCADE;
@@ -78,6 +78,34 @@
78
78
  "when": 1791107553412,
79
79
  "tag": "0010_oss_auth_removals",
80
80
  "breakpoints": true
81
+ },
82
+ {
83
+ "idx": 11,
84
+ "version": "7",
85
+ "when": 1791196553461,
86
+ "tag": "0011_key_roles",
87
+ "breakpoints": true
88
+ },
89
+ {
90
+ "idx": 12,
91
+ "version": "7",
92
+ "when": 1791209959436,
93
+ "tag": "0012_session_reads",
94
+ "breakpoints": true
95
+ },
96
+ {
97
+ "idx": 13,
98
+ "version": "7",
99
+ "when": 1791214258298,
100
+ "tag": "0013_definition_files",
101
+ "breakpoints": true
102
+ },
103
+ {
104
+ "idx": 14,
105
+ "version": "7",
106
+ "when": 1791218903443,
107
+ "tag": "0014_action_endpoints_removed",
108
+ "breakpoints": true
81
109
  }
82
110
  ]
83
111
  }
@@ -0,0 +1,3 @@
1
+ import type { ReadStore } from "../../reads/types.js";
2
+ import { type PostgresClient } from "./connect.js";
3
+ export declare function createPostgresReadStore(source: PostgresClient, tenantId: string): ReadStore;