@nylorun/runtime 0.32.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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.33.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 95f98f1: A model can use the Host's own identity (ADR-71, Host feature `model-host-identity`). `PUT /v1/tenant/model` takes `auth: { type: "host_identity", project, location }` for `google-vertex`. It keeps no secret, and the model gate calls Agent Platform with the Host's Google Application Default Credentials: on Google Cloud, the attached service account from the metadata server. It's off unless the operator lists the provider in `NYLORUN_MODEL_HOST_IDENTITY` on the process that runs the gates, which `nylorun start` passes from its shell to the `gateway` container. Every Tenant on that Host may use it. `nylorun tenant models set --provider google-vertex --model <id> --host-identity --project <id> --location <location>` sets it with no key.
8
+ - d99b3ac: A session can have its own model (optional Host feature `session-model`). `PUT /v1/sessions/{id}/model` chooses one of a connected provider's models for that session's turns, `DELETE` clears it, and `GET` answers the model the next turn uses (`source: "session"` or `"tenant"`). The Tenant's model and every other session's stay as they are. The Model Gate calls the session's model on its provider's credential (a refreshed OAuth credential is written back to that provider), `turn.start.model` describes it, and the usage ledger records it. A choice names a connected provider and a model its catalog lists, is set by the application only (a trusted issuer's token can read it, not change it), and is refused while a turn runs or waits (`409 session_busy`); while its provider is disconnected the session uses the Tenant's model. The SDK adds `session.model()`, `setModel({ provider, model })` and `clearModel()`.
9
+
10
+ Studio's model picker under the message box now sets the model of that session only, instead of the Tenant's: "Tenant model" clears the session's choice, the picker is locked while a turn runs, it groups models by provider, and its search keeps what is typed. A Runtime without `session-model` shows no picker.
11
+
12
+ ### Patch Changes
13
+
14
+ - 683de1a: The model catalog comes from pi-ai 1.3.0, which adds Gemini 4 Argon: `google-vertex/gemini-4-argon` on Google's Agent Platform (Vertex AI) and `google/gemini-4-argon` on the Gemini API, with a 1,048,576-token context and thinking. `nylorun tenant models set --provider google-vertex --model gemini-4-argon` takes a Google Cloud API key for the Agent Platform API; the Runtime still reads no ambient Google credentials.
15
+ - Pin core to the tested release.
16
+ - Pin harness to the tested release.
17
+ - Updated dependencies [95f98f1]
18
+ - Updated dependencies [d99b3ac]
19
+ - Updated dependencies
20
+ - @nylorun/core@0.29.0-beta
21
+ - @nylorun/harness@0.29.5-beta
22
+
3
23
  ## 0.32.0-beta
4
24
 
5
25
  ### Minor Changes
@@ -2421,6 +2421,30 @@ export declare const SessionView: z.ZodObject<{
2421
2421
  error: z.ZodUnknown;
2422
2422
  }, z.core.$strict>>;
2423
2423
  }, z.core.$strict>;
2424
+ export declare const PutSessionModelRequest: z.ZodObject<{
2425
+ provider: z.ZodString;
2426
+ model: z.ZodString;
2427
+ }, z.core.$strict>;
2428
+ export declare const SessionModelView: z.ZodUnion<readonly [z.ZodObject<{
2429
+ configured: z.ZodLiteral<false>;
2430
+ choice: z.ZodOptional<z.ZodObject<{
2431
+ provider: z.ZodString;
2432
+ model: z.ZodString;
2433
+ }, z.core.$strict>>;
2434
+ }, z.core.$strict>, z.ZodObject<{
2435
+ configured: z.ZodLiteral<true>;
2436
+ source: z.ZodEnum<{
2437
+ session: "session";
2438
+ tenant: "tenant";
2439
+ }>;
2440
+ provider: z.ZodString;
2441
+ model: z.ZodString;
2442
+ baseUrl: z.ZodOptional<z.ZodString>;
2443
+ choice: z.ZodOptional<z.ZodObject<{
2444
+ provider: z.ZodString;
2445
+ model: z.ZodString;
2446
+ }, z.core.$strict>>;
2447
+ }, z.core.$strict>]>;
2424
2448
  export declare const LiveEvent: z.ZodObject<{
2425
2449
  schema: z.ZodLiteral<"nylorun.event/2">;
2426
2450
  eventId: z.ZodString;
@@ -13457,6 +13481,7 @@ export declare const TenantStatus: z.ZodObject<{
13457
13481
  model: z.ZodString;
13458
13482
  authType: z.ZodEnum<{
13459
13483
  api_key: "api_key";
13484
+ host_identity: "host_identity";
13460
13485
  oauth: "oauth";
13461
13486
  workload: "workload";
13462
13487
  }>;
@@ -13622,6 +13647,10 @@ export declare const SeedTenantConfigRequest: z.ZodObject<{
13622
13647
  expires: z.ZodNumber;
13623
13648
  }, z.core.$loose>, z.ZodObject<{
13624
13649
  type: z.ZodLiteral<"workload">;
13650
+ }, z.core.$strict>, z.ZodObject<{
13651
+ type: z.ZodLiteral<"host_identity">;
13652
+ project: z.ZodString;
13653
+ location: z.ZodString;
13625
13654
  }, z.core.$strict>], "type">;
13626
13655
  }, z.core.$strict>>;
13627
13656
  fixtureModel: z.ZodOptional<z.ZodLiteral<true>>;
