@nylorun/runtime 0.24.0-beta → 0.26.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 (171) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +18 -4
  3. package/dist/api/a2a/card.d.ts +11 -1
  4. package/dist/api/a2a/card.js +26 -2
  5. package/dist/api/a2a/endpoint.js +4 -2
  6. package/dist/api/a2a/protocol.d.ts +25 -2
  7. package/dist/api/a2a/protocol.js +20 -3
  8. package/dist/api/a2a/routes.d.ts +4 -1
  9. package/dist/api/a2a/routes.js +65 -7
  10. package/dist/api/ag-ui/endpoint.js +5 -2
  11. package/dist/api/ag-ui/history.d.ts +6 -1
  12. package/dist/api/ag-ui/history.js +35 -2
  13. package/dist/api/ag-ui/routes.js +53 -11
  14. package/dist/api/components.d.ts +1165 -678
  15. package/dist/api/components.js +16 -1
  16. package/dist/api/gate/keys.d.ts +2 -1
  17. package/dist/api/gate/keys.js +7 -0
  18. package/dist/api/gate/routes.js +13 -0
  19. package/dist/api/http/app.d.ts +2 -2
  20. package/dist/api/http/app.js +15 -4
  21. package/dist/api/http/body.d.ts +3 -3
  22. package/dist/api/http/body.js +7 -7
  23. package/dist/api/http/define.d.ts +1 -1
  24. package/dist/api/http/respond.js +1 -1
  25. package/dist/api/http/routes/environment-manifests.d.ts +9 -0
  26. package/dist/api/http/routes/environment-manifests.js +118 -0
  27. package/dist/api/http/routes/environments.js +61 -12
  28. package/dist/api/http/routes/reads.d.ts +52 -3
  29. package/dist/api/http/routes/sessions.js +14 -4
  30. package/dist/api/http/routes/tenant.js +35 -7
  31. package/dist/api/inbound-files.d.ts +65 -0
  32. package/dist/api/inbound-files.js +115 -0
  33. package/dist/api/openapi.js +18 -3
  34. package/dist/artifacts/disk.js +20 -14
  35. package/dist/artifacts/media-types.d.ts +15 -0
  36. package/dist/artifacts/media-types.js +56 -4
  37. package/dist/artifacts/parts.d.ts +12 -3
  38. package/dist/artifacts/parts.js +15 -4
  39. package/dist/artifacts/workspace.d.ts +4 -4
  40. package/dist/artifacts/workspace.js +6 -18
  41. package/dist/core/flow-host.d.ts +1 -1
  42. package/dist/core/flow-host.js +2 -2
  43. package/dist/core/sandbox-routes.js +2 -2
  44. package/dist/core/turn-manifest.d.ts +3 -2
  45. package/dist/core/turn-manifest.js +29 -5
  46. package/dist/gates/egress-credentials.js +1 -1
  47. package/dist/gates/egress.d.ts +13 -19
  48. package/dist/gates/egress.js +32 -29
  49. package/dist/gates/http-tool.d.ts +3 -4
  50. package/dist/gates/http-tool.js +23 -19
  51. package/dist/gates/journal.js +0 -1
  52. package/dist/gates/mcp-handler.js +2 -3
  53. package/dist/gates/policy.js +1 -3
  54. package/dist/gates/tool-contract.d.ts +1 -1
  55. package/dist/harness/calls.d.ts +2 -2
  56. package/dist/harness/calls.js +9 -12
  57. package/dist/harness/main.js +7 -1
  58. package/dist/harness/pod.d.ts +6 -1
  59. package/dist/harness/pod.js +5 -0
  60. package/dist/harness/service.js +5 -5
  61. package/dist/harness/setup.d.ts +2 -0
  62. package/dist/harness/setup.js +25 -1
  63. package/dist/harness-api/record.d.ts +1 -1
  64. package/dist/harness-api/record.js +13 -13
  65. package/dist/harness-api/server.d.ts +27 -4
  66. package/dist/harness-api/server.js +41 -8
  67. package/dist/harness-api/tool-journal.d.ts +17 -8
  68. package/dist/harness-api/tool-journal.js +41 -11
  69. package/dist/harness-api/workspace.d.ts +7 -5
  70. package/dist/harness-api/workspace.js +25 -19
  71. package/dist/host/gates.js +2 -2
  72. package/dist/host/stack-config.js +1 -1
  73. package/dist/keys/contract.d.ts +1 -1
  74. package/dist/keys/contract.js +1 -1
  75. package/dist/keys/keys.d.ts +9 -2
  76. package/dist/keys/keys.js +4 -1
  77. package/dist/management-openapi.json +366 -61
  78. package/dist/mcp/connect.d.ts +1 -2
  79. package/dist/mcp/connect.js +1 -4
  80. package/dist/mcp/pool.d.ts +1 -4
  81. package/dist/mcp/pool.js +13 -27
  82. package/dist/mcp/preview.d.ts +1 -1
  83. package/dist/mcp/preview.js +1 -2
  84. package/dist/mcp/snapshot.d.ts +11 -13
  85. package/dist/mcp/snapshot.js +27 -38
  86. package/dist/model/capabilities.d.ts +26 -0
  87. package/dist/model/capabilities.js +44 -0
  88. package/dist/model/catalog.d.ts +34 -3
  89. package/dist/model/catalog.js +38 -2
  90. package/dist/model/models.d.ts +2 -0
  91. package/dist/model/models.js +1 -1
  92. package/dist/model/native-blocks.d.ts +35 -0
  93. package/dist/model/native-blocks.js +170 -0
  94. package/dist/model/pi-model.js +106 -94
  95. package/dist/openapi.json +9554 -5076
  96. package/dist/record/environment.js +1 -1
  97. package/dist/sandbox/join.js +4 -0
  98. package/dist/sandbox/manager.d.ts +32 -14
  99. package/dist/sandbox/manager.js +132 -93
  100. package/dist/sandbox/placement.d.ts +1 -1
  101. package/dist/sandbox/placement.js +2 -2
  102. package/dist/sandbox/pods/client.d.ts +3 -1
  103. package/dist/sandbox/pods/lifecycle.d.ts +8 -0
  104. package/dist/sandbox/pods/lifecycle.js +15 -0
  105. package/dist/sandbox/pods/spec.js +2 -1
  106. package/dist/sandbox/resolve.d.ts +5 -3
  107. package/dist/sandbox/resolve.js +9 -2
  108. package/dist/sandbox/runtime-tools.d.ts +9 -0
  109. package/dist/sandbox/runtime-tools.js +16 -0
  110. package/dist/sandbox/session-sandbox.d.ts +4 -6
  111. package/dist/sandbox/session-sandbox.js +47 -94
  112. package/dist/sandbox/setup.d.ts +4 -1
  113. package/dist/sandbox/setup.js +46 -9
  114. package/dist/sandbox/share.d.ts +12 -7
  115. package/dist/sandbox/share.js +27 -25
  116. package/dist/sandbox/skills.d.ts +8 -6
  117. package/dist/sandbox/skills.js +24 -23
  118. package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
  119. package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
  120. package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
  121. package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
  122. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  123. package/dist/store/postgres/migrate.js +1 -1
  124. package/dist/store/postgres/reads.js +8 -2
  125. package/dist/store/postgres/schema.d.ts +217 -0
  126. package/dist/store/postgres/schema.js +36 -2
  127. package/dist/store/postgres/store.js +79 -4
  128. package/dist/store/types.d.ts +68 -14
  129. package/dist/tenant/advance.js +3 -0
  130. package/dist/tenant/artifact-tool.d.ts +1 -1
  131. package/dist/tenant/artifact-tool.js +43 -22
  132. package/dist/tenant/commands.js +24 -4
  133. package/dist/tenant/context.d.ts +10 -4
  134. package/dist/tenant/effects.js +7 -9
  135. package/dist/tenant/environment-manifests.d.ts +57 -0
  136. package/dist/tenant/environment-manifests.js +200 -0
  137. package/dist/tenant/environments.d.ts +54 -47
  138. package/dist/tenant/environments.js +309 -85
  139. package/dist/tenant/ephemeral.d.ts +1 -1
  140. package/dist/tenant/host-token.js +1 -1
  141. package/dist/tenant/identity-file.d.ts +1 -1
  142. package/dist/tenant/identity-file.js +1 -1
  143. package/dist/tenant/model-setting.d.ts +14 -0
  144. package/dist/tenant/model-setting.js +51 -0
  145. package/dist/tenant/outbound.d.ts +6 -5
  146. package/dist/tenant/outbound.js +8 -6
  147. package/dist/tenant/sessions.d.ts +5 -1
  148. package/dist/tenant/sessions.js +45 -12
  149. package/dist/tenant/skill-tool.d.ts +3 -3
  150. package/dist/tenant/skill-tool.js +6 -11
  151. package/dist/tenant/sweep.js +1 -1
  152. package/dist/tenant/tool-results.js +1 -1
  153. package/dist/tenant/tool-search.d.ts +7 -11
  154. package/dist/tenant/tool-search.js +8 -5
  155. package/dist/tenant/transcript.d.ts +51 -32
  156. package/dist/tenant/transcript.js +2 -1
  157. package/dist/tenant/types.d.ts +2 -2
  158. package/dist/traces/ids.d.ts +0 -1
  159. package/dist/traces/ids.js +0 -4
  160. package/dist/traces/map.d.ts +2 -2
  161. package/dist/traces/map.js +1 -29
  162. package/dist/vault/coverage.js +11 -13
  163. package/dist/vault/host-model.d.ts +4 -1
  164. package/dist/vault/host-model.js +10 -2
  165. package/dist/vault/service.d.ts +49 -4
  166. package/dist/vault/service.js +153 -6
  167. package/dist/version.d.ts +1 -1
  168. package/dist/version.js +1 -1
  169. package/package.json +3 -3
  170. package/dist/sandbox/capability.d.ts +0 -3
  171. package/dist/sandbox/capability.js +0 -10
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Our engine's built-in artifact tools, of the `nylorun.artifacts` capability
3
- * (`sandbox/session-sandbox.ts`). The Runtime runs them itself, like the sandbox tools.
2
+ * Our engine's built-in artifact tools, which the Runtime pins in a session agent's `runtime`
3
+ * block (`sandbox/session-sandbox.ts`). The Runtime runs them itself, like the sandbox tools.
4
4
  *
