@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
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* keyed by the Environment id and created on the first tool call), `pod` (an agent-sandbox pod
|
|
12
12
|
* on the Tenant's cluster, Host feature `sandbox-pods`, created at once) or `none` (no compute).
|
|
13
13
|
* Each has one vault (`vault/service.ts`), created and deleted with it in one transaction.
|
|
14
|
-
* - **
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* what
|
|
19
|
-
*
|
|
14
|
+
* - **Made once, never changed** (protocol 14, D63). An Environment is made from one version of
|
|
15
|
+
* an environment manifest (`manifest: "name"` or `"name@n"`, `environment-manifests.ts`) or
|
|
16
|
+
* from an inline manifest (the fields of the `PUT`, its `recipe`), and keeps what it runs for
|
|
17
|
+
* life: a `PUT` on an existing id may change its labels and owner guard, a field that matches
|
|
18
|
+
* what it runs is accepted (so get-or-create stays one call), and anything else is
|
|
19
|
+
* `409 environment_immutable`, naming the fork that makes a new Environment instead.
|
|
20
|
+
* `version` and `appliedVersion` stay 1 for an Environment made since then.
|
|
21
|
+
* - **Secret slots** (D64): when what it runs declares `secrets`, a secret write sends only the
|
|
22
|
+
* value, the binding comes from the slot, and a name it does not declare is
|
|
23
|
+
* `409 secret_not_in_manifest` (`secretRequestFor`).
|
|
20
24
|
* - **Access.** An application key reaches every Environment, and so does an application key
|
|
21
25
|
* acting for a subject (`Nylorun-Subject`), which needs `environments:write` to change one. A
|
|
22
26
|
* token caller reaches only the ids its grants match, and changes them only with
|
|
@@ -24,8 +28,10 @@
|
|
|
24
28
|
* - **The owner guard.** When `ownerUserId` is set, only that owner's sessions may name the
|
|
25
29
|
* Environment, checked when a session opens and at every turn start (`403
|
|
26
30
|
* environment_owner_mismatch`; the opaque 404 to a caller acting for a person).
|
|
27
|
-
* - **
|
|
28
|
-
*
|
|
31
|
+
* - **Parallel turns (D66).** Sessions on one Environment run their turns at the same time and
|
|
32
|
+
* share its `/workspace`; one turn per session stays. Which sessions work on one disk, and when,
|
|
33
|
+
* is the app's call. Disk work (snapshot, restore, stop, reset) waits until no turn runs
|
|
34
|
+
* (`409 environment_busy`), and a turn waits while a seed copies a disk.
|
|
29
35
|
* - **Lifecycle.** `POST .../stop` suspends a pod (the volume is kept; the next turn starts it),
|
|
30
36
|
* `POST .../reset` gives it a new pod on a new volume. A lost pod refuses turns
|
|
31
37
|
* (`environment_lost`) until a reset. `lifecycle.deleteAfter` deletes an Environment unused
|
|
@@ -34,7 +40,9 @@
|
|
|
34
40
|
* - **Lifecycle events** go to the Environment's own stream in the record (`Tx.environmentEvent`,
|
|
35
41
|
* `record/environment.ts`); the session's log records `environment.attached` too.
|
|
36
42
|
*/
|
|
37
|
-
import {
|
|
43
|
+
import { randomUUID } from "node:crypto";
|
|
44
|
+
import { environmentGranted, parseEnvironmentManifestRef, PutEnvironmentSecretRequestSchema, PutEnvironmentSecretValueRequestSchema, } from "@nylorun/core/contracts";
|
|
45
|
+
import { hashManifest } from "@nylorun/core/compatibility";
|
|
38
46
|
import { isWorkflowManifest, parseSandboxDuration } from "@nylorun/core/define";
|
|
39
47
|
import { canonical } from "../store/canonical.js";
|
|
40
48
|
import { resolveRecipe } from "../sandbox/resolve.js";
|
|
@@ -44,7 +52,8 @@ import { podName } from "../sandbox/pods/name.js";
|
|
|
44
52
|
import { podLifecycleConfig } from "../sandbox/pods/spec.js";
|
|
45
53
|
import { SETUP_FAILED } from "../sandbox/pods/lifecycle.js";
|
|
46
54
|
import { requirePods } from "../sandbox/placement.js";
|
|
47
|
-
import { deleteDiskArtifact, forkSeedOf, runPendingSeed } from "../artifacts/disk.js";
|
|
55
|
+
import { deleteDiskArtifact, forkSeedOf, runPendingSeed, snapshotEnvironment } from "../artifacts/disk.js";
|
|
56
|
+
import { manifestChanges, manifestRecipeRequest } from "./environment-manifests.js";
|
|
48
57
|
import { harnessOf, sessionOf } from "./context.js";
|
|
49
58
|
import { fail } from "./http.js";
|
|
50
59
|
/** The Environment grants that limit `scope`, or undefined when it reaches every Environment. */
|
|
@@ -91,11 +100,17 @@ async function viewOf(ctx, t, environment, owner) {
|
|
|
91
100
|
? await t.get("sandboxes", sandboxWorkspaceKey(ctx.config.tenantId, environment.id))
|
|
92
101
|
: undefined;
|
|
93
102
|
const pod = environment.pod;
|
|
103
|
+
const slots = await slotsOf(t, environment);
|
|
94
104
|
return {
|
|
95
105
|
id: environment.id,
|
|
96
106
|
kind: environment.kind,
|
|
97
107
|
labels: environment.labels,
|
|
98
|
-
|
|
108
|
+
manifest: environment.recipe,
|
|
109
|
+
...(environment.manifestName === undefined ? {} : { manifestName: environment.manifestName }),
|
|
110
|
+
...(environment.manifestVersion === undefined ? {} : { manifestVersion: environment.manifestVersion }),
|
|
111
|
+
...(environment.manifestHash === undefined ? {} : { manifestHash: environment.manifestHash }),
|
|
112
|
+
...(environment.forkedFrom === undefined ? {} : { forkedFrom: environment.forkedFrom }),
|
|
113
|
+
...(slots === undefined ? {} : { slots }),
|
|
99
114
|
version: environment.version,
|
|
100
115
|
appliedVersion: environment.appliedVersion,
|
|
101
116
|
ownerUserId: environment.ownerUserId ?? null,
|
|
@@ -108,6 +123,7 @@ async function viewOf(ctx, t, environment, owner) {
|
|
|
108
123
|
volumeGeneration: pod.volumeGen,
|
|
109
124
|
hostEpoch: pod.hostEpoch,
|
|
110
125
|
...(pod.reason ? { reason: pod.reason } : {}),
|
|
126
|
+
...(pod.imageDigest ? { imageDigest: pod.imageDigest } : {}),
|
|
111
127
|
},
|
|
112
128
|
}
|
|
113
129
|
: {}),
|
|
@@ -135,6 +151,34 @@ async function viewOf(ctx, t, environment, owner) {
|
|
|
135
151
|
updatedAt: environment.updatedAt,
|
|
136
152
|
};
|
|
137
153
|
}
|
|
154
|
+
/** Each secret slot `environment` declares, and whether its vault holds a value for it. */
|
|
155
|
+
async function slotsOf(t, environment) {
|
|
156
|
+
const declared = environment.recipe.secrets;
|
|
157
|
+
if (declared === undefined)
|
|
158
|
+
return undefined;
|
|
159
|
+
const vault = await t.vaultOfEnvironment(environment.id);
|
|
160
|
+
const held = new Set(vault ? (await t.credentialsForVault(vault.id)).map((row) => row.name) : []);
|
|
161
|
+
return Object.entries(declared)
|
|
162
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
163
|
+
.map(([name, slot]) => ({ name, type: slot.type, required: slot.required ?? false, set: held.has(name) }));
|
|
164
|
+
}
|
|
165
|
+
/** The inline fields of a `PUT`: what it would run, besides labels, owner and `from`. */
|
|
166
|
+
const INLINE_FIELDS = [
|
|
167
|
+
"kind",
|
|
168
|
+
"image",
|
|
169
|
+
"resources",
|
|
170
|
+
"storage",
|
|
171
|
+
"network",
|
|
172
|
+
"harness",
|
|
173
|
+
"lifecycle",
|
|
174
|
+
"setup",
|
|
175
|
+
"files",
|
|
176
|
+
"secrets",
|
|
177
|
+
];
|
|
178
|
+
/** The hash of what an Environment runs, recorded when it is made. */
|
|
179
|
+
function recipeHash(recipe) {
|
|
180
|
+
return hashManifest(recipe);
|
|
181
|
+
}
|
|
138
182
|
/** What a `PUT` asks of the recipe: its fields over `existing`'s (fixed `storage` kept). */
|
|
139
183
|
function recipeRequest(body, existing) {
|
|
140
184
|
const recipe = existing?.recipe;
|
|
@@ -152,20 +196,38 @@ function recipeRequest(body, existing) {
|
|
|
152
196
|
...pick("lifecycle", body.lifecycle),
|
|
153
197
|
...pick("setup", body.setup),
|
|
154
198
|
...pick("files", body.files),
|
|
199
|
+
...pick("secrets", body.secrets),
|
|
155
200
|
};
|
|
156
201
|
}
|
|
157
202
|
/**
|
|
158
|
-
*
|
|
159
|
-
* go with the session)
|
|
203
|
+
* Why input files cannot be used: each that is not a version of a Tenant-wide artifact (a
|
|
204
|
+
* session's artifacts go with the session). Pins nothing else: the version is the one named.
|
|
160
205
|
*/
|
|
161
|
-
async function
|
|
206
|
+
export async function fileErrors(t, files) {
|
|
207
|
+
const errors = [];
|
|
162
208
|
for (const file of files ?? []) {
|
|
209
|
+
if ("files" in file) {
|
|
210
|
+
// Files by hash (Q6): each a definition file the Tenant holds.
|
|
211
|
+
const wanted = [...new Set(Object.values(file.files))];
|
|
212
|
+
const held = await t.heldDefinitionFiles(wanted);
|
|
213
|
+
for (const [path, sha256] of Object.entries(file.files))
|
|
214
|
+
if (!held.has(sha256))
|
|
215
|
+
errors.push(`files: ${file.path}/${path} names ${sha256}, which the Tenant does not hold: upload it first (PUT /v1/files/${sha256}).`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
163
218
|
const artifact = await t.artifact(file.artifactId);
|
|
164
219
|
if (!artifact || artifact.sessionId !== null)
|
|
165
|
-
|
|
166
|
-
if (!(await t.artifactVersion(file.artifactId, file.version)))
|
|
167
|
-
|
|
220
|
+
errors.push(`files: ${file.artifactId} is not an artifact of the Tenant (an artifact of a session cannot be one).`);
|
|
221
|
+
else if (!(await t.artifactVersion(file.artifactId, file.version)))
|
|
222
|
+
errors.push(`files: ${file.artifactId} has no version ${file.version}.`);
|
|
168
223
|
}
|
|
224
|
+
return errors;
|
|
225
|
+
}
|
|
226
|
+
/** Refuses input files that cannot be used (`fileErrors`). */
|
|
227
|
+
async function checkFiles(t, files) {
|
|
228
|
+
const errors = await fileErrors(t, files);
|
|
229
|
+
if (errors.length > 0)
|
|
230
|
+
fail(400, errors.join(" "));
|
|
169
231
|
}
|
|
170
232
|
/** Refuses an owner guard a caller acting for a person may not set. */
|
|
171
233
|
function checkGuard(scope, asked, current) {
|
|
@@ -213,15 +275,33 @@ async function wantRunning(t, environment, now) {
|
|
|
213
275
|
}
|
|
214
276
|
/**
|
|
215
277
|
* `PUT /v1/environments/{id}`: creates the Environment and its vault (within the Tenant's limit
|
|
216
|
-
* on their number)
|
|
217
|
-
*
|
|
218
|
-
*
|
|
278
|
+
* on their number) from an environment manifest's version or from the inline fields, or finds
|
|
279
|
+
* the one with this id, so "get or create" is one call. An existing one never changes what it
|
|
280
|
+
* runs (D63): its labels and owner guard may change, fields that match are accepted, and any
|
|
281
|
+
* other difference is `409 environment_immutable`. Every `PUT` counts as the Environment's use.
|
|
219
282
|
*/
|
|
220
|
-
export async function putEnvironment(ctx, id, body, scope) {
|
|
283
|
+
export async function putEnvironment(ctx, id, body, scope, options = {}) {
|
|
284
|
+
const fork = options.fork;
|
|
221
285
|
reachable(environmentGrantsOf(scope), id);
|
|
286
|
+
const ref = body.manifest === undefined ? undefined : parseEnvironmentManifestRef(body.manifest);
|
|
287
|
+
if (body.manifest !== undefined) {
|
|
288
|
+
if (ref === undefined)
|
|
289
|
+
fail(400, "manifest is an environment manifest's name, or name@version such as analyst-box@3.");
|
|
290
|
+
const inline = INLINE_FIELDS.filter((field) => body[field] !== undefined);
|
|
291
|
+
if (inline.length > 0)
|
|
292
|
+
fail(400, `An Environment made from manifest ${body.manifest} runs what the manifest says: send no ${inline.join(", ")} with it.`);
|
|
293
|
+
}
|
|
222
294
|
if (body.kind === "pod")
|
|
223
295
|
await requirePods(ctx.pods, "A pod Environment");
|
|
224
|
-
const
|
|
296
|
+
const immutable = (fields, existing) => fail(409, `Environment ${id} exists and never changes what it runs (${fields.join(", ")} differ${fields.length === 1 ? "s" : ""}): fork it into a new Environment with POST /v1/environments/${encodeURIComponent(id)}/fork, or use another id.`, {
|
|
297
|
+
code: "environment_immutable",
|
|
298
|
+
details: {
|
|
299
|
+
fields: [...fields],
|
|
300
|
+
...(existing.manifestName === undefined
|
|
301
|
+
? {}
|
|
302
|
+
: { manifest: `${existing.manifestName}@${existing.manifestVersion}` }),
|
|
303
|
+
},
|
|
304
|
+
});
|
|
225
305
|
const view = await ctx.store.tx(async (t) => {
|
|
226
306
|
const now = new Date().toISOString();
|
|
227
307
|
const config = effectiveSandboxConfig(await readSandboxConfig(t));
|
|
@@ -237,56 +317,36 @@ export async function putEnvironment(ctx, id, body, scope) {
|
|
|
237
317
|
}, { "retry-after": "5" });
|
|
238
318
|
};
|
|
239
319
|
const found = async (existing) => {
|
|
320
|
+
if (fork !== undefined)
|
|
321
|
+
fail(409, `Environment ${id} exists: a fork makes a new Environment, so name another id.`);
|
|
240
322
|
if (body.from !== undefined)
|
|
241
323
|
fail(409, `Environment ${id} exists: from applies only when a PUT creates one.`);
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
324
|
+
if (ref !== undefined) {
|
|
325
|
+
// A bare name matches the manifest it was made from, whatever its latest version is now.
|
|
326
|
+
if (ref.name !== existing.manifestName)
|
|
327
|
+
immutable(["manifest"], existing);
|
|
328
|
+
if (ref.version !== undefined && ref.version !== existing.manifestVersion)
|
|
329
|
+
immutable(["manifest"], existing);
|
|
330
|
+
}
|
|
331
|
+
else if (INLINE_FIELDS.some((field) => body[field] !== undefined)) {
|
|
332
|
+
if (body.kind !== undefined && body.kind !== existing.kind)
|
|
333
|
+
immutable(["kind"], existing);
|
|
334
|
+
if (body.storage !== undefined && body.storage !== existing.recipe.storage)
|
|
335
|
+
immutable(["storage"], existing);
|
|
336
|
+
const recipe = resolve(existing.kind, recipeRequest(body, existing));
|
|
337
|
+
const differing = Object.keys({ ...recipe, ...existing.recipe })
|
|
338
|
+
.filter((field) => canonical(recipe[field]) !== canonical(existing.recipe[field]))
|
|
339
|
+
.sort();
|
|
340
|
+
if (differing.length > 0)
|
|
341
|
+
immutable(differing, existing);
|
|
342
|
+
}
|
|
246
343
|
checkGuard(scope, body.ownerUserId, existing.ownerUserId);
|
|
247
|
-
const recipe = resolve(existing.kind, recipeRequest(body, existing));
|
|
248
|
-
if (body.files !== undefined)
|
|
249
|
-
await checkFiles(t, recipe.files);
|
|
250
344
|
const patch = { lastUsedAt: now };
|
|
251
345
|
if (body.labels !== undefined && canonical(body.labels) !== canonical(existing.labels))
|
|
252
346
|
patch.labels = body.labels;
|
|
253
347
|
if (body.ownerUserId !== undefined && (body.ownerUserId ?? undefined) !== existing.ownerUserId)
|
|
254
348
|
patch.ownerUserId = body.ownerUserId;
|
|
255
|
-
if (canonical(recipe) !== canonical(existing.recipe)) {
|
|
256
|
-
const version = existing.version + 1;
|
|
257
|
-
patch.recipe = recipe;
|
|
258
|
-
patch.version = version;
|
|
259
|
-
// What waits for the pod to start from stopped; setup and files run only on a new disk.
|
|
260
|
-
// The harness is what a pod runs (R4.4): it changes with the pod, like its image.
|
|
261
|
-
const compute = (r) => canonical({
|
|
262
|
-
image: r.image ?? null,
|
|
263
|
-
resources: r.resources ?? null,
|
|
264
|
-
setup: r.setup ?? null,
|
|
265
|
-
files: r.files ?? null,
|
|
266
|
-
harness: r.harness,
|
|
267
|
-
});
|
|
268
|
-
if (existing.kind !== "pod" || compute(recipe) === compute(existing.applied)) {
|
|
269
|
-
// Nothing waits for a wake: apply all of it.
|
|
270
|
-
patch.applied = recipe;
|
|
271
|
-
patch.appliedVersion = version;
|
|
272
|
-
}
|
|
273
|
-
else
|
|
274
|
-
// Network and lifecycle apply at once; image, resources and harness at the next wake.
|
|
275
|
-
patch.applied = {
|
|
276
|
-
...existing.applied,
|
|
277
|
-
...(recipe.network === undefined ? {} : { network: recipe.network }),
|
|
278
|
-
lifecycle: recipe.lifecycle,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
349
|
await t.updateEnvironment(id, patch, now);
|
|
282
|
-
if (patch.recipe !== undefined && existing.pod?.desired === "running")
|
|
283
|
-
// A new `lifecycle.idle` applies to the running pod's idle timer.
|
|
284
|
-
await t.signalSandbox(id, {
|
|
285
|
-
kind: "arm",
|
|
286
|
-
timer: "idle",
|
|
287
|
-
at: Date.parse(existing.pod.lastActiveAt ?? existing.createdAt) +
|
|
288
|
-
podLifecycleConfig(await readSandboxConfig(t), patch.recipe).idleMs,
|
|
289
|
-
});
|
|
290
350
|
return viewOf(ctx, t, (await t.environment(id)) ?? existing, subjectOf(scope));
|
|
291
351
|
};
|
|
292
352
|
const existing = await t.environment(id, { lock: true });
|
|
@@ -294,10 +354,20 @@ export async function putEnvironment(ctx, id, body, scope) {
|
|
|
294
354
|
if (existing)
|
|
295
355
|
return found(existing);
|
|
296
356
|
checkGuard(scope, body.ownerUserId);
|
|
297
|
-
const
|
|
298
|
-
|
|
357
|
+
const manifest = ref === undefined
|
|
358
|
+
? undefined
|
|
359
|
+
: ((await t.environmentManifest(ref.name, ref.version)) ??
|
|
360
|
+
fail(404, ref.version === undefined
|
|
361
|
+
? `No environment manifest is named ${ref.name}: save it first (PUT /v1/environment-manifests/${ref.name}).`
|
|
362
|
+
: `Environment manifest ${ref.name} has no version ${ref.version}.`, { code: "manifest_not_found" }));
|
|
363
|
+
const kind = manifest ? (manifest.body.kind ?? "virtual") : (body.kind ?? "virtual");
|
|
364
|
+
if (manifest && kind === "pod")
|
|
365
|
+
await requirePods(ctx.pods, `Environment manifest ${manifest.name} is of kind pod, and a pod Environment`);
|
|
366
|
+
const recipe = resolve(kind, manifest ? manifestRecipeRequest(manifest.body) : recipeRequest(body));
|
|
299
367
|
await checkFiles(t, recipe.files);
|
|
300
|
-
const
|
|
368
|
+
const from = body.from === undefined ? undefined : await forkSource(t, scope, kind, body.from);
|
|
369
|
+
// A fork onto other setup runs it on the written disk (Q17).
|
|
370
|
+
const seed = from && fork?.setup ? { ...from, setup: true } : from;
|
|
301
371
|
const row = {
|
|
302
372
|
id,
|
|
303
373
|
kind,
|
|
@@ -310,6 +380,9 @@ export async function putEnvironment(ctx, id, body, scope) {
|
|
|
310
380
|
lastUsedAt: now,
|
|
311
381
|
createdAt: now,
|
|
312
382
|
updatedAt: now,
|
|
383
|
+
...(manifest ? { manifestName: manifest.name, manifestVersion: manifest.version } : {}),
|
|
384
|
+
manifestHash: recipeHash(recipe),
|
|
385
|
+
...(fork ? { forkedFrom: { environment: fork.source, disk: seed?.version ?? null } } : {}),
|
|
313
386
|
...(seed
|
|
314
387
|
? {
|
|
315
388
|
diskSeed: seed,
|
|
@@ -348,12 +421,24 @@ export async function putEnvironment(ctx, id, body, scope) {
|
|
|
348
421
|
deleting(again);
|
|
349
422
|
return found(again);
|
|
350
423
|
}
|
|
351
|
-
await ctx.vault.createEnvironmentVault(t, id, actorOf(scope));
|
|
424
|
+
const vaultId = await ctx.vault.createEnvironmentVault(t, id, actorOf(scope), fork?.sealed?.vaultId);
|
|
425
|
+
if (fork?.sealed)
|
|
426
|
+
fork.copied = await ctx.vault.insertEnvironmentSecretCopies(t, vaultId, fork.source, actorOf(scope), fork.sealed.copies);
|
|
427
|
+
const madeFrom = manifest ? { manifest: { name: manifest.name, version: manifest.version } } : {};
|
|
352
428
|
await t.environmentEvent(id, "environment.created", {
|
|
353
429
|
kind,
|
|
354
430
|
labels: row.labels,
|
|
431
|
+
...madeFrom,
|
|
355
432
|
...(seed ? { from: { environment: body.from.environment, version: seed.version } } : {}),
|
|
433
|
+
...(row.forkedFrom ? { forkedFrom: row.forkedFrom } : {}),
|
|
356
434
|
});
|
|
435
|
+
if (fork)
|
|
436
|
+
await t.environmentEvent(fork.source, "environment.forked", {
|
|
437
|
+
to: id,
|
|
438
|
+
disk: seed?.version ?? null,
|
|
439
|
+
...madeFrom,
|
|
440
|
+
secrets: fork.copied?.copied.length ?? 0,
|
|
441
|
+
});
|
|
357
442
|
if (row.pod) {
|
|
358
443
|
// Created at once; idle from now if no turn comes.
|
|
359
444
|
await reconcileAfter(t, id);
|
|
@@ -369,6 +454,45 @@ export async function putEnvironment(ctx, id, body, scope) {
|
|
|
369
454
|
}
|
|
370
455
|
return view;
|
|
371
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* The secret write `raw` asks of Environment `id` (D64). When what it runs declares secret
|
|
459
|
+
* slots, the name must be one (`409 secret_not_in_manifest`), and the body is `{ value }` (or
|
|
460
|
+
* `{ headers }` for a `headers` slot) with the binding from the slot; a full typed body is
|
|
461
|
+
* accepted only when its binding is the slot's. Without slots, today's typed body.
|
|
462
|
+
*/
|
|
463
|
+
export async function secretRequestFor(ctx, id, name, raw) {
|
|
464
|
+
const environment = await ctx.store.tx((t) => liveEnvironment(t, id));
|
|
465
|
+
const slots = environment?.recipe.secrets;
|
|
466
|
+
if (!environment || slots === undefined)
|
|
467
|
+
return PutEnvironmentSecretRequestSchema.parse(raw);
|
|
468
|
+
const slot = slots[name] ??
|
|
469
|
+
fail(409, `What Environment ${id} runs declares no secret ${name}${environment.manifestName ? ` (manifest ${environment.manifestName}@${environment.manifestVersion})` : ""}: add the slot to the manifest, then make or fork an Environment from the new version.`, { code: "secret_not_in_manifest", details: { declared: Object.keys(slots).sort() } });
|
|
470
|
+
const full = slotRequest(slot);
|
|
471
|
+
if (raw !== null && typeof raw === "object" && "type" in raw) {
|
|
472
|
+
const body = PutEnvironmentSecretRequestSchema.parse(raw);
|
|
473
|
+
const { requestId: _, value: _v, headers: _h, ...binding } = body;
|
|
474
|
+
if (canonical(binding) !== canonical(full))
|
|
475
|
+
fail(409, `Secret ${name}'s type and binding come from what Environment ${id} runs: send only its value ({ "value": "…" }).`, {
|
|
476
|
+
code: "secret_not_in_manifest",
|
|
477
|
+
});
|
|
478
|
+
return body;
|
|
479
|
+
}
|
|
480
|
+
const { requestId, value, headers } = PutEnvironmentSecretValueRequestSchema.parse(raw);
|
|
481
|
+
const withId = requestId === undefined ? {} : { requestId };
|
|
482
|
+
if (slot.type === "headers") {
|
|
483
|
+
if (headers === undefined)
|
|
484
|
+
fail(400, `Secret ${name} is a headers credential: send { "headers": { … } }.`);
|
|
485
|
+
return PutEnvironmentSecretRequestSchema.parse({ ...withId, ...full, headers });
|
|
486
|
+
}
|
|
487
|
+
if (value === undefined)
|
|
488
|
+
fail(400, `Secret ${name} takes a value: send { "value": "…" }.`);
|
|
489
|
+
return PutEnvironmentSecretRequestSchema.parse({ ...withId, ...full, value });
|
|
490
|
+
}
|
|
491
|
+
/** A slot's type and binding, as a secret write's fields (without the value). */
|
|
492
|
+
function slotRequest(slot) {
|
|
493
|
+
const { required: _r, description: _d, ...binding } = slot;
|
|
494
|
+
return binding;
|
|
495
|
+
}
|
|
372
496
|
/**
|
|
373
497
|
* The disk version a create's `from` names (R2d step 3): the caller's grants must reach the
|
|
374
498
|
* source, a caller acting for a person must be its guard's owner, and kind `none` has no disk.
|
|
@@ -383,6 +507,113 @@ async function forkSource(t, scope, kind, from) {
|
|
|
383
507
|
notFound();
|
|
384
508
|
return forkSeedOf(t, from);
|
|
385
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* `POST /v1/environments/{id}/fork` (Q15): how an Environment gets a new manifest version. A new
|
|
512
|
+
* Environment `body.id` is made from the manifest version (default the source's manifest at its
|
|
513
|
+
* latest), its disk started from a version of the source's (`latest` takes a snapshot first,
|
|
514
|
+
* refused during a turn), and its secrets copied inside the Runtime. A fork onto other setup
|
|
515
|
+
* runs that setup on the copied disk before its first turn (Q17). The source keeps what it runs,
|
|
516
|
+
* its disk and its sessions. With `dryRun`, the plan only.
|
|
517
|
+
*/
|
|
518
|
+
export async function forkEnvironment(ctx, sourceId, body, scope) {
|
|
519
|
+
const grants = environmentGrantsOf(scope);
|
|
520
|
+
reachable(grants, sourceId);
|
|
521
|
+
reachable(grants, body.id);
|
|
522
|
+
if (body.id === sourceId)
|
|
523
|
+
fail(400, "A fork is a new Environment: give it another id.");
|
|
524
|
+
const { plan: planned, slots } = await ctx.store.tx(async (t) => {
|
|
525
|
+
const source = (await liveEnvironment(t, sourceId)) ?? notFound();
|
|
526
|
+
const subject = subjectOf(scope);
|
|
527
|
+
if (subject !== undefined && source.ownerUserId !== undefined && source.ownerUserId !== subject)
|
|
528
|
+
notFound();
|
|
529
|
+
const ref = body.manifest === undefined ? undefined : parseEnvironmentManifestRef(body.manifest);
|
|
530
|
+
if (body.manifest !== undefined && ref === undefined)
|
|
531
|
+
fail(400, "manifest is an environment manifest's name, or name@version such as analyst-box@3.");
|
|
532
|
+
const name = ref?.name ??
|
|
533
|
+
source.manifestName ??
|
|
534
|
+
fail(400, `Environment ${sourceId} was made from inline fields, not a manifest: name the manifest to fork it onto (manifest).`);
|
|
535
|
+
const manifest = (await t.environmentManifest(name, ref?.version)) ??
|
|
536
|
+
fail(404, ref?.version === undefined
|
|
537
|
+
? `No environment manifest is named ${name}.`
|
|
538
|
+
: `Environment manifest ${name} has no version ${ref.version}.`, { code: "manifest_not_found" });
|
|
539
|
+
const kind = manifest.body.kind ?? "virtual";
|
|
540
|
+
const resolved = resolveRecipe(kind, manifestRecipeRequest(manifest.body), effectiveSandboxConfig(await readSandboxConfig(t)));
|
|
541
|
+
if (resolved.kind === "error")
|
|
542
|
+
return fail(400, resolved.errors.join(" "));
|
|
543
|
+
const recipe = resolved.recipe;
|
|
544
|
+
const asked = body.disk ?? "latest";
|
|
545
|
+
const disk = kind === "none" || source.kind === "none" || asked === "empty"
|
|
546
|
+
? null
|
|
547
|
+
: asked === "latest"
|
|
548
|
+
? "latest"
|
|
549
|
+
: (await forkSeedOf(t, { environment: sourceId, version: asked })).version;
|
|
550
|
+
const vault = await t.vaultOfEnvironment(sourceId);
|
|
551
|
+
const names = vault
|
|
552
|
+
? (await t.credentialsForVault(vault.id)).filter((row) => row.type !== "model").map((row) => row.name).sort()
|
|
553
|
+
: [];
|
|
554
|
+
const copied = body.secrets === "none" ? [] : names.filter((item) => recipe.secrets === undefined || item in recipe.secrets);
|
|
555
|
+
const plan = {
|
|
556
|
+
source: sourceId,
|
|
557
|
+
id: body.id,
|
|
558
|
+
kind,
|
|
559
|
+
manifest: { name: manifest.name, version: manifest.version },
|
|
560
|
+
disk,
|
|
561
|
+
setup: kind === "pod" &&
|
|
562
|
+
disk !== null &&
|
|
563
|
+
recipeHash(recipe) !== source.manifestHash &&
|
|
564
|
+
Boolean(recipe.setup?.length || recipe.files?.length),
|
|
565
|
+
secrets: {
|
|
566
|
+
copied,
|
|
567
|
+
dropped: names.filter((item) => !copied.includes(item)),
|
|
568
|
+
missing: Object.entries(recipe.secrets ?? {})
|
|
569
|
+
.filter(([item, slot]) => slot.required === true && !copied.includes(item))
|
|
570
|
+
.map(([item]) => item)
|
|
571
|
+
.sort(),
|
|
572
|
+
},
|
|
573
|
+
changes: manifestChanges(source.recipe, recipe),
|
|
574
|
+
sessions: (await t.sessionsOnEnvironment(sourceId)).length,
|
|
575
|
+
};
|
|
576
|
+
const slots = recipe.secrets === undefined
|
|
577
|
+
? null
|
|
578
|
+
: Object.fromEntries(Object.entries(recipe.secrets).map(([item, slot]) => [item, slotRequest(slot)]));
|
|
579
|
+
return { plan, slots };
|
|
580
|
+
});
|
|
581
|
+
if (body.dryRun)
|
|
582
|
+
return { dryRun: true, plan: planned };
|
|
583
|
+
// `latest`: a version of the disk as it is now, between turns (an unchanged disk makes none).
|
|
584
|
+
const disk = planned.disk === "latest" ? (await snapshotEnvironment(ctx, sourceId, scope)).version : planned.disk;
|
|
585
|
+
// Keys holds the vault key (F4.2): it seals the copies first, outside the create's transaction,
|
|
586
|
+
// and the create inserts them, so the fork is still one commit.
|
|
587
|
+
const vaultId = randomUUID();
|
|
588
|
+
const sealed = (body.secrets ?? "copy") === "copy"
|
|
589
|
+
? { vaultId, copies: await ctx.keys.sealEnvironmentSecretCopies(sourceId, vaultId, slots) }
|
|
590
|
+
: undefined;
|
|
591
|
+
const fork = { source: sourceId, setup: planned.setup, ...(sealed ? { sealed } : {}) };
|
|
592
|
+
const environment = await putEnvironment(ctx, body.id, {
|
|
593
|
+
...(body.requestId === undefined ? {} : { requestId: body.requestId }),
|
|
594
|
+
manifest: `${planned.manifest.name}@${planned.manifest.version}`,
|
|
595
|
+
...(body.labels === undefined ? {} : { labels: body.labels }),
|
|
596
|
+
...(body.ownerUserId === undefined ? {} : { ownerUserId: body.ownerUserId }),
|
|
597
|
+
...(disk === null ? {} : { from: { environment: sourceId, version: disk } }),
|
|
598
|
+
}, scope, { fork });
|
|
599
|
+
const copied = fork.copied ?? { copied: [], dropped: planned.secrets.copied };
|
|
600
|
+
const dropped = [...new Set([...planned.secrets.dropped, ...copied.dropped])].sort();
|
|
601
|
+
return {
|
|
602
|
+
environment,
|
|
603
|
+
plan: {
|
|
604
|
+
...planned,
|
|
605
|
+
disk,
|
|
606
|
+
secrets: {
|
|
607
|
+
copied: copied.copied,
|
|
608
|
+
dropped,
|
|
609
|
+
missing: Object.entries(environment.manifest.secrets ?? {})
|
|
610
|
+
.filter(([item, slot]) => slot.required === true && !copied.copied.includes(item))
|
|
611
|
+
.map(([item]) => item)
|
|
612
|
+
.sort(),
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
386
617
|
/** `GET /v1/environments/{id}`. */
|
|
387
618
|
export async function getEnvironment(ctx, id, scope) {
|
|
388
619
|
reachable(environmentGrantsOf(scope), id);
|
|
@@ -645,11 +876,9 @@ export async function newVolume(ctx, t, environment, options = {}) {
|
|
|
645
876
|
/** How long a pending seed holds the disk before another process may take it over. */
|
|
646
877
|
export const SEED_LEASE_MS = 10 * 60_000;
|
|
647
878
|
/** `GET /v1/environments/{id}/events`: the Environment's lifecycle stream. */
|
|
648
|
-
export async function environmentEventsOf(ctx, id, scope,
|
|
879
|
+
export async function environmentEventsOf(ctx, id, scope, query = {}) {
|
|
649
880
|
reachable(environmentGrantsOf(scope), id);
|
|
650
|
-
return ctx.store.tx(async (t) => ({
|
|
651
|
-
events: await t.environmentEvents(id, fromSeq === undefined ? {} : { fromSeq }),
|
|
652
|
-
}));
|
|
881
|
+
return ctx.store.tx(async (t) => ({ events: await t.environmentEvents(id, query) }));
|
|
653
882
|
}
|
|
654
883
|
/**
|
|
655
884
|
* The owner guard (Q7): refuses a session of `ownerUserId` on an Environment guarded for
|
|
@@ -681,9 +910,9 @@ export async function recordAttachment(t, sessionId, environmentId) {
|
|
|
681
910
|
/**
|
|
682
911
|
* Checks a turn start on a session that names an Environment, in the command's transaction
|
|
683
912
|
* after the session row lock: the Environment still exists, a token caller's grants reach it,
|
|
684
|
-
* its owner guard admits the session's owner, and
|
|
685
|
-
*
|
|
686
|
-
* one Environment are decided one after the other.
|
|
913
|
+
* its owner guard admits the session's owner, and no disk copy holds it. Other sessions' turns
|
|
914
|
+
* do not stop it (D66). Holds the Environment row's lock until the transaction ends, so a turn
|
|
915
|
+
* start and disk work on one Environment are decided one after the other.
|
|
687
916
|
*
|
|
688
917
|
* A pod Environment must also be usable: not lost (`environment_lost`), on a Runtime with
|
|
689
918
|
* sandbox pods (`environment_unavailable`). A stopped or failed pod is asked to run again, with
|
|
@@ -718,19 +947,11 @@ export async function checkEnvironmentTurn(t, session, scope, ctx) {
|
|
|
718
947
|
return;
|
|
719
948
|
// A snapshot or restore copies the disk between turns (R2d step 3); a seed's turn waits.
|
|
720
949
|
refuseDiskCopy(environment);
|
|
721
|
-
// A flow tree's sessions nest their turns in its root's: one holder.
|
|
722
|
-
const tree = session.flowRootId ?? session.id;
|
|
723
|
-
const holder = (await t.sessionsOnEnvironment(id)).find((other) => other.id !== session.id && other.activeTurnId !== null && (other.flowRootId ?? other.id) !== tree);
|
|
724
|
-
if (holder)
|
|
725
|
-
fail(409, `Environment ${id} is running another session's turn. Turns are serial per Environment.`, {
|
|
726
|
-
code: "environment_busy",
|
|
727
|
-
details: { retryAfterSeconds: 5 },
|
|
728
|
-
}, { "retry-after": "5" });
|
|
729
950
|
const pod = environment.pod;
|
|
730
951
|
if (!pod)
|
|
731
952
|
return;
|
|
732
953
|
if (!ctx?.pods)
|
|
733
|
-
fail(409, `Environment ${id} is a pod Environment, and this Runtime has no sandbox pods: run \`nylorun sandbox enable --context <name>\`.`, {
|
|
954
|
+
fail(409, `Environment ${id} is a pod Environment, and this Runtime has no sandbox pods: run \`nylorun local sandbox enable --context <name>\`.`, {
|
|
734
955
|
code: "environment_unavailable",
|
|
735
956
|
});
|
|
736
957
|
if (pod.observed === "lost")
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* The Tenant's model settings: the fixture model, and the inputs the operator added to catalog
|
|
3
|
+
* models (`model.inputs`, below).
|
|
4
|
+
*
|
|
2
5
|
* The Tenant-level fixture model: Tenant setting `model.fixture`, seeded with
|
|
3
6
|
* `fixtureModel: true` (`PUT /v1/tenant/config/seed`, Host feature `tenant-fixture-model`).
|
|
4
7
|
* A Tenant with it answers every model call with the Runtime's deterministic fixture model
|
|
@@ -8,9 +11,20 @@
|
|
|
8
11
|
* The setting is read once per advance, in a transaction the advance makes anyway, so a Tenant
|
|
9
12
|
* seeded on another node takes it at its next advance.
|
|
10
13
|
*/
|
|
14
|
+
import { type ModelInput } from "@nylorun/core/contracts";
|
|
11
15
|
import type { Tx } from "../store/types.js";
|
|
12
16
|
export declare const FIXTURE_MODEL_SETTING = "model.fixture";
|
|
13
17
|
/** Whether the Tenant uses the fixture model. */
|
|
14
18
|
export declare function usesFixtureModel(t: Tx): Promise<boolean>;
|
|
15
19
|
/** Seeds the setting if absent. Returns whether it was inserted. */
|
|
16
20
|
export declare function seedFixtureModel(t: Tx): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* The inputs the operator added to catalog models (protocol 12, `PUT /v1/tenant/models/inputs`):
|
|
23
|
+
* Tenant setting `model.inputs`, JSON `{ "<provider>/<model>": ModelInput[] }`. A custom
|
|
24
|
+
* endpoint keeps its own in its `settings.inputs` instead.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MODEL_INPUTS_SETTING = "model.inputs";
|
|
27
|
+
/** The added inputs by `<provider>/<model>`. Bad JSON, keys and inputs are left out: never throws. */
|
|
28
|
+
export declare function readAddedInputs(t: Pick<Tx, "getSetting">): Promise<Record<string, ModelInput[]>>;
|
|
29
|
+
/** Sets the inputs added to `<provider>/<model>`; an empty list removes its entry. */
|
|
30
|
+
export declare function writeAddedInputs(t: Pick<Tx, "getSetting" | "putSetting">, key: string, inputs: readonly ModelInput[]): Promise<void>;
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tenant's model settings: the fixture model, and the inputs the operator added to catalog
|
|
3
|
+
* models (`model.inputs`, below).
|
|
4
|
+
*
|
|
5
|
+
* The Tenant-level fixture model: Tenant setting `model.fixture`, seeded with
|
|
6
|
+
* `fixtureModel: true` (`PUT /v1/tenant/config/seed`, Host feature `tenant-fixture-model`).
|
|
7
|
+
* A Tenant with it answers every model call with the Runtime's deterministic fixture model
|
|
8
|
+
* (`core/provider.ts` `toolFixtureModel`) instead of its configured model. Smoke checks seed
|
|
9
|
+
* it (scripts/lib/stack-tenant.mjs).
|
|
10
|
+
*
|
|
11
|
+
* The setting is read once per advance, in a transaction the advance makes anyway, so a Tenant
|
|
12
|
+
* seeded on another node takes it at its next advance.
|
|
13
|
+
*/
|
|
14
|
+
import { ModelInputSchema } from "@nylorun/core/contracts";
|
|
1
15
|
export const FIXTURE_MODEL_SETTING = "model.fixture";
|
|
2
16
|
/** Whether the Tenant uses the fixture model. */
|
|
3
17
|
export async function usesFixtureModel(t) {
|
|
@@ -10,3 +24,40 @@ export async function seedFixtureModel(t) {
|
|
|
10
24
|
await t.putSetting(FIXTURE_MODEL_SETTING, "true");
|
|
11
25
|
return true;
|
|
12
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The inputs the operator added to catalog models (protocol 12, `PUT /v1/tenant/models/inputs`):
|
|
29
|
+
* Tenant setting `model.inputs`, JSON `{ "<provider>/<model>": ModelInput[] }`. A custom
|
|
30
|
+
* endpoint keeps its own in its `settings.inputs` instead.
|
|
31
|
+
*/
|
|
32
|
+
export const MODEL_INPUTS_SETTING = "model.inputs";
|
|
33
|
+
/** The added inputs by `<provider>/<model>`. Bad JSON, keys and inputs are left out: never throws. */
|
|
34
|
+
export async function readAddedInputs(t) {
|
|
35
|
+
const raw = await t.getSetting(MODEL_INPUTS_SETTING);
|
|
36
|
+
if (raw === undefined)
|
|
37
|
+
return {};
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
46
|
+
return {};
|
|
47
|
+
const added = {};
|
|
48
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
49
|
+
if (!key.includes("/") || !Array.isArray(value))
|
|
50
|
+
continue;
|
|
51
|
+
added[key] = value.filter((input) => ModelInputSchema.safeParse(input).success);
|
|
52
|
+
}
|
|
53
|
+
return added;
|
|
54
|
+
}
|
|
55
|
+
/** Sets the inputs added to `<provider>/<model>`; an empty list removes its entry. */
|
|
56
|
+
export async function writeAddedInputs(t, key, inputs) {
|
|
57
|
+
const added = await readAddedInputs(t);
|
|
58
|
+
if (inputs.length === 0)
|
|
59
|
+
delete added[key];
|
|
60
|
+
else
|
|
61
|
+
added[key] = [...inputs];
|
|
62
|
+
await t.putSetting(MODEL_INPUTS_SETTING, JSON.stringify(added));
|
|
63
|
+
}
|
|
@@ -25,10 +25,14 @@ export declare function listAgentsPublic(ctx: TenantContext, agents: ReadonlySet
|
|
|
25
25
|
description?: any;
|
|
26
26
|
}[];
|
|
27
27
|
}>;
|
|
28
|
-
export declare function putDefinition(ctx: TenantContext, agentId: string, body: PutAgentRequest
|
|
28
|
+
export declare function putDefinition(ctx: TenantContext, agentId: string, body: PutAgentRequest, options?: {
|
|
29
|
+
/** Run every check and save nothing (`?dryRun=true`, protocol 14). */
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
}): Promise<{
|
|
29
32
|
agentId: string;
|
|
30
33
|
manifestHash: string;
|
|
31
34
|
implementationVersion: string;
|
|
35
|
+
dryRun?: boolean | undefined;
|
|
32
36
|
}>;
|
|
33
37
|
/**
|
|
34
38
|
* `access`, when the request acts for a person, limits the list to that person's sessions of
|