@@ -13880,6 +13909,7 @@ export declare const ListProvidersResponse: z.ZodObject<{
13880
13909
  model: z.ZodString;
13881
13910
  authType: z.ZodEnum<{
13882
13911
  api_key: "api_key";
13912
+ host_identity: "host_identity";
13883
13913
  oauth: "oauth";
13884
13914
  workload: "workload";
13885
13915
  }>;
@@ -13927,6 +13957,7 @@ export declare const HostModelView: z.ZodUnion<readonly [z.ZodObject<{
13927
13957
  model: z.ZodString;
13928
13958
  authType: z.ZodEnum<{
13929
13959
  api_key: "api_key";
13960
+ host_identity: "host_identity";
13930
13961
  oauth: "oauth";
13931
13962
  workload: "workload";
13932
13963
  }>;
@@ -13995,6 +14026,10 @@ export declare const PutHostModelRequest: z.ZodObject<{
13995
14026
  expires: z.ZodNumber;
13996
14027
  }, z.core.$loose>, z.ZodObject<{
13997
14028
  type: z.ZodLiteral<"workload">;
14029
+ }, z.core.$strict>, z.ZodObject<{
14030
+ type: z.ZodLiteral<"host_identity">;
14031
+ project: z.ZodString;
14032
+ location: z.ZodString;
13998
14033
  }, z.core.$strict>], "type">;
13999
14034
  }, z.core.$strict>;
14000
14035
  export declare const SelectHostModelRequest: z.ZodObject<{
@@ -5,7 +5,7 @@
5
5
  * global registry.
6
6
  */
7
7
  import { z } from "zod";
8
- import { QueuedResponseSchema, TriggerListSchema, MemoryListSchema, MemorySchema, PutMemoryRequestSchema, MemoryVersionPageSchema, MemoryVersionDetailSchema, TriggerRunPageSchema, RunTriggerResponseSchema, WebhookSecretResponseSchema, WebhookAcceptedResponseSchema, AgentManifestV8Schema, EnvironmentSecretSlotsSchema, DeleteAgentManifestResponseSchema, AgentManifestPlanSchema, AgentManifestVersionsSchema, AgentManifestViewSchema, ListAgentManifestsResponseSchema, PutAgentManifestRequestSchema, PutAgentFolderRequestSchema, AgentManifestSourceSchema, AgentManifestSourceDiffSchema, PutAgentSecretValueRequestSchema, SaveAgentManifestResponseSchema, SessionListItemSchema, ModelCallSchema, SessionPageSchema, SessionManifestViewSchema, HistoryPageSchema, SessionUsageTotalsSchema, ModelCallsPageSchema, ModelCallExportPageSchema, AgentPageSchema, ToolCallSchema, ToolCallsPageSchema, TurnTraceSchema, JwksSchema, SigningKeyListSchema, SigningKeyViewSchema, RotateSigningKeysRequestSchema, RevokeSigningKeyRequestSchema, MeResponseSchema, TenantStatusSchema, ResetTenantRequestSchema, ResetTenantResponseSchema, SeedTenantConfigRequestSchema, SeedTenantConfigResponseSchema, HostModelCatalogSchema, TenantSandboxViewSchema, ModelUsageTotalsSchema, ModelBudgetsSchema, PutModelBudgetsRequestSchema, ToolPolicySchema, PutToolPolicyRequestSchema, PutModelInputsRequestSchema, ModelInputsViewSchema, PutTenantEnvironmentsRequestSchema, ListProvidersResponseSchema, HostModelViewSchema, PutHostModelRequestSchema, SelectHostModelRequestSchema, PutAgentSecretRequestSchema, AgentSecretViewSchema, ListAgentSecretsResponseSchema, McpPreviewRequestSchema, McpPreviewSchema, CredentialCoverageRequestSchema, CredentialCoverageSchema, DeletedResponseSchema, ListSessionsResponseSchema, LiveEventSchema, PutAgentRequestSchema, PutSessionRequestSchema, SessionItemsResponseSchema, SessionViewSchema, StreamClosedFrameSchema, AcceptedResponseSchema, SandboxToolOutcomeSchema, AgentViewSchema, AgentSnapshotSchema, RestoreAgentRequestSchema, ForkAgentRequestSchema, AgentForkPlanSchema, ForkAgentResponseSchema, PutAgentDryRunResponseSchema, RestartAgentRequestSchema, AgentRestoreSchema, AgentDiskVersionsSchema, ListAgentsResponseSchema, DeleteAgentResponseSchema, ListAgentEventsResponseSchema, ArtifactViewSchema, ArtifactListItemSchema, ArtifactPageSchema, ArtifactVersionViewSchema, ListArtifactsResponseSchema, UploadArtifactResponseSchema, DefinitionFileViewSchema, DeleteArtifactResponseSchema, CreateArtifactLinkRequestSchema, ArtifactLinkSchema, PutTenantArtifactsRequestSchema, PutTenantSkillsRequestSchema, PutTenantLimitsRequestSchema, TenantLimitsViewSchema, TenantArtifactsViewSchema, TenantSkillsViewSchema, ArtifactTreeSchema, ArtifactDiffSchema, SessionCommandSchema, ListOperatorKeysResponseSchema, PutOperatorKeyResponseSchema, DeleteOperatorKeyResponseSchema, OperatorKeySchema, ProtocolRejectedResponseSchema, RejectedResponseSchema, EVENT_SCHEMAS, EVENT_TYPES, } from "@nylorun/core/contracts";
8
+ import { PutSessionModelRequestSchema, SessionModelViewSchema, QueuedResponseSchema, TriggerListSchema, MemoryListSchema, MemorySchema, PutMemoryRequestSchema, MemoryVersionPageSchema, MemoryVersionDetailSchema, TriggerRunPageSchema, RunTriggerResponseSchema, WebhookSecretResponseSchema, WebhookAcceptedResponseSchema, AgentManifestV8Schema, EnvironmentSecretSlotsSchema, DeleteAgentManifestResponseSchema, AgentManifestPlanSchema, AgentManifestVersionsSchema, AgentManifestViewSchema, ListAgentManifestsResponseSchema, PutAgentManifestRequestSchema, PutAgentFolderRequestSchema, AgentManifestSourceSchema, AgentManifestSourceDiffSchema, PutAgentSecretValueRequestSchema, SaveAgentManifestResponseSchema, SessionListItemSchema, ModelCallSchema, SessionPageSchema, SessionManifestViewSchema, HistoryPageSchema, SessionUsageTotalsSchema, ModelCallsPageSchema, ModelCallExportPageSchema, AgentPageSchema, ToolCallSchema, ToolCallsPageSchema, TurnTraceSchema, JwksSchema, SigningKeyListSchema, SigningKeyViewSchema, RotateSigningKeysRequestSchema, RevokeSigningKeyRequestSchema, MeResponseSchema, TenantStatusSchema, ResetTenantRequestSchema, ResetTenantResponseSchema, SeedTenantConfigRequestSchema, SeedTenantConfigResponseSchema, HostModelCatalogSchema, TenantSandboxViewSchema, ModelUsageTotalsSchema, ModelBudgetsSchema, PutModelBudgetsRequestSchema, ToolPolicySchema, PutToolPolicyRequestSchema, PutModelInputsRequestSchema, ModelInputsViewSchema, PutTenantEnvironmentsRequestSchema, ListProvidersResponseSchema, HostModelViewSchema, PutHostModelRequestSchema, SelectHostModelRequestSchema, PutAgentSecretRequestSchema, AgentSecretViewSchema, ListAgentSecretsResponseSchema, McpPreviewRequestSchema, McpPreviewSchema, CredentialCoverageRequestSchema, CredentialCoverageSchema, DeletedResponseSchema, ListSessionsResponseSchema, LiveEventSchema, PutAgentRequestSchema, PutSessionRequestSchema, SessionItemsResponseSchema, SessionViewSchema, StreamClosedFrameSchema, AcceptedResponseSchema, SandboxToolOutcomeSchema, AgentViewSchema, AgentSnapshotSchema, RestoreAgentRequestSchema, ForkAgentRequestSchema, AgentForkPlanSchema, ForkAgentResponseSchema, PutAgentDryRunResponseSchema, RestartAgentRequestSchema, AgentRestoreSchema, AgentDiskVersionsSchema, ListAgentsResponseSchema, DeleteAgentResponseSchema, ListAgentEventsResponseSchema, ArtifactViewSchema, ArtifactListItemSchema, ArtifactPageSchema, ArtifactVersionViewSchema, ListArtifactsResponseSchema, UploadArtifactResponseSchema, DefinitionFileViewSchema, DeleteArtifactResponseSchema, CreateArtifactLinkRequestSchema, ArtifactLinkSchema, PutTenantArtifactsRequestSchema, PutTenantSkillsRequestSchema, PutTenantLimitsRequestSchema, TenantLimitsViewSchema, TenantArtifactsViewSchema, TenantSkillsViewSchema, ArtifactTreeSchema, ArtifactDiffSchema, SessionCommandSchema, ListOperatorKeysResponseSchema, PutOperatorKeyResponseSchema, DeleteOperatorKeyResponseSchema, OperatorKeySchema, ProtocolRejectedResponseSchema, RejectedResponseSchema, EVENT_SCHEMAS, EVENT_TYPES, } from "@nylorun/core/contracts";
9
9
  function named(id, schema) {
10
10
  return schema.meta({ id });
11
11
  }
@@ -77,6 +77,8 @@ export const WebhookAcceptedResponse = named("WebhookAcceptedResponse", WebhookA
77
77
  export const ListSessionsResponse = named("ListSessionsResponse", ListSessionsResponseSchema);
78
78
  export const PutSessionRequest = named("PutSessionRequest", PutSessionRequestSchema);
79
79
  export const SessionView = named("SessionView", SessionViewSchema);
80
+ export const PutSessionModelRequest = named("PutSessionModelRequest", PutSessionModelRequestSchema);
81
+ export const SessionModelView = named("SessionModelView", SessionModelViewSchema);
80
82
  export const LiveEvent = named("LiveEvent", LiveEventSchema);
81
83
  /** `environment.state` → `EnvironmentStateEvent`. */
82
84
  export function eventComponentName(type) {
@@ -1,10 +1,10 @@
1
1
  import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
2
2
  import { z } from "zod";
3
3
  import { PROTOCOL_HEADER } from "@nylorun/core/compatibility";
4
- import { PutSessionRequestSchema, SessionCommandSchema, } from "@nylorun/core/contracts";
4
+ import { PutSessionModelRequestSchema, PutSessionRequestSchema, SessionCommandSchema, } from "@nylorun/core/contracts";
5
5
  import { handleSessionSandboxTool, SandboxRouteError } from "../../../core/sandbox-routes.js";
6
6
  import { traceContextOf } from "../../../traces/ids.js";
7
- import { AcceptedResponse, QueuedResponse, ListSessionsResponse, SessionPage, HistoryPage, SessionEvent, PutSessionRequest, SandboxToolOutcome, SessionCommand, SessionItemsResponse, SessionView, StreamClosedFrame, } from "../../components.js";
7
+ import { AcceptedResponse, QueuedResponse, ListSessionsResponse, SessionPage, HistoryPage, SessionEvent, PutSessionModelRequest, PutSessionRequest, SandboxToolOutcome, SessionCommand, SessionItemsResponse, SessionModelView, SessionView, StreamClosedFrame, } from "../../components.js";
8
8
  import { accessOf, requireApplication } from "../../../tenant/auth.js";
9
9
  import { command } from "../../../tenant/commands.js";
10
10
  import { loadSession, sessionOf } from "../../../tenant/context.js";
@@ -12,6 +12,7 @@ import { fail } from "../../../tenant/http.js";
12
12
  import { agentGrantsOf, sandboxRouteDeps } from "../../../tenant/agents.js";
13
13
  import { readHistory, requestCursor, streamHolderOf, streamSessionEvents } from "../../../tenant/session-streams.js";
14
14
  import { listSessions, putSession, sessionView } from "../../../tenant/sessions.js";
15
+ import { chooseSessionModel, readSessionModel } from "../../../tenant/session-model.js";
15
16
  import { readJson } from "../body.js";
16
17
  import { TraceHeaders, tenantRoute } from "../define.js";
17
18
  import { pageQuery, pageSize, sessionPage, sessionPageQuery, parseQuery } from "./reads.js";
@@ -20,6 +21,18 @@ const OWN_SESSIONS = {
20
21
  credentials: ["application", "subject", "token"],
21
22
  scopes: ["sessions:own"],
22
23
  };
24
+ /**
25
+ * A session's own model costs what its model costs: the application chooses it (for a subject
26
+ * too), never a token a person's browser holds.
27
+ */
28
+ const APP_SESSIONS = {
29
+ credentials: ["application", "subject"],
30
+ scopes: ["sessions:own"],
31
+ };
32
+ function refuseToken(scope) {
33
+ if (scope.kind === "token")
34
+ fail(403, "A token caller cannot choose a session's model", { code: "scope_required" });
35
+ }
23
36
  const json = (schema, description) => ({
24
37
  description,
25
38
  content: { "application/json": { schema } },
@@ -122,6 +135,54 @@ export function sessionRoutes(api) {
122
135
  const id = c.req.param("sessionId");
123
136
  return jsonResponse(200, await ctx.store.tx(async (t) => sessionView(t, await sessionOf(t, id, accessOf(scope)))));
124
137
  });
138
+ tenantRoute(api, OWN_SESSIONS, {
139
+ method: "get",
140
+ path: "/v1/sessions/{sessionId}/model",
141
+ tags: ["Sessions"],
142
+ summary: "Get a session's model",
143
+ description: "The model the session's next turn uses (feature `session-model`): its own (`source: \"session\"`) while its provider is connected, else the Tenant's (`source: \"tenant\"`). `choice` is the session's own model, kept while its provider is disconnected.",
144
+ request: { params: sessionId },
145
+ responses: { 200: json(SessionModelView, "The session's model") },
146
+ }, async (c) => {
147
+ const scope = c.get("scope");
148
+ return jsonResponse(200, await readSessionModel(c.env.tenant, c.req.param("sessionId"), accessOf(scope)));
149
+ });
150
+ tenantRoute(api, APP_SESSIONS, {
151
+ method: "put",
152
+ path: "/v1/sessions/{sessionId}/model",
153
+ tags: ["Sessions"],
154
+ summary: "Choose a session's model",
155
+ description: "Chooses one of a connected provider's models for the session's turns (feature `session-model`), in place of the Tenant's model. The Tenant's model and every other session's stay as they are. Only the application sets it, for itself or for a subject: a trusted issuer's token is refused. A turn calls one model, so the choice changes only while no turn runs or waits (`409 session_busy`). A provider that is not connected, or a model its catalog does not list, is `422 request_rejected`.",
156
+ request: { params: sessionId, body: body(PutSessionModelRequest) },
157
+ responses: {
158
+ 200: json(SessionModelView, "The session's model"),
159
+ 403: { description: "A trusted issuer's token (`scope_required`)" },
160
+ 409: { description: "A turn is running or waits (`session_busy`)" },
161
+ 422: { description: "The provider is not connected, or the model is not one of its models" },
162
+ },
163
+ }, async (c) => {
164
+ const scope = c.get("scope");
165
+ refuseToken(scope);
166
+ const choice = PutSessionModelRequestSchema.parse(await readJson(c));
167
+ return jsonResponse(200, await chooseSessionModel(c.env.tenant, c.req.param("sessionId"), choice, accessOf(scope)));
168
+ });
169
+ tenantRoute(api, APP_SESSIONS, {
170
+ method: "delete",
171
+ path: "/v1/sessions/{sessionId}/model",
172
+ tags: ["Sessions"],
173
+ summary: "Clear a session's model",
174
+ description: "Clears the session's own model (feature `session-model`): its next turns use the Tenant's model. Refused while a turn runs or waits (`409 session_busy`).",
175
+ request: { params: sessionId },
176
+ responses: {
177
+ 200: json(SessionModelView, "The session's model"),
178
+ 403: { description: "A trusted issuer's token (`scope_required`)" },
179
+ 409: { description: "A turn is running or waits (`session_busy`)" },
180
+ },
181
+ }, async (c) => {
182
+ const scope = c.get("scope");
183
+ refuseToken(scope);
184
+ return jsonResponse(200, await chooseSessionModel(c.env.tenant, c.req.param("sessionId"), undefined, accessOf(scope)));
185
+ });
125
186
  tenantRoute(api, OWN_SESSIONS, {
126
187
  method: "get",
127
188
  path: "/v1/sessions/{sessionId}/items",
@@ -194,6 +194,9 @@ const RUNTIME_TAGS = [
194
194
  "GET /v1/sessions/{sessionId}/events",
195
195
  "GET /v1/sessions/{sessionId}/items",
196
196
  "GET /v1/sessions/{sessionId}/manifest",
197
+ "GET /v1/sessions/{sessionId}/model",
198
+ "PUT /v1/sessions/{sessionId}/model",
199
+ "DELETE /v1/sessions/{sessionId}/model",
197
200
  "GET /v1/sessions/{sessionId}/usage",
198
201
  "GET /v1/sessions/{sessionId}/calls/model",
199
202
  "GET /v1/sessions/{sessionId}/calls/tool",
@@ -23,8 +23,8 @@ export function createModelCallHandler(options) {
23
23
  request = call;
24
24
  const gate = inProcessModelGate({
25
25
  root: vault.root,
26
- readHostModel: () => vault.readHostModel(),
27
- writeHostCredential: (credential) => vault.writeHostCredential(credential),
26
+ readHostModel: (request) => vault.readHostModel(request.sessionId),
27
+ writeHostCredential: (credential, provider) => vault.writeHostCredential(credential, provider),
28
28
  // The Tenant's Runtime limits `model*`, read for this call.
29
29
  settings: await modelCallSettings(vault.store),
30
30
  ...(blobs ? { files: callFiles(vault.store, blobs) } : {}),
@@ -15,9 +15,10 @@ import type { ModelCaller } from "../model/call-headers.js";
15
15
  export interface InProcessModelGateOptions {
16
16
  /** The Tenant's home: `piModel` redacts secrets it finds there from failure messages. */
17
17
  readonly root?: string;
18
- readonly readHostModel: () => Promise<HostModelSecret | undefined>;
19
- /** Writes back a credential pi-ai refreshed (OAuth). */
20
- readonly writeHostCredential: (credential: Credential) => Promise<void>;
18
+ /** The host model a call uses: its session's own model (`session-model`), else the Tenant's. */
19
+ readonly readHostModel: (request: ModelGateRequest) => Promise<HostModelSecret | undefined>;
20
+ /** Writes back a credential pi-ai refreshed (OAuth) to `provider`'s, the one the call read. */
21
+ readonly writeHostCredential: (credential: Credential, provider: string) => Promise<void>;
21
22
  readonly settings?: ModelCallSettings;
22
23
  /** Reads the files a call's prompt names (protocol 6): artifacts, for model-gate. */
23
24
  readonly files?: (request: ModelGateRequest) => FileResolver;
@@ -45,8 +45,8 @@ export function tenantModelGate(options) {
45
45
  // The Tenant's Runtime limits `model*`, read for this call.
46
46
  const gate = inProcessModelGate({
47
47
  root: options.root,
48
- readHostModel: () => vault.readHostModel(),
49
- writeHostCredential: (credential) => vault.updateHostCredential(credential),
48
+ readHostModel: (request) => vault.readHostModel(request.sessionId),
49
+ writeHostCredential: (credential, provider) => vault.updateHostCredential(credential, provider),
50
50
  settings: await modelCallSettings(options.store),
51
51
  ...(options.blobs ? { files: callFiles(options.store, options.blobs) } : {}),
52
52
  caller: callCaller(options.store),
@@ -71,11 +71,17 @@ export async function serve(store, gate, request, signal) {
71
71
  export function inProcessModelGate(options) {
72
72
  return {
73
73
  async call(request, signal) {
74
- // One adapter per call, as before the gate: it reads the host model when invoked.
74
+ // One adapter per call, as before the gate: it reads the host model when invoked, and a
75
+ // refreshed credential goes back to the provider it read.
76
+ let provider;
75
77
  const adapter = piModel({
76
78
  ...(options.root !== undefined ? { root: options.root } : {}),
77
- readHostModel: options.readHostModel,
78
- writeHostCredential: options.writeHostCredential,
79
+ readHostModel: async () => {
80
+ const stored = await options.readHostModel(request);
81
+ provider = stored?.provider;
82
+ return stored;
83
+ },
84
+ writeHostCredential: (credential) => options.writeHostCredential(credential, provider),
79
85
  ...(options.settings ? { settings: options.settings } : {}),
80
86
  ...(options.files ? { files: options.files(request) } : {}),
81
87
  ...(options.caller ? { caller: () => options.caller(request) } : {}),
@@ -22,9 +22,10 @@ export interface TenantVault {
22
22
  readonly store: SessionStore;
23
23
  /** The Tenant's home: secrets found there are redacted from failure messages. */
24
24
  readonly root: string;
25
- readHostModel(): Promise<HostModelSecret | undefined>;
26
- /** Writes back a credential pi-ai refreshed (OAuth). */
27
- writeHostCredential(credential: Credential): Promise<void>;
25
+ /** The host model a call uses: its session's own model (`session-model`), else the Tenant's. */
26
+ readHostModel(sessionId?: string): Promise<HostModelSecret | undefined>;
27
+ /** Writes back a credential pi-ai refreshed (OAuth) to `provider`'s, else the active one's. */
28
+ writeHostCredential(credential: Credential, provider?: string): Promise<void>;
28
29
  /** A session, for the remote MCP servers and HTTP tools its pinned manifest declares. */
29
30
  session(sessionId: string): Promise<Session | undefined>;
30
31
  /**
@@ -47,8 +47,8 @@ export function createTenantVaults(options) {
47
47
  tenantId,
48
48
  store,
49
49
  root: paths.home,
50
- readHostModel: () => vault.readHostModel(),
51
- writeHostCredential: (credential) => vault.updateHostCredential(credential),
50
+ readHostModel: (sessionId) => vault.readHostModel(sessionId),
51
+ writeHostCredential: (credential, provider) => vault.updateHostCredential(credential, provider),
52
52
  session,
53
53
  authorizeMcp: (sessionId, request) => authorizeSessionMcp(credentials, session, sessionId, request),
54
54
  keys: () => keys,
@@ -82,7 +82,7 @@ options = {}) {
82
82
  });
83
83
  // The model an agent's harness decides files by, read outside the session's transaction.
84
84
  const model = options.harness && !isWorkflowManifest(opened.current.manifest)
85
- ? await currentModelDescriptor(ctx)
85
+ ? await currentModelDescriptor(ctx, opened.current.model)
86
86
  : undefined;
87
87
  const started = model ? { ...opened, model } : opened;
88
88
  // One history (P0.3): the engine resumes from the transcript folded from the record.
package/dist/host/main.js CHANGED
@@ -65,6 +65,7 @@ import { startHarnessListener } from "../harness-api/ws-server.js";
65
65
  import { httpSandboxesClient } from "../sandbox/pods/client.js";
66
66
  import { retry } from "../retry.js";
67
67
  import { hostTuning, useHostTuning } from "../tuning.js";
68
+ import { useHostIdentity } from "../vault/host-identity.js";
68
69
  import { useWorkloadToken } from "../vault/workload-token.js";
69
70
  const nodeRequire = createRequire(import.meta.url);
70
71
  function coreVersion() {
@@ -220,6 +221,7 @@ export async function main() {
220
221
  if (stack.tuning)
221
222
  useHostTuning(stack.tuning.values, stack.tuning.overrides);
222
223
  useWorkloadToken(stack.workloadToken ?? {});
224
+ useHostIdentity(stack.modelHostIdentity ?? []);
223
225
  if (stack.services.has("gates") || stack.services.has("keys") || stack.services.has("egress"))
224
226
  return runGates(stack);
225
227
  if (stack.services.has("harness"))
@@ -188,6 +188,11 @@ export interface StackConfig {
188
188
  file?: string;
189
189
  origins?: string[];
190
190
  };
191
+ /**
192
+ * `NYLORUN_MODEL_HOST_IDENTITY`: the comma-separated providers whose models may use the Host's
193
+ * own identity (ADR-71, `vault/host-identity.ts`). Absent: none.
194
+ */
195
+ modelHostIdentity?: string[];
191
196
  /** Present when the process runs gates. */
192
197
  gates?: GatesConfig;
193
198
  /** Present when the process runs egress. */
@@ -50,6 +50,7 @@ import { isTenantId } from "@nylorun/core/compatibility";
50
50
  import { LOOPBACK_HOSTS } from "@nylorun/core/contracts";
51
51
  import { blockedAddresses } from "../sandbox/pods/network-gate.js";
52
52
  import { HostTuningError, parseHostTuning } from "../tuning.js";
53
+ import { HOST_IDENTITY_PROVIDERS } from "../vault/host-identity.js";
53
54
  export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys", "egress", "harness"];
54
55
  /**
55
56
  * Services that may share a process (D12): they hold the same secrets and parse the same
@@ -327,6 +328,7 @@ export function parseStackConfig(env, argv) {
327
328
  const delivery = parseDelivery(env);
328
329
  const tenant = servesApi ? parseTenant(env) : undefined;
329
330
  const workloadToken = parseWorkloadToken(env);
331
+ const modelHostIdentity = parseModelHostIdentity(env);
330
332
  const objectStore = parseObjectStore(env);
331
333
  const traces = services.has("core") ? parseTraces(env) : undefined;
332
334
  const removedResolverVariables = Object.keys(env)
@@ -350,6 +352,7 @@ export function parseStackConfig(env, argv) {
350
352
  ...(delivery ? { delivery } : {}),
351
353
  ...(tenant ? { tenant } : {}),
352
354
  ...(workloadToken ? { workloadToken } : {}),
355
+ ...(modelHostIdentity ? { modelHostIdentity } : {}),
353
356
  ...(objectStore ? { objectStore } : {}),
354
357
  ...(traces ? { traces } : {}),
355
358
  ...(tuning ? { tuning } : {}),
@@ -468,6 +471,18 @@ function parseWorkloadToken(env) {
468
471
  return undefined;
469
472
  return { ...(file ? { file } : {}), ...(origins ? { origins } : {}) };
470
473
  }
474
+ /** `StackConfig.modelHostIdentity` (ADR-71): each a provider that can use the Host's identity. */
475
+ function parseModelHostIdentity(env) {
476
+ const raw = read(env, "NYLORUN_MODEL_HOST_IDENTITY");
477
+ if (raw === undefined)
478
+ return undefined;
479
+ const providers = [...new Set(raw.split(",").map((item) => item.trim()).filter((item) => item !== ""))];
480
+ for (const provider of providers) {
481
+ if (!HOST_IDENTITY_PROVIDERS.includes(provider))
482
+ throw new StackConfigError(`NYLORUN_MODEL_HOST_IDENTITY: ${provider} can't use the Host's identity (only ${HOST_IDENTITY_PROVIDERS.join(", ")})`);
483
+ }
484
+ return providers.length ? providers : undefined;
485
+ }
471
486
  /** `NYLORUN_WORKLOAD_TOKEN_ORIGINS`: each an `https` origin (`http` only to a loopback host). */
472
487
  function parseOrigins(raw) {
473
488
  if (raw === undefined)
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.2.0",
3
3
  "info": {
4
4
  "title": "Nylorun Management API",
5
- "version": "0.32.0-beta",
5
+ "version": "0.33.0-beta",
6
6
  "x-nylorun-protocol": {
7
7
  "min": 16,
8
8
  "max": 18,
@@ -34,7 +34,9 @@
34
34
  "traces",
35
35
  "artifact-reads",
36
36
  "gateway-credentials",
37
- "agent-folders"
37
+ "agent-folders",
38
+ "session-model",
39
+ "model-host-identity"
38
40
  ]
39
41
  },
40
42
  "description": "For operators: the Tenant's models, vaults, signing keys, settings and application keys, at `/v1/tenant/*` on the Tenant's URL, through `@nylorun/admin`. Every operation takes a management key, acting as itself: never for a subject and never from a browser. Management keys are issued only on the Tenant's machine (`nylorun tenant keys put <id> --management`) or from `NYLORUN_MANAGEMENT_KEY_FILE`."
@@ -427,7 +429,8 @@
427
429
  "enum": [
428
430
  "api_key",
429
431
  "oauth",
430
- "workload"
432
+ "workload",
433
+ "host_identity"
431
434
  ]
432
435
  },
433
436
  "baseUrl": {
@@ -1137,6 +1140,31 @@
1137
1140
  "type"
1138
1141
  ],
1139
1142
  "additionalProperties": false
1143
+ },
1144
+ {
1145
+ "type": "object",
1146
+ "properties": {
1147
+ "type": {
1148
+ "type": "string",
1149
+ "enum": [
1150
+ "host_identity"
1151
+ ]
1152
+ },
1153
+ "project": {
1154
+ "type": "string",
1155
+ "pattern": "^[a-z][a-z0-9-]{4,28}[a-z0-9]$"
1156
+ },
1157
+ "location": {
1158
+ "type": "string",
1159
+ "pattern": "^[a-z][a-z0-9-]*$"
1160
+ }
1161
+ },
1162
+ "required": [
1163
+ "type",
1164
+ "project",
1165
+ "location"
1166
+ ],
1167
+ "additionalProperties": false
1140
1168
  }
1141
1169
  ]
1142
1170
  }
@@ -2680,7 +2708,8 @@
2680
2708
  "enum": [
2681
2709
  "api_key",
2682
2710
  "oauth",
2683
- "workload"
2711
+ "workload",
2712
+ "host_identity"
2684
2713
  ]
2685
2714
  },
2686
2715
  "baseUrl": {
@@ -2860,7 +2889,8 @@
2860
2889
  "enum": [
2861
2890
  "api_key",
2862
2891
  "oauth",
2863
- "workload"
2892
+ "workload",
2893
+ "host_identity"
2864
2894
  ]
2865
2895
  },
2866
2896
  "baseUrl": {
@@ -3170,6 +3200,31 @@
3170
3200
  "type"
3171
3201
  ],
3172
3202
  "additionalProperties": false
3203
+ },
3204
+ {
3205
+ "type": "object",
3206
+ "properties": {
3207
+ "type": {
3208
+ "type": "string",
3209
+ "enum": [
3210
+ "host_identity"
3211
+ ]
3212
+ },
3213
+ "project": {
3214
+ "type": "string",
3215
+ "pattern": "^[a-z][a-z0-9-]{4,28}[a-z0-9]$"
3216
+ },
3217
+ "location": {
3218
+ "type": "string",
3219
+ "pattern": "^[a-z][a-z0-9-]*$"
3220
+ }
3221
+ },
3222
+ "required": [
3223
+ "type",
3224
+ "project",
3225
+ "location"
3226
+ ],
3227
+ "additionalProperties": false
3173
3228
  }
3174
3229
  ]
3175
3230
  }
@@ -7,7 +7,7 @@
7
7
  * gate takes cannot disagree.
8
8
  */
9
9
  import type { Api, Model } from "@earendil-works/pi-ai";
10
- import { type CustomModelSettings, type HostModelView, type ModelInput } from "@nylorun/core/contracts";
10
+ import { type CustomModelSettings, type HostModelView, type ModelInput, type SessionModelChoice } from "@nylorun/core/contracts";
11
11
  import type { ModelDescriptor } from "@nylorun/core/harness-api";
12
12
  import type { SessionStore } from "../store/types.js";
13
13
  import { type Selection } from "./models.js";
@@ -36,14 +36,15 @@ export declare const FIXTURE_DESCRIPTOR: ModelDescriptor;
36
36
  export declare function hostModelDescriptor(binding: ModelBindingView, added?: Readonly<Record<string, readonly ModelInput[]>>): ModelDescriptor | undefined;
37
37
  /**
38
38
  * The model the Tenant's turns use now: the fixture model's descriptor when the Tenant uses it,
39
- * else its active model's; undefined when none is configured (a test's scripted provider, say).
39
+ * else a session's own model's (`choice`, while its provider is connected), else its active
40
+ * model's; undefined when none is configured (a test's scripted provider, say).
40
41
  */
41
42
  export declare function currentModelDescriptor(ctx: {
42
43
  readonly store: SessionStore;
43
44
  readonly vault: {
44
- getHostModel(): Promise<HostModelView>;
45
+ getHostModel(choice?: SessionModelChoice): Promise<HostModelView>;
45
46
  };
46
- }): Promise<ModelDescriptor | undefined>;
47
+ }, choice?: SessionModelChoice): Promise<ModelDescriptor | undefined>;
47
48
  /**
48
49
  * The context window, in tokens, of the Tenant's model now; without one, or one the catalog does
49
50
  * not know, it is assumed small (`CUSTOM_MODEL_SETTING_DEFAULTS`), as for a custom endpoint.
@@ -62,16 +62,17 @@ export function hostModelDescriptor(binding, added = {}) {
62
62
  }
63
63
  /**
64
64
  * The model the Tenant's turns use now: the fixture model's descriptor when the Tenant uses it,
65
- * else its active model's; undefined when none is configured (a test's scripted provider, say).
65
+ * else a session's own model's (`choice`, while its provider is connected), else its active
66
+ * model's; undefined when none is configured (a test's scripted provider, say).
66
67
  */
67
- export async function currentModelDescriptor(ctx) {
68
+ export async function currentModelDescriptor(ctx, choice) {
68
69
  const { fixture, added } = await ctx.store.tx(async (t) => ({
69
70
  fixture: await usesFixtureModel(t),
70
71
  added: await readAddedInputs(t),
71
72
  }));
72
73
  if (fixture)
73
74
  return FIXTURE_DESCRIPTOR;
74
- const view = await ctx.vault.getHostModel();
75
+ const view = await ctx.vault.getHostModel(choice);
75
76
  return view.configured ? hostModelDescriptor(view, added) : undefined;
76
77
  }
77
78
  /**
@@ -8,6 +8,7 @@ import { classifyAssistantError, failure } from "./classify.js";
8
8
  import { addedInputsOf, describeModel, selectionOf } from "./descriptor.js";
9
9
  import { modelsFor } from "./models.js";
10
10
  import { callHeaders } from "./call-headers.js";
11
+ import { hostIdentityRefused, VERTEX_ADC_MARKER } from "../vault/host-identity.js";
11
12
  import { workloadToken, WorkloadTokenUnavailable } from "../vault/workload-token.js";
12
13
  import { NativeBlocks, nativeKindsOf } from "./native-blocks.js";
13
14
  import { projectSecrets } from "./settings.js";
@@ -67,6 +68,12 @@ export function piModel(options = {}) {
67
68
  throw error;
68
69
  }
69
70
  }
71
+ // The Host's identity (ADR-71): only while its operator still lends it.
72
+ if (stored.authType === "host_identity") {
73
+ const refused = hostIdentityRefused(stored.provider);
74
+ if (refused)
75
+ return failure("auth", refused, false);
76
+ }
70
77
  const secrets = [
71
78
  ...projectSecrets(root),
72
79
  ...credentialSecrets(stored.credential),
@@ -119,6 +126,8 @@ export function piModel(options = {}) {
119
126
  sessionId: call.executionId,
120
127
  cacheRetention: "short",
121
128
  ...(Object.keys(perCall).length > 0 ? { headers: perCall } : {}),
129
+ // With the Host's identity: where Agent Platform runs the call (ADR-71).
130
+ ...(stored.authType === "host_identity" && stored.credential.env ? { env: stored.credential.env } : {}),
122
131
  timeoutMs: settings.requestTimeoutMs,
123
132
  maxRetries: settings.providerRetries,
124
133
  maxRetryDelayMs: settings.maxRetryDelayMs,
@@ -504,7 +513,11 @@ function idleTimeout(selected, idleTimeoutMs) {
504
513
  };
505
514
  }
506
515
  function hostCredentialStore(stored, write) {
507
- let current = stored.credential;
516
+ // The Host's identity (ADR-71): pi-ai's Vertex AI client uses Application Default Credentials
517
+ // for this marker in place of a key.
518
+ let current = stored.authType === "host_identity"
519
+ ? { type: "api_key", key: VERTEX_ADC_MARKER }
520
+ : stored.credential;
508
521
  // A workload model stores no credential: pi-ai then asks the provider's `resolve` (`modelsFor`).
509
522
  const stores = stored.authType !== "workload";
510
523
  return {
@@ -532,6 +545,9 @@ function hostCredentialStore(stored, write) {
532
545
  };
533
546
  }
534
547
  function credentialSecrets(credential) {
548
+ // The Host's identity keeps no secret: its project and location stay readable in messages.
549
+ if (credential.type === "host_identity")
550
+ return [];
535
551
  const values = [credential.key, credential.access, credential.refresh];
536
552
  if (credential.env)
537
553
  values.push(...Object.values(credential.env));