@nylorun/runtime 0.31.0-beta → 0.33.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 +63 -0
- package/dist/api/components.d.ts +100 -0
- package/dist/api/components.js +4 -1
- package/dist/api/http/routes/agent-manifests.d.ts +2 -1
- package/dist/api/http/routes/agent-manifests.js +29 -2
- package/dist/api/http/routes/sessions.js +63 -2
- package/dist/api/openapi.js +4 -0
- package/dist/gates/handler.js +2 -2
- package/dist/gates/in-process.d.ts +4 -3
- package/dist/gates/in-process.js +11 -5
- package/dist/gates/tenant-vaults.d.ts +4 -3
- package/dist/gates/tenant-vaults.js +2 -2
- package/dist/harness-api/start.js +1 -1
- package/dist/host/main.js +2 -0
- package/dist/host/stack-config.d.ts +5 -0
- package/dist/host/stack-config.js +15 -0
- package/dist/management-openapi.json +90 -8
- package/dist/model/descriptor.d.ts +5 -4
- package/dist/model/descriptor.js +4 -3
- package/dist/model/pi-model.js +17 -1
- package/dist/openapi.json +880 -2
- package/dist/tenant/agent-manifests.d.ts +6 -1
- package/dist/tenant/agent-manifests.js +50 -8
- package/dist/tenant/agents.d.ts +4 -3
- package/dist/tenant/agents.js +26 -5
- package/dist/tenant/context.d.ts +6 -1
- package/dist/tenant/session-model.d.ts +16 -0
- package/dist/tenant/session-model.js +46 -0
- package/dist/vault/host-identity.d.ts +24 -0
- package/dist/vault/host-identity.js +35 -0
- package/dist/vault/host-model.d.ts +9 -5
- package/dist/vault/host-model.js +30 -19
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +52 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentManifestChange, type AgentManifestPlan, type AgentManifestSource, type AgentManifestV8, type AgentManifestVersions, type AgentManifestView, type PutAgentFolderRequest, type PutAgentManifestRequest, type SaveAgentManifestResponse } from "@nylorun/core/contracts";
|
|
1
|
+
import { type AgentManifestChange, type AgentManifestPlan, type AgentManifestSource, type AgentManifestSourceDiff, type AgentManifestV8, type AgentManifestVersions, type AgentManifestView, type PutAgentFolderRequest, type PutAgentManifestRequest, type SaveAgentManifestResponse } from "@nylorun/core/contracts";
|
|
2
2
|
import { type RecipeRequest } from "../sandbox/resolve.js";
|
|
3
3
|
import type { AuthScope, TenantContext } from "./context.js";
|
|
4
4
|
/** What a manifest's runtime section asks of an Agent's recipe, with the harness its loop names. */
|
|
@@ -47,6 +47,11 @@ export declare function readAgentManifestSourceFile(ctx: TenantContext, name: st
|
|
|
47
47
|
body: ReadableStream<Uint8Array>;
|
|
48
48
|
size: number;
|
|
49
49
|
}>;
|
|
50
|
+
/**
|
|
51
|
+
* `GET /v1/agent-manifests/{name}/source/diff?from=&to=`: the files added, removed and changed
|
|
52
|
+
* (by SHA-256) from version `from`'s source to version `to`'s; a `404` when either was saved as JSON.
|
|
53
|
+
*/
|
|
54
|
+
export declare function diffAgentManifestSources(ctx: TenantContext, name: string, from: number, to: number): Promise<AgentManifestSourceDiff>;
|
|
50
55
|
/** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
|
|
51
56
|
export declare function getAgentManifest(ctx: TenantContext, name: string, version?: number): Promise<AgentManifestView>;
|
|
52
57
|
/** `GET /v1/agent-manifests`: each manifest's latest version. */
|
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
* compile is `422 folder_invalid`, every diagnostic in `details.diagnostics`), and saves the
|
|
28
28
|
* manifest as above, with the folder kept as the version's source (`source`, its files recorded
|
|
29
29
|
* as the version's definition files). A folder whose manifest is unchanged saves no new
|
|
30
|
-
* version, so its source is not saved either.
|
|
30
|
+
* version, so its source is not saved either. What a folder may hold is the Tenant's Runtime
|
|
31
|
+
* limits `folderFiles`, `folderBytes` and `folderTextFileBytes` (track T7), which the compiler
|
|
32
|
+
* is given too; `source/diff` compares two versions' sources by path and hash.
|
|
31
33
|
*/
|
|
32
|
-
import { compileAgentFolder, folderTextPaths,
|
|
34
|
+
import { compileAgentFolder, folderTextPaths, } from "@nylorun/core/folder";
|
|
33
35
|
import { definitionFilesOf, } from "@nylorun/core/contracts";
|
|
34
36
|
import { hashManifest } from "@nylorun/core/compatibility";
|
|
35
37
|
import { approvalRefusal, codeToolRefusal, harnessIdOf, loopManifestOf, runtimeOf, sectionGaps, } from "@nylorun/core/define";
|
|
@@ -356,10 +358,17 @@ async function saveManifest(ctx, name, manifest, scope, options, folder) {
|
|
|
356
358
|
*/
|
|
357
359
|
export async function saveAgentFolder(ctx, name, body, scope, options = {}) {
|
|
358
360
|
const entries = [...body.entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
359
|
-
|
|
360
|
-
|
|
361
|
+
const limits = folderLimitsOf(await ctx.store.tx((t) => readRuntimeLimits(t)));
|
|
362
|
+
if (entries.length > limits.files)
|
|
363
|
+
fail(413, `An agent folder holds at most ${limits.files} files (the Runtime limit folderFiles); this one holds ${entries.length}`, {
|
|
361
364
|
code: "limit_exceeded",
|
|
362
|
-
details: { limit:
|
|
365
|
+
details: { limit: "folderFiles", value: limits.files },
|
|
366
|
+
});
|
|
367
|
+
const bytes = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
368
|
+
if (bytes > limits.totalBytes)
|
|
369
|
+
fail(413, `An agent folder holds at most ${limits.totalBytes} bytes (the Runtime limit folderBytes); this one holds ${bytes}`, {
|
|
370
|
+
code: "limit_exceeded",
|
|
371
|
+
details: { limit: "folderBytes", value: limits.totalBytes },
|
|
363
372
|
});
|
|
364
373
|
// Every file must be held: the version keeps them all as its source.
|
|
365
374
|
const shas = [...new Set(entries.map((entry) => `sha256:${entry.sha256}`))].sort();
|
|
@@ -377,13 +386,13 @@ export async function saveAgentFolder(ctx, name, body, scope, options = {}) {
|
|
|
377
386
|
const files = new Map();
|
|
378
387
|
for (const path of folderTextPaths(entries)) {
|
|
379
388
|
const entry = entries.find((item) => item.path === path);
|
|
380
|
-
if (entry.size >
|
|
389
|
+
if (entry.size > limits.textFileBytes)
|
|
381
390
|
continue;
|
|
382
391
|
const bytes = await readDefinitionFile(ctx.blobs, `sha256:${entry.sha256}`);
|
|
383
392
|
if (bytes)
|
|
384
393
|
files.set(path, bytes);
|
|
385
394
|
}
|
|
386
|
-
const compiled = compileAgentFolder({ entries, files, name });
|
|
395
|
+
const compiled = compileAgentFolder({ entries, files, name, limits });
|
|
387
396
|
if (!compiled.manifest) {
|
|
388
397
|
const errors = compiled.diagnostics.filter((item) => item.severity === "error");
|
|
389
398
|
fail(422, `Agent folder ${name} does not compile: ${errors.length} error(s), the first: ${located(errors[0])}`, {
|
|
@@ -394,7 +403,7 @@ export async function saveAgentFolder(ctx, name, body, scope, options = {}) {
|
|
|
394
403
|
const source = {
|
|
395
404
|
compiler: compiled.compiler,
|
|
396
405
|
files: entries.length,
|
|
397
|
-
bytes
|
|
406
|
+
bytes,
|
|
398
407
|
entries: entries.map((entry) => ({ path: entry.path, size: entry.size, sha256: entry.sha256 })),
|
|
399
408
|
};
|
|
400
409
|
return saveManifest(ctx, name, compiled.manifest, scope, options, {
|
|
@@ -403,6 +412,10 @@ export async function saveAgentFolder(ctx, name, body, scope, options = {}) {
|
|
|
403
412
|
compiler: compiled.compiler,
|
|
404
413
|
});
|
|
405
414
|
}
|
|
415
|
+
/** What a folder may hold: the Tenant's Runtime limits, as the compiler takes them. */
|
|
416
|
+
function folderLimitsOf(limits) {
|
|
417
|
+
return { files: limits.folderFiles, totalBytes: limits.folderBytes, textFileBytes: limits.folderTextFileBytes };
|
|
418
|
+
}
|
|
406
419
|
/** A diagnostic as a person reads it: `file:line:column message`. */
|
|
407
420
|
function located(diagnostic) {
|
|
408
421
|
if (!diagnostic)
|
|
@@ -435,6 +448,35 @@ export async function readAgentManifestSourceFile(ctx, name, version, path) {
|
|
|
435
448
|
fail(404, `The Runtime no longer holds ${path} of version ${version} of agent manifest ${name}.`, { code: "not_found" });
|
|
436
449
|
return { body: body, size: entry.size };
|
|
437
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* `GET /v1/agent-manifests/{name}/source/diff?from=&to=`: the files added, removed and changed
|
|
453
|
+
* (by SHA-256) from version `from`'s source to version `to`'s; a `404` when either was saved as JSON.
|
|
454
|
+
*/
|
|
455
|
+
export async function diffAgentManifestSources(ctx, name, from, to) {
|
|
456
|
+
const before = new Map((await sourceOf(ctx, name, from)).entries.map((entry) => [entry.path, entry]));
|
|
457
|
+
const after = (await sourceOf(ctx, name, to)).entries;
|
|
458
|
+
const diff = { name, from, to, added: [], removed: [], changed: [], unchanged: 0 };
|
|
459
|
+
for (const entry of after) {
|
|
460
|
+
const was = before.get(entry.path);
|
|
461
|
+
before.delete(entry.path);
|
|
462
|
+
if (!was)
|
|
463
|
+
diff.added.push(entry);
|
|
464
|
+
else if (was.sha256 === entry.sha256)
|
|
465
|
+
diff.unchanged++;
|
|
466
|
+
else
|
|
467
|
+
diff.changed.push({
|
|
468
|
+
path: entry.path,
|
|
469
|
+
from: { size: was.size, sha256: was.sha256 },
|
|
470
|
+
to: { size: entry.size, sha256: entry.sha256 },
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
diff.removed.push(...before.values());
|
|
474
|
+
const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
475
|
+
diff.added.sort(byPath);
|
|
476
|
+
diff.removed.sort(byPath);
|
|
477
|
+
diff.changed.sort(byPath);
|
|
478
|
+
return diff;
|
|
479
|
+
}
|
|
438
480
|
/** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
|
|
439
481
|
export async function getAgentManifest(ctx, name, version) {
|
|
440
482
|
return ctx.store.tx(async (t) => {
|
package/dist/tenant/agents.d.ts
CHANGED
|
@@ -17,10 +17,11 @@ export declare function liveEnvironment(t: Tx, id: string, options?: {
|
|
|
17
17
|
* An Agent's state (protocol 16), from its desired state, its compute and its sessions, and
|
|
18
18
|
* every state that holds with the session behind it (`attention`), highest first. A session waits
|
|
19
19
|
* on a person while it is paused (an approval or an interaction) or uncertain, and the Agent while
|
|
20
|
-
* a move waits for its required secrets (track T3)
|
|
21
|
-
*
|
|
20
|
+
* a move waits for its required secrets (track T3), or a required secret of the version it runs
|
|
21
|
+
* has no value (`unsetSecrets`, track T7: a tool would fail when it is called); a session's last
|
|
22
|
+
* turn failed while it is `failed`; a turn runs while it is `running`, `runnable` or `waiting`.
|
|
22
23
|
*/
|
|
23
|
-
export declare function stateOf(agent: Pick<Environment, "desiredState" | "pod"> & Partial<Pick<Environment, "targetVersion" | "appliedVersion" | "version" | "failedVersion">>, sessions: readonly Pick<Session, "id" | "status">[]): {
|
|
24
|
+
export declare function stateOf(agent: Pick<Environment, "desiredState" | "pod"> & Partial<Pick<Environment, "targetVersion" | "appliedVersion" | "version" | "failedVersion">>, sessions: readonly Pick<Session, "id" | "status">[], unsetSecrets?: readonly string[]): {
|
|
24
25
|
state: AgentState;
|
|
25
26
|
attention: AgentAttention[];
|
|
26
27
|
};
|
package/dist/tenant/agents.js
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* the Agent's id).
|
|
23
23
|
* - **State on read.** `state` is derived when it is read, the first rule that holds: `paused`
|
|
24
24
|
* (desired `off`, track T6), `failing` (its compute failed or was lost, or a session's last
|
|
25
|
-
* turn failed), `input_required` (a session waits on a person
|
|
25
|
+
* turn failed), `input_required` (a session waits on a person, or a required secret is unset),
|
|
26
|
+
* `working` (a turn runs),
|
|
26
27
|
* `sleeping`; `attention` names the sessions behind each that holds (`stateOf`).
|
|
27
28
|
* - **Secret slots** (D64): when its manifest declares `secrets`, a secret write sends only the
|
|
28
29
|
* value, the binding comes from the slot, and a name it does not declare is
|
|
@@ -110,16 +111,20 @@ const STATE_RANK = ["paused", "failing", "input_required", "working", "sleeping"
|
|
|
110
111
|
* An Agent's state (protocol 16), from its desired state, its compute and its sessions, and
|
|
111
112
|
* every state that holds with the session behind it (`attention`), highest first. A session waits
|
|
112
113
|
* on a person while it is paused (an approval or an interaction) or uncertain, and the Agent while
|
|
113
|
-
* a move waits for its required secrets (track T3)
|
|
114
|
-
*
|
|
114
|
+
* a move waits for its required secrets (track T3), or a required secret of the version it runs
|
|
115
|
+
* has no value (`unsetSecrets`, track T7: a tool would fail when it is called); a session's last
|
|
116
|
+
* turn failed while it is `failed`; a turn runs while it is `running`, `runnable` or `waiting`.
|
|
115
117
|
*/
|
|
116
|
-
export function stateOf(agent, sessions) {
|
|
118
|
+
export function stateOf(agent, sessions, unsetSecrets = []) {
|
|
117
119
|
const attention = [];
|
|
118
120
|
if (agent.desiredState === "off")
|
|
119
121
|
attention.push({ state: "paused", reason: "desired off" });
|
|
120
122
|
// A move that waits for a person to set its required secrets (track T3).
|
|
121
123
|
if (agent.targetVersion !== undefined && agent.appliedVersion === agent.version)
|
|
122
124
|
attention.push({ state: "input_required", reason: `secret_required: version ${agent.targetVersion} waits for its required secrets` });
|
|
125
|
+
// A required secret of the version it runs, unset (track T7).
|
|
126
|
+
for (const name of unsetSecrets)
|
|
127
|
+
attention.push({ state: "input_required", reason: `secret_unset: required secret ${name} unset` });
|
|
123
128
|
const pod = agent.pod;
|
|
124
129
|
if (pod && (pod.observed === "failed" || pod.observed === "lost") && pod.desired !== "deleted")
|
|
125
130
|
attention.push({ state: "failing", reason: `compute ${pod.observed}${pod.reason ? `: ${pod.reason}` : ""}` });
|
|
@@ -152,7 +157,7 @@ async function viewOf(ctx, t, environment, owner) {
|
|
|
152
157
|
: undefined;
|
|
153
158
|
const pod = environment.pod;
|
|
154
159
|
const visible = sessions.filter((session) => owner === undefined || session.ownerUserId === owner);
|
|
155
|
-
const { state, attention } = stateOf(environment, visible);
|
|
160
|
+
const { state, attention } = stateOf(environment, visible, await unsetRequiredSecrets(t, environment));
|
|
156
161
|
return {
|
|
157
162
|
id: environment.id,
|
|
158
163
|
manifest: manifestRefOf(environment),
|
|
@@ -230,6 +235,22 @@ async function slotsAhead(t, environment) {
|
|
|
230
235
|
const target = await t.agentManifest(environment.manifestName, environment.targetVersion);
|
|
231
236
|
return target ? runtimeOf(target.body).secrets : environment.applied.secrets;
|
|
232
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* The required secret slots of the version `environment` runs (its applied runtime) that its vault
|
|
240
|
+
* holds no value for, by name: the Agent's attention (track T7), before a tool fails for want of
|
|
241
|
+
* one. A move waiting on the next version's secrets is its own attention item (`stateOf`).
|
|
242
|
+
*/
|
|
243
|
+
async function unsetRequiredSecrets(t, environment) {
|
|
244
|
+
const required = Object.entries(environment.applied.secrets ?? {}).filter(([, slot]) => slot.required === true);
|
|
245
|
+
if (required.length === 0)
|
|
246
|
+
return [];
|
|
247
|
+
const vault = await t.vaultOfEnvironment(environment.id);
|
|
248
|
+
const rows = vault ? await t.credentialsForVault(vault.id) : [];
|
|
249
|
+
return required
|
|
250
|
+
.filter(([name, slot]) => !rows.some((row) => row.name === name && slotHolds(slot, row)))
|
|
251
|
+
.map(([name]) => name)
|
|
252
|
+
.sort();
|
|
253
|
+
}
|
|
233
254
|
/** Each secret slot `environment` declares (`slotsAhead`), and whether its vault holds a value that fills it. */
|
|
234
255
|
async function slotsOf(t, environment) {
|
|
235
256
|
const declared = await slotsAhead(t, environment);
|
package/dist/tenant/context.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* under ownership (§10.6); `abortLocal` aborts an advance running on this process.
|
|
16
16
|
*/
|
|
17
17
|
import type { SessionHistory } from "./history.js";
|
|
18
|
-
import type { IssuerScope, SubjectScope, TenantEnvelope } from "@nylorun/core/contracts";
|
|
18
|
+
import type { IssuerScope, SessionModelChoice, SubjectScope, TenantEnvelope } from "@nylorun/core/contracts";
|
|
19
19
|
import type { DurableCheckpoint, FlowCheckpoint } from "@nylorun/harness/run";
|
|
20
20
|
import type { AgentManifest, JsonValue, SandboxManifest } from "@nylorun/core/define";
|
|
21
21
|
import type { OutboxRequest, SessionStore, Tx } from "../store/types.js";
|
|
@@ -141,6 +141,11 @@ export interface Session {
|
|
|
141
141
|
* Tenant-wide ones beside the session's own. Absent until a message names a file.
|
|
142
142
|
*/
|
|
143
143
|
fileArtifacts?: string[];
|
|
144
|
+
/**
|
|
145
|
+
* The session's own model (Host feature `session-model`): its turns' model calls use it in
|
|
146
|
+
* place of the Tenant's while its provider is connected. Absent: the Tenant's model.
|
|
147
|
+
*/
|
|
148
|
+
model?: SessionModelChoice;
|
|
144
149
|
}
|
|
145
150
|
export type AuthScope = {
|
|
146
151
|
kind: "application";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session's own model (Host feature `session-model`): one of a connected provider's models,
|
|
3
|
+
* which the session's turns use in place of the Tenant's. It is kept on the session
|
|
4
|
+
* (`Session.model`): the Model Gate reads it at each call (`HostModelVault.readHostModel`) and a
|
|
5
|
+
* turn's start describes it (`turn.start.model`). The Tenant's model, and every other session's,
|
|
6
|
+
* stay as they are. While its provider is disconnected the session uses the Tenant's model again.
|
|
7
|
+
*/
|
|
8
|
+
import type { SessionModelChoice, SessionModelView } from "@nylorun/core/contracts";
|
|
9
|
+
import { type SessionAccess, type TenantContext } from "./context.js";
|
|
10
|
+
/** The model the session's next turn uses, and where it comes from. */
|
|
11
|
+
export declare function readSessionModel(ctx: TenantContext, id: string, access: SessionAccess | undefined): Promise<SessionModelView>;
|
|
12
|
+
/**
|
|
13
|
+
* Chooses the session's own model, or clears it (`choice` undefined) so its turns use the
|
|
14
|
+
* Tenant's. A turn calls one model: the choice changes only while no turn runs or waits.
|
|
15
|
+
*/
|
|
16
|
+
export declare function chooseSessionModel(ctx: TenantContext, id: string, choice: SessionModelChoice | undefined, access: SessionAccess | undefined): Promise<SessionModelView>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { catalogModel, hostModelCatalog } from "../model/catalog.js";
|
|
2
|
+
import { hostCredentialRowFor, modelBinding } from "../vault/service.js";
|
|
3
|
+
import { takesMessages } from "./commands.js";
|
|
4
|
+
import { lockedSession, sessionOf } from "./context.js";
|
|
5
|
+
import { fail } from "./http.js";
|
|
6
|
+
/** The model the session's next turn uses, and where it comes from. */
|
|
7
|
+
export async function readSessionModel(ctx, id, access) {
|
|
8
|
+
const session = await ctx.store.tx((t) => sessionOf(t, id, access));
|
|
9
|
+
return ctx.vault.getSessionModel(session.model);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Chooses the session's own model, or clears it (`choice` undefined) so its turns use the
|
|
13
|
+
* Tenant's. A turn calls one model: the choice changes only while no turn runs or waits.
|
|
14
|
+
*/
|
|
15
|
+
export async function chooseSessionModel(ctx, id, choice, access) {
|
|
16
|
+
const session = await ctx.store.tx(async (t) => {
|
|
17
|
+
const s = await lockedSession(t, id, access);
|
|
18
|
+
if (!takesMessages(s))
|
|
19
|
+
fail(409, "The session's turn is running or waits: choose its model when the turn ends", {
|
|
20
|
+
code: "session_busy",
|
|
21
|
+
});
|
|
22
|
+
if (choice)
|
|
23
|
+
await checkChoice(t, choice);
|
|
24
|
+
const { model: _model, ...rest } = s;
|
|
25
|
+
const next = choice ? { ...rest, model: { provider: choice.provider, model: choice.model } } : rest;
|
|
26
|
+
await t.put("sessions", id, next);
|
|
27
|
+
return next;
|
|
28
|
+
});
|
|
29
|
+
return ctx.vault.getSessionModel(session.model);
|
|
30
|
+
}
|
|
31
|
+
/** A choice names a connected provider and, when its catalog lists models, one of them. */
|
|
32
|
+
async function checkChoice(t, choice) {
|
|
33
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(choice.provider))
|
|
34
|
+
fail(400, "Invalid provider id", { code: "invalid_request" });
|
|
35
|
+
const row = await hostCredentialRowFor(t, choice.provider);
|
|
36
|
+
if (!row)
|
|
37
|
+
fail(422, `Model provider ${choice.provider} is not connected: connect it in Model Settings first`, {
|
|
38
|
+
code: "request_rejected",
|
|
39
|
+
});
|
|
40
|
+
// A custom endpoint serves the models it names; the provider's own model is always one.
|
|
41
|
+
if (choice.provider === "custom" || modelBinding(row).model === choice.model)
|
|
42
|
+
return;
|
|
43
|
+
const listed = hostModelCatalog().providers.find((provider) => provider.id === choice.provider);
|
|
44
|
+
if (listed && listed.models.length > 0 && !catalogModel(choice.provider, choice.model))
|
|
45
|
+
fail(422, `Unknown model ${choice.model} for provider ${choice.provider}`, { code: "request_rejected" });
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host's identity for model calls (ADR-71): a model credential of type `host_identity` holds
|
|
3
|
+
* no secret, only the Google Cloud project and location. The model gate calls Agent Platform with
|
|
4
|
+
* the Host's Google Application Default Credentials, which Google's client library finds at each
|
|
5
|
+
* call (`GOOGLE_APPLICATION_CREDENTIALS`, gcloud's file, or the metadata server on Google Cloud).
|
|
6
|
+
* Nothing of them is stored, logged or passed to a sandbox.
|
|
7
|
+
*
|
|
8
|
+
* Off unless the Host's operator lends it, per provider, with `NYLORUN_MODEL_HOST_IDENTITY` on the
|
|
9
|
+
* process that runs the gates. Every Tenant on the Host may then use it, so a Host that serves
|
|
10
|
+
* several organizations must not set it.
|
|
11
|
+
*/
|
|
12
|
+
/** The providers that can use the Host's identity. */
|
|
13
|
+
export declare const HOST_IDENTITY_PROVIDERS: readonly ["google-vertex"];
|
|
14
|
+
/**
|
|
15
|
+
* What pi-ai's Vertex AI client takes in place of an API key to use Application Default
|
|
16
|
+
* Credentials, with the project and location in the call's `env`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const VERTEX_ADC_MARKER = "gcp-vertex-credentials";
|
|
19
|
+
/** Sets the providers this process lends the Host's identity to (at startup). */
|
|
20
|
+
export declare function useHostIdentity(providers: readonly string[]): void;
|
|
21
|
+
/** Why a model of `provider` may not use the Host's identity here, or undefined when it may. */
|
|
22
|
+
export declare function hostIdentityRefused(provider: string): string | undefined;
|
|
23
|
+
/** The call's `env` for a `host_identity` credential: where Agent Platform runs it. */
|
|
24
|
+
export declare function hostIdentityEnv(project: string, location: string): Record<string, string>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host's identity for model calls (ADR-71): a model credential of type `host_identity` holds
|
|
3
|
+
* no secret, only the Google Cloud project and location. The model gate calls Agent Platform with
|
|
4
|
+
* the Host's Google Application Default Credentials, which Google's client library finds at each
|
|
5
|
+
* call (`GOOGLE_APPLICATION_CREDENTIALS`, gcloud's file, or the metadata server on Google Cloud).
|
|
6
|
+
* Nothing of them is stored, logged or passed to a sandbox.
|
|
7
|
+
*
|
|
8
|
+
* Off unless the Host's operator lends it, per provider, with `NYLORUN_MODEL_HOST_IDENTITY` on the
|
|
9
|
+
* process that runs the gates. Every Tenant on the Host may then use it, so a Host that serves
|
|
10
|
+
* several organizations must not set it.
|
|
11
|
+
*/
|
|
12
|
+
/** The providers that can use the Host's identity. */
|
|
13
|
+
export const HOST_IDENTITY_PROVIDERS = ["google-vertex"];
|
|
14
|
+
/**
|
|
15
|
+
* What pi-ai's Vertex AI client takes in place of an API key to use Application Default
|
|
16
|
+
* Credentials, with the project and location in the call's `env`.
|
|
17
|
+
*/
|
|
18
|
+
export const VERTEX_ADC_MARKER = "gcp-vertex-credentials";
|
|
19
|
+
let lent = [];
|
|
20
|
+
/** Sets the providers this process lends the Host's identity to (at startup). */
|
|
21
|
+
export function useHostIdentity(providers) {
|
|
22
|
+
lent = [...providers];
|
|
23
|
+
}
|
|
24
|
+
/** Why a model of `provider` may not use the Host's identity here, or undefined when it may. */
|
|
25
|
+
export function hostIdentityRefused(provider) {
|
|
26
|
+
if (!HOST_IDENTITY_PROVIDERS.includes(provider))
|
|
27
|
+
return `The Host's identity authenticates ${HOST_IDENTITY_PROVIDERS.join(", ")} only, not ${provider}.`;
|
|
28
|
+
if (!lent.includes(provider))
|
|
29
|
+
return `This Host doesn't lend its identity to ${provider}: its operator sets NYLORUN_MODEL_HOST_IDENTITY=${provider}.`;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/** The call's `env` for a `host_identity` credential: where Agent Platform runs it. */
|
|
33
|
+
export function hostIdentityEnv(project, location) {
|
|
34
|
+
return { GOOGLE_CLOUD_PROJECT: project, GOOGLE_CLOUD_LOCATION: location };
|
|
35
|
+
}
|
|
@@ -18,11 +18,15 @@ export declare class HostModelVault {
|
|
|
18
18
|
private readonly kek;
|
|
19
19
|
constructor(options: HostModelVaultOptions);
|
|
20
20
|
/**
|
|
21
|
-
* Reads the
|
|
22
|
-
* (protocol 12)
|
|
21
|
+
* Reads the host model secret a call uses, with the inputs the operator added to catalog
|
|
22
|
+
* models (protocol 12): the session's own model while its provider is connected (Host feature
|
|
23
|
+
* `session-model`), else the Tenant's active one. Decrypts after the read commits.
|
|
24
|
+
*/
|
|
25
|
+
readHostModel(sessionId?: string): Promise<HostModelSecret | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* Replaces a host model's secret, keeping its binding: `provider`'s, the one the refreshed
|
|
28
|
+
* credential was read for, else the active one's.
|
|
23
29
|
*/
|
|
24
|
-
readHostModel(): Promise<HostModelSecret | undefined>;
|
|
25
|
-
/** Replaces the active host model's secret, keeping its binding. */
|
|
26
30
|
updateHostCredential(credential: {
|
|
27
31
|
type: "api_key" | "oauth";
|
|
28
32
|
key?: string;
|
|
@@ -30,5 +34,5 @@ export declare class HostModelVault {
|
|
|
30
34
|
refresh?: string;
|
|
31
35
|
access?: string;
|
|
32
36
|
expires?: number;
|
|
33
|
-
}): Promise<void>;
|
|
37
|
+
}, provider?: string): Promise<void>;
|
|
34
38
|
}
|
package/dist/vault/host-model.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readAddedInputs } from "../tenant/model-setting.js";
|
|
2
2
|
import { encryptSecret } from "./crypto.js";
|
|
3
3
|
import { VaultError } from "./error.js";
|
|
4
|
-
import { activeHostCredentialRow, HOST_VAULT_ID, hostModelAad, modelBinding, readHostPayload, } from "./service.js";
|
|
4
|
+
import { activeHostCredentialRow, hostCredentialRowFor, hostCredentialRowOf, HOST_VAULT_ID, hostModelAad, modelBinding, readHostPayload, } from "./service.js";
|
|
5
5
|
export class HostModelVault {
|
|
6
6
|
store;
|
|
7
7
|
kek;
|
|
@@ -10,28 +10,36 @@ export class HostModelVault {
|
|
|
10
10
|
this.kek = options.kek;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Reads the
|
|
14
|
-
* (protocol 12)
|
|
13
|
+
* Reads the host model secret a call uses, with the inputs the operator added to catalog
|
|
14
|
+
* models (protocol 12): the session's own model while its provider is connected (Host feature
|
|
15
|
+
* `session-model`), else the Tenant's active one. Decrypts after the read commits.
|
|
15
16
|
*/
|
|
16
|
-
async readHostModel() {
|
|
17
|
-
const {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
async readHostModel(sessionId) {
|
|
18
|
+
const { found, addedInputs } = await this.store.tx(async (t) => {
|
|
19
|
+
const session = sessionId === undefined ? undefined : await t.get("sessions", sessionId);
|
|
20
|
+
return {
|
|
21
|
+
found: await hostCredentialRowOf(t, session?.model),
|
|
22
|
+
addedInputs: await readAddedInputs(t),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
if (!found)
|
|
22
26
|
return undefined;
|
|
23
|
-
const binding =
|
|
24
|
-
|
|
27
|
+
const { row, binding } = found;
|
|
28
|
+
// The row's own binding seals its secret; a session's model only changes what is called.
|
|
29
|
+
const payload = readHostPayload(this.kek(), row, modelBinding(row));
|
|
25
30
|
const credential = binding.authType === "oauth"
|
|
26
31
|
? { type: "oauth", ...payload.oauth }
|
|
27
32
|
: binding.authType === "workload"
|
|
28
33
|
? // The workload token is read from its file at each call (#496), never stored.
|
|
29
34
|
{ type: "workload" }
|
|
30
|
-
:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
: binding.authType === "host_identity"
|
|
36
|
+
? // The Host's identity is found at each call (ADR-71): only where the calls go is kept.
|
|
37
|
+
{ type: "host_identity", ...(payload.env ? { env: payload.env } : {}) }
|
|
38
|
+
: {
|
|
39
|
+
type: "api_key",
|
|
40
|
+
key: payload.apiKey,
|
|
41
|
+
...(payload.env ? { env: payload.env } : {}),
|
|
42
|
+
};
|
|
35
43
|
return {
|
|
36
44
|
provider: binding.provider,
|
|
37
45
|
model: binding.model,
|
|
@@ -42,11 +50,14 @@ export class HostModelVault {
|
|
|
42
50
|
credential,
|
|
43
51
|
};
|
|
44
52
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Replaces a host model's secret, keeping its binding: `provider`'s, the one the refreshed
|
|
55
|
+
* credential was read for, else the active one's.
|
|
56
|
+
*/
|
|
57
|
+
async updateHostCredential(credential, provider) {
|
|
47
58
|
const kek = this.kek();
|
|
48
59
|
await this.store.tx(async (t) => {
|
|
49
|
-
const row = await activeHostCredentialRow(t);
|
|
60
|
+
const row = provider === undefined ? await activeHostCredentialRow(t) : await hostCredentialRowFor(t, provider);
|
|
50
61
|
if (!row)
|
|
51
62
|
throw new VaultError(404, "Model provider is not configured");
|
|
52
63
|
const binding = modelBinding(row);
|
package/dist/vault/service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretSlot, AgentSecretView, HostModelAuthType, HostModelProviderInfo, HostModelView, PutAgentSecretRequest, PutHostModelRequest, CustomModelSettings, ModelInput, SelectHostModelRequest } from "@nylorun/core/contracts";
|
|
1
|
+
import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretSlot, AgentSecretView, HostModelAuthType, HostModelProviderInfo, HostModelView, SessionModelChoice, SessionModelView, PutAgentSecretRequest, PutHostModelRequest, CustomModelSettings, ModelInput, SelectHostModelRequest } from "@nylorun/core/contracts";
|
|
2
2
|
import type { AgentManifest, WorkflowManifest } from "@nylorun/core/define";
|
|
3
3
|
import type { SessionStore, Tx, VaultCredentialRow } from "../store/types.js";
|
|
4
4
|
export declare const HOST_VAULT_ID = "host";
|
|
@@ -166,7 +166,13 @@ export declare class VaultService {
|
|
|
166
166
|
* the credential of the Agent's vault bound to the URL. Reads no secret, writes no audit row.
|
|
167
167
|
*/
|
|
168
168
|
coverage(manifest: AgentManifest | WorkflowManifest, request: CredentialCoverageRequest): Promise<CredentialCoverage>;
|
|
169
|
-
|
|
169
|
+
/**
|
|
170
|
+
* The Tenant's model, or the model a session's turns use (`choice`, its own, while its provider
|
|
171
|
+
* is connected).
|
|
172
|
+
*/
|
|
173
|
+
getHostModel(choice?: SessionModelChoice): Promise<HostModelView>;
|
|
174
|
+
/** The model a session's next turn uses, and where it comes from (Host feature `session-model`). */
|
|
175
|
+
getSessionModel(choice?: SessionModelChoice): Promise<SessionModelView>;
|
|
170
176
|
listHostProviders(): Promise<{
|
|
171
177
|
providers: HostModelProviderInfo[];
|
|
172
178
|
}>;
|
|
@@ -194,6 +200,16 @@ export declare function hostModelRows(t: Tx): Promise<VaultCredentialRow[]>;
|
|
|
194
200
|
export declare function hostCredentialRowFor(t: Tx, provider: string): Promise<VaultCredentialRow | undefined>;
|
|
195
201
|
/** The provider the host model selection names, or the only (or legacy) one configured. */
|
|
196
202
|
export declare function activeProviderId(t: Tx): Promise<string | undefined>;
|
|
203
|
+
/**
|
|
204
|
+
* The host model credential row a session's calls use (ciphertext), in `t`, with the binding
|
|
205
|
+
* they call: its own model (`choice`) on its provider's row while that provider is connected,
|
|
206
|
+
* else the Tenant's active one. `row`'s own binding seals its secret (`readHostPayload`).
|
|
207
|
+
*/
|
|
208
|
+
export declare function hostCredentialRowOf(t: Tx, choice: SessionModelChoice | undefined): Promise<{
|
|
209
|
+
row: VaultCredentialRow;
|
|
210
|
+
binding: ModelBinding;
|
|
211
|
+
source: "session" | "tenant";
|
|
212
|
+
} | undefined>;
|
|
197
213
|
/** The active host model credential row (ciphertext), in `t`. */
|
|
198
214
|
export declare function activeHostCredentialRow(t: Tx): Promise<VaultCredentialRow | undefined>;
|
|
199
215
|
export declare function readHostPayload(kek: Buffer, row: VaultCredentialRow, binding: ModelBinding): HostSecretPayload;
|
package/dist/vault/service.js
CHANGED
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
* coverage(manifest: AgentManifest | WorkflowManifest, request: CredentialCoverageRequest): Promise<CredentialCoverage>
|
|
40
40
|
*
|
|
41
41
|
* // Host model credential: each opens its own transaction.
|
|
42
|
-
* getHostModel(): Promise<HostModelView>
|
|
42
|
+
* getHostModel(choice?: SessionModelChoice): Promise<HostModelView>
|
|
43
|
+
* getSessionModel(choice?: SessionModelChoice): Promise<SessionModelView>
|
|
43
44
|
* listHostProviders(): Promise<{ providers: HostModelProviderInfo[] }>
|
|
44
45
|
* putHostModel(body: PutHostModelRequest): Promise<HostModelView>
|
|
45
46
|
* selectHostModel(body: SelectHostModelRequest): Promise<HostModelView>
|
|
@@ -88,6 +89,7 @@ import { hostModelCatalog, inputsOfHostModel } from "../model/catalog.js";
|
|
|
88
89
|
import { inputsError } from "../model/capabilities.js";
|
|
89
90
|
import { readAddedInputs } from "../tenant/model-setting.js";
|
|
90
91
|
import { normalizeVaultUrl } from "./url.js";
|
|
92
|
+
import { hostIdentityEnv, hostIdentityRefused } from "./host-identity.js";
|
|
91
93
|
export const HOST_VAULT_ID = "host";
|
|
92
94
|
const HOST_MODEL_ID = "host-model";
|
|
93
95
|
export class VaultService {
|
|
@@ -510,8 +512,29 @@ export class VaultService {
|
|
|
510
512
|
});
|
|
511
513
|
}
|
|
512
514
|
// --- host model ----------------------------------------------------------------
|
|
513
|
-
|
|
514
|
-
|
|
515
|
+
/**
|
|
516
|
+
* The Tenant's model, or the model a session's turns use (`choice`, its own, while its provider
|
|
517
|
+
* is connected).
|
|
518
|
+
*/
|
|
519
|
+
async getHostModel(choice) {
|
|
520
|
+
return this.store.tx((t) => this.hostModelView(t, choice));
|
|
521
|
+
}
|
|
522
|
+
/** The model a session's next turn uses, and where it comes from (Host feature `session-model`). */
|
|
523
|
+
async getSessionModel(choice) {
|
|
524
|
+
return this.store.tx(async (t) => {
|
|
525
|
+
const found = await hostCredentialRowOf(t, choice);
|
|
526
|
+
const kept = choice ? { choice } : {};
|
|
527
|
+
if (!found)
|
|
528
|
+
return { configured: false, ...kept };
|
|
529
|
+
return {
|
|
530
|
+
configured: true,
|
|
531
|
+
source: found.source,
|
|
532
|
+
provider: found.binding.provider,
|
|
533
|
+
model: found.binding.model,
|
|
534
|
+
...(found.binding.baseUrl ? { baseUrl: found.binding.baseUrl } : {}),
|
|
535
|
+
...kept,
|
|
536
|
+
};
|
|
537
|
+
});
|
|
515
538
|
}
|
|
516
539
|
async listHostProviders() {
|
|
517
540
|
return this.store.tx(async (t) => {
|
|
@@ -546,10 +569,16 @@ export class VaultService {
|
|
|
546
569
|
this.validateHostModel(body);
|
|
547
570
|
if (body.auth.type === "workload" && body.provider !== "custom")
|
|
548
571
|
throw new VaultError(400, "The workload token authenticates a custom provider only.");
|
|
572
|
+
if (body.auth.type === "host_identity") {
|
|
573
|
+
const refused = hostIdentityRefused(body.provider);
|
|
574
|
+
if (refused)
|
|
575
|
+
throw new VaultError(400, refused);
|
|
576
|
+
}
|
|
549
577
|
await this.ensureHostVault(t);
|
|
550
578
|
const credentialId = hostModelCredentialId(body.provider);
|
|
551
579
|
await this.deleteHostProviderRows(t, body.provider);
|
|
552
|
-
// The workload token is read on every call and never stored (#496)
|
|
580
|
+
// The workload token is read on every call and never stored (#496); the Host's identity
|
|
581
|
+
// too (ADR-71), which keeps only where its calls go.
|
|
553
582
|
const payload = body.auth.type === "api_key"
|
|
554
583
|
? {
|
|
555
584
|
apiKey: body.auth.key,
|
|
@@ -557,7 +586,9 @@ export class VaultService {
|
|
|
557
586
|
}
|
|
558
587
|
: body.auth.type === "oauth"
|
|
559
588
|
? { oauth: body.auth }
|
|
560
|
-
:
|
|
589
|
+
: body.auth.type === "host_identity"
|
|
590
|
+
? { env: hostIdentityEnv(body.auth.project, body.auth.location) }
|
|
591
|
+
: {};
|
|
561
592
|
await this.insertHostCredential(t, kek, {
|
|
562
593
|
id: credentialId,
|
|
563
594
|
provider: body.provider,
|
|
@@ -714,11 +745,11 @@ export class VaultService {
|
|
|
714
745
|
...sealed,
|
|
715
746
|
});
|
|
716
747
|
}
|
|
717
|
-
async hostModelView(t) {
|
|
718
|
-
const
|
|
719
|
-
if (!
|
|
748
|
+
async hostModelView(t, choice) {
|
|
749
|
+
const found = await hostCredentialRowOf(t, choice);
|
|
750
|
+
if (!found)
|
|
720
751
|
return { configured: false };
|
|
721
|
-
const binding =
|
|
752
|
+
const { binding } = found;
|
|
722
753
|
return {
|
|
723
754
|
configured: true,
|
|
724
755
|
provider: binding.provider,
|
|
@@ -904,6 +935,18 @@ export async function activeProviderId(t) {
|
|
|
904
935
|
return modelBinding(legacy).provider;
|
|
905
936
|
return modelBinding(rows[0]).provider;
|
|
906
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* The host model credential row a session's calls use (ciphertext), in `t`, with the binding
|
|
940
|
+
* they call: its own model (`choice`) on its provider's row while that provider is connected,
|
|
941
|
+
* else the Tenant's active one. `row`'s own binding seals its secret (`readHostPayload`).
|
|
942
|
+
*/
|
|
943
|
+
export async function hostCredentialRowOf(t, choice) {
|
|
944
|
+
const chosen = choice && (await hostCredentialRowFor(t, choice.provider));
|
|
945
|
+
if (chosen)
|
|
946
|
+
return { row: chosen, binding: { ...modelBinding(chosen), model: choice.model }, source: "session" };
|
|
947
|
+
const row = await activeHostCredentialRow(t);
|
|
948
|
+
return row && { row, binding: modelBinding(row), source: "tenant" };
|
|
949
|
+
}
|
|
907
950
|
/** The active host model credential row (ciphertext), in `t`. */
|
|
908
951
|
export async function activeHostCredentialRow(t) {
|
|
909
952
|
const active = await activeProviderId(t);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.33.0-beta";
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Kept in step with package.json by runtime/server/scripts/check-package.mjs, which runs in npm run check.
|
|
2
2
|
// release:prepare also rewrites this when it bumps @nylorun/runtime.
|
|
3
|
-
export const RUNTIME_VERSION = "0.
|
|
3
|
+
export const RUNTIME_VERSION = "0.33.0-beta";
|