@nylorun/runtime 0.15.1-beta → 0.16.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 +82 -0
- package/README.md +33 -6
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/replication/pgoutput.js +3 -1
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +37 -3
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.js +5 -1
- package/dist/api/components.d.ts +2673 -655
- package/dist/api/components.js +17 -1
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +4 -0
- package/dist/api/http/define.d.ts +10 -0
- package/dist/api/http/define.js +2 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +518 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +152 -0
- package/dist/api/http/routes/sessions.js +3 -1
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/app.js +23 -2
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +46 -1
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +135 -27
- package/dist/host/stack-config.d.ts +101 -1
- package/dist/host/stack-config.js +199 -8
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +15245 -8850
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +894 -0
- package/dist/store/postgres/schema.js +130 -2
- package/dist/store/postgres/store.js +289 -4
- package/dist/store/types.d.ts +143 -6
- package/dist/streams/control.d.ts +2 -0
- package/dist/streams/control.js +5 -0
- package/dist/streams/types.d.ts +10 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/commands.js +30 -5
- package/dist/tenant/context.d.ts +64 -5
- package/dist/tenant/effects.d.ts +19 -13
- package/dist/tenant/effects.js +5 -382
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +38 -0
- package/dist/tenant/runtime.js +120 -32
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +5 -0
- package/dist/tenant/sessions.js +28 -1
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +7 -0
- package/dist/tenant/streams.js +24 -1
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/tokens.js +9 -2
- package/dist/tenant/types.d.ts +18 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isSandboxId, PutSandboxRequestSchema } from "@nylorun/core/contracts";
|
|
3
|
+
import { DeleteSandboxResponse, ListSandboxEventsResponse, ListSandboxesResponse, PutSandboxRequest, SandboxView, } from "../../components.js";
|
|
4
|
+
import { fail } from "../../../tenant/http.js";
|
|
5
|
+
import { deleteSandbox, getSandbox, listSandboxes, putSandbox, resetSandbox, sandboxEventsOf, stopSandbox, } from "../../../tenant/sandboxes.js";
|
|
6
|
+
import { readJson } from "../body.js";
|
|
7
|
+
import { tenantRoute } from "../define.js";
|
|
8
|
+
import { jsonResponse } from "../respond.js";
|
|
9
|
+
/** Reading a sandbox: any caller that may use sessions, or change sandboxes. */
|
|
10
|
+
const READ = {
|
|
11
|
+
credentials: ["application", "subject", "token"],
|
|
12
|
+
scopes: ["sessions:own", "sandboxes:write"],
|
|
13
|
+
browser: true,
|
|
14
|
+
};
|
|
15
|
+
/** Creating or deleting one. */
|
|
16
|
+
const WRITE = {
|
|
17
|
+
credentials: ["application", "subject", "token"],
|
|
18
|
+
scopes: ["sandboxes:write"],
|
|
19
|
+
browser: true,
|
|
20
|
+
};
|
|
21
|
+
const json = (schema, description) => ({
|
|
22
|
+
description,
|
|
23
|
+
content: { "application/json": { schema } },
|
|
24
|
+
});
|
|
25
|
+
const sandboxId = z.object({
|
|
26
|
+
sandboxId: z
|
|
27
|
+
.string()
|
|
28
|
+
.meta({ description: "The sandbox id, percent-encoded: `team-a%2Fproj-42` for `team-a/proj-42`" }),
|
|
29
|
+
});
|
|
30
|
+
/** The path's sandbox id, or a 400 naming the rule. */
|
|
31
|
+
function idOf(value) {
|
|
32
|
+
if (!isSandboxId(value))
|
|
33
|
+
return fail(400, "A sandbox id is up to 200 characters: /-separated segments of letters, digits, '.', '_' and '-'");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/** `label=k=v`, repeated: every one must match. */
|
|
37
|
+
function labelsOf(values) {
|
|
38
|
+
const labels = {};
|
|
39
|
+
for (const value of values ?? []) {
|
|
40
|
+
const at = value.indexOf("=");
|
|
41
|
+
if (at <= 0)
|
|
42
|
+
fail(400, `label must be key=value, not ${value}`);
|
|
43
|
+
labels[value.slice(0, at)] = value.slice(at + 1);
|
|
44
|
+
}
|
|
45
|
+
return labels;
|
|
46
|
+
}
|
|
47
|
+
export function sandboxRoutes(api) {
|
|
48
|
+
tenantRoute(api, READ, {
|
|
49
|
+
method: "get",
|
|
50
|
+
path: "/v1/sandboxes",
|
|
51
|
+
tags: ["Sandboxes"],
|
|
52
|
+
summary: "List sandboxes",
|
|
53
|
+
description: "Every sandbox with all the labels asked for. A subject token sees only the sandboxes its grants reach.",
|
|
54
|
+
request: {
|
|
55
|
+
query: z.object({
|
|
56
|
+
label: z
|
|
57
|
+
.union([z.string(), z.array(z.string())])
|
|
58
|
+
.optional()
|
|
59
|
+
.meta({ description: "`key=value`; repeat it to require several labels" }),
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
responses: { 200: json(ListSandboxesResponse, "The sandboxes") },
|
|
63
|
+
}, async (c) => jsonResponse(200, await listSandboxes(c.env.tenant, labelsOf(c.req.queries("label")), c.get("scope"))));
|
|
64
|
+
tenantRoute(api, WRITE, {
|
|
65
|
+
method: "put",
|
|
66
|
+
path: "/v1/sandboxes/{sandboxId}",
|
|
67
|
+
tags: ["Sandboxes"],
|
|
68
|
+
summary: "Create or find a sandbox",
|
|
69
|
+
description: "Creates the sandbox within the Tenant's limits, or answers the one with this id, so get-or-create is one call. Its spec is fixed once it exists; labels, when sent, replace its labels. Kind `pod` needs sandbox pods (`nylorun sandbox enable`): it is created at once, and a PUT on an existing pod sandbox starts it again when it was stopped, or revives it with a longer `lifecycle.ttl`.",
|
|
70
|
+
request: {
|
|
71
|
+
params: sandboxId,
|
|
72
|
+
body: { required: true, content: { "application/json": { schema: PutSandboxRequest } } },
|
|
73
|
+
},
|
|
74
|
+
responses: {
|
|
75
|
+
200: json(SandboxView, "The sandbox"),
|
|
76
|
+
409: {
|
|
77
|
+
description: "A sandbox with this id has another spec, the Tenant holds as many sandboxes as it allows (`limit_exceeded`), kind pod without sandbox pods (`sandbox_unavailable`), or a lost pod sandbox (`sandbox_lost`)",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}, async (c) => {
|
|
81
|
+
const id = idOf(c.req.param("sandboxId"));
|
|
82
|
+
const body = PutSandboxRequestSchema.parse(await readJson(c.req.raw));
|
|
83
|
+
return jsonResponse(200, await putSandbox(c.env.tenant, id, body, c.get("scope")));
|
|
84
|
+
});
|
|
85
|
+
tenantRoute(api, READ, {
|
|
86
|
+
method: "get",
|
|
87
|
+
path: "/v1/sandboxes/{sandboxId}",
|
|
88
|
+
tags: ["Sandboxes"],
|
|
89
|
+
summary: "Get a sandbox",
|
|
90
|
+
description: "Its spec, labels, state and the sessions attached to it (acting for a person, only theirs).",
|
|
91
|
+
request: { params: sandboxId },
|
|
92
|
+
responses: { 200: json(SandboxView, "The sandbox") },
|
|
93
|
+
}, async (c) => jsonResponse(200, await getSandbox(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
|
|
94
|
+
tenantRoute(api, READ, {
|
|
95
|
+
method: "get",
|
|
96
|
+
path: "/v1/sandboxes/{sandboxId}/events",
|
|
97
|
+
tags: ["Sandboxes"],
|
|
98
|
+
summary: "Read a sandbox's lifecycle events",
|
|
99
|
+
description: "`sandbox.created`, `sandbox.attached`, `sandbox.detached` and `sandbox.deleted`, in order, numbered from 0.",
|
|
100
|
+
request: {
|
|
101
|
+
params: sandboxId,
|
|
102
|
+
query: z.object({
|
|
103
|
+
from: z.string().optional().meta({ description: "The first `seq` to return" }),
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
responses: { 200: json(ListSandboxEventsResponse, "The events") },
|
|
107
|
+
}, async (c) => {
|
|
108
|
+
const from = c.req.query("from");
|
|
109
|
+
if (from !== undefined && !/^\d+$/.test(from))
|
|
110
|
+
fail(400, "from must be a seq");
|
|
111
|
+
return jsonResponse(200, await sandboxEventsOf(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"), from === undefined ? undefined : Number(from)));
|
|
112
|
+
});
|
|
113
|
+
tenantRoute(api, WRITE, {
|
|
114
|
+
method: "delete",
|
|
115
|
+
path: "/v1/sandboxes/{sandboxId}",
|
|
116
|
+
tags: ["Sandboxes"],
|
|
117
|
+
summary: "Delete a sandbox",
|
|
118
|
+
description: "Deletes the sandbox and its files. Sessions attached to it stay attached by id; their next turn is refused until a sandbox with this id exists again.",
|
|
119
|
+
request: { params: sandboxId },
|
|
120
|
+
responses: {
|
|
121
|
+
200: json(DeleteSandboxResponse, "Deleted, or there was none"),
|
|
122
|
+
409: { description: "A session attached to it has a turn running (`sandbox_busy`)" },
|
|
123
|
+
},
|
|
124
|
+
}, async (c) => jsonResponse(200, await deleteSandbox(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
|
|
125
|
+
for (const [action, summary, description, run] of [
|
|
126
|
+
[
|
|
127
|
+
"stop",
|
|
128
|
+
"Stop a pod sandbox",
|
|
129
|
+
"Suspends the pod: its volume is kept, and the next turn of a session attached to it (or a PUT) starts it again. Refused during a turn (`sandbox_busy`).",
|
|
130
|
+
stopSandbox,
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"reset",
|
|
134
|
+
"Reset a pod sandbox",
|
|
135
|
+
"A new pod on a new, empty volume; the old pod and volume are deleted. The way out of `sandbox_lost`. Refused during a turn (`sandbox_busy`).",
|
|
136
|
+
resetSandbox,
|
|
137
|
+
],
|
|
138
|
+
])
|
|
139
|
+
tenantRoute(api, WRITE, {
|
|
140
|
+
method: "post",
|
|
141
|
+
path: `/v1/sandboxes/{sandboxId}/${action}`,
|
|
142
|
+
tags: ["Sandboxes"],
|
|
143
|
+
summary,
|
|
144
|
+
description,
|
|
145
|
+
request: { params: sandboxId },
|
|
146
|
+
responses: {
|
|
147
|
+
200: json(SandboxView, "The sandbox"),
|
|
148
|
+
400: { description: "The sandbox is virtual" },
|
|
149
|
+
409: { description: "A turn holds it (`sandbox_busy`), or no sandbox pods (`sandbox_unavailable`)" },
|
|
150
|
+
},
|
|
151
|
+
}, async (c) => jsonResponse(200, await run(c.env.tenant, idOf(c.req.param("sandboxId")), c.get("scope"))));
|
|
152
|
+
}
|
|
@@ -7,6 +7,7 @@ import { sandboxRouteDeps } from "../../../tenant/actions.js";
|
|
|
7
7
|
import { accessOf, requireApplication } from "../../../tenant/auth.js";
|
|
8
8
|
import { loadSession, sessionOf } from "../../../tenant/context.js";
|
|
9
9
|
import { fail } from "../../../tenant/http.js";
|
|
10
|
+
import { sandboxGrantsOf } from "../../../tenant/sandboxes.js";
|
|
10
11
|
import { readHistory, requestCursor, streamSessionEvents } from "../../../tenant/session-streams.js";
|
|
11
12
|
import { listAgentsPublic, listDefinitions, listSessions, putDefinition, putSession, sessionView, } from "../../../tenant/sessions.js";
|
|
12
13
|
import { readJson } from "../body.js";
|
|
@@ -107,7 +108,8 @@ export function sessionRoutes(api) {
|
|
|
107
108
|
// Agent code may trust `info`: only an app server sets it.
|
|
108
109
|
if (scope.kind === "token" && request.info !== undefined)
|
|
109
110
|
fail(403, "A subject token cannot set session info", { code: "scope_required" });
|
|
110
|
-
const
|
|
111
|
+
const grants = sandboxGrantsOf(scope);
|
|
112
|
+
const session = await putSession(ctx, c.req.param("sessionId"), request, accessOf(scope), grants === undefined ? {} : { sandboxGrants: grants });
|
|
111
113
|
return jsonResponse(200, await ctx.store.tx((t) => sessionView(t, session)));
|
|
112
114
|
});
|
|
113
115
|
tenantRoute(api, OWN_SESSIONS, {
|
|
@@ -10,11 +10,12 @@ import { clearObservers } from "../../../tenant/session-streams.js";
|
|
|
10
10
|
import { usesFixtureModel } from "../../../tenant/model-setting.js";
|
|
11
11
|
import { resetTenant } from "../../../tenant/reset.js";
|
|
12
12
|
import { clearWork, drain } from "../../../tenant/scheduler.js";
|
|
13
|
-
import { buildTenantStatus, seedTenantConfig } from "../../../tenant/status.js";
|
|
13
|
+
import { buildTenantStatus, harnessStatusOf, seedTenantConfig } from "../../../tenant/status.js";
|
|
14
14
|
import { tenantReset, streamsStatus, } from "../../../tenant/streams.js";
|
|
15
15
|
import { readJson } from "../body.js";
|
|
16
16
|
import { pathSegments, tenantRoute } from "../define.js";
|
|
17
17
|
import { jsonResponse } from "../respond.js";
|
|
18
|
+
import { rearmPodTimers } from "../../../tenant/sandboxes.js";
|
|
18
19
|
const settings = (scopes) => ({
|
|
19
20
|
credentials: scopes === "never" ? ["application"] : ["application", "subject"],
|
|
20
21
|
scopes,
|
|
@@ -46,7 +47,36 @@ const budgetView = (row) => ({
|
|
|
46
47
|
/** `GET /v1/tenant/sandbox`: the backend report and the configuration with defaults applied. */
|
|
47
48
|
async function sandboxView(ctx) {
|
|
48
49
|
const config = await ctx.store.tx((t) => readSandboxConfig(t));
|
|
49
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
...(await ctx.sandbox.report()),
|
|
52
|
+
config: effectiveSandboxConfig(config),
|
|
53
|
+
cluster: await clusterOf(ctx),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The Tenant's cluster for pod sandboxes (F7.2), from the sandboxes service; null without one. */
|
|
57
|
+
async function clusterOf(ctx) {
|
|
58
|
+
if (!ctx.pods)
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
const [info, ready] = await Promise.all([ctx.pods.client.info(), ctx.pods.client.ready()]);
|
|
62
|
+
return {
|
|
63
|
+
namespace: info.namespace,
|
|
64
|
+
context: info.context,
|
|
65
|
+
ready,
|
|
66
|
+
...(info.controllerVersion ? { controllerVersion: info.controllerVersion } : {}),
|
|
67
|
+
...(info.networkPolicy
|
|
68
|
+
? {
|
|
69
|
+
networkPolicy: {
|
|
70
|
+
enforced: info.networkPolicy.enforced === true,
|
|
71
|
+
...(info.networkPolicy.probedAt ? { probedAt: info.networkPolicy.probedAt } : {}),
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
: {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
50
80
|
}
|
|
51
81
|
export function tenantRoutes(api) {
|
|
52
82
|
tenantRoute(api, SETTINGS, {
|
|
@@ -70,6 +100,7 @@ export function tenantRoutes(api) {
|
|
|
70
100
|
(await vault.getHostModel()).configured,
|
|
71
101
|
...(ctx.stuckInvocations ? { stuckInvocations: ctx.stuckInvocations } : {}),
|
|
72
102
|
streamsStatus: () => streamsStatus(ctx),
|
|
103
|
+
harness: harnessStatusOf(ctx),
|
|
73
104
|
}));
|
|
74
105
|
});
|
|
75
106
|
tenantRoute(api, settings("never"), {
|
|
@@ -91,6 +122,7 @@ export function tenantRoutes(api) {
|
|
|
91
122
|
store: ctx.store,
|
|
92
123
|
sandbox: ctx.sandbox,
|
|
93
124
|
paths: ctx.config.paths,
|
|
125
|
+
blobs: ctx.blobs,
|
|
94
126
|
clearSessionState: () => {
|
|
95
127
|
clearWork(ctx);
|
|
96
128
|
clearObservers(ctx.sessionStreams);
|
|
@@ -147,6 +179,8 @@ export function tenantRoutes(api) {
|
|
|
147
179
|
if (errors.length > 0)
|
|
148
180
|
fail(400, errors.join(" "));
|
|
149
181
|
await ctx.store.tx((t) => writeSandboxConfig(t, config));
|
|
182
|
+
// Pods' idle timers follow the new limits.idle (D36).
|
|
183
|
+
await rearmPodTimers(ctx);
|
|
150
184
|
return jsonResponse(200, await sandboxView(ctx));
|
|
151
185
|
});
|
|
152
186
|
tenantRoute(api, SETTINGS, {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Tenant's artifact configuration, stored as Tenant setting `artifacts.config`: the largest
|
|
3
|
+
* file one upload may store and the most bytes every artifact version together may hold.
|
|
4
|
+
* Unset limits take the defaults (100 MiB and 10 GiB).
|
|
5
|
+
*/
|
|
6
|
+
import { type TenantArtifactsConfig } from "@nylorun/core/contracts";
|
|
7
|
+
export declare const ARTIFACTS_CONFIG_SETTING = "artifacts.config";
|
|
8
|
+
export interface ArtifactLimits {
|
|
9
|
+
readonly fileBytes: number;
|
|
10
|
+
readonly totalBytes: number;
|
|
11
|
+
}
|
|
12
|
+
interface SettingsReader {
|
|
13
|
+
getSetting(key: string): Promise<string | undefined>;
|
|
14
|
+
}
|
|
15
|
+
interface SettingsWriter extends SettingsReader {
|
|
16
|
+
putSetting(key: string, value: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/** The limits in force: the Tenant's, with the defaults for what it leaves unset. */
|
|
19
|
+
export declare function effectiveArtifactLimits(config: TenantArtifactsConfig): ArtifactLimits;
|
|
20
|
+
export declare function readArtifactsConfig(t: SettingsReader): Promise<TenantArtifactsConfig>;
|
|
21
|
+
export declare function readArtifactLimits(t: SettingsReader): Promise<ArtifactLimits>;
|
|
22
|
+
export declare function writeArtifactsConfig(t: SettingsWriter, config: TenantArtifactsConfig): Promise<void>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Tenant's artifact configuration, stored as Tenant setting `artifacts.config`: the largest
|
|
3
|
+
* file one upload may store and the most bytes every artifact version together may hold.
|
|
4
|
+
* Unset limits take the defaults (100 MiB and 10 GiB).
|
|
5
|
+
*/
|
|
6
|
+
import { ARTIFACT_FILE_BYTES_DEFAULT, ARTIFACT_TOTAL_BYTES_DEFAULT, TenantArtifactsConfigSchema, } from "@nylorun/core/contracts";
|
|
7
|
+
export const ARTIFACTS_CONFIG_SETTING = "artifacts.config";
|
|
8
|
+
/** The limits in force: the Tenant's, with the defaults for what it leaves unset. */
|
|
9
|
+
export function effectiveArtifactLimits(config) {
|
|
10
|
+
return {
|
|
11
|
+
fileBytes: config.limits?.fileBytes ?? ARTIFACT_FILE_BYTES_DEFAULT,
|
|
12
|
+
totalBytes: config.limits?.totalBytes ?? ARTIFACT_TOTAL_BYTES_DEFAULT,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export async function readArtifactsConfig(t) {
|
|
16
|
+
const raw = await t.getSetting(ARTIFACTS_CONFIG_SETTING);
|
|
17
|
+
if (raw === undefined)
|
|
18
|
+
return {};
|
|
19
|
+
const parsed = TenantArtifactsConfigSchema.safeParse(JSON.parse(raw));
|
|
20
|
+
return parsed.success ? parsed.data : {};
|
|
21
|
+
}
|
|
22
|
+
export async function readArtifactLimits(t) {
|
|
23
|
+
return effectiveArtifactLimits(await readArtifactsConfig(t));
|
|
24
|
+
}
|
|
25
|
+
export async function writeArtifactsConfig(t, config) {
|
|
26
|
+
await t.putSetting(ARTIFACTS_CONFIG_SETTING, JSON.stringify(TenantArtifactsConfigSchema.parse(config)));
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TenantContext } from "../tenant/context.js";
|
|
2
|
+
/** The directory every sandbox's outputs are exported from. */
|
|
3
|
+
export declare const OUTPUTS_DIR = "/workspace/outputs";
|
|
4
|
+
/**
|
|
5
|
+
* Exports the outputs of turn `turnId` of session `sessionId`, if it completed and its sandbox
|
|
6
|
+
* has any. Never throws.
|
|
7
|
+
*/
|
|
8
|
+
export declare function exportOutputs(ctx: TenantContext, sessionId: string, turnId: string | null, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn-end export (F8.2): when an agent's turn completes, core reads `/workspace/outputs` of
|
|
3
|
+
* the session's sandbox through the `WorkspaceReader` seam and keeps it as a version of the
|
|
4
|
+
* session's folder artifact `outputs` (the first export creates it; later turns add versions,
|
|
5
|
+
* and a turn whose outputs did not change adds none).
|
|
6
|
+
*
|
|
7
|
+
* - **After the turn.** The advance calls `exportOutputs` once `settle` has committed the turn,
|
|
8
|
+
* outside its transaction and while it still holds the session's lease, so the next turn
|
|
9
|
+
* cannot change the workspace under it. Nothing here fails the turn: a sandbox or store
|
|
10
|
+
* failure is logged and recorded as `artifact.export.failed`.
|
|
11
|
+
* - **Claimed.** The listing and the bytes are what the holder of the workspace supplied: the
|
|
12
|
+
* version's source is `export` and its event says `claimed: true`.
|
|
13
|
+
* - **Bounded.** At most `EXPORT_MAX_FILES` files and `EXPORT_MAX_BYTES` bytes per export, each
|
|
14
|
+
* file within the Tenant's per-file limit, and the new bytes within its total; past any of
|
|
15
|
+
* them nothing is stored and `artifact.export.skipped` says why.
|
|
16
|
+
* - **Deduplicated.** Each file is stored content-addressed (`folders.ts`); a file the store
|
|
17
|
+
* already holds is not stored again.
|
|
18
|
+
*
|
|
19
|
+
* No sandbox, a sandbox never created, or an empty or missing `/workspace/outputs` exports
|
|
20
|
+
* nothing and records nothing. The package's declared outputs are left for later: the manifest
|
|
21
|
+
* has no `outputs` field yet.
|
|
22
|
+
*/
|
|
23
|
+
import { posix } from "node:path";
|
|
24
|
+
import { EXPORT_MAX_BYTES, EXPORT_MAX_FILES, OUTPUTS_ARTIFACT_NAME, } from "@nylorun/core/contracts";
|
|
25
|
+
import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
|
|
26
|
+
import { isWorkflowManifest } from "../core/flow-host.js";
|
|
27
|
+
import { sandboxWorkspaceOf } from "../sandbox/share.js";
|
|
28
|
+
import { SandboxFileTooLargeError } from "../sandbox/types.js";
|
|
29
|
+
import { sandboxLookup } from "../tenant/context.js";
|
|
30
|
+
import { readArtifactLimits } from "./config.js";
|
|
31
|
+
import { collectContent, commitFolderVersion, contentEpoch, contentKey, encodeManifest, FolderCommitRefused, sha256Hex, } from "./folders.js";
|
|
32
|
+
import { contentTypeFor } from "./media-types.js";
|
|
33
|
+
/** The directory every sandbox's outputs are exported from. */
|
|
34
|
+
export const OUTPUTS_DIR = `${SANDBOX_WORKSPACE}/outputs`;
|
|
35
|
+
/** Stops the export with `artifact.export.skipped`. */
|
|
36
|
+
class Skip extends Error {
|
|
37
|
+
reason;
|
|
38
|
+
limit;
|
|
39
|
+
path;
|
|
40
|
+
constructor(reason, message, limit, path) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.reason = reason;
|
|
43
|
+
this.limit = limit;
|
|
44
|
+
this.path = path;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Exports the outputs of turn `turnId` of session `sessionId`, if it completed and its sandbox
|
|
49
|
+
* has any. Never throws.
|
|
50
|
+
*/
|
|
51
|
+
export async function exportOutputs(ctx, sessionId, turnId, signal) {
|
|
52
|
+
try {
|
|
53
|
+
await runExport(ctx, sessionId, turnId, signal);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
if (signal.aborted) {
|
|
58
|
+
ctx.config.logger.info("outputs export stopped", { sessionId, message });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const skip = error instanceof Skip
|
|
62
|
+
? error
|
|
63
|
+
: error instanceof FolderCommitRefused && error.reason === "tenant_total"
|
|
64
|
+
? new Skip("tenant_total", error.message, error.limit)
|
|
65
|
+
: undefined;
|
|
66
|
+
if (!skip)
|
|
67
|
+
ctx.config.logger.warn("outputs export failed", { sessionId, message });
|
|
68
|
+
try {
|
|
69
|
+
await ctx.store.tx(async (t) => {
|
|
70
|
+
if (skip)
|
|
71
|
+
await t.event(sessionId, turnId, "artifact.export.skipped", {
|
|
72
|
+
name: OUTPUTS_ARTIFACT_NAME,
|
|
73
|
+
reason: skip.reason,
|
|
74
|
+
message: skip.message,
|
|
75
|
+
...(skip.limit === undefined ? {} : { limit: skip.limit }),
|
|
76
|
+
...(skip.path === undefined ? {} : { path: skip.path }),
|
|
77
|
+
});
|
|
78
|
+
else
|
|
79
|
+
await t.event(sessionId, turnId, "artifact.export.failed", {
|
|
80
|
+
name: OUTPUTS_ARTIFACT_NAME,
|
|
81
|
+
message,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* the session is gone (reset) or the store closed */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function runExport(ctx, sessionId, turnId, signal) {
|
|
91
|
+
const found = await ctx.store.tx(async (t) => {
|
|
92
|
+
const s = await t.get("sessions", sessionId);
|
|
93
|
+
// Only a turn that completed, and only an agent's (a flow agent's sandbox is its agents').
|
|
94
|
+
if (!s || s.status !== "completed" || s.lastTurnId !== turnId || isWorkflowManifest(s.manifest))
|
|
95
|
+
return undefined;
|
|
96
|
+
const workspace = sandboxWorkspaceOf(s, await sandboxLookup(t, s.id));
|
|
97
|
+
const prior = (await t.listArtifacts({ sessionId })).find((row) => row.kind === "folder" && row.name === OUTPUTS_ARTIFACT_NAME);
|
|
98
|
+
const latest = prior ? await t.artifactVersion(prior.id, prior.latestVersion) : undefined;
|
|
99
|
+
return {
|
|
100
|
+
session: {
|
|
101
|
+
sessionId,
|
|
102
|
+
turnId,
|
|
103
|
+
ownerId: workspace.ownerId,
|
|
104
|
+
...(workspace.sandboxId === undefined ? {} : { sandboxId: workspace.sandboxId }),
|
|
105
|
+
manifest: s.manifest,
|
|
106
|
+
},
|
|
107
|
+
limits: await readArtifactLimits(t),
|
|
108
|
+
priorId: prior?.id,
|
|
109
|
+
latestSha: latest?.sha256,
|
|
110
|
+
epoch: await contentEpoch(t),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
if (!found)
|
|
114
|
+
return;
|
|
115
|
+
const { session, limits } = found;
|
|
116
|
+
const listing = await ctx.workspaces.list(session, OUTPUTS_DIR, {
|
|
117
|
+
maxEntries: EXPORT_MAX_FILES,
|
|
118
|
+
signal,
|
|
119
|
+
});
|
|
120
|
+
if (!listing || listing.entries.length === 0)
|
|
121
|
+
return;
|
|
122
|
+
if (listing.truncated)
|
|
123
|
+
throw new Skip("too_many_files", `${OUTPUTS_DIR} holds more than ${EXPORT_MAX_FILES} files`, EXPORT_MAX_FILES);
|
|
124
|
+
const large = listing.entries.find((entry) => entry.size > limits.fileBytes);
|
|
125
|
+
if (large)
|
|
126
|
+
throw tooLargeFile(large.path, large.size, limits.fileBytes);
|
|
127
|
+
const maxBytes = Math.min(EXPORT_MAX_BYTES, limits.totalBytes);
|
|
128
|
+
const listed = listing.entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
129
|
+
if (listed > maxBytes)
|
|
130
|
+
throw tooLarge(listed, maxBytes);
|
|
131
|
+
// The files this export stored: removed again if it commits nothing, unless another version
|
|
132
|
+
// names them by then.
|
|
133
|
+
const put = [];
|
|
134
|
+
try {
|
|
135
|
+
await storeAndCommit(ctx, found, listing.entries, maxBytes, put, signal);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (put.length > 0)
|
|
139
|
+
await ctx.store
|
|
140
|
+
.tx(async (t) => {
|
|
141
|
+
await t.lockArtifactQuota();
|
|
142
|
+
await collectContent(ctx.blobs, t, put);
|
|
143
|
+
})
|
|
144
|
+
.catch(() => undefined);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function storeAndCommit(ctx, found, listed, maxBytes, put, signal) {
|
|
149
|
+
const { session, limits } = found;
|
|
150
|
+
const { sessionId, turnId } = session;
|
|
151
|
+
const entries = [];
|
|
152
|
+
const stored = new Set();
|
|
153
|
+
let read = 0;
|
|
154
|
+
for (const entry of listed) {
|
|
155
|
+
let bytes;
|
|
156
|
+
try {
|
|
157
|
+
bytes = await ctx.workspaces.readBytes(session, posix.join(OUTPUTS_DIR, entry.path), {
|
|
158
|
+
maxBytes: limits.fileBytes,
|
|
159
|
+
signal,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof SandboxFileTooLargeError)
|
|
164
|
+
throw tooLargeFile(entry.path, error.size, limits.fileBytes);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
// Gone since the listing: the folder is what is there now.
|
|
168
|
+
if (bytes === undefined)
|
|
169
|
+
continue;
|
|
170
|
+
read += bytes.length;
|
|
171
|
+
if (read > maxBytes)
|
|
172
|
+
throw tooLarge(read, maxBytes);
|
|
173
|
+
const sha256 = sha256Hex(bytes);
|
|
174
|
+
const contentType = contentTypeFor(posix.basename(entry.path));
|
|
175
|
+
if (!stored.has(sha256)) {
|
|
176
|
+
stored.add(sha256);
|
|
177
|
+
const key = contentKey(sha256);
|
|
178
|
+
if (!(await ctx.blobs.head(key))) {
|
|
179
|
+
put.push(sha256);
|
|
180
|
+
await ctx.blobs.put(key, bytes, { contentType, signal });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
entries.push({ path: entry.path, size: bytes.length, sha256, contentType });
|
|
184
|
+
}
|
|
185
|
+
// Every file vanished, or nothing changed since the last export: no new version.
|
|
186
|
+
if (entries.length === 0)
|
|
187
|
+
return;
|
|
188
|
+
if (found.latestSha !== undefined && sha256Hex(encodeManifest(entries)) === found.latestSha)
|
|
189
|
+
return;
|
|
190
|
+
await commitFolderVersion(ctx, {
|
|
191
|
+
sessionId,
|
|
192
|
+
turnId,
|
|
193
|
+
...(found.priorId === undefined ? {} : { artifactId: found.priorId }),
|
|
194
|
+
name: OUTPUTS_ARTIFACT_NAME,
|
|
195
|
+
entries,
|
|
196
|
+
epoch: found.epoch,
|
|
197
|
+
totalBytes: limits.totalBytes,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function tooLargeFile(path, size, limit) {
|
|
201
|
+
return new Skip("file_too_large", `${OUTPUTS_DIR}/${path} is ${size} bytes; a file may hold at most ${limit}`, limit, path);
|
|
202
|
+
}
|
|
203
|
+
function tooLarge(bytes, limit) {
|
|
204
|
+
return new Skip("too_large", `${OUTPUTS_DIR} holds ${bytes} bytes; an export stores at most ${limit}`, limit);
|
|
205
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How model-gate reads the files a prompt names (protocol 6): a media part's reference is an
|
|
3
|
+
* artifact version (`./parts.ts`), resolved here to its bytes from the Object store. The gate
|
|
4
|
+
* holds the store credential; the loop and the transcript only ever hold the reference.
|
|
5
|
+
*
|
|
6
|
+
* It replaces `MediaStore`: images are file artifacts, and their bytes reach the provider only in
|
|
7
|
+
* the request model-gate builds.
|
|
8
|
+
*/
|
|
9
|
+
import type { JsonValue } from "@nylorun/core/define";
|
|
10
|
+
import type { BlobStore } from "../blob/index.js";
|
|
11
|
+
import type { SessionStore } from "../store/types.js";
|
|
12
|
+
/** A file a prompt names, with its bytes. */
|
|
13
|
+
export interface ResolvedFile {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/** The version's media type, as stored. */
|
|
16
|
+
readonly mediaType: string;
|
|
17
|
+
readonly bytes: Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
/** Why a file cannot go to the model: the message is the outcome's, so it names no secret. */
|
|
20
|
+
export declare class FileUnavailableError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
/** Resolves a media part's reference to the file's bytes, or throws `FileUnavailableError`. */
|
|
24
|
+
export type FileResolver = (reference: JsonValue) => Promise<ResolvedFile>;
|
|
25
|
+
/**
|
|
26
|
+
* Files from the Tenant's artifacts: `store` for the version's row, `blobs` for its bytes, for
|
|
27
|
+
* the call of session `sessionId`. An artifact of another session is refused (a Tenant-wide one
|
|
28
|
+
* is not), and without a session every file is: the check is never skipped.
|
|
29
|
+
*/
|
|
30
|
+
export declare function artifactFiles(options: {
|
|
31
|
+
readonly store: SessionStore;
|
|
32
|
+
readonly blobs: BlobStore;
|
|
33
|
+
/** The session the call is for: the run token's, over HTTP. */
|
|
34
|
+
readonly sessionId: string | undefined;
|
|
35
|
+
readonly maxBytes?: number;
|
|
36
|
+
}): FileResolver;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MODEL_FILE_MAX_BYTES } from "./media-types.js";
|
|
2
|
+
import { fileReferenceOf } from "./parts.js";
|
|
3
|
+
/** Why a file cannot go to the model: the message is the outcome's, so it names no secret. */
|
|
4
|
+
export class FileUnavailableError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "FileUnavailableError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Files from the Tenant's artifacts: `store` for the version's row, `blobs` for its bytes, for
|
|
12
|
+
* the call of session `sessionId`. An artifact of another session is refused (a Tenant-wide one
|
|
13
|
+
* is not), and without a session every file is: the check is never skipped.
|
|
14
|
+
*/
|
|
15
|
+
export function artifactFiles(options) {
|
|
16
|
+
const maxBytes = options.maxBytes ?? MODEL_FILE_MAX_BYTES;
|
|
17
|
+
return async (value) => {
|
|
18
|
+
const reference = fileReferenceOf(value);
|
|
19
|
+
if (!reference)
|
|
20
|
+
throw new FileUnavailableError("Expected an artifact reference.");
|
|
21
|
+
const sessionId = options.sessionId;
|
|
22
|
+
if (typeof sessionId !== "string" || sessionId === "")
|
|
23
|
+
throw new FileUnavailableError("A model call reads files only for its own session.");
|
|
24
|
+
const found = await options.store.tx(async (t) => {
|
|
25
|
+
const artifact = await t.artifact(reference.artifactId);
|
|
26
|
+
if (!artifact)
|
|
27
|
+
return undefined;
|
|
28
|
+
const version = await t.artifactVersion(artifact.id, reference.version);
|
|
29
|
+
return version ? { artifact, version } : undefined;
|
|
30
|
+
});
|
|
31
|
+
if (!found ||
|
|
32
|
+
(found.artifact.sessionId !== null && found.artifact.sessionId !== sessionId))
|
|
33
|
+
throw new FileUnavailableError(`Artifact ${reference.artifactId} version ${reference.version} is not available.`);
|
|
34
|
+
const { artifact, version } = found;
|
|
35
|
+
if (artifact.kind !== "file")
|
|
36
|
+
throw new FileUnavailableError(`${artifact.name} is a folder; a model reads files only.`);
|
|
37
|
+
if (version.size > maxBytes)
|
|
38
|
+
throw new FileUnavailableError(`${artifact.name} is ${version.size} bytes; a model reads files of at most ${maxBytes} bytes.`);
|
|
39
|
+
const got = await options.blobs.get(version.blobKey);
|
|
40
|
+
if (!got)
|
|
41
|
+
throw new FileUnavailableError(`The bytes of ${artifact.name} are missing from the Object store.`);
|
|
42
|
+
const bytes = new Uint8Array(await new Response(got.body).arrayBuffer());
|
|
43
|
+
return { name: artifact.name, mediaType: version.contentType, bytes };
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ArtifactDiff, FolderEntry, FolderManifest } from "@nylorun/core/contracts";
|
|
2
|
+
import type { BlobStore } from "../blob/index.js";
|
|
3
|
+
import type { ArtifactRow, ArtifactVersionRow, Tx } from "../store/types.js";
|
|
4
|
+
import { type TenantContext } from "../tenant/context.js";
|
|
5
|
+
/** Where a folder file's bytes are: content-addressed by their SHA-256. */
|
|
6
|
+
export declare function contentKey(sha256: string): string;
|
|
7
|
+
export declare function sha256Hex(bytes: Uint8Array): string;
|
|
8
|
+
/** Entries in manifest order: by path, comparing UTF-16 code units. */
|
|
9
|
+
export declare function sortEntries(entries: readonly FolderEntry[]): FolderEntry[];
|
|
10
|
+
/** The manifest's bytes, as stored: deterministic for the same entries. */
|
|
11
|
+
export declare function encodeManifest(entries: readonly FolderEntry[]): Uint8Array;
|
|
12
|
+
/** A folder version's manifest, read from the Object store. */
|
|
13
|
+
export declare function readManifest(blobs: BlobStore, version: ArtifactVersionRow): Promise<FolderManifest>;
|
|
14
|
+
/** The entry at `path`, or the 404 of a missing file. */
|
|
15
|
+
export declare function entryAt(manifest: FolderManifest, path: string): FolderEntry;
|
|
16
|
+
/** What changed from `from` (absent: nothing) to `to`. */
|
|
17
|
+
export declare function diffManifests(artifactId: string, from: {
|
|
18
|
+
version: number;
|
|
19
|
+
manifest: FolderManifest;
|
|
20
|
+
} | undefined, to: {
|
|
21
|
+
version: number;
|
|
22
|
+
manifest: FolderManifest;
|
|
23
|
+
}): ArtifactDiff;
|
|
24
|
+
/** The Tenant's content epoch: moves whenever content-addressed files are deleted. */
|
|
25
|
+
export declare function contentEpoch(t: Tx): Promise<number>;
|
|
26
|
+
/**
|
|
27
|
+
* Deletes those of `shas` no committed folder version references any more, and moves the content
|
|
28
|
+
* epoch when it deletes any. Call it in a transaction holding the quota lock, after the rows that
|
|
29
|
+
* named them are gone: the deletes happen before the transaction commits, under the lock.
|
|
30
|
+
*/
|
|
31
|
+
export declare function collectContent(blobs: BlobStore, t: Tx, shas: readonly string[]): Promise<number>;
|
|
32
|
+
/** The commit could not go ahead: a limit, or content removed while the export ran. */
|
|
33
|
+
export declare class FolderCommitRefused extends Error {
|
|
34
|
+
readonly reason: "tenant_total" | "content_gone" | "artifact_gone";
|
|
35
|
+
readonly limit?: number | undefined;
|
|
36
|
+
constructor(message: string, reason: "tenant_total" | "content_gone" | "artifact_gone", limit?: number | undefined);
|
|
37
|
+
}
|
|
38
|
+
export interface FolderVersionInput {
|
|
39
|
+
readonly sessionId: string;
|
|
40
|
+
readonly turnId: string | null;
|
|
41
|
+
/** A new version of this folder of the session; otherwise a new folder named `name`. */
|
|
42
|
+
readonly artifactId?: string;
|
|
43
|
+
readonly name: string;
|
|
44
|
+
/** Every file, each one's bytes already at `contentKey(sha256)`. */
|
|
45
|
+
readonly entries: readonly FolderEntry[];
|
|
46
|
+
/** The content epoch read before the first `head` of those bytes. */
|
|
47
|
+
readonly epoch: number;
|
|
48
|
+
/** The Tenant total in force. */
|
|
49
|
+
readonly totalBytes: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Commits a folder version whose files are already stored: its manifest to the Object store,
|
|
53
|
+
* then, in one transaction under the session and quota locks, the version row, its content rows
|
|
54
|
+
* and `artifact.created` or `artifact.version.created` (`claimed: true`, source `export`).
|
|
55
|
+
* Throws `FolderCommitRefused` when the Tenant total would be passed or a file is gone.
|
|
56
|
+
*/
|
|
57
|
+
export declare function commitFolderVersion(ctx: TenantContext, input: FolderVersionInput): Promise<{
|
|
58
|
+
artifact: ArtifactRow;
|
|
59
|
+
version: ArtifactVersionRow;
|
|
60
|
+
}>;
|