5
5
  * `save_artifact` (F8.1), which comes with a session's sandbox, saves a file as an artifact of
6
6
  * its session:
@@ -9,23 +9,26 @@
9
9
  * The version commits with `artifact.created` (or `artifact.version.created`) on the session's
10
10
  * stream, carrying the turn and the tool call.
11
11
  *
12
- * `read_artifact` (R2b C11, Q24), which an agent with a remote MCP server or an HTTP tool has,
13
- * reads a text artifact of its own session in pages of at most 32 KiB, from a byte `offset`:
14
- * how the model reads a tool result the Runtime stored because it was too large to show
15
- * (`tool-results.ts`). An image is shown to a model that reads images, as a file of the result.
16
- * It reaches only the session's own artifacts, never a Tenant-wide one or another session's.
12
+ * `read_artifact` (R2b C11, Q24; every agent since protocol 12) reads a text artifact in pages of
13
+ * at most 32 KiB, from a byte `offset`: how the model reads a tool result the Runtime stored
14
+ * because it was too large to show (`tool-results.ts`), and a file of an earlier turn again, by
15
+ * its reference line. Another file is shown as a file of the result when the Tenant's model reads
16
+ * its type, and refused (`artifact.unsupported`) when it does not. It reaches the session's own
17
+ * artifacts and the Tenant-wide ones its messages named (`Session.fileArtifacts`), never another
18
+ * session's. `version` picks a version; the latest by default.
17
19
  *
