@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,14 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { ModelUsageQuerySchema, PutModelBudgetsRequestSchema, PutToolPolicyRequestSchema, PutHostModelRequestSchema, PutTenantEnvironmentsRequestSchema, ResetTenantRequestSchema, SeedTenantConfigRequestSchema, SelectHostModelRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
-
import { HostModelCatalog, HostModelView, ListProvidersResponse, ModelBudgets, ModelUsageTotals, PutModelBudgetsRequest, PutToolPolicyRequest, ToolPolicy, PutHostModelRequest, PutTenantEnvironmentsRequest, ResetTenantRequest, ResetTenantResponse, SeedTenantConfigRequest, SeedTenantConfigResponse, SelectHostModelRequest, TenantSandboxView, TenantStatus, } from "../../components.js";
|
|
2
|
+
import { ModelUsageQuerySchema, PutModelBudgetsRequestSchema, PutToolPolicyRequestSchema, PutModelInputsRequestSchema, PutHostModelRequestSchema, PutTenantEnvironmentsRequestSchema, ResetTenantRequestSchema, SeedTenantConfigRequestSchema, SelectHostModelRequestSchema, } from "@nylorun/core/contracts";
|
|
3
|
+
import { HostModelCatalog, HostModelView, ListProvidersResponse, ModelBudgets, ModelInputsView, ModelUsageTotals, PutModelBudgetsRequest, PutToolPolicyRequest, PutModelInputsRequest, ToolPolicy, PutHostModelRequest, PutTenantEnvironmentsRequest, ResetTenantRequest, ResetTenantResponse, SeedTenantConfigRequest, SeedTenantConfigResponse, SelectHostModelRequest, TenantSandboxView, TenantStatus, } from "../../components.js";
|
|
4
4
|
import { periodStart } from "../../../gates/meter.js";
|
|
5
5
|
import { readToolPolicy, TOOL_POLICY_SETTING } from "../../../gates/policy.js";
|
|
6
|
-
import {
|
|
6
|
+
import { addableInputs, capabilitiesOf, inputsError } from "../../../model/capabilities.js";
|
|
7
|
+
import { catalogModel, hostModelCatalog } from "../../../model/catalog.js";
|
|
7
8
|
import { sandboxConfigErrors } from "../../../sandbox/resolve.js";
|
|
8
9
|
import { effectiveSandboxConfig, readSandboxConfig, writeSandboxConfig, } from "../../../sandbox/tenant-config.js";
|
|
9
10
|
import { fail } from "../../../tenant/http.js";
|
|
10
11
|
import { clearObservers } from "../../../tenant/session-streams.js";
|
|
11
|
-
import { usesFixtureModel } from "../../../tenant/model-setting.js";
|
|
12
|
+
import { readAddedInputs, usesFixtureModel, writeAddedInputs } from "../../../tenant/model-setting.js";
|
|
12
13
|
import { resetTenant } from "../../../tenant/reset.js";
|
|
13
14
|
import { clearWork, drain } from "../../../tenant/scheduler.js";
|
|
14
15
|
import { buildTenantStatus, harnessStatusOf, seedTenantConfig } from "../../../tenant/status.js";
|
|
@@ -150,10 +151,37 @@ export function tenantRoutes(api) {
|
|
|
150
151
|
path: "/v1/tenant/models",
|
|
151
152
|
tags: ["Tenant"],
|
|
152
153
|
summary: "List model providers and models",
|
|
153
|
-
|
|
154
|
+
description: "Each model says what it reads (`inputs`: its catalog's inputs and those the operator added) and what may be added (`addableInputs`).",
|
|
155
|
+
responses: { 200: json(HostModelCatalog, "Provider and model names, and their inputs") },
|
|
154
156
|
}, async (c) => {
|
|
155
|
-
c.env.tenant;
|
|
156
|
-
return jsonResponse(200, hostModelCatalog());
|
|
157
|
+
const added = await c.env.tenant.store.tx((t) => readAddedInputs(t));
|
|
158
|
+
return jsonResponse(200, hostModelCatalog(added));
|
|
159
|
+
});
|
|
160
|
+
tenantRoute(api, MANAGEMENT, {
|
|
161
|
+
method: "put",
|
|
162
|
+
path: "/v1/tenant/models/inputs",
|
|
163
|
+
tags: ["Tenant"],
|
|
164
|
+
summary: "Set what a catalog model reads",
|
|
165
|
+
description: "Replaces the inputs the operator added to one catalog model (Tenant setting `model.inputs`): only those in its `addableInputs`, which its provider API carries. Its catalog's own inputs always stay; an empty list removes the additions. A message naming a file the Tenant's model does not read is refused with `422 attachment_unsupported`. A custom endpoint sets `settings.inputs` on `PUT /v1/tenant/model` instead.",
|
|
166
|
+
request: { body: body(PutModelInputsRequest) },
|
|
167
|
+
responses: { 200: json(ModelInputsView, "What the model reads now") },
|
|
168
|
+
}, async (c) => {
|
|
169
|
+
const { provider, model, inputs } = PutModelInputsRequestSchema.parse(await readJson(c.req.raw));
|
|
170
|
+
if (provider === "custom")
|
|
171
|
+
fail(400, "A custom endpoint's inputs are its settings: set settings.inputs on PUT /v1/tenant/model.");
|
|
172
|
+
const found = catalogModel(provider, model);
|
|
173
|
+
if (!found)
|
|
174
|
+
fail(400, `Unknown model ${provider}/${model}.`);
|
|
175
|
+
const refused = inputsError(found, inputs);
|
|
176
|
+
if (refused)
|
|
177
|
+
fail(400, refused);
|
|
178
|
+
const added = inputs.filter((input) => addableInputs(found).includes(input));
|
|
179
|
+
await c.env.tenant.store.tx((t) => writeAddedInputs(t, `${provider}/${model}`, added));
|
|
180
|
+
return jsonResponse(200, {
|
|
181
|
+
provider,
|
|
182
|
+
model,
|
|
183
|
+
inputs: capabilitiesOf(found, added).inputs,
|
|
184
|
+
});
|
|
157
185
|
});
|
|
158
186
|
tenantRoute(api, MANAGEMENT, {
|
|
159
187
|
method: "get",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files that arrive inside a message (multimodal messages, M7): the AG-UI and A2A endpoints take
|
|
3
|
+
* a file's bytes inline (base64), store them as an artifact of the message's session, and send
|
|
4
|
+
* the message as `parts` through the session's normal command path, so every check a message
|
|
5
|
+
* with file parts meets applies. A file by URL is refused: the Runtime fetches nothing.
|
|
6
|
+
*
|
|
7
|
+
* A retried message (the same idempotency key) reuses the files the first one stored rather than
|
|
8
|
+
* storing them again, so it replays the first turn instead of conflicting with it. A message the
|
|
9
|
+
* Runtime refuses takes the files it stored with it (`discardFiles`).
|
|
10
|
+
*/
|
|
11
|
+
import type { MessagePart } from "@nylorun/core/contracts";
|
|
12
|
+
import { type ArtifactAccess } from "../artifacts/service.js";
|
|
13
|
+
import type { TenantContext } from "../tenant/context.js";
|
|
14
|
+
/** The largest file a message may carry inline, decoded. */
|
|
15
|
+
export declare const INLINE_FILE_MAX_BYTES: number;
|
|
16
|
+
/**
|
|
17
|
+
* The request body of an endpoint that takes inline files: one file at the cap, base64-encoded
|
|
18
|
+
* (a third larger), and the rest of the request.
|
|
19
|
+
*/
|
|
20
|
+
export declare const INLINE_BODY_MAX_BYTES: number;
|
|
21
|
+
/** A file sent inline: its name, declared media type and base64 bytes. */
|
|
22
|
+
export interface InlineFile {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly mediaType: string;
|
|
25
|
+
/** The bytes, base64 (standard or URL-safe alphabet). */
|
|
26
|
+
readonly data: string;
|
|
27
|
+
}
|
|
28
|
+
/** One part of an inbound message: text, a file sent inline, or an artifact the Runtime holds. */
|
|
29
|
+
export type InboundPart = {
|
|
30
|
+
readonly type: "text";
|
|
31
|
+
readonly text: string;
|
|
32
|
+
} | ({
|
|
33
|
+
readonly type: "inline";
|
|
34
|
+
} & InlineFile) | {
|
|
35
|
+
readonly type: "artifact";
|
|
36
|
+
readonly artifactId: string;
|
|
37
|
+
};
|
|
38
|
+
/** The 400 for a file sent by URL. */
|
|
39
|
+
export declare function refuseUrl(): never;
|
|
40
|
+
/** A name for a file its client did not name: its place in the message and its type's extension. */
|
|
41
|
+
export declare function inlineFileName(mediaType: string, index: number): string;
|
|
42
|
+
/**
|
|
43
|
+
* Stores `file` as a new artifact of `sessionId`, within the inline cap and the Tenant's
|
|
44
|
+
* artifact limits: `400` for bad base64 or a bad media type, `413 limit_exceeded` past a limit.
|
|
45
|
+
*/
|
|
46
|
+
export declare function storeInlineFile(ctx: TenantContext, sessionId: string, file: InlineFile, access: ArtifactAccess): Promise<{
|
|
47
|
+
artifactId: string;
|
|
48
|
+
version: number;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* The parts of the message the session already took under `idempotencyKey`, when it took one
|
|
52
|
+
* with parts: a retry sends the same files again.
|
|
53
|
+
*/
|
|
54
|
+
export declare function priorParts(ctx: TenantContext, sessionId: string, idempotencyKey: string): Promise<readonly MessagePart[] | undefined>;
|
|
55
|
+
/**
|
|
56
|
+
* A message's parts, its inline files stored as artifacts of `sessionId` (or, on a retry, the
|
|
57
|
+
* first message's files in their place, by position). `stored` lists the artifacts this call
|
|
58
|
+
* created, for `discardFiles` when the message is refused.
|
|
59
|
+
*/
|
|
60
|
+
export declare function messageParts(ctx: TenantContext, sessionId: string, idempotencyKey: string, inbound: readonly InboundPart[], access: ArtifactAccess): Promise<{
|
|
61
|
+
parts: MessagePart[];
|
|
62
|
+
stored: string[];
|
|
63
|
+
}>;
|
|
64
|
+
/** Deletes the artifacts a refused message stored; one already gone is no error. */
|
|
65
|
+
export declare function discardFiles(ctx: TenantContext, artifactIds: readonly string[], access: ArtifactAccess): Promise<void>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { MAX_MESSAGE_PARTS } from "@nylorun/core/contracts";
|
|
2
|
+
import { commandKey } from "../core/flow-host.js";
|
|
3
|
+
import { extensionFor, normalizeContentType } from "../artifacts/media-types.js";
|
|
4
|
+
import { deleteArtifact, uploadArtifact } from "../artifacts/service.js";
|
|
5
|
+
import { fail } from "../tenant/http.js";
|
|
6
|
+
/** The largest file a message may carry inline, decoded. */
|
|
7
|
+
export const INLINE_FILE_MAX_BYTES = 20 * 1024 * 1024;
|
|
8
|
+
/**
|
|
9
|
+
* The request body of an endpoint that takes inline files: one file at the cap, base64-encoded
|
|
10
|
+
* (a third larger), and the rest of the request.
|
|
11
|
+
*/
|
|
12
|
+
export const INLINE_BODY_MAX_BYTES = 28 * 1024 * 1024;
|
|
13
|
+
const BASE64 = /^[A-Za-z0-9+/_-]*={0,2}$/;
|
|
14
|
+
/** The 400 for a file sent by URL. */
|
|
15
|
+
export function refuseUrl() {
|
|
16
|
+
return fail(400, "File URLs are not fetched yet; send the bytes inline", {
|
|
17
|
+
code: "invalid_request",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** A name for a file its client did not name: its place in the message and its type's extension. */
|
|
21
|
+
export function inlineFileName(mediaType, index) {
|
|
22
|
+
const extension = extensionFor(mediaType);
|
|
23
|
+
return `file-${index + 1}${extension ? `.${extension}` : ""}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Stores `file` as a new artifact of `sessionId`, within the inline cap and the Tenant's
|
|
27
|
+
* artifact limits: `400` for bad base64 or a bad media type, `413 limit_exceeded` past a limit.
|
|
28
|
+
*/
|
|
29
|
+
export async function storeInlineFile(ctx, sessionId, file, access) {
|
|
30
|
+
const data = file.data.replace(/\s+/gu, "");
|
|
31
|
+
if (!BASE64.test(data) || data.length % 4 === 1)
|
|
32
|
+
fail(400, `File "${file.name}" is not valid base64`, { code: "invalid_request" });
|
|
33
|
+
// Three bytes per four characters, before decoding a byte.
|
|
34
|
+
if (Math.floor((data.length * 3) / 4) > INLINE_FILE_MAX_BYTES + 2)
|
|
35
|
+
fail(413, `A file sent inline may hold at most ${INLINE_FILE_MAX_BYTES} bytes`, {
|
|
36
|
+
code: "limit_exceeded",
|
|
37
|
+
details: { limitBytes: INLINE_FILE_MAX_BYTES },
|
|
38
|
+
});
|
|
39
|
+
// Node's base64 decoder reads both alphabets.
|
|
40
|
+
const bytes = Buffer.from(data, "base64");
|
|
41
|
+
if (bytes.byteLength > INLINE_FILE_MAX_BYTES)
|
|
42
|
+
fail(413, `A file sent inline may hold at most ${INLINE_FILE_MAX_BYTES} bytes`, {
|
|
43
|
+
code: "limit_exceeded",
|
|
44
|
+
details: { limitBytes: INLINE_FILE_MAX_BYTES },
|
|
45
|
+
});
|
|
46
|
+
if (bytes.byteLength === 0)
|
|
47
|
+
fail(400, `File "${file.name}" is empty`, { code: "invalid_request" });
|
|
48
|
+
const contentType = normalizeContentType(file.mediaType) ??
|
|
49
|
+
fail(400, `File "${file.name}" has no valid media type`, { code: "invalid_request" });
|
|
50
|
+
const { artifact, version } = await uploadArtifact(ctx, {
|
|
51
|
+
name: file.name,
|
|
52
|
+
contentType,
|
|
53
|
+
sessionId,
|
|
54
|
+
declaredBytes: bytes.byteLength,
|
|
55
|
+
source: "upload",
|
|
56
|
+
}, new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), access);
|
|
57
|
+
return { artifactId: artifact.artifactId, version: version.version };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The parts of the message the session already took under `idempotencyKey`, when it took one
|
|
61
|
+
* with parts: a retry sends the same files again.
|
|
62
|
+
*/
|
|
63
|
+
export async function priorParts(ctx, sessionId, idempotencyKey) {
|
|
64
|
+
const prior = await ctx.store.tx((t) => t.get("commands", commandKey(sessionId, idempotencyKey)));
|
|
65
|
+
return prior?.command.type === "message" && Array.isArray(prior.command.parts)
|
|
66
|
+
? prior.command.parts
|
|
67
|
+
: undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A message's parts, its inline files stored as artifacts of `sessionId` (or, on a retry, the
|
|
71
|
+
* first message's files in their place, by position). `stored` lists the artifacts this call
|
|
72
|
+
* created, for `discardFiles` when the message is refused.
|
|
73
|
+
*/
|
|
74
|
+
export async function messageParts(ctx, sessionId, idempotencyKey, inbound, access) {
|
|
75
|
+
const parts = inbound.filter((part) => part.type !== "text" || part.text !== "");
|
|
76
|
+
if (parts.length > MAX_MESSAGE_PARTS)
|
|
77
|
+
fail(400, `A message may carry at most ${MAX_MESSAGE_PARTS} parts`, { code: "invalid_request" });
|
|
78
|
+
const prior = parts.some((part) => part.type === "inline")
|
|
79
|
+
? (await priorParts(ctx, sessionId, idempotencyKey))?.filter((part) => part.type === "file")
|
|
80
|
+
: undefined;
|
|
81
|
+
const out = [];
|
|
82
|
+
const stored = [];
|
|
83
|
+
let files = 0;
|
|
84
|
+
try {
|
|
85
|
+
for (const part of parts) {
|
|
86
|
+
if (part.type === "text") {
|
|
87
|
+
out.push({ type: "text", text: part.text });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const index = files++;
|
|
91
|
+
if (part.type === "artifact") {
|
|
92
|
+
out.push({ type: "file", artifactId: part.artifactId });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const again = prior?.[index];
|
|
96
|
+
if (again) {
|
|
97
|
+
out.push(again);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const file = await storeInlineFile(ctx, sessionId, part, access);
|
|
101
|
+
stored.push(file.artifactId);
|
|
102
|
+
out.push({ type: "file", artifactId: file.artifactId, version: file.version });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
await discardFiles(ctx, stored, access);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return { parts: out, stored };
|
|
110
|
+
}
|
|
111
|
+
/** Deletes the artifacts a refused message stored; one already gone is no error. */
|
|
112
|
+
export async function discardFiles(ctx, artifactIds, access) {
|
|
113
|
+
for (const id of artifactIds)
|
|
114
|
+
await deleteArtifact(ctx, id, access).catch(() => undefined);
|
|
115
|
+
}
|
package/dist/api/openapi.js
CHANGED
|
@@ -160,7 +160,7 @@ const RUNTIME_TAGS = [
|
|
|
160
160
|
{
|
|
161
161
|
name: "Environments",
|
|
162
162
|
group: "Environments",
|
|
163
|
-
description: "Where sessions run: one sandbox (a virtual workspace, a pod with its own volume, or none)
|
|
163
|
+
description: "Where sessions run: one sandbox (a virtual workspace, a pod with its own volume, or none) made from an environment manifest or inline fields and never changed after that, and one vault of secrets for its shell, MCP servers and HTTP tools. Create or find one, watch its lifecycle, stop, reset or delete it, and write its secrets. In `@nylorun/agents`.",
|
|
164
164
|
operations: [
|
|
165
165
|
"PUT /v1/environments/{environmentId}",
|
|
166
166
|
"GET /v1/environments",
|
|
@@ -175,10 +175,24 @@ const RUNTIME_TAGS = [
|
|
|
175
175
|
"DELETE /v1/environments/{environmentId}/secrets/{name}",
|
|
176
176
|
"POST /v1/environments/{environmentId}/snapshot",
|
|
177
177
|
"POST /v1/environments/{environmentId}/restore",
|
|
178
|
+
"POST /v1/environments/{environmentId}/fork",
|
|
178
179
|
"GET /v1/environments/{environmentId}/versions",
|
|
179
180
|
"DELETE /v1/environments/{environmentId}/versions/{version}",
|
|
180
181
|
],
|
|
181
182
|
},
|
|
183
|
+
{
|
|
184
|
+
name: "Environment manifests",
|
|
185
|
+
group: "Environments",
|
|
186
|
+
description: "Named, versioned definitions of a kind of Environment, saved like agents (protocol 14). Save one with a plan first (`?dryRun=true`) and `If-Match`; an Environment is made from one version and keeps it, so a save changes no Environment. In `@nylorun/agents`.",
|
|
187
|
+
operations: [
|
|
188
|
+
"PUT /v1/environment-manifests/{name}",
|
|
189
|
+
"GET /v1/environment-manifests",
|
|
190
|
+
"GET /v1/environment-manifests/{name}",
|
|
191
|
+
"GET /v1/environment-manifests/{name}/versions",
|
|
192
|
+
"GET /v1/environment-manifests/{name}/versions/{version}",
|
|
193
|
+
"DELETE /v1/environment-manifests/{name}",
|
|
194
|
+
],
|
|
195
|
+
},
|
|
182
196
|
{
|
|
183
197
|
name: "Artifacts",
|
|
184
198
|
group: "Artifacts",
|
|
@@ -231,9 +245,10 @@ const MANAGEMENT_TAGS = [
|
|
|
231
245
|
},
|
|
232
246
|
{
|
|
233
247
|
name: "Models",
|
|
234
|
-
description: "Model providers, their credentials, the model the Tenant uses, what it spent, every model call for export, and the budgets that cap it. `admin.models`.",
|
|
248
|
+
description: "Model providers, their credentials, the model the Tenant uses, what each model reads, what it spent, every model call for export, and the budgets that cap it. `admin.models`.",
|
|
235
249
|
operations: [
|
|
236
250
|
"GET /v1/tenant/models",
|
|
251
|
+
"PUT /v1/tenant/models/inputs",
|
|
237
252
|
"GET /v1/tenant/providers",
|
|
238
253
|
"GET /v1/tenant/model",
|
|
239
254
|
"PUT /v1/tenant/model",
|
|
@@ -377,7 +392,7 @@ function documents() {
|
|
|
377
392
|
}),
|
|
378
393
|
management: document(all, MANAGEMENT_TAGS, {
|
|
379
394
|
title: "Nylorun Management API",
|
|
380
|
-
description: "For operators: the Tenant's models, vaults, signing keys, settings and application keys, at `/v1/tenant/*` on the Tenant's URL, through `@nylorun/admin`. Every operation takes a management key, acting as itself: never for a subject and never from a browser. Management keys are issued only on the Tenant's machine (`nylorun
|
|
395
|
+
description: "For operators: the Tenant's models, vaults, signing keys, settings and application keys, at `/v1/tenant/*` on the Tenant's URL, through `@nylorun/admin`. Every operation takes a management key, acting as itself: never for a subject and never from a browser. Management keys are issued only on the Tenant's machine (`nylorun tenant keys put <id> --management`) or from `NYLORUN_MANAGEMENT_KEY_FILE`.",
|
|
381
396
|
}),
|
|
382
397
|
};
|
|
383
398
|
return built;
|
package/dist/artifacts/disk.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* version back: directories, files (large ones in appended ranges), then links.
|
|
14
14
|
* - **Restore** puts a version back: on a virtual Environment at once; on a pod as a reset whose
|
|
15
15
|
* new volume is seeded before any turn (`disk_seed`, run once the new pod's engine connects).
|
|
16
|
-
* **Fork** (`from` on a create) and the **recovery of a lost volume** (from
|
|
17
|
-
* are seeds too.
|
|
16
|
+
* **Fork** (`from` on a create, and `POST .../fork`) and the **recovery of a lost volume** (from
|
|
17
|
+
* the latest version) are seeds too. A fork onto other setup (Q17) keeps its seed, `written`,
|
|
18
|
+
* until the engine has run that setup on the written disk; turns wait for both.
|
|
18
19
|
*
|
|
19
20
|
* Nothing here runs inside a transaction but the lease and the commit. No event type is new:
|
|
20
21
|
* a restore or recovery is `environment.reset` with `reason` and `from`.
|
|
@@ -23,7 +24,6 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
23
24
|
import { newArtifactId } from "@nylorun/core/compatibility";
|
|
24
25
|
import { FOLDER_CONTENT_TYPE, FOLDER_MANIFEST_FORMAT_V2, } from "@nylorun/core/contracts";
|
|
25
26
|
import { sandboxSpecOf } from "../sandbox/resolve.js";
|
|
26
|
-
import { capabilityForSandbox } from "../sandbox/share.js";
|
|
27
27
|
import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
|
|
28
28
|
import { assertReaches, diskHeld, liveEnvironment, newVolume, SEED_LEASE_MS, wakePod, } from "../tenant/environments.js";
|
|
29
29
|
import { fail } from "../tenant/http.js";
|
|
@@ -51,15 +51,17 @@ async function diskTarget(ctx, environment) {
|
|
|
51
51
|
ref: {
|
|
52
52
|
id: `disk:${environment.id}`,
|
|
53
53
|
activeTurnId: null,
|
|
54
|
-
manifest: { id: "disk"
|
|
54
|
+
manifest: { manifestSchemaVersion: 7, id: "disk" },
|
|
55
55
|
environmentId: environment.id,
|
|
56
|
+
// Not through a host a restore or a lost volume retired: its connection outlives the commit.
|
|
57
|
+
...(environment.pod === undefined ? {} : { hostEpoch: environment.pod.hostEpoch }),
|
|
56
58
|
},
|
|
57
|
-
|
|
59
|
+
spec,
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
/** One disk operation through the workspace, or a `409 environment_unavailable`. */
|
|
61
63
|
async function diskOp(ctx, target, op) {
|
|
62
|
-
const answer = await ctx.sandbox.disk(target.ref, target.
|
|
64
|
+
const answer = await ctx.sandbox.disk(target.ref, target.spec, op, new AbortController().signal);
|
|
63
65
|
if ("code" in answer)
|
|
64
66
|
return fail(409, `The disk could not be reached: ${answer.message}`, { code: "environment_unavailable" });
|
|
65
67
|
return answer;
|
|
@@ -111,10 +113,13 @@ function heartbeat(ctx, id, ms = LEASE_MS) {
|
|
|
111
113
|
const environment = await t.environment(id, { lock: true });
|
|
112
114
|
if (!environment)
|
|
113
115
|
return;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const seed = environment.diskSeed;
|
|
117
|
+
const written = options.seed !== undefined &&
|
|
118
|
+
seed?.artifactId === options.seed.artifactId &&
|
|
119
|
+
seed.version === options.seed.version;
|
|
120
|
+
// A seed with setup stays, written, until the engine reports the setup done (Q17).
|
|
121
|
+
const next = !written ? {} : seed.setup ? { diskSeed: { ...seed, written: true } } : { diskSeed: null };
|
|
122
|
+
await t.updateEnvironment(id, { diskOp: null, ...next }, new Date().toISOString());
|
|
118
123
|
});
|
|
119
124
|
},
|
|
120
125
|
};
|
|
@@ -437,11 +442,12 @@ export async function runPendingSeed(ctx, id) {
|
|
|
437
442
|
try {
|
|
438
443
|
const environment = await ctx.store.tx((t) => liveEnvironment(t, id));
|
|
439
444
|
const seed = environment?.diskSeed;
|
|
440
|
-
if (!environment || !seed)
|
|
445
|
+
if (!environment || !seed || seed.written)
|
|
441
446
|
return;
|
|
442
447
|
if (environment.kind === "pod") {
|
|
443
448
|
const deadline = Date.now() + ENGINE_WAIT_MS;
|
|
444
|
-
|
|
449
|
+
// The engine on the new volume: the old one's connection may still be open.
|
|
450
|
+
while (!ctx.harness.hosting(id, environment.pod?.hostEpoch)) {
|
|
445
451
|
if (Date.now() > deadline)
|
|
446
452
|
return;
|
|
447
453
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
@@ -471,9 +477,9 @@ export async function runPendingSeed(ctx, id) {
|
|
|
471
477
|
}
|
|
472
478
|
/** Starts the pending seeds whose engine is connected and whose lease lapsed (Tenant sweep). */
|
|
473
479
|
export async function sweepPendingSeeds(ctx) {
|
|
474
|
-
const pending = (await ctx.store.tx((t) => t.listEnvironments())).filter((environment) => environment.diskSeed !== undefined && environment.pod?.desired !== "deleted");
|
|
480
|
+
const pending = (await ctx.store.tx((t) => t.listEnvironments())).filter((environment) => environment.diskSeed !== undefined && !environment.diskSeed.written && environment.pod?.desired !== "deleted");
|
|
475
481
|
for (const environment of pending)
|
|
476
|
-
if (environment.kind !== "pod" || ctx.harness.hosting(environment.id))
|
|
482
|
+
if (environment.kind !== "pod" || ctx.harness.hosting(environment.id, environment.pod?.hostEpoch))
|
|
477
483
|
void runPendingSeed(ctx, environment.id);
|
|
478
484
|
}
|
|
479
485
|
/**
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media types of file artifacts: what an upload may declare, what a name's extension implies,
|
|
3
|
+
* and which model input a file is (`modelInputOf`, protocol 12): text, an image, a PDF, an Office
|
|
4
|
+
* document, audio or video. A model reads a file only when its inputs include that one.
|
|
5
|
+
*/
|
|
6
|
+
import type { ModelInput } from "@nylorun/core/contracts";
|
|
1
7
|
/** Images the model reads as images. */
|
|
2
8
|
export declare const MODEL_IMAGE_TYPES: readonly string[];
|
|
3
9
|
/** The largest image or text file a model call carries inline. */
|
|
@@ -13,3 +19,12 @@ export declare function isModelImage(contentType: string): boolean;
|
|
|
13
19
|
export declare function isText(contentType: string): boolean;
|
|
14
20
|
/** The usual extension of a media type, for a name the Runtime gives a file; undefined if none. */
|
|
15
21
|
export declare function extensionFor(contentType: string): string | undefined;
|
|
22
|
+
/** The model input a file of `contentType` is, or undefined for a type no model reads. */
|
|
23
|
+
export declare function modelInputOf(contentType: string): ModelInput | undefined;
|
|
24
|
+
/** An input, in a sentence: "PDFs", "Office documents"; "files of this type" for none. */
|
|
25
|
+
export declare function inputLabel(input: ModelInput | undefined): string;
|
|
26
|
+
/**
|
|
27
|
+
* Why a model that reads `inputs` cannot read a file of `contentType`, or undefined when it can.
|
|
28
|
+
* An image must also be PNG, JPEG, GIF or WebP.
|
|
29
|
+
*/
|
|
30
|
+
export declare function unreadableReason(contentType: string, inputs: readonly ModelInput[]): string | undefined;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Media types of file artifacts: what an upload may declare, what a name's extension implies,
|
|
3
|
-
* and how the model reads a file (an image as an image, text as text, anything else refused).
|
|
4
|
-
*/
|
|
5
1
|
import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
|
|
6
2
|
const MEDIA_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$/;
|
|
7
3
|
const BY_EXTENSION = {
|
|
@@ -12,6 +8,12 @@ const BY_EXTENSION = {
|
|
|
12
8
|
webp: "image/webp",
|
|
13
9
|
svg: "image/svg+xml",
|
|
14
10
|
pdf: "application/pdf",
|
|
11
|
+
doc: "application/msword",
|
|
12
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
13
|
+
xls: "application/vnd.ms-excel",
|
|
14
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
15
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
16
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
15
17
|
txt: "text/plain",
|
|
16
18
|
md: "text/markdown",
|
|
17
19
|
csv: "text/csv",
|
|
@@ -83,3 +85,53 @@ export function extensionFor(contentType) {
|
|
|
83
85
|
const type = essence(contentType);
|
|
84
86
|
return Object.keys(BY_EXTENSION).find((extension) => BY_EXTENSION[extension] === type);
|
|
85
87
|
}
|
|
88
|
+
/** Word, Excel and PowerPoint documents, old and Open XML. */
|
|
89
|
+
function isOffice(type) {
|
|
90
|
+
return (type === "application/msword" ||
|
|
91
|
+
type === "application/vnd.ms-excel" ||
|
|
92
|
+
type === "application/vnd.ms-powerpoint" ||
|
|
93
|
+
type.startsWith("application/vnd.openxmlformats-officedocument."));
|
|
94
|
+
}
|
|
95
|
+
/** The model input a file of `contentType` is, or undefined for a type no model reads. */
|
|
96
|
+
export function modelInputOf(contentType) {
|
|
97
|
+
const type = essence(contentType);
|
|
98
|
+
if (isText(type))
|
|
99
|
+
return "text";
|
|
100
|
+
if (type.startsWith("image/"))
|
|
101
|
+
return "image";
|
|
102
|
+
if (type === "application/pdf")
|
|
103
|
+
return "pdf";
|
|
104
|
+
if (isOffice(type))
|
|
105
|
+
return "office";
|
|
106
|
+
if (type.startsWith("audio/"))
|
|
107
|
+
return "audio";
|
|
108
|
+
if (type.startsWith("video/"))
|
|
109
|
+
return "video";
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const LABELS = {
|
|
113
|
+
text: "text files",
|
|
114
|
+
image: "images",
|
|
115
|
+
pdf: "PDFs",
|
|
116
|
+
office: "Office documents",
|
|
117
|
+
audio: "audio",
|
|
118
|
+
video: "video",
|
|
119
|
+
};
|
|
120
|
+
/** An input, in a sentence: "PDFs", "Office documents"; "files of this type" for none. */
|
|
121
|
+
export function inputLabel(input) {
|
|
122
|
+
return input === undefined ? "files of this type" : LABELS[input];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Why a model that reads `inputs` cannot read a file of `contentType`, or undefined when it can.
|
|
126
|
+
* An image must also be PNG, JPEG, GIF or WebP.
|
|
127
|
+
*/
|
|
128
|
+
export function unreadableReason(contentType, inputs) {
|
|
129
|
+
const input = modelInputOf(contentType);
|
|
130
|
+
if (input === undefined)
|
|
131
|
+
return `no model reads ${essence(contentType)} files`;
|
|
132
|
+
if (!inputs.includes(input))
|
|
133
|
+
return `this model does not read ${LABELS[input]}`;
|
|
134
|
+
if (input === "image" && !isModelImage(contentType))
|
|
135
|
+
return "the model reads PNG, JPEG, GIF and WebP images only";
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* The engine gets an opaque media part whose `reference` is `{ artifactId, version }`: model-gate
|
|
6
6
|
* resolves it to bytes (`./files.ts`), and the transcript and the record never hold them.
|
|
7
7
|
*/
|
|
8
|
-
import type { MessagePart } from "@nylorun/core/contracts";
|
|
8
|
+
import type { MessagePart, ModelInput } from "@nylorun/core/contracts";
|
|
9
9
|
import type { JsonObject, JsonValue } from "@nylorun/core/define";
|
|
10
10
|
import type { Tx } from "../store/types.js";
|
|
11
11
|
import { type ArtifactAccess } from "./service.js";
|
|
@@ -24,11 +24,20 @@ export interface FileReference {
|
|
|
24
24
|
readonly version: number;
|
|
25
25
|
}
|
|
26
26
|
export declare function fileReferenceOf(value: JsonValue | undefined): FileReference | undefined;
|
|
27
|
+
/** A file part the Tenant's model does not read (protocol 12, `attachment_unsupported`). */
|
|
28
|
+
export interface RefusedPart {
|
|
29
|
+
readonly artifactId: string;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly mediaType: string;
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
}
|
|
27
34
|
/**
|
|
28
35
|
* The parts with each file pinned to a version: for the engine (`engine`) and for a workflow's
|
|
29
|
-
* JSON input (`pinned`). Reads in the caller's transaction.
|
|
36
|
+
* JSON input (`pinned`). Reads in the caller's transaction. With `inputs`, what the Tenant's model
|
|
37
|
+
* reads, each file of a type it does not read is in `refused` (its type only: no size is checked).
|
|
30
38
|
*/
|
|
31
|
-
export declare function resolveMessageParts(t: Tx, parts: readonly MessagePart[], sessionId: string, access: ArtifactAccess): Promise<{
|
|
39
|
+
export declare function resolveMessageParts(t: Tx, parts: readonly MessagePart[], sessionId: string, access: ArtifactAccess, inputs?: readonly ModelInput[]): Promise<{
|
|
32
40
|
engine: EnginePart[];
|
|
33
41
|
pinned: MessagePart[];
|
|
42
|
+
refused: RefusedPart[];
|
|
34
43
|
}>;
|
package/dist/artifacts/parts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fail } from "../tenant/http.js";
|
|
2
|
+
import { unreadableReason } from "./media-types.js";
|
|
2
3
|
import { readableArtifact, versionOf } from "./service.js";
|
|
3
4
|
export function fileReferenceOf(value) {
|
|
4
5
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -10,11 +11,13 @@ export function fileReferenceOf(value) {
|
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* The parts with each file pinned to a version: for the engine (`engine`) and for a workflow's
|
|
13
|
-
* JSON input (`pinned`). Reads in the caller's transaction.
|
|
14
|
+
* JSON input (`pinned`). Reads in the caller's transaction. With `inputs`, what the Tenant's model
|
|
15
|
+
* reads, each file of a type it does not read is in `refused` (its type only: no size is checked).
|
|
14
16
|
*/
|
|
15
|
-
export async function resolveMessageParts(t, parts, sessionId, access) {
|
|
17
|
+
export async function resolveMessageParts(t, parts, sessionId, access, inputs) {
|
|
16
18
|
const engine = [];
|
|
17
19
|
const pinned = [];
|
|
20
|
+
const refused = [];
|
|
18
21
|
for (const part of parts) {
|
|
19
22
|
if (part.type === "text") {
|
|
20
23
|
engine.push({ type: "text", text: part.text });
|
|
@@ -27,12 +30,20 @@ export async function resolveMessageParts(t, parts, sessionId, access) {
|
|
|
27
30
|
if (artifact.kind !== "file")
|
|
28
31
|
fail(400, `Artifact ${artifact.id} is a folder; a file part names a file artifact`);
|
|
29
32
|
const version = await versionOf(t, artifact, part.version);
|
|
33
|
+
const reason = inputs && unreadableReason(version.contentType, inputs);
|
|
34
|
+
if (reason)
|
|
35
|
+
refused.push({ artifactId: artifact.id, name: artifact.name, mediaType: version.contentType, reason });
|
|
30
36
|
engine.push({
|
|
31
37
|
type: "media",
|
|
32
38
|
mediaType: version.contentType,
|
|
33
|
-
reference: {
|
|
39
|
+
reference: {
|
|
40
|
+
artifactId: artifact.id,
|
|
41
|
+
version: version.version,
|
|
42
|
+
name: artifact.name,
|
|
43
|
+
size: version.size,
|
|
44
|
+
},
|
|
34
45
|
});
|
|
35
46
|
pinned.push({ type: "file", artifactId: artifact.id, version: version.version });
|
|
36
47
|
}
|
|
37
|
-
return { engine, pinned };
|
|
48
|
+
return { engine, pinned, refused };
|
|
38
49
|
}
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* export its outputs as a folder artifact. Two calls, a listing and one file's bytes, both the
|
|
4
4
|
* claim of whoever holds the workspace: the Runtime stores what it is given without observing it.
|
|
5
5
|
*
|
|
6
|
-
* One implementation, `sandboxWorkspaceReader`, over the workspace
|
|
6
|
+
* One implementation, `sandboxWorkspaceReader`, over the workspace feature (F6.2,
|
|
7
7
|
* `harness-api/workspace.ts`): the in-process `SandboxManager`, or the harness that serves the
|
|
8
8
|
* Tenant's workspaces (`workspace.read`). F6.2 reimplements it over the Harness API's `workspace.read`, and F7.2's pod
|
|
9
9
|
* sandboxes are read through the same seam, so the export never knows which kind of sandbox it
|
|
10
10
|
* reads.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import type { AgentManifest } from "@nylorun/core/define";
|
|
13
13
|
import { type SandboxManager } from "../sandbox/manager.js";
|
|
14
|
-
/** What the reader needs of a workspace: a `SandboxManager`, or the workspace
|
|
14
|
+
/** What the reader needs of a workspace: a `SandboxManager`, or the workspace feature. */
|
|
15
15
|
export type WorkspaceFiles = Pick<SandboxManager, "listFiles" | "readBytes">;
|
|
16
16
|
/** Whose workspace to read: the session whose turn ended, resolved to the workspace it uses. */
|
|
17
17
|
export interface WorkspaceSession {
|
|
@@ -23,7 +23,7 @@ export interface WorkspaceSession {
|
|
|
23
23
|
readonly ownerId: string;
|
|
24
24
|
/** The sandbox resource the workspace belongs to, when the owner is attached to one. */
|
|
25
25
|
readonly environmentId?: string;
|
|
26
|
-
/** The session's pinned manifest: its
|
|
26
|
+
/** The session's pinned manifest: its `runtime` block carries the sandbox's spec. */
|
|
27
27
|
readonly manifest: AgentManifest;
|
|
28
28
|
}
|
|
29
29
|
/** One regular file under the listed directory. */
|
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
* The `WorkspaceReader` seam (F8.2): how core reads a session's sandbox workspace at turn end, to
|
|
3
|
-
* export its outputs as a folder artifact. Two calls, a listing and one file's bytes, both the
|
|
4
|
-
* claim of whoever holds the workspace: the Runtime stores what it is given without observing it.
|
|
5
|
-
*
|
|
6
|
-
* One implementation, `sandboxWorkspaceReader`, over the workspace capability (F6.2,
|
|
7
|
-
* `harness-api/workspace.ts`): the in-process `SandboxManager`, or the harness that serves the
|
|
8
|
-
* Tenant's workspaces (`workspace.read`). F6.2 reimplements it over the Harness API's `workspace.read`, and F7.2's pod
|
|
9
|
-
* sandboxes are read through the same seam, so the export never knows which kind of sandbox it
|
|
10
|
-
* reads.
|
|
11
|
-
*/
|
|
12
|
-
import { SANDBOX_CAPABILITY_ID, } from "@nylorun/core/define";
|
|
13
|
-
import { sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
1
|
+
import { sandboxToolSpec } from "../sandbox/manager.js";
|
|
14
2
|
import { SandboxFileTooLargeError } from "../sandbox/types.js";
|
|
15
3
|
/** The reader over the workspaces, in each sandbox's queue of tool calls. */
|
|
16
4
|
export function sandboxWorkspaceReader(sandbox) {
|
|
17
5
|
const target = (session) => {
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
6
|
+
const spec = sandboxToolSpec(session.manifest, "read");
|
|
7
|
+
if (!spec)
|
|
20
8
|
return undefined;
|
|
21
9
|
return {
|
|
22
|
-
|
|
10
|
+
spec,
|
|
23
11
|
ref: {
|
|
24
12
|
id: session.ownerId,
|
|
25
13
|
activeTurnId: session.turnId,
|
|
@@ -33,7 +21,7 @@ export function sandboxWorkspaceReader(sandbox) {
|
|
|
33
21
|
const found = target(session);
|
|
34
22
|
if (!found)
|
|
35
23
|
return undefined;
|
|
36
|
-
const listed = await sandbox.listFiles(found.ref, found.
|
|
24
|
+
const listed = await sandbox.listFiles(found.ref, found.spec, dir, options.maxEntries, options.signal);
|
|
37
25
|
if (listed.kind === "missing")
|
|
38
26
|
return undefined;
|
|
39
27
|
if (listed.kind === "failed")
|
|
@@ -44,7 +32,7 @@ export function sandboxWorkspaceReader(sandbox) {
|
|
|
44
32
|
const found = target(session);
|
|
45
33
|
if (!found)
|
|
46
34
|
return undefined;
|
|
47
|
-
const read = await sandbox.readBytes(found.ref, found.
|
|
35
|
+
const read = await sandbox.readBytes(found.ref, found.spec, path, options.maxBytes, options.signal);
|
|
48
36
|
if (read.kind === "read")
|
|
49
37
|
return read.bytes;
|
|
50
38
|
if (read.kind === "missing")
|
package/dist/core/flow-host.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function deriveAgentEffectSessionId(workflowSessionId: string, pa
|
|
|
16
16
|
readonly context?: Record<string, unknown>;
|
|
17
17
|
}): string;
|
|
18
18
|
export declare function isWorkflowManifest(manifest: unknown): manifest is WorkflowManifest;
|
|
19
|
-
/** Flow tool-node effect: `tool` with path/key and no agent
|
|
19
|
+
/** Flow tool-node effect: `tool` with path/key and no tool name (an agent's tool call names one). */
|
|
20
20
|
export declare function isFlowToolEffect(request: HostEffect): boolean;
|
|
21
21
|
export declare function isFlowEffect(request: HostEffect): boolean;
|
|
22
22
|
/**
|