@nylorun/runtime 0.24.0-beta → 0.25.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.
- package/CHANGELOG.md +104 -0
- package/README.md +10 -2
- package/dist/api/a2a/card.d.ts +11 -1
- package/dist/api/a2a/card.js +26 -2
- package/dist/api/a2a/endpoint.js +4 -2
- package/dist/api/a2a/protocol.d.ts +25 -2
- package/dist/api/a2a/protocol.js +20 -3
- package/dist/api/a2a/routes.d.ts +4 -1
- package/dist/api/a2a/routes.js +65 -7
- package/dist/api/ag-ui/endpoint.js +5 -2
- package/dist/api/ag-ui/history.d.ts +6 -1
- package/dist/api/ag-ui/history.js +35 -2
- package/dist/api/ag-ui/routes.js +53 -11
- package/dist/api/components.d.ts +1165 -678
- package/dist/api/components.js +16 -1
- package/dist/api/gate/keys.d.ts +2 -1
- package/dist/api/gate/keys.js +7 -0
- package/dist/api/gate/routes.js +13 -0
- package/dist/api/http/app.js +13 -2
- package/dist/api/http/body.d.ts +3 -3
- package/dist/api/http/body.js +7 -7
- package/dist/api/http/routes/environment-manifests.d.ts +9 -0
- package/dist/api/http/routes/environment-manifests.js +118 -0
- package/dist/api/http/routes/environments.js +61 -12
- package/dist/api/http/routes/reads.d.ts +52 -3
- package/dist/api/http/routes/sessions.js +13 -3
- package/dist/api/http/routes/tenant.js +35 -7
- package/dist/api/inbound-files.d.ts +65 -0
- package/dist/api/inbound-files.js +115 -0
- package/dist/api/openapi.js +18 -3
- package/dist/artifacts/disk.js +20 -14
- package/dist/artifacts/media-types.d.ts +15 -0
- package/dist/artifacts/media-types.js +56 -4
- package/dist/artifacts/parts.d.ts +12 -3
- package/dist/artifacts/parts.js +15 -4
- package/dist/artifacts/workspace.d.ts +4 -4
- package/dist/artifacts/workspace.js +6 -18
- package/dist/core/flow-host.d.ts +1 -1
- package/dist/core/flow-host.js +2 -2
- package/dist/core/sandbox-routes.js +2 -2
- package/dist/core/turn-manifest.d.ts +3 -2
- package/dist/core/turn-manifest.js +29 -5
- package/dist/gates/egress-credentials.js +1 -1
- package/dist/gates/egress.d.ts +13 -19
- package/dist/gates/egress.js +30 -29
- package/dist/gates/http-tool.d.ts +3 -4
- package/dist/gates/http-tool.js +8 -12
- package/dist/gates/journal.js +0 -1
- package/dist/gates/mcp-handler.js +2 -3
- package/dist/gates/policy.js +1 -3
- package/dist/gates/tool-contract.d.ts +1 -1
- package/dist/harness/calls.d.ts +2 -2
- package/dist/harness/calls.js +9 -12
- package/dist/harness/main.js +7 -1
- package/dist/harness/pod.d.ts +6 -1
- package/dist/harness/pod.js +5 -0
- package/dist/harness/service.js +5 -5
- package/dist/harness/setup.d.ts +2 -0
- package/dist/harness/setup.js +25 -1
- package/dist/harness-api/record.d.ts +1 -1
- package/dist/harness-api/record.js +13 -13
- package/dist/harness-api/server.d.ts +27 -4
- package/dist/harness-api/server.js +41 -8
- package/dist/harness-api/tool-journal.d.ts +17 -8
- package/dist/harness-api/tool-journal.js +41 -11
- package/dist/harness-api/workspace.d.ts +7 -5
- package/dist/harness-api/workspace.js +25 -19
- package/dist/host/gates.js +2 -2
- package/dist/host/stack-config.js +1 -1
- package/dist/keys/keys.d.ts +9 -2
- package/dist/keys/keys.js +4 -1
- package/dist/management-openapi.json +366 -61
- package/dist/mcp/connect.d.ts +1 -2
- package/dist/mcp/connect.js +1 -4
- package/dist/mcp/pool.d.ts +1 -4
- package/dist/mcp/pool.js +13 -27
- package/dist/mcp/preview.js +0 -1
- package/dist/mcp/snapshot.d.ts +11 -13
- package/dist/mcp/snapshot.js +27 -38
- package/dist/model/capabilities.d.ts +26 -0
- package/dist/model/capabilities.js +44 -0
- package/dist/model/catalog.d.ts +34 -3
- package/dist/model/catalog.js +38 -2
- package/dist/model/models.d.ts +2 -0
- package/dist/model/models.js +1 -1
- package/dist/model/native-blocks.d.ts +35 -0
- package/dist/model/native-blocks.js +170 -0
- package/dist/model/pi-model.js +106 -94
- package/dist/openapi.json +9554 -5104
- package/dist/record/environment.js +1 -1
- package/dist/sandbox/join.js +4 -0
- package/dist/sandbox/manager.d.ts +32 -14
- package/dist/sandbox/manager.js +132 -93
- package/dist/sandbox/placement.d.ts +1 -1
- package/dist/sandbox/placement.js +2 -2
- package/dist/sandbox/pods/client.d.ts +3 -1
- package/dist/sandbox/pods/lifecycle.d.ts +8 -0
- package/dist/sandbox/pods/lifecycle.js +15 -0
- package/dist/sandbox/pods/spec.js +2 -1
- package/dist/sandbox/resolve.d.ts +5 -3
- package/dist/sandbox/resolve.js +9 -2
- package/dist/sandbox/runtime-tools.d.ts +9 -0
- package/dist/sandbox/runtime-tools.js +16 -0
- package/dist/sandbox/session-sandbox.d.ts +4 -6
- package/dist/sandbox/session-sandbox.js +47 -94
- package/dist/sandbox/setup.d.ts +4 -1
- package/dist/sandbox/setup.js +46 -9
- package/dist/sandbox/share.d.ts +12 -7
- package/dist/sandbox/share.js +27 -25
- package/dist/sandbox/skills.d.ts +8 -6
- package/dist/sandbox/skills.js +24 -23
- package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
- package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
- package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
- package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/reads.js +8 -2
- package/dist/store/postgres/schema.d.ts +217 -0
- package/dist/store/postgres/schema.js +36 -2
- package/dist/store/postgres/store.js +79 -4
- package/dist/store/types.d.ts +68 -14
- package/dist/tenant/advance.js +3 -0
- package/dist/tenant/artifact-tool.d.ts +1 -1
- package/dist/tenant/artifact-tool.js +43 -22
- package/dist/tenant/commands.js +24 -4
- package/dist/tenant/context.d.ts +10 -4
- package/dist/tenant/effects.js +7 -9
- package/dist/tenant/environment-manifests.d.ts +57 -0
- package/dist/tenant/environment-manifests.js +200 -0
- package/dist/tenant/environments.d.ts +54 -47
- package/dist/tenant/environments.js +304 -83
- package/dist/tenant/model-setting.d.ts +14 -0
- package/dist/tenant/model-setting.js +51 -0
- package/dist/tenant/sessions.d.ts +5 -1
- package/dist/tenant/sessions.js +45 -12
- package/dist/tenant/skill-tool.d.ts +3 -3
- package/dist/tenant/skill-tool.js +6 -11
- package/dist/tenant/sweep.js +1 -1
- package/dist/tenant/tool-results.js +1 -1
- package/dist/tenant/tool-search.d.ts +7 -11
- package/dist/tenant/tool-search.js +8 -5
- package/dist/tenant/transcript.d.ts +51 -32
- package/dist/tenant/transcript.js +2 -1
- package/dist/traces/ids.d.ts +0 -1
- package/dist/traces/ids.js +0 -4
- package/dist/traces/map.d.ts +2 -2
- package/dist/traces/map.js +1 -29
- package/dist/vault/coverage.js +11 -13
- package/dist/vault/host-model.d.ts +4 -1
- package/dist/vault/host-model.js +10 -2
- package/dist/vault/service.d.ts +49 -4
- package/dist/vault/service.js +153 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/sandbox/capability.d.ts +0 -3
- package/dist/sandbox/capability.js +0 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Our engine's built-in artifact tools,
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
66
|
-
if (!
|
|
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
|
-
},
|
|
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
|
-
//
|
|
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 ||
|
|
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
|
|
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 (
|
|
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: {
|
|
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
|
-
|
|
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);
|
package/dist/tenant/commands.js
CHANGED
|
@@ -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)
|
|
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,
|
|
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
|
package/dist/tenant/context.d.ts
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
|
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
|
/**
|
package/dist/tenant/effects.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
168
|
-
const pinned = inherited?.manifest ? inherited :
|
|
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 =
|
|
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 `
|
|
266
|
-
function
|
|
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 =
|
|
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
|
+
}
|