18
20
  * Expected problems (no such file, too large, the Tenant's total reached, not text) are failed
19
21
  * outcomes the model sees; only infrastructure errors throw.
20
22
  */
21
23
  import { basename } from "node:path";
22
- import { READ_ARTIFACT_MAX_BYTES, SANDBOX_CAPABILITY_ID, } from "@nylorun/core/define";
24
+ import { READ_ARTIFACT_MAX_BYTES } from "@nylorun/core/define";
23
25
  import { isArtifactId } from "@nylorun/core/compatibility";
24
26
  import { readArtifactLimits } from "../artifacts/config.js";
25
- import { MODEL_FILE_MAX_BYTES, contentTypeFor, essence, isModelImage, isText, normalizeContentType, } from "../artifacts/media-types.js";
27
+ import { contentTypeFor, essence, isText, normalizeContentType, unreadableReason, } from "../artifacts/media-types.js";
28
+ import { currentModelInputs } from "../model/catalog.js";
26
29
  import { uploadArtifact } from "../artifacts/service.js";
27
30
  import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
28
- import { sandboxCapabilityOf } from "../sandbox/manager.js";
31
+ import { sandboxToolSpec } from "../sandbox/manager.js";
29
32
  import { sandboxWorkspaceOf } from "../sandbox/share.js";
30
33
  import { sessionOf } from "./context.js";
31
34
  import { HttpError } from "./http.js";
@@ -62,15 +65,15 @@ export async function callSaveArtifact(ctx, request, signal) {
62
65
  let bytes;
63
66
  let fileName;
64
67
  if (path !== undefined) {
65
- const capability = sandboxCapabilityOf(manifestFor(s.manifest, request.agent), SANDBOX_CAPABILITY_ID, "read");
66
- if (!capability)
68
+ const spec = sandboxToolSpec(manifestFor(s.manifest, request.agent), "read");
69
+ if (!spec)
67
70
  return failed("artifact.no_sandbox", "This session has no sandbox to read a path from.");
68
71
  const read = await ctx.sandbox.readBytes({
69
72
  id: workspace.ownerId,
70
73
  activeTurnId: s.activeTurnId,
71
74
  manifest: s.manifest,
72
75
  ...(workspace.environmentId === undefined ? {} : { environmentId: workspace.environmentId }),
73
- }, capability, path, limits.fileBytes, signal);
76
+ }, spec, path, limits.fileBytes, signal);
74
77
  if (read.kind === "failed")
75
78
  return read;
76
79
  if (read.kind === "missing")
@@ -128,9 +131,12 @@ export async function callSaveArtifact(ctx, request, signal) {
128
131
  export async function callReadArtifact(ctx, request, signal) {
129
132
  const input = (request.input ?? {});
130
133
  const artifactId = typeof input.artifactId === "string" ? input.artifactId : "";
134
+ const wanted = input.version;
131
135
  const offset = input.offset ?? 0;
132
136
  const length = input.length ?? READ_ARTIFACT_MAX_BYTES;
133
137
  if (artifactId === "" ||
138
+ (wanted !== undefined &&
139
+ (typeof wanted !== "number" || !Number.isSafeInteger(wanted) || wanted < 1)) ||
134
140
  typeof offset !== "number" ||
135
141
  !Number.isSafeInteger(offset) ||
136
142
  offset < 0 ||
@@ -138,20 +144,25 @@ export async function callReadArtifact(ctx, request, signal) {
138
144
  !Number.isSafeInteger(length) ||
139
145
  length < 1 ||
140
146
  length > READ_ARTIFACT_MAX_BYTES)
141
- return failed("artifact.invalid_input", `Give artifactId, an offset of 0 or more, and a length of 1 to ${READ_ARTIFACT_MAX_BYTES}.`);
147
+ return failed("artifact.invalid_input", `Give artifactId, a version of 1 or more if any, an offset of 0 or more, and a length of 1 to ${READ_ARTIFACT_MAX_BYTES}.`);
142
148
  const found = await ctx.store.tx(async (t) => {
143
149
  const s = await sessionOf(t, request.sessionId);
144
150
  if (!isArtifactId(artifactId))
145
151
  return undefined;
146
- // Only an artifact of this session: never a Tenant-wide one, nor another session's.
152
+ // An artifact of this session, or a Tenant-wide one its messages named; never another
153
+ // session's.
147
154
  const artifact = await t.artifact(artifactId);
148
- if (!artifact || artifact.sessionId !== s.id)
155
+ if (!artifact ||
156
+ (artifact.sessionId !== s.id &&
157
+ !(artifact.sessionId === null && s.fileArtifacts?.includes(artifact.id))))
149
158
  return undefined;
150
- const version = await t.artifactVersion(artifact.id, artifact.latestVersion);
151
- return version ? { artifact, version } : undefined;
159
+ const version = await t.artifactVersion(artifact.id, typeof wanted === "number" ? wanted : artifact.latestVersion);
160
+ return { artifact, version };
152
161
  });
153
162
  if (!found)
154
163
  return failed("artifact.not_found", `Artifact ${artifactId} is not an artifact of this session.`);
164
+ if (!found.version)
165
+ return failed("artifact.not_found", `Artifact ${artifactId} has no version ${String(wanted)}.`);
155
166
  const { artifact, version } = found;
156
167
  const about = {
157
168
  artifactId: artifact.id,
@@ -162,19 +173,29 @@ export async function callReadArtifact(ctx, request, signal) {
162
173
  };
163
174
  if (artifact.kind !== "file")
164
175
  return failed("artifact.not_text", `${artifact.name} is a folder; read_artifact reads a file.`);
165
- if (isModelImage(version.contentType) && version.size <= MODEL_FILE_MAX_BYTES)
176
+ if (!isText(version.contentType)) {
177
+ // A file the Tenant's model reads goes to it beside the result (protocol 12). Without a
178
+ // model in the vault (a test's fake provider), text and images.
179
+ const inputs = (await currentModelInputs(ctx)) ?? ["text", "image"];
180
+ const reason = unreadableReason(version.contentType, inputs);
181
+ if (reason)
182
+ return failed("artifact.unsupported", `${artifact.name} is ${essence(version.contentType)}: ${reason}.`);
166
183
  return {
167
184
  kind: "completed",
168
185
  output: about,
169
186
  files: [
170
187
  {
171
188
  mediaType: version.contentType,
172
- reference: { artifactId: artifact.id, version: version.version, name: artifact.name },
189
+ reference: {
190
+ artifactId: artifact.id,
191
+ version: version.version,
192
+ name: artifact.name,
193
+ size: version.size,
194
+ },
173
195
  },
174
196
  ],
175
197
  };
176
- if (!isText(version.contentType))
177
- return failed("artifact.not_text", `${artifact.name} is ${essence(version.contentType)}, not text; read_artifact reads text and shows images.`);
198
+ }
178
199
  if (offset > version.size || (offset === version.size && version.size > 0))
179
200
  return failed("artifact.invalid_input", `Offset ${offset} is past the end of ${artifact.name}, which is ${version.size} bytes.`);
180
201
  const end = Math.min(offset + length, version.size);
@@ -28,6 +28,7 @@ import { checkEnvironmentTurn } from "./environments.js";
28
28
  import { turnManifestOf, variantStore } from "./session.js";
29
29
  import { slimModelEffects } from "./slim.js";
30
30
  import { resolveMessageParts } from "../artifacts/parts.js";
31
+ import { currentModelInputs } from "../model/catalog.js";
31
32
  import { endTurn, resumeTurn, startTurn } from "./turn-timing.js";
32
33
  /** A command's identity for idempotency: everything but the request id. */
33
34
  const semantic = (value) => {
@@ -42,6 +43,11 @@ export async function command(ctx, id, input, scope,
42
43
  options = {}) {
43
44
  const { store } = ctx;
44
45
  const cascadeCancelIds = [];
46
+ // What the Tenant's model reads, for a message that names a file (protocol 12): read before
47
+ // the session's transaction. Undefined without a model in the vault: nothing is refused.
48
+ const inputs = input.type === "message" && "parts" in input && input.parts.some((part) => part.type === "file")
49
+ ? await currentModelInputs(ctx)
50
+ : undefined;
45
51
  const response = await store.tx(async (t) => {
46
52
  const s = await lockedSession(t, id, accessOf(scope));
47
53
  const command = input;
@@ -128,9 +134,12 @@ options = {}) {
128
134
  }
129
135
  }
130
136
  else {
131
- // A session opened before protocol 11 (migration 0020) keeps its history but takes no turn.
137
+ // A session opened before protocol 11 (migration 0020) or 13 (migration 0024) keeps its
138
+ // history but takes no turn.
132
139
  if (s.closedReason !== undefined)
133
- fail(409, "This session was opened before protocol 11: open a new session that names an Environment.", { code: "session_closed" });
140
+ fail(409, s.closedReason === "protocol_11"
141
+ ? "This session was opened before protocol 11: open a new session that names an Environment."
142
+ : "This session was opened before protocol 13, on a manifest the Runtime no longer runs: register the agent again (manifest 7) and open a new session.", { code: "session_closed" });
134
143
  if (command.type === "message") {
135
144
  if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
136
145
  fail(409, "Session has active or unresolved work");
@@ -147,10 +156,21 @@ options = {}) {
147
156
  s.turnTrace = options.trace;
148
157
  else
149
158
  delete s.turnTrace;
150
- // File parts name artifacts the caller may read; each is pinned to a version now.
159
+ // File parts name artifacts the caller may read; each is pinned to a version now. An
160
+ // agent's model must read each one (a workflow passes them on as data).
151
161
  const parts = "parts" in command
152
- ? await resolveMessageParts(t, command.parts, id, accessOf(scope))
162
+ ? await resolveMessageParts(t, command.parts, id, accessOf(scope), isWorkflowManifest(s.manifest) ? undefined : inputs)
153
163
  : undefined;
164
+ if (parts && parts.refused.length > 0)
165
+ fail(422, `The model does not read ${parts.refused
166
+ .map((part) => `${part.name} (${part.reason})`)
167
+ .join(", ")}.`, { code: "attachment_unsupported", details: { parts: parts.refused } });
168
+ // The files the session's messages named: `read_artifact` reads them again (protocol 12).
169
+ if (parts) {
170
+ const named = parts.pinned.flatMap((part) => (part.type === "file" ? [part.artifactId] : []));
171
+ if (named.length > 0)
172
+ s.fileArtifacts = [...new Set([...(s.fileArtifacts ?? []), ...named])];
173
+ }
154
174
  const data = "data" in command ? command.data : undefined;
155
175
  if (isWorkflowManifest(s.manifest)) {
156
176
  const input = "content" in command
@@ -50,7 +50,7 @@ export interface Session {
50
50
  manifestHash: string;
51
51
  /**
52
52
  * The hash of the definition the session was opened from; `manifestHash` differs when the
53
- * Runtime pinned its own capabilities beside it (R2b C11). Absent on older sessions.
53
+ * Runtime pinned its `runtime` block on it (R2b C11). Absent on older sessions.
54
54
  */
55
55
  definitionHash?: string;
56
56
  /** Validated turn-manifest variants, keyed by hash (loops.md §4.2). */
@@ -115,9 +115,15 @@ export interface Session {
115
115
  sandbox?: SandboxManifest;
116
116
  /**
117
117
  * Why the session takes no new turn: `protocol_11` for a session opened before protocol 11
118
- * (migration 0020). Its history stays readable.
118
+ * (migration 0020), `protocol_13` for one opened before protocol 13 (migration 0024). Its
119
+ * history stays readable.
119
120
  */
120
121
  closedReason?: string;
122
+ /**
123
+ * The artifacts the session's messages named (protocol 12): `read_artifact` reads these
124
+ * Tenant-wide ones beside the session's own. Absent until a message names a file.
125
+ */
126
+ fileArtifacts?: string[];
121
127
  }
122
128
  export type AuthScope = {
123
129
  kind: "application";
@@ -175,7 +181,7 @@ export interface TenantContext {
175
181
  */
176
182
  readonly mcp?: McpPool;
177
183
  /**
178
- * The workspace capability (F6.2): the Tenant's SandboxManager with an in-process harness,
184
+ * The workspace feature (F6.2): the Tenant's SandboxManager with an in-process harness,
179
185
  * else the harness that serves workspaces (`harness-api/workspace.ts`).
180
186
  */
181
187
  readonly sandbox: WorkspacePort;
@@ -261,7 +267,7 @@ export interface TenantContext {
261
267
  abortLocal(sessionId: string, turnId?: string): void;
262
268
  /**
263
269
  * Sandbox pods (F7.2): the sandboxes service and the Runtime image pods copy the engine from.
264
- * Absent without a cluster (`nylorun sandbox enable`): kind `pod` is `environment_unavailable`.
270
+ * Absent without a cluster (`nylorun local sandbox enable`): kind `pod` is `environment_unavailable`.
265
271
  */
266
272
  readonly pods?: TenantPods;
267
273
  /**
@@ -5,7 +5,7 @@ import { sessionCredentials } from "../vault/sources.js";
5
5
  import { isRemoteMcpCall } from "../harness/calls.js";
6
6
  import { isHttpToolCall } from "../gates/http-tool.js";
7
7
  import { sandboxSpecOf } from "../sandbox/share.js";
8
- import { withPlatformTools, withSandboxCapability } from "../sandbox/session-sandbox.js";
8
+ import { withRuntimeTools, withSandbox } from "../sandbox/session-sandbox.js";
9
9
  import { loadSession, ownedSession, sessionOf, } from "./context.js";
10
10
  import { fail } from "./http.js";
11
11
  import { turnManifestOf } from "./session.js";
@@ -164,8 +164,8 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
164
164
  const definition = leafDefinition(workflow, body, request);
165
165
  // The tree's Environment reaches every agent in it: its sandbox, with its tools, and its vault.
166
166
  const inherited = inheritedSandbox(workflow, definition);
167
- // Without one, an agent with MCP or HTTP tools still reads its stored results (R2b C11).
168
- const pinned = inherited?.manifest ? inherited : platformTools(definition.manifest);
167
+ // Without one, the agent still gets its runtime block: read_artifact (R2b C11) and its skill tools.
168
+ const pinned = inherited?.manifest ? inherited : runtimeTools(definition.manifest);
169
169
  const created = {
170
170
  id: agentSessionId,
171
171
  agentId: body.agentId,
@@ -257,18 +257,16 @@ function inheritedSandbox(owner, definition) {
257
257
  return undefined;
258
258
  if (isWorkflowManifest(definition.manifest))
259
259
  return { spec };
260
- const sandboxed = withSandboxCapability(definition.manifest, spec);
260
+ const sandboxed = withSandbox(definition.manifest, spec);
261
261
  if (!sandboxed.ok)
262
262
  throw new Error(sandboxed.message);
263
263
  return { spec, manifest: sandboxed.manifest, manifestHash: sandboxed.manifestHash };
264
264
  }
265
- /** A linked agent session's `read_artifact` without a sandbox (R2b C11); undefined for none. */
266
- function platformTools(manifest) {
265
+ /** A linked agent session's `runtime` block without a sandbox (R2b C11); undefined for a workflow. */
266
+ function runtimeTools(manifest) {
267
267
  if (isWorkflowManifest(manifest))
268
268
  return undefined;
269
- const pinned = withPlatformTools(manifest);
270
- if (pinned === undefined)
271
- return undefined;
269
+ const pinned = withRuntimeTools(manifest);
272
270
  if (!pinned.ok)
273
271
  throw new Error(pinned.message);
274
272
  return { manifest: pinned.manifest, manifestHash: pinned.manifestHash };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Environment manifests (protocol 14, D63): `/v1/environment-manifests/{name}`, named and
3
+ * versioned definitions of a kind of Environment, saved like agents.
4
+ *
5
+ * - **Versions.** A save whose manifest hashes the same as the latest answers it unchanged;
6
+ * any other writes the next version. A version is never changed or deleted alone.
7
+ * - **No Environment changes.** Saving a version reaches no Environment: an Environment is made
8
+ * from one version and keeps it for life (`putEnvironment`). The plan counts those behind.
9
+ * - **Plan and drift.** `?dryRun=true` answers what a save would do and saves nothing: the
10
+ * changes from the latest, every reason the Runtime would refuse it (the Tenant's limits,
11
+ * missing artifacts), warnings, and how many Environments stay on older versions.
12
+ * `If-Match: <version>` refuses (`412 manifest_changed`) a save whose latest version moved
13
+ * since the client read it; `0` asks for a manifest that does not exist yet.
14
+ * - **Every client is equal** (Q12): a version records who saved it (`savedBy`), and nothing is
15
+ * read-only by where it was saved from.
16
+ */
17
+ import { type EnvironmentManifest, type EnvironmentManifestPlan, type EnvironmentManifestVersions, type EnvironmentManifestView, type PutEnvironmentManifestRequest, type SaveEnvironmentManifestResponse } from "@nylorun/core/contracts";
18
+ import { type RecipeRequest } from "../sandbox/resolve.js";
19
+ import type { AuthScope, TenantContext } from "./context.js";
20
+ /** What an environment manifest asks of an Environment's recipe. */
21
+ export declare function manifestRecipeRequest(manifest: EnvironmentManifest): RecipeRequest;
22
+ /** The hash a manifest's version records: equal manifests hash the same. */
23
+ export declare function environmentManifestHash(manifest: EnvironmentManifest): string;
24
+ /** Each field that differs between `before` and `after`, by path; arrays compare whole. */
25
+ export declare function manifestChanges(before: unknown, after: unknown, path?: string): {
26
+ path: string;
27
+ from?: unknown;
28
+ to?: unknown;
29
+ }[];
30
+ /**
31
+ * `PUT /v1/environment-manifests/{name}`: saves `body` as manifest `name`'s next version, or
32
+ * answers the latest unchanged; with `dryRun`, the plan of that save and nothing saved.
33
+ */
34
+ export declare function saveEnvironmentManifest(ctx: TenantContext, name: string, body: PutEnvironmentManifestRequest, scope: AuthScope, options: {
35
+ dryRun: true;
36
+ ifMatch?: number;
37
+ }): Promise<EnvironmentManifestPlan>;
38
+ export declare function saveEnvironmentManifest(ctx: TenantContext, name: string, body: PutEnvironmentManifestRequest, scope: AuthScope, options?: {
39
+ dryRun?: false;
40
+ ifMatch?: number;
41
+ }): Promise<SaveEnvironmentManifestResponse>;
42
+ /** `GET /v1/environment-manifests/{name}` (the latest) and `.../versions/{n}`. */
43
+ export declare function getEnvironmentManifest(ctx: TenantContext, name: string, version?: number): Promise<EnvironmentManifestView>;
44
+ /** `GET /v1/environment-manifests`: each manifest's latest version. */
45
+ export declare function listEnvironmentManifests(ctx: TenantContext): Promise<{
46
+ manifests: EnvironmentManifestView[];
47
+ }>;
48
+ /** `GET /v1/environment-manifests/{name}/versions`: every version, newest first. */
49
+ export declare function environmentManifestVersions(ctx: TenantContext, name: string): Promise<EnvironmentManifestVersions>;
50
+ /**
51
+ * `DELETE /v1/environment-manifests/{name}`: deletes every version, refused while any Environment
52
+ * was made from one (`409 manifest_in_use`): the Environment names the version it runs.
53
+ */
54
+ export declare function deleteEnvironmentManifest(ctx: TenantContext, name: string): Promise<{
55
+ name: string;
56
+ deleted: boolean;
57
+ }>;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Environment manifests (protocol 14, D63): `/v1/environment-manifests/{name}`, named and
3
+ * versioned definitions of a kind of Environment, saved like agents.
4
+ *
5
+ * - **Versions.** A save whose manifest hashes the same as the latest answers it unchanged;
6
+ * any other writes the next version. A version is never changed or deleted alone.
7
+ * - **No Environment changes.** Saving a version reaches no Environment: an Environment is made
8
+ * from one version and keeps it for life (`putEnvironment`). The plan counts those behind.
9
+ * - **Plan and drift.** `?dryRun=true` answers what a save would do and saves nothing: the
10
+ * changes from the latest, every reason the Runtime would refuse it (the Tenant's limits,
11
+ * missing artifacts), warnings, and how many Environments stay on older versions.
12
+ * `If-Match: <version>` refuses (`412 manifest_changed`) a save whose latest version moved
13
+ * since the client read it; `0` asks for a manifest that does not exist yet.
14
+ * - **Every client is equal** (Q12): a version records who saved it (`savedBy`), and nothing is
15
+ * read-only by where it was saved from.
16
+ */
17
+ import {} from "@nylorun/core/contracts";
18
+ import { hashManifest } from "@nylorun/core/compatibility";
19
+ import { canonical } from "../store/canonical.js";
20
+ import { resolveRecipe } from "../sandbox/resolve.js";
21
+ import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
22
+ import { actorOf, fileErrors } from "./environments.js";
23
+ import { fail } from "./http.js";
24
+ /** What an environment manifest asks of an Environment's recipe. */
25
+ export function manifestRecipeRequest(manifest) {
26
+ return {
27
+ ...(manifest.image === undefined ? {} : { image: manifest.image }),
28
+ ...(manifest.resources === undefined ? {} : { resources: manifest.resources }),
29
+ ...(manifest.storage === undefined ? {} : { storage: manifest.storage }),
30
+ ...(manifest.network === undefined ? {} : { network: manifest.network }),
31
+ ...(manifest.harness === undefined ? {} : { harness: manifest.harness }),
32
+ ...(manifest.lifecycle === undefined ? {} : { lifecycle: manifest.lifecycle }),
33
+ ...(manifest.setup === undefined ? {} : { setup: manifest.setup }),
34
+ ...(manifest.files === undefined ? {} : { files: manifest.files }),
35
+ ...(manifest.secrets === undefined ? {} : { secrets: manifest.secrets }),
36
+ };
37
+ }
38
+ /** The hash a manifest's version records: equal manifests hash the same. */
39
+ export function environmentManifestHash(manifest) {
40
+ return hashManifest(manifest);
41
+ }
42
+ const notFound = (name, version) => fail(404, version === undefined ? `No environment manifest is named ${name}.` : `Environment manifest ${name} has no version ${version}.`, {
43
+ code: "manifest_not_found",
44
+ });
45
+ function viewOf(row, counts) {
46
+ return {
47
+ name: row.name,
48
+ version: row.version,
49
+ hash: row.hash,
50
+ manifest: row.body,
51
+ savedBy: row.savedBy,
52
+ createdAt: row.createdAt,
53
+ environments: counts.get(row.version) ?? 0,
54
+ };
55
+ }
56
+ /** Each field that differs between `before` and `after`, by path; arrays compare whole. */
57
+ export function manifestChanges(before, after, path = "") {
58
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
59
+ if (object(before) && object(after)) {
60
+ const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
61
+ return keys.flatMap((key) => manifestChanges(before[key], after[key], path ? `${path}.${key}` : key));
62
+ }
63
+ if (canonical(before) === canonical(after))
64
+ return [];
65
+ return [
66
+ {
67
+ path,
68
+ ...(before === undefined ? {} : { from: before }),
69
+ ...(after === undefined ? {} : { to: after }),
70
+ },
71
+ ];
72
+ }
73
+ /** What a save of `manifest` should tell its saver, though it would be saved. */
74
+ function warningsOf(ctx, manifest) {
75
+ const warnings = [];
76
+ if (manifest.image !== undefined && !manifest.image.includes("@sha256:"))
77
+ warnings.push({
78
+ code: "image_tag",
79
+ message: `image ${manifest.image} is a tag, not a digest: each Environment pins what its first pod pulls, so two Tenants may run different bytes.`,
80
+ });
81
+ if (manifest.kind === "pod" && !ctx.pods)
82
+ warnings.push({
83
+ code: "pods_unavailable",
84
+ message: "kind pod needs sandbox pods, which this Tenant does not have: an Environment made from it is refused until `nylorun local sandbox enable`.",
85
+ });
86
+ return warnings;
87
+ }
88
+ /** Every reason the Runtime would refuse `manifest` now: the Tenant's limits and its files. */
89
+ async function errorsOf(t, manifest) {
90
+ const config = effectiveSandboxConfig(await readSandboxConfig(t));
91
+ const resolved = resolveRecipe(manifest.kind ?? "virtual", manifestRecipeRequest(manifest), config);
92
+ return [...(resolved.kind === "error" ? resolved.errors : []), ...(await fileErrors(t, manifest.files))];
93
+ }
94
+ export async function saveEnvironmentManifest(ctx, name, body, scope, options = {}) {
95
+ const { requestId: _, ...manifest } = body;
96
+ if (manifest.name !== name)
97
+ fail(400, `The manifest is named ${manifest.name}, and the path names ${name}: they must match.`);
98
+ const hash = environmentManifestHash(manifest);
99
+ const warnings = warningsOf(ctx, manifest);
100
+ return ctx.store.tx(async (t) => {
101
+ await t.lockEnvironmentManifest(name);
102
+ const latest = await t.environmentManifest(name);
103
+ if (options.ifMatch !== undefined && (latest?.version ?? 0) !== options.ifMatch)
104
+ fail(412, latest === undefined
105
+ ? `Environment manifest ${name} does not exist, and If-Match named version ${options.ifMatch}.`
106
+ : `Environment manifest ${name} is at version ${latest.version}, not ${options.ifMatch}: someone saved it since you read it. Read it again, or save without If-Match to replace it.`, { code: "manifest_changed", details: { latest: latest?.version ?? 0 } });
107
+ const unchanged = latest?.hash === hash;
108
+ const to = unchanged ? latest.version : (latest?.version ?? 0) + 1;
109
+ const counts = await t.environmentsOfManifest(name);
110
+ const errors = await errorsOf(t, manifest);
111
+ if (options.dryRun) {
112
+ let behind = 0;
113
+ for (const [version, n] of counts)
114
+ if (version < to)
115
+ behind += n;
116
+ return {
117
+ name,
118
+ from: latest?.version ?? null,
119
+ to,
120
+ unchanged,
121
+ changes: unchanged ? [] : manifestChanges(latest?.body ?? {}, manifest),
122
+ warnings,
123
+ errors,
124
+ behind,
125
+ };
126
+ }
127
+ if (errors.length > 0)
128
+ fail(400, errors.join(" "));
129
+ if (unchanged)
130
+ return { manifest: viewOf(latest, counts), unchanged: true, warnings };
131
+ const row = {
132
+ name,
133
+ version: to,
134
+ body: manifest,
135
+ hash,
136
+ savedBy: { actor: actorOf(scope) },
137
+ createdAt: new Date().toISOString(),
138
+ };
139
+ await t.insertEnvironmentManifest(row);
140
+ // Its files by hash (Q6), recorded as a definition's are, for the sweep that deletes unused ones.
141
+ const shas = [...new Set((manifest.files ?? []).flatMap((file) => ("files" in file ? Object.values(file.files) : [])))];
142
+ if (shas.length > 0)
143
+ await t.insertDefinitionFileUses(`environment-manifest:${name}`, hash, shas);
144
+ return { manifest: viewOf(row, counts), unchanged: false, warnings };
145
+ });
146
+ }
147
+ /** `GET /v1/environment-manifests/{name}` (the latest) and `.../versions/{n}`. */
148
+ export async function getEnvironmentManifest(ctx, name, version) {
149
+ return ctx.store.tx(async (t) => {
150
+ const row = (await t.environmentManifest(name, version)) ?? notFound(name, version);
151
+ return viewOf(row, await t.environmentsOfManifest(name));
152
+ });
153
+ }
154
+ /** `GET /v1/environment-manifests`: each manifest's latest version. */
155
+ export async function listEnvironmentManifests(ctx) {
156
+ return ctx.store.tx(async (t) => {
157
+ const manifests = [];
158
+ for (const row of await t.listEnvironmentManifests())
159
+ manifests.push(viewOf(row, await t.environmentsOfManifest(row.name)));
160
+ return { manifests };
161
+ });
162
+ }
163
+ /** `GET /v1/environment-manifests/{name}/versions`: every version, newest first. */
164
+ export async function environmentManifestVersions(ctx, name) {
165
+ return ctx.store.tx(async (t) => {
166
+ const rows = await t.environmentManifestVersions(name);
167
+ if (rows.length === 0)
168
+ notFound(name);
169
+ const counts = await t.environmentsOfManifest(name);
170
+ return {
171
+ name,
172
+ latest: rows[0].version,
173
+ versions: rows.map((row) => ({
174
+ version: row.version,
175
+ hash: row.hash,
176
+ savedBy: row.savedBy,
177
+ createdAt: row.createdAt,
178
+ environments: counts.get(row.version) ?? 0,
179
+ })),
180
+ };
181
+ });
182
+ }
183
+ /**
184
+ * `DELETE /v1/environment-manifests/{name}`: deletes every version, refused while any Environment
185
+ * was made from one (`409 manifest_in_use`): the Environment names the version it runs.
186
+ */
187
+ export async function deleteEnvironmentManifest(ctx, name) {
188
+ return ctx.store.tx(async (t) => {
189
+ await t.lockEnvironmentManifest(name);
190
+ let made = 0;
191
+ for (const n of (await t.environmentsOfManifest(name)).values())
192
+ made += n;
193
+ if (made > 0)
194
+ fail(409, `${made} Environment(s) were made from environment manifest ${name}: delete them first, or keep the manifest.`, {
195
+ code: "manifest_in_use",
196
+ details: { environments: made },
197
+ });
198
+ return { name, deleted: (await t.deleteEnvironmentManifest(name)) > 0 };
199
+ });
200
+ }