@nylorun/runtime 0.37.0-beta → 0.39.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 +91 -0
- package/dist/api/components.d.ts +1732 -80
- package/dist/api/http/routes/agents.js +3 -3
- package/dist/core/turn-manifest.d.ts +23 -0
- package/dist/core/turn-manifest.js +36 -1
- package/dist/gates/policy.d.ts +13 -3
- package/dist/gates/policy.js +17 -4
- package/dist/harness/executors.d.ts +4 -2
- package/dist/harness/executors.js +26 -4
- package/dist/harness/image-engine.d.ts +14 -0
- package/dist/harness/image-engine.js +16 -0
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/pod.js +5 -0
- package/dist/harness-api/record.js +4 -2
- package/dist/harness-api/server.js +37 -13
- package/dist/harness-api/start.js +43 -5
- package/dist/harness-api/ws-server.js +7 -1
- package/dist/keys/keys.d.ts +6 -3
- package/dist/keys/keys.js +1 -1
- package/dist/management-openapi.json +38 -4
- package/dist/mcp/pool.d.ts +5 -0
- package/dist/mcp/pool.js +1 -1
- package/dist/mcp/snapshot.d.ts +3 -1
- package/dist/mcp/snapshot.js +5 -3
- package/dist/openapi.json +1636 -563
- package/dist/sandbox/join.d.ts +7 -1
- package/dist/sandbox/join.js +5 -1
- package/dist/sandbox/pods/builtin.d.ts +18 -0
- package/dist/sandbox/pods/builtin.js +33 -0
- package/dist/sandbox/pods/client.d.ts +5 -4
- package/dist/sandbox/pods/spec.js +14 -1
- package/dist/store/postgres/drizzle/0042_worker_engine_image.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/schema.d.ts +19 -0
- package/dist/store/postgres/schema.js +2 -0
- package/dist/store/postgres/store.js +16 -0
- package/dist/store/types.d.ts +18 -0
- package/dist/tenant/agent-versions.d.ts +13 -5
- package/dist/tenant/agent-versions.js +30 -6
- package/dist/tenant/agents.d.ts +14 -2
- package/dist/tenant/agents.js +51 -13
- package/dist/tenant/commands.js +16 -8
- package/dist/tenant/context.d.ts +30 -1
- package/dist/tenant/session-model.d.ts +35 -6
- package/dist/tenant/session-model.js +77 -9
- package/dist/tenant/sessions.js +13 -3
- package/dist/vault/service.d.ts +44 -3
- package/dist/vault/service.js +68 -27
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
* (`Session.model`): the Model Gate reads it at each call (`HostModelVault.readHostModel`) and a
|
|
5
5
|
* turn's start describes it (`turn.start.model`). The Tenant's model, and every other session's,
|
|
6
6
|
* stay as they are. While its provider is disconnected the session uses the Tenant's model again.
|
|
7
|
+
*
|
|
8
|
+
* What its turns call, and why, is kept too (`Session.modelRecord`, `SessionModelRecord`): set when
|
|
9
|
+
* the session opens (the manifest's model, or the Tenant's and why), when it chooses or clears its
|
|
10
|
+
* model, and again at each segment's start, when a provider was connected or disconnected or the
|
|
11
|
+
* Tenant's model changed. Each change writes `session.model`.
|
|
7
12
|
*/
|
|
8
|
-
import { type SessionModelChoice, type SessionModelView } from "@nylorun/core/contracts";
|
|
13
|
+
import { type SessionModelChoice, type SessionModelRecord, type SessionModelView } from "@nylorun/core/contracts";
|
|
9
14
|
import type { Tx } from "../store/types.js";
|
|
10
|
-
import { type SessionAccess, type TenantContext } from "./context.js";
|
|
15
|
+
import { type Session, type SessionAccess, type TenantContext } from "./context.js";
|
|
11
16
|
/** The model the session's next turn uses, and where it comes from. */
|
|
12
17
|
export declare function readSessionModel(ctx: TenantContext, id: string, access: SessionAccess | undefined): Promise<SessionModelView>;
|
|
13
18
|
/**
|
|
@@ -15,12 +20,36 @@ export declare function readSessionModel(ctx: TenantContext, id: string, access:
|
|
|
15
20
|
* Tenant's. A turn calls one model: the choice changes only while no turn runs or waits.
|
|
16
21
|
*/
|
|
17
22
|
export declare function chooseSessionModel(ctx: TenantContext, id: string, choice: SessionModelChoice | undefined, access: SessionAccess | undefined): Promise<SessionModelView>;
|
|
23
|
+
/** Why a session's turns call the Tenant's model in place of the one asked for, and that one. */
|
|
24
|
+
export interface ModelFallback {
|
|
25
|
+
readonly reason: NonNullable<SessionModelRecord["reason"]>;
|
|
26
|
+
readonly requested: NonNullable<SessionModelRecord["requested"]>;
|
|
27
|
+
}
|
|
18
28
|
/**
|
|
19
29
|
* The model a new session starts with (Host feature `agent-model`): the one its version's
|
|
20
|
-
* `harness.model` names, as `chooseSessionModel` would take it, or
|
|
21
|
-
*
|
|
22
|
-
* the Tenant's model applies. Never fails the
|
|
30
|
+
* `harness.model` names, as `chooseSessionModel` would take it (`choice`), or, when it names one
|
|
31
|
+
* the Tenant cannot use now (a provider not connected, a model its catalog does not list), why
|
|
32
|
+
* (`fallback`), so the Tenant's model applies. Undefined when it names none. Never fails the
|
|
33
|
+
* session.
|
|
23
34
|
*/
|
|
24
35
|
export declare function agentModelOf(t: Tx, harness: {
|
|
25
36
|
readonly model?: unknown;
|
|
26
|
-
}): Promise<
|
|
37
|
+
}): Promise<{
|
|
38
|
+
choice: SessionModelChoice;
|
|
39
|
+
} | {
|
|
40
|
+
fallback: ModelFallback;
|
|
41
|
+
} | undefined>;
|
|
42
|
+
/**
|
|
43
|
+
* The model session `s`'s turns call now, and why (`SessionModelRecord`, as the Model Gate picks
|
|
44
|
+
* it: `hostCredentialRowOf`): its own model while its provider is connected (`source` `manifest`
|
|
45
|
+
* when its Agent's version named it, else `session`), else the Tenant's, with the reason when it
|
|
46
|
+
* falls back. `fallback` is why the session opened without the model its manifest names; kept
|
|
47
|
+
* from the session's record otherwise, until it chooses or clears a model. Undefined while the
|
|
48
|
+
* Tenant has no model to fall back to.
|
|
49
|
+
*/
|
|
50
|
+
export declare function sessionModelRecordOf(t: Tx, s: Pick<Session, "model" | "modelSource" | "modelRecord">, fallback?: ModelFallback): Promise<SessionModelRecord | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Keeps `record` as session `s`'s model record (the caller saves `s`) and, when it differs from
|
|
53
|
+
* the one it had, writes `session.model` with the one before. Returns whether it changed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function noteSessionModel(t: Tx, s: Session, record: SessionModelRecord | undefined): Promise<boolean>;
|
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
* (`Session.model`): the Model Gate reads it at each call (`HostModelVault.readHostModel`) and a
|
|
5
5
|
* turn's start describes it (`turn.start.model`). The Tenant's model, and every other session's,
|
|
6
6
|
* stay as they are. While its provider is disconnected the session uses the Tenant's model again.
|
|
7
|
+
*
|
|
8
|
+
* What its turns call, and why, is kept too (`Session.modelRecord`, `SessionModelRecord`): set when
|
|
9
|
+
* the session opens (the manifest's model, or the Tenant's and why), when it chooses or clears its
|
|
10
|
+
* model, and again at each segment's start, when a provider was connected or disconnected or the
|
|
11
|
+
* Tenant's model changed. Each change writes `session.model`.
|
|
7
12
|
*/
|
|
8
|
-
import { agentModelChoice } from "@nylorun/core/contracts";
|
|
13
|
+
import { agentModelChoice, } from "@nylorun/core/contracts";
|
|
14
|
+
import { canonical } from "../store/canonical.js";
|
|
9
15
|
import { catalogModel, hostModelCatalog } from "../model/catalog.js";
|
|
10
|
-
import { hostCredentialRowFor, modelBinding } from "../vault/service.js";
|
|
16
|
+
import { activeHostCredentialRow, hostCredentialRowFor, modelBinding } from "../vault/service.js";
|
|
11
17
|
import { takesMessages } from "./commands.js";
|
|
12
18
|
import { lockedSession, sessionOf } from "./context.js";
|
|
13
19
|
import { fail } from "./http.js";
|
|
@@ -29,8 +35,9 @@ export async function chooseSessionModel(ctx, id, choice, access) {
|
|
|
29
35
|
});
|
|
30
36
|
if (choice)
|
|
31
37
|
await checkChoice(t, choice);
|
|
32
|
-
const { model: _model, ...rest } = s;
|
|
38
|
+
const { model: _model, modelSource: _source, ...rest } = s;
|
|
33
39
|
const next = choice ? { ...rest, model: { provider: choice.provider, model: choice.model } } : rest;
|
|
40
|
+
await noteSessionModel(t, next, await sessionModelRecordOf(t, next));
|
|
34
41
|
await t.put("sessions", id, next);
|
|
35
42
|
return next;
|
|
36
43
|
});
|
|
@@ -38,15 +45,70 @@ export async function chooseSessionModel(ctx, id, choice, access) {
|
|
|
38
45
|
}
|
|
39
46
|
/**
|
|
40
47
|
* The model a new session starts with (Host feature `agent-model`): the one its version's
|
|
41
|
-
* `harness.model` names, as `chooseSessionModel` would take it, or
|
|
42
|
-
*
|
|
43
|
-
* the Tenant's model applies. Never fails the
|
|
48
|
+
* `harness.model` names, as `chooseSessionModel` would take it (`choice`), or, when it names one
|
|
49
|
+
* the Tenant cannot use now (a provider not connected, a model its catalog does not list), why
|
|
50
|
+
* (`fallback`), so the Tenant's model applies. Undefined when it names none. Never fails the
|
|
51
|
+
* session.
|
|
44
52
|
*/
|
|
45
53
|
export async function agentModelOf(t, harness) {
|
|
46
54
|
if (typeof harness.model !== "string")
|
|
47
55
|
return undefined;
|
|
48
56
|
const choice = agentModelChoice(harness.model);
|
|
49
|
-
|
|
57
|
+
if (!choice)
|
|
58
|
+
return undefined;
|
|
59
|
+
const issue = await choiceIssue(t, choice);
|
|
60
|
+
return issue === undefined
|
|
61
|
+
? { choice }
|
|
62
|
+
: { fallback: { reason: issue.reason, requested: { ...choice, by: "manifest" } } };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The model session `s`'s turns call now, and why (`SessionModelRecord`, as the Model Gate picks
|
|
66
|
+
* it: `hostCredentialRowOf`): its own model while its provider is connected (`source` `manifest`
|
|
67
|
+
* when its Agent's version named it, else `session`), else the Tenant's, with the reason when it
|
|
68
|
+
* falls back. `fallback` is why the session opened without the model its manifest names; kept
|
|
69
|
+
* from the session's record otherwise, until it chooses or clears a model. Undefined while the
|
|
70
|
+
* Tenant has no model to fall back to.
|
|
71
|
+
*/
|
|
72
|
+
export async function sessionModelRecordOf(t, s, fallback) {
|
|
73
|
+
if (s.model) {
|
|
74
|
+
const by = s.modelSource ?? "session";
|
|
75
|
+
if (await hostCredentialRowFor(t, s.model.provider))
|
|
76
|
+
return { provider: s.model.provider, model: s.model.model, source: by };
|
|
77
|
+
return tenantModelRecord(t, { reason: "provider_not_connected", requested: { ...s.model, by } });
|
|
78
|
+
}
|
|
79
|
+
const kept = fallback ??
|
|
80
|
+
(s.modelRecord?.reason !== undefined && s.modelRecord.requested?.by === "manifest"
|
|
81
|
+
? { reason: s.modelRecord.reason, requested: s.modelRecord.requested }
|
|
82
|
+
: undefined);
|
|
83
|
+
return tenantModelRecord(t, kept);
|
|
84
|
+
}
|
|
85
|
+
async function tenantModelRecord(t, fallback) {
|
|
86
|
+
const row = await activeHostCredentialRow(t);
|
|
87
|
+
if (!row)
|
|
88
|
+
return undefined;
|
|
89
|
+
const binding = modelBinding(row);
|
|
90
|
+
return {
|
|
91
|
+
provider: binding.provider,
|
|
92
|
+
model: binding.model,
|
|
93
|
+
source: "tenant",
|
|
94
|
+
...(fallback === undefined ? {} : { reason: fallback.reason, requested: { ...fallback.requested } }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Keeps `record` as session `s`'s model record (the caller saves `s`) and, when it differs from
|
|
99
|
+
* the one it had, writes `session.model` with the one before. Returns whether it changed.
|
|
100
|
+
*/
|
|
101
|
+
export async function noteSessionModel(t, s, record) {
|
|
102
|
+
if (canonical(record ?? null) === canonical(s.modelRecord ?? null))
|
|
103
|
+
return false;
|
|
104
|
+
const previous = s.modelRecord;
|
|
105
|
+
if (record === undefined) {
|
|
106
|
+
delete s.modelRecord;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
s.modelRecord = record;
|
|
110
|
+
await t.event(s.id, s.activeTurnId, "session.model", { ...record, ...(previous ? { previous } : {}) });
|
|
111
|
+
return true;
|
|
50
112
|
}
|
|
51
113
|
/** A choice names a connected provider and, when its catalog lists models, one of them. */
|
|
52
114
|
async function checkChoice(t, choice) {
|
|
@@ -57,19 +119,25 @@ async function checkChoice(t, choice) {
|
|
|
57
119
|
/** Why `choice` is not one the Tenant can use now; undefined when it is. */
|
|
58
120
|
async function choiceIssue(t, choice) {
|
|
59
121
|
if (!/^[a-zA-Z0-9._-]+$/.test(choice.provider))
|
|
60
|
-
return { status: 400, message: "Invalid provider id", code: "invalid_request" };
|
|
122
|
+
return { status: 400, message: "Invalid provider id", code: "invalid_request", reason: "invalid_model" };
|
|
61
123
|
const row = await hostCredentialRowFor(t, choice.provider);
|
|
62
124
|
if (!row)
|
|
63
125
|
return {
|
|
64
126
|
status: 422,
|
|
65
127
|
message: `Model provider ${choice.provider} is not connected: connect it in Model Settings first`,
|
|
66
128
|
code: "request_rejected",
|
|
129
|
+
reason: "provider_not_connected",
|
|
67
130
|
};
|
|
68
131
|
// A custom endpoint serves the models it names; the provider's own model is always one.
|
|
69
132
|
if (choice.provider === "custom" || modelBinding(row).model === choice.model)
|
|
70
133
|
return undefined;
|
|
71
134
|
const listed = hostModelCatalog().providers.find((provider) => provider.id === choice.provider);
|
|
72
135
|
if (listed && listed.models.length > 0 && !catalogModel(choice.provider, choice.model))
|
|
73
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
status: 422,
|
|
138
|
+
message: `Unknown model ${choice.model} for provider ${choice.provider}`,
|
|
139
|
+
code: "request_rejected",
|
|
140
|
+
reason: "not_in_catalog",
|
|
141
|
+
};
|
|
74
142
|
return undefined;
|
|
75
143
|
}
|
package/dist/tenant/sessions.js
CHANGED
|
@@ -15,7 +15,7 @@ import { openAgent, recordAttachment } from "./agents.js";
|
|
|
15
15
|
import { harnessOf, reachesAgent, } from "./context.js";
|
|
16
16
|
import { fail } from "./http.js";
|
|
17
17
|
import { definitionFileIssues } from "./definition-files.js";
|
|
18
|
-
import { agentModelOf } from "./session-model.js";
|
|
18
|
+
import { agentModelOf, noteSessionModel, sessionModelRecordOf } from "./session-model.js";
|
|
19
19
|
import { limitsInForce, readTenantLimits } from "./limits.js";
|
|
20
20
|
/** A session's creation identity: everything but the request id. */
|
|
21
21
|
const sessionIdentity = (value) => {
|
|
@@ -98,7 +98,8 @@ export async function openSessionIn(ctx, t, id, body, access, options = {}) {
|
|
|
98
98
|
await requirePods(ctx.pods, `Agent ${agent.id}`);
|
|
99
99
|
// The model the version names (`harness.model`, Host feature `agent-model`) is the new
|
|
100
100
|
// session's own, as `chooseSessionModel` would make it, when the Tenant can use it now.
|
|
101
|
-
const
|
|
101
|
+
const asked = await agentModelOf(t, version.body.harness);
|
|
102
|
+
const model = asked && "choice" in asked ? asked.choice : undefined;
|
|
102
103
|
const created = {
|
|
103
104
|
id,
|
|
104
105
|
agentId: agent.id,
|
|
@@ -115,10 +116,16 @@ export async function openSessionIn(ctx, t, id, body, access, options = {}) {
|
|
|
115
116
|
environmentId: agent.id,
|
|
116
117
|
environmentVersion: agent.version,
|
|
117
118
|
...(pin.sandbox === undefined ? {} : { sandbox: pin.sandbox }),
|
|
118
|
-
...(model === undefined ? {} : { model }),
|
|
119
|
+
...(model === undefined ? {} : { model, modelSource: "manifest" }),
|
|
119
120
|
};
|
|
120
121
|
await t.put("sessions", id, created);
|
|
121
122
|
await recordAttachment(t, id, agent.id);
|
|
123
|
+
// The model its turns call, and why: a flow's agents call theirs in their own sessions.
|
|
124
|
+
if (!isWorkflowManifest(pin.manifest)) {
|
|
125
|
+
const fallback = asked && "fallback" in asked ? asked.fallback : undefined;
|
|
126
|
+
if (await noteSessionModel(t, created, await sessionModelRecordOf(t, created, fallback)))
|
|
127
|
+
await t.put("sessions", id, created);
|
|
128
|
+
}
|
|
122
129
|
return created;
|
|
123
130
|
}
|
|
124
131
|
/**
|
|
@@ -165,6 +172,9 @@ export async function sessionView(t, s) {
|
|
|
165
172
|
...(s.closedReason === undefined ? {} : { closedReason: s.closedReason }),
|
|
166
173
|
mcpSnapshot: s.mcpSnapshot ?? null,
|
|
167
174
|
mcpDiagnostics: s.mcpDiagnostics ?? [],
|
|
175
|
+
// As its last turn started; a session that kept none yet (older, or opened before the
|
|
176
|
+
// Tenant had a model) answers what its next turn would call.
|
|
177
|
+
model: isWorkflowManifest(s.manifest) ? null : (s.modelRecord ?? (await sessionModelRecordOf(t, s)) ?? null),
|
|
168
178
|
waits,
|
|
169
179
|
error: s.error,
|
|
170
180
|
uncertainEffects: uncertain.map((e) => ({
|
package/dist/vault/service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretSlot, AgentSecretView, HostModelAuthType, HostModelProviderInfo, HostModelView, SessionModelChoice, SessionModelView, PutAgentSecretRequest, PutHostModelRequest, CustomModelSettings, ModelInput, SelectHostModelRequest } from "@nylorun/core/contracts";
|
|
1
|
+
import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretInject, 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";
|
|
@@ -43,6 +43,35 @@ export interface SealedSecretCopies {
|
|
|
43
43
|
/** Names left behind: no slot of the fork declares them, or their slot cannot take the value. */
|
|
44
44
|
readonly dropped: readonly string[];
|
|
45
45
|
}
|
|
46
|
+
/** A user credential's `bindingJson`: everything about it that is not secret. */
|
|
47
|
+
type CredentialBinding = {
|
|
48
|
+
/** A `bearer` or `headers` credential's server URL; absent for the environment kinds. */
|
|
49
|
+
url?: string;
|
|
50
|
+
/** An `environment_secret`'s variable, hosts and header (R2c); sealed into its AAD. */
|
|
51
|
+
secretName?: string;
|
|
52
|
+
allowedHosts?: string[];
|
|
53
|
+
inject?: EnvironmentSecretInject;
|
|
54
|
+
/** An `environment_variable`'s name and plain value (R2c): not a secret. */
|
|
55
|
+
variableName?: string;
|
|
56
|
+
variableValue?: string;
|
|
57
|
+
via?: string;
|
|
58
|
+
/**
|
|
59
|
+
* `slot`: `via` is the secret slot's, copied when it was written, not the write's own
|
|
60
|
+
* (`secret-routing`): a request follows the slot of the version the Agent runs (`routingOf`).
|
|
61
|
+
* Absent: `via` is the secret's own (or, written before the marker, the slot's then).
|
|
62
|
+
*/
|
|
63
|
+
viaFrom?: "slot";
|
|
64
|
+
/** How the gateway at `via` prefixes tool names (`gateway-credentials`). */
|
|
65
|
+
toolPrefix?: string;
|
|
66
|
+
identity?: CredentialIdentity;
|
|
67
|
+
/** A `headers` credential's names, lower-cased. */
|
|
68
|
+
headerNames?: string[];
|
|
69
|
+
};
|
|
70
|
+
/** How a secret write was made (`putEnvironmentSecret`); `null` crosses the keys wire as none. */
|
|
71
|
+
export interface SecretWriteOptions {
|
|
72
|
+
/** The body's `via` is its secret slot's, not the write's own (`secret-routing`). */
|
|
73
|
+
readonly viaFromSlot?: boolean;
|
|
74
|
+
}
|
|
46
75
|
export type AuthorizeResult = {
|
|
47
76
|
status: "unauthenticated";
|
|
48
77
|
url: string;
|
|
@@ -114,8 +143,8 @@ export declare class VaultService {
|
|
|
114
143
|
* bound to one of its hosts or to its URL. Runs under the Environment row's lock, which also
|
|
115
144
|
* orders it with session opens and deletes, and counts as the Environment's use.
|
|
116
145
|
*/
|
|
117
|
-
putEnvironmentSecret(environmentId: string, name: string, body: PutAgentSecretRequest, actor: string): Promise<AgentSecretView>;
|
|
118
|
-
/** The Environment's secrets: names, types and
|
|
146
|
+
putEnvironmentSecret(environmentId: string, name: string, body: PutAgentSecretRequest, actor: string, options?: SecretWriteOptions | null): Promise<AgentSecretView>;
|
|
147
|
+
/** The Environment's secrets: names, types, bindings and the routing in force, never values. */
|
|
119
148
|
listEnvironmentSecrets(environmentId: string): Promise<AgentSecretView[]>;
|
|
120
149
|
getEnvironmentSecret(environmentId: string, name: string): Promise<AgentSecretView>;
|
|
121
150
|
deleteEnvironmentSecret(environmentId: string, name: string, actor: string): Promise<{
|
|
@@ -232,4 +261,16 @@ export declare function hostModelAad(provider: string, model: string): Buffer;
|
|
|
232
261
|
* binding (the hosts and header of an `environment_secret`, the URL of a `bearer` or `headers`).
|
|
233
262
|
*/
|
|
234
263
|
export declare function slotHolds(slot: EnvironmentSecretSlot, row: Pick<VaultCredentialRow, "type" | "bindingJson">): boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Where a `bearer` or `headers` credential's requests go, and how the gateway there names tools,
|
|
266
|
+
* at the time of the request (Host feature `secret-routing`), under `slot`, the slot it fills of
|
|
267
|
+
* the version its Agent runs: `via` is the secret's own (written with it), else the slot's, else
|
|
268
|
+
* none (the request goes to the URL); a `via` copied from the slot (`viaFrom: "slot"`) follows the
|
|
269
|
+
* slot. `toolPrefix` is the slot's, else the secret's, and only with a `via`. The credential is
|
|
270
|
+
* chosen by the URL either way.
|
|
271
|
+
*/
|
|
272
|
+
export declare function routingOf(binding: Pick<CredentialBinding, "via" | "viaFrom" | "toolPrefix">, slot?: EnvironmentSecretSlot): {
|
|
273
|
+
via?: string;
|
|
274
|
+
toolPrefix?: string;
|
|
275
|
+
};
|
|
235
276
|
export {};
|
package/dist/vault/service.js
CHANGED
|
@@ -239,7 +239,7 @@ export class VaultService {
|
|
|
239
239
|
* bound to one of its hosts or to its URL. Runs under the Environment row's lock, which also
|
|
240
240
|
* orders it with session opens and deletes, and counts as the Environment's use.
|
|
241
241
|
*/
|
|
242
|
-
async putEnvironmentSecret(environmentId, name, body, actor) {
|
|
242
|
+
async putEnvironmentSecret(environmentId, name, body, actor, options) {
|
|
243
243
|
const invalid = environmentSecretNameError(body.type, name);
|
|
244
244
|
if (invalid)
|
|
245
245
|
throw new VaultError(400, invalid);
|
|
@@ -252,6 +252,8 @@ export class VaultService {
|
|
|
252
252
|
// A workload credential holds no value: the bearer is the Runtime's workload token (#496).
|
|
253
253
|
payload = body.type === "workload" ? {} : payloadOf(body);
|
|
254
254
|
binding = bindingOf(url, payload, body);
|
|
255
|
+
if (options?.viaFromSlot === true && binding.via !== undefined && body.type !== "workload")
|
|
256
|
+
binding.viaFrom = "slot";
|
|
255
257
|
}
|
|
256
258
|
const kek = this.kek();
|
|
257
259
|
return this.store.tx(async (t) => {
|
|
@@ -289,17 +291,18 @@ export class VaultService {
|
|
|
289
291
|
await this.audit(t, { actor, action: "create", vaultId: vault.id, credentialId: id, outcome: "created" });
|
|
290
292
|
}
|
|
291
293
|
await t.updateEnvironment(environmentId, { lastUsedAt: now }, now);
|
|
292
|
-
return secretViewOf(row);
|
|
294
|
+
return secretViewOf(row, slotOf(await appliedSlots(t, environmentId), row));
|
|
293
295
|
});
|
|
294
296
|
}
|
|
295
|
-
/** The Environment's secrets: names, types and
|
|
297
|
+
/** The Environment's secrets: names, types, bindings and the routing in force, never values. */
|
|
296
298
|
async listEnvironmentSecrets(environmentId) {
|
|
297
299
|
return this.store.tx(async (t) => {
|
|
298
300
|
const vault = await this.environmentVault(t, environmentId);
|
|
301
|
+
const slots = await appliedSlots(t, environmentId);
|
|
299
302
|
return (await t.credentialsForVault(vault.id))
|
|
300
303
|
.filter(isUserCredential)
|
|
301
304
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
302
|
-
.map(secretViewOf);
|
|
305
|
+
.map((row) => secretViewOf(row, slotOf(slots, row)));
|
|
303
306
|
});
|
|
304
307
|
}
|
|
305
308
|
async getEnvironmentSecret(environmentId, name) {
|
|
@@ -308,7 +311,7 @@ export class VaultService {
|
|
|
308
311
|
const row = await t.credentialByName(vault.id, name);
|
|
309
312
|
if (!row || !isUserCredential(row))
|
|
310
313
|
throw new VaultError(404, "Secret not found");
|
|
311
|
-
return secretViewOf(row);
|
|
314
|
+
return secretViewOf(row, slotOf(await appliedSlots(t, environmentId), row));
|
|
312
315
|
});
|
|
313
316
|
}
|
|
314
317
|
async deleteEnvironmentSecret(environmentId, name, actor) {
|
|
@@ -404,7 +407,8 @@ export class VaultService {
|
|
|
404
407
|
});
|
|
405
408
|
return { result: { status: "refused", url, credentialIds: [], reason: "vault_missing" } };
|
|
406
409
|
}
|
|
407
|
-
const
|
|
410
|
+
const slots = await appliedSlots(t, environmentId);
|
|
411
|
+
const live = liveFilterOf(slots);
|
|
408
412
|
const matches = (await t.credentialsForVault(vault.id))
|
|
409
413
|
.filter(isUserCredential)
|
|
410
414
|
.filter(live)
|
|
@@ -426,7 +430,9 @@ export class VaultService {
|
|
|
426
430
|
result: { status: "refused", url, credentialIds: matches.map((row) => row.id), reason: "ambiguous" },
|
|
427
431
|
};
|
|
428
432
|
}
|
|
429
|
-
|
|
433
|
+
const row = matches[0];
|
|
434
|
+
const slot = slotOf(slots, row);
|
|
435
|
+
return slot === undefined ? { row } : { row, slot };
|
|
430
436
|
});
|
|
431
437
|
if ("result" in decided)
|
|
432
438
|
return decided.result;
|
|
@@ -468,8 +474,8 @@ export class VaultService {
|
|
|
468
474
|
status: "authorized",
|
|
469
475
|
url,
|
|
470
476
|
headers,
|
|
471
|
-
|
|
472
|
-
...(binding
|
|
477
|
+
// The secret's via, else its slot's; the slot's tool prefix, else the secret's (`secret-routing`).
|
|
478
|
+
...routingOf(binding, decided.slot),
|
|
473
479
|
...(row.type === "workload" ? { workload: true } : {}),
|
|
474
480
|
...(binding.identity === undefined ? {} : { identity: binding.identity }),
|
|
475
481
|
};
|
|
@@ -486,10 +492,13 @@ export class VaultService {
|
|
|
486
492
|
const credentials = [];
|
|
487
493
|
{
|
|
488
494
|
const vault = await this.environmentVault(t, environmentId);
|
|
489
|
-
const
|
|
495
|
+
const slots = environmentId === undefined ? undefined : await appliedSlots(t, environmentId);
|
|
496
|
+
const live = liveFilterOf(slots);
|
|
490
497
|
for (const row of await t.credentialsForVault(vault.id))
|
|
491
|
-
if (isUserCredential(row) && live(row))
|
|
492
|
-
|
|
498
|
+
if (isUserCredential(row) && live(row)) {
|
|
499
|
+
const slot = slotOf(slots, row);
|
|
500
|
+
credentials.push({ row, url: bindingUrl(row), ...(slot === undefined ? {} : { slot }) });
|
|
501
|
+
}
|
|
493
502
|
}
|
|
494
503
|
const entries = credentialNeeds(manifest).map((need) => {
|
|
495
504
|
let url;
|
|
@@ -502,7 +511,7 @@ export class VaultService {
|
|
|
502
511
|
return coverageEntry(need, need.url, undefined, environmentId, error.message);
|
|
503
512
|
}
|
|
504
513
|
const bound = credentials.find((item) => item.url === url);
|
|
505
|
-
return coverageEntry(need, url, bound && coverageCredential(environmentId, bound.row), environmentId);
|
|
514
|
+
return coverageEntry(need, url, bound && coverageCredential(environmentId, bound.row, bound.slot), environmentId);
|
|
506
515
|
});
|
|
507
516
|
return {
|
|
508
517
|
agentId: request.agentId,
|
|
@@ -791,9 +800,9 @@ function isUserCredential(row) {
|
|
|
791
800
|
function bindingUrl(row) {
|
|
792
801
|
return JSON.parse(row.bindingJson).url;
|
|
793
802
|
}
|
|
794
|
-
/** Where a credential is, for coverage: never its value. */
|
|
795
|
-
function coverageCredential(environmentId, row) {
|
|
796
|
-
const via =
|
|
803
|
+
/** Where a credential is, for coverage, with the `via` in force (`routingOf`): never its value. */
|
|
804
|
+
function coverageCredential(environmentId, row, slot) {
|
|
805
|
+
const { via } = routingOf(credentialBinding(row), slot);
|
|
797
806
|
return { agentId: environmentId, name: row.name, ...(via === undefined ? {} : { via }) };
|
|
798
807
|
}
|
|
799
808
|
/** One coverage entry, with the sentence that says what the session would do. */
|
|
@@ -853,9 +862,13 @@ function conflictOf(type, binding, others) {
|
|
|
853
862
|
function credentialBinding(row) {
|
|
854
863
|
return JSON.parse(row.bindingJson);
|
|
855
864
|
}
|
|
856
|
-
/**
|
|
857
|
-
|
|
865
|
+
/**
|
|
866
|
+
* A secret as `GET .../secrets/{name}` shows it, with the routing in force under `slot`, the
|
|
867
|
+
* slot of the version its Agent runs that it fills (`routingOf`): never its value.
|
|
868
|
+
*/
|
|
869
|
+
function secretViewOf(row, slot) {
|
|
858
870
|
const binding = credentialBinding(row);
|
|
871
|
+
const routing = routingOf(binding, slot);
|
|
859
872
|
return {
|
|
860
873
|
name: row.name,
|
|
861
874
|
type: row.type,
|
|
@@ -868,8 +881,7 @@ function secretViewOf(row) {
|
|
|
868
881
|
? {}
|
|
869
882
|
: { url: binding.url },
|
|
870
883
|
...(binding.headerNames === undefined ? {} : { headerNames: binding.headerNames }),
|
|
871
|
-
...
|
|
872
|
-
...(binding.toolPrefix === undefined ? {} : { toolPrefix: binding.toolPrefix }),
|
|
884
|
+
...routing,
|
|
873
885
|
...(binding.identity === undefined ? {} : { identity: binding.identity }),
|
|
874
886
|
createdAt: row.createdAt,
|
|
875
887
|
...(row.rotatedAt ? { rotatedAt: row.rotatedAt } : {}),
|
|
@@ -1074,11 +1086,15 @@ function rebind(name, slot, row, payload, binding) {
|
|
|
1074
1086
|
return undefined;
|
|
1075
1087
|
const sealed = payloadOf(auth);
|
|
1076
1088
|
const routing = slot;
|
|
1077
|
-
// The
|
|
1089
|
+
// The secret's own via goes with it (`secret-routing`); else the fork's slot's, which its
|
|
1090
|
+
// sessions then follow. The gateway's tool prefix is the vault's; the slot's is read at use.
|
|
1091
|
+
const own = binding.via !== undefined && binding.viaFrom !== "slot" ? binding.via : undefined;
|
|
1092
|
+
const via = own ?? routing.via ?? null;
|
|
1093
|
+
const rebound = bindingOf(normalizeVaultUrl(routing.url), sealed, { ...routing, via, toolPrefix: binding.toolPrefix ?? null });
|
|
1078
1094
|
return {
|
|
1079
1095
|
type,
|
|
1080
1096
|
payload: sealed,
|
|
1081
|
-
binding:
|
|
1097
|
+
binding: own === undefined && rebound.via !== undefined ? { ...rebound, viaFrom: "slot" } : rebound,
|
|
1082
1098
|
};
|
|
1083
1099
|
}
|
|
1084
1100
|
catch (error) {
|
|
@@ -1146,11 +1162,36 @@ export function slotHolds(slot, row) {
|
|
|
1146
1162
|
* again. Without slots, every one.
|
|
1147
1163
|
*/
|
|
1148
1164
|
async function liveFilter(t, environmentId) {
|
|
1149
|
-
|
|
1165
|
+
return liveFilterOf(await appliedSlots(t, environmentId));
|
|
1166
|
+
}
|
|
1167
|
+
/** `liveFilter` under `slots`, the secret slots of the runtime an Agent applies. */
|
|
1168
|
+
function liveFilterOf(slots) {
|
|
1150
1169
|
if (slots === undefined)
|
|
1151
1170
|
return () => true;
|
|
1152
|
-
return (row) =>
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1171
|
+
return (row) => slotOf(slots, row) !== undefined;
|
|
1172
|
+
}
|
|
1173
|
+
/** The secret slots of the runtime Agent `environmentId` applies; undefined when it declares none. */
|
|
1174
|
+
async function appliedSlots(t, environmentId) {
|
|
1175
|
+
return (await t.environment(environmentId))?.applied.secrets;
|
|
1176
|
+
}
|
|
1177
|
+
/** The slot of `slots` that the credential in `row` fills (`slotHolds`), if any. */
|
|
1178
|
+
function slotOf(slots, row) {
|
|
1179
|
+
const slot = slots?.[row.name];
|
|
1180
|
+
return slot !== undefined && slotHolds(slot, row) ? slot : undefined;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Where a `bearer` or `headers` credential's requests go, and how the gateway there names tools,
|
|
1184
|
+
* at the time of the request (Host feature `secret-routing`), under `slot`, the slot it fills of
|
|
1185
|
+
* the version its Agent runs: `via` is the secret's own (written with it), else the slot's, else
|
|
1186
|
+
* none (the request goes to the URL); a `via` copied from the slot (`viaFrom: "slot"`) follows the
|
|
1187
|
+
* slot. `toolPrefix` is the slot's, else the secret's, and only with a `via`. The credential is
|
|
1188
|
+
* chosen by the URL either way.
|
|
1189
|
+
*/
|
|
1190
|
+
export function routingOf(binding, slot) {
|
|
1191
|
+
const routed = slot?.type === "bearer" || slot?.type === "headers" ? slot : undefined;
|
|
1192
|
+
const via = binding.viaFrom === "slot" && routed !== undefined ? routed.via : (binding.via ?? routed?.via);
|
|
1193
|
+
if (via === undefined)
|
|
1194
|
+
return {};
|
|
1195
|
+
const toolPrefix = routed?.toolPrefix ?? binding.toolPrefix;
|
|
1196
|
+
return toolPrefix === undefined ? { via } : { via, toolPrefix };
|
|
1156
1197
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.39.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.39.0-beta";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0-beta",
|
|
4
4
|
"description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"@hono/node-server": "^2.1.5",
|
|
66
66
|
"@hono/zod-openapi": "^1.6.3",
|
|
67
67
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
68
|
-
"@nylorun/core": "0.
|
|
69
|
-
"@nylorun/harness": "0.
|
|
68
|
+
"@nylorun/core": "0.34.0-beta",
|
|
69
|
+
"@nylorun/harness": "0.31.0-beta",
|
|
70
70
|
"@restatedev/restate-sdk": "1.17.3",
|
|
71
71
|
"@restatedev/restate-sdk-clients": "1.17.3",
|
|
72
72
|
"@s2-dev/streamstore": "0.28.0",
|