@nylorun/runtime 0.32.0-beta → 0.34.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/api/ag-ui/routes.js +4 -2
  3. package/dist/api/ag-ui/translate.js +3 -1
  4. package/dist/api/components.d.ts +35 -0
  5. package/dist/api/components.js +3 -1
  6. package/dist/api/http/routes/sessions.js +63 -2
  7. package/dist/api/openapi.js +3 -0
  8. package/dist/gates/credential-provider.d.ts +42 -0
  9. package/dist/gates/credential-provider.js +140 -0
  10. package/dist/gates/egress-credentials.d.ts +12 -0
  11. package/dist/gates/egress-credentials.js +60 -6
  12. package/dist/gates/egress.d.ts +14 -1
  13. package/dist/gates/egress.js +67 -19
  14. package/dist/gates/handler.js +2 -2
  15. package/dist/gates/in-process.d.ts +4 -3
  16. package/dist/gates/in-process.js +11 -5
  17. package/dist/gates/tenant-vaults.d.ts +4 -3
  18. package/dist/gates/tenant-vaults.js +2 -2
  19. package/dist/harness-api/start.js +30 -3
  20. package/dist/host/gates.d.ts +8 -1
  21. package/dist/host/gates.js +6 -1
  22. package/dist/host/main.js +17 -1
  23. package/dist/host/stack-config.d.ts +17 -1
  24. package/dist/host/stack-config.js +47 -1
  25. package/dist/management-openapi.json +61 -5
  26. package/dist/model/descriptor.d.ts +5 -4
  27. package/dist/model/descriptor.js +4 -3
  28. package/dist/model/pi-model.js +17 -1
  29. package/dist/openapi.json +516 -2
  30. package/dist/tenant/context.d.ts +12 -1
  31. package/dist/tenant/runtime.d.ts +6 -0
  32. package/dist/tenant/runtime.js +1 -0
  33. package/dist/tenant/session-model.d.ts +16 -0
  34. package/dist/tenant/session-model.js +46 -0
  35. package/dist/tuning.d.ts +6 -0
  36. package/dist/tuning.js +14 -0
  37. package/dist/vault/host-identity.d.ts +24 -0
  38. package/dist/vault/host-identity.js +35 -0
  39. package/dist/vault/host-model.d.ts +9 -5
  40. package/dist/vault/host-model.js +30 -19
  41. package/dist/vault/service.d.ts +18 -2
  42. package/dist/vault/service.js +52 -9
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.34.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 2ef610b: An outside credential provider for the egress gate (Host feature `credential-provider`; no protocol change). A Host's operator names a service that holds people's keys with `NYLORUN_CREDENTIAL_PROVIDER_URL` and `NYLORUN_CREDENTIAL_PROVIDER_TOKEN`, which `nylorun start` passes from its shell to the `gateway` and `runtime` containers. For a pod Agent with an owner, egress-gate asks the provider (`POST /v1/hosts`, with the Agent and its owner) which hosts the owner connected, lets them through on 443 inside the Tenant's egress ceiling, terminates TLS for them and hands each request to `<url>/v1/forward/<path>` with the sandbox's own headers plus `nylorun-provider-host`, `nylorun-provider-agent`, `nylorun-provider-owner` and `nylorun-provider-token`; the provider adds the key and sends it on, so the Runtime never sees a value. Each segment's sandbox sees the variables the provider names (`POST /v1/environment`) as `nylorun_managed`, under the vault's own. A secret in the Agent's vault bound to the same host still wins. Answers are cached per Agent for 15 s (Host tuning `credentialProvider.*`); an Agent with no owner is never asked about, and a provider that fails answers nothing, so its hosts are refused as before.
8
+
9
+ ### Patch Changes
10
+
11
+ - 145dc06: The AG-UI endpoint follows AG-UI 1.0 more closely, on `@ag-ui/core` 1.0.3. `RUN_STARTED` now declares `protocolVersion: "1.0"`, which 1.0 asks every producer to set. A run from a client older than 1.0, which sends `null` for optional fields such as `forwardedProps`, `parentRunId`, `tools[].parameters`, `resume[].payload` or a media part's `metadata`, is now accepted, with those fields treated as absent; it used to be refused with `400 Invalid RunAgentInput`.
12
+ - Pin core to the tested release.
13
+ - Pin harness to the tested release.
14
+ - Updated dependencies [2ef610b]
15
+ - Updated dependencies
16
+ - @nylorun/core@0.30.0-beta
17
+ - @nylorun/harness@0.29.6-beta
18
+
19
+ ## 0.33.0-beta
20
+
21
+ ### Minor Changes
22
+
23
+ - 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.
24
+ - 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()`.
25
+
26
+ 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.
27
+
28
+ ### Patch Changes
29
+
30
+ - 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.
31
+ - Pin core to the tested release.
32
+ - Pin harness to the tested release.
33
+ - Updated dependencies [95f98f1]
34
+ - Updated dependencies [d99b3ac]
35
+ - Updated dependencies
36
+ - @nylorun/core@0.29.0-beta
37
+ - @nylorun/harness@0.29.5-beta
38
+
3
39
  ## 0.32.0-beta
4
40
 
5
41
  ### Minor Changes
@@ -21,7 +21,7 @@
21
21
  * `CUSTOM nylorun.stream_closed`, and the client reattaches with a new token.
22
22
  */
23
23
  import { randomUUID } from "node:crypto";
24
- import { EventType, } from "@ag-ui/core";
24
+ import { EventType, omitOptionalNulls, } from "@ag-ui/core";
25
25
  import { RunAgentInputSchema } from "@ag-ui/core/schemas";
26
26
  import { PutSessionRequestSchema, } from "@nylorun/core/contracts";
27
27
  import { messagesFromEvents, NYLORUN_FILE_PROVIDER } from "./history.js";
@@ -269,7 +269,9 @@ export function agUiCaller(scope, agentId) {
269
269
  export async function startRun(ctx, scope, agentId, caller, body, response,
270
270
  /** The request's trace context (R7.4): the turn the run starts joins the caller's trace. */
271
271
  trace) {
272
- const parsed = RunAgentInputSchema.safeParse(body);
272
+ // Clients before AG-UI 1.0 send `null` for optional fields (`forwardedProps`,
273
+ // `tools[].parameters`, `resume[].payload`, a media part's `metadata`), which 1.0 refuses.
274
+ const parsed = RunAgentInputSchema.safeParse(omitOptionalNulls(body, "RunAgentInput"));
273
275
  if (!parsed.success)
274
276
  return fail(400, "Invalid RunAgentInput", { code: "invalid_request" });
275
277
  const input = parsed.data;
@@ -3,7 +3,7 @@
3
3
  * model call's `invocationId`, a tool call is the model's `callId`, a tool result is
4
4
  * `<callId>:result`, so live runs and `messagesFromEvents` agree.
5
5
  */
6
- import { EventType } from "@ag-ui/core";
6
+ import { EventType, PROTOCOL_VERSION } from "@ag-ui/core";
7
7
  import { isEmptyAssistantMessage, parseTranscriptEvent, } from "@nylorun/core/contracts";
8
8
  /** The text a tool result shows; `undefined` while there is no result yet. */
9
9
  export function toolResultText(result) {
@@ -170,6 +170,8 @@ export class RunTranslator {
170
170
  type: EventType.RUN_STARTED,
171
171
  threadId: this.threadId,
172
172
  runId: this.runId,
173
+ // AG-UI 1.0 producers declare the version; the SDKs do not fill it in.
174
+ protocolVersion: PROTOCOL_VERSION,
173
175
  };
174
176
  }
175
177
  finished(outcome, result) {
@@ -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",
@@ -0,0 +1,42 @@
1
+ import type { Logger } from "../tenant/types.js";
2
+ /** The headers the engine adds to a request it hands to the provider's forward route. */
3
+ export declare const PROVIDER_HEADERS: {
4
+ /** The host the sandbox addressed, e.g. `api.vercel.com`. */
5
+ readonly host: "nylorun-provider-host";
6
+ /** The Agent (the egress sandbox) the request comes from. */
7
+ readonly agent: "nylorun-provider-agent";
8
+ /** The Agent's owner, e.g. `u:<org>:<user>`. */
9
+ readonly owner: "nylorun-provider-owner";
10
+ /** The provider's token: the sandbox's own `Authorization` header is left as it sent it. */
11
+ readonly token: "nylorun-provider-token";
12
+ };
13
+ /** Every header of this prefix a sandbox sends is dropped before the engine sets its own. */
14
+ export declare const PROVIDER_HEADER_PREFIX = "nylorun-provider-";
15
+ /** Where the provider is and its token (`StackConfig.credentialProvider`). */
16
+ export interface CredentialProviderSettings {
17
+ /** `NYLORUN_CREDENTIAL_PROVIDER_URL`, without a trailing slash. */
18
+ readonly url: string;
19
+ /** `NYLORUN_CREDENTIAL_PROVIDER_TOKEN`. */
20
+ readonly token: string;
21
+ }
22
+ export interface CredentialProvider {
23
+ /** The provider's base URL, without a trailing slash. */
24
+ readonly url: string;
25
+ /** The variables to set to the stand-in for `agentId`'s sessions; none without an owner. */
26
+ environment(agentId: string, owner: string | undefined): Promise<readonly string[]>;
27
+ /** The exact host names `owner`'s keys are bound to (lowercase); none without an owner. */
28
+ hosts(agentId: string, owner: string | undefined): Promise<readonly string[]>;
29
+ /** Where one request to `path` (origin form, `/v2/projects?x=1`) is handed. */
30
+ forwardUrl(path: string): URL;
31
+ /** The headers that go with it, the provider's token among them. */
32
+ forwardHeaders(at: {
33
+ readonly host: string;
34
+ readonly agentId: string;
35
+ readonly owner: string;
36
+ }): Record<string, string>;
37
+ }
38
+ export declare function createCredentialProvider(settings: CredentialProviderSettings, options: {
39
+ readonly logger: Logger;
40
+ /** Tests replace the HTTP client. */
41
+ readonly fetch?: typeof fetch;
42
+ }): CredentialProvider;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The credential provider (v0.25 CP-E): a service outside the Runtime that holds a person's keys
3
+ * and adds them to the requests their Agents' sandboxes send, so the Runtime never sees a value.
4
+ * The Host's operator names it with `NYLORUN_CREDENTIAL_PROVIDER_URL` and
5
+ * `NYLORUN_CREDENTIAL_PROVIDER_TOKEN` (Host feature `credential-provider`); without them nothing
6
+ * here runs.
7
+ *
8
+ * Three calls, each with `Authorization: Bearer <token>`:
9
+ * - `POST <url>/v1/hosts` `{ agentId, owner }` → `{ hosts: string[] }`: the exact host names the
10
+ * owner's keys are bound to. egress-gate lets a pod Agent's sandbox reach them (inside the
11
+ * Tenant's egress ceiling), terminates TLS for them on 443 and hands each request on;
12
+ * - `POST <url>/v1/environment` `{ agentId, owner }` → `{ variables: string[] }`: the variables
13
+ * each of a pod Agent's segments sets to the stand-in `nylorun_managed`, under the vault's own;
14
+ * - `ANY <url>/v1/forward/<path and query>`: one request of the sandbox's, with its headers as it
15
+ * sent them (its own `Authorization` or inject header included, which the provider replaces),
16
+ * plus `nylorun-provider-host`, `-agent`, `-owner` and, in place of a bearer the sandbox's own
17
+ * `Authorization` would clash with, `nylorun-provider-token`. The provider sends it on to
18
+ * `https://<host>/<path>` and streams the answer back (`egress-credentials.ts`).
19
+ *
20
+ * Only an Agent with an owner (its owner guard) is asked about; one without gets nothing and no
21
+ * call. An answer is kept per Agent and owner for `credentialProvider.cacheMs` (15 s). A call
22
+ * that fails (unreachable, slow past `timeoutMs`, a non-2xx answer, a body of another shape)
23
+ * answers nothing, kept for `retryMs`, so the gate refuses what it refused before; it is logged
24
+ * once until a call succeeds again. The token is never logged.
25
+ */
26
+ import { isSandboxHostPattern } from "@nylorun/core/define";
27
+ import { hostTuning } from "../tuning.js";
28
+ /** The headers the engine adds to a request it hands to the provider's forward route. */
29
+ export const PROVIDER_HEADERS = {
30
+ /** The host the sandbox addressed, e.g. `api.vercel.com`. */
31
+ host: "nylorun-provider-host",
32
+ /** The Agent (the egress sandbox) the request comes from. */
33
+ agent: "nylorun-provider-agent",
34
+ /** The Agent's owner, e.g. `u:<org>:<user>`. */
35
+ owner: "nylorun-provider-owner",
36
+ /** The provider's token: the sandbox's own `Authorization` header is left as it sent it. */
37
+ token: "nylorun-provider-token",
38
+ };
39
+ /** Every header of this prefix a sandbox sends is dropped before the engine sets its own. */
40
+ export const PROVIDER_HEADER_PREFIX = "nylorun-provider-";
41
+ /** The most host names, or variables, one answer may hold; the rest are ignored. */
42
+ const ANSWER_MAX = 256;
43
+ /** A variable name a shell can set. */
44
+ const VARIABLE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
45
+ export function createCredentialProvider(settings, options) {
46
+ const url = settings.url.replace(/\/+$/, "");
47
+ const { logger } = options;
48
+ const fetchImpl = options.fetch ?? fetch;
49
+ const cache = new Map();
50
+ const pending = new Map();
51
+ /** The calls failing now: each is logged once, until one succeeds. */
52
+ const failing = new Set();
53
+ function fail(call, agentId, reason, fields = {}) {
54
+ if (!failing.has(call)) {
55
+ failing.add(call);
56
+ logger.warn("credential_provider_failed", { call, agentId, reason, ...fields });
57
+ }
58
+ return { ok: false, value: [] };
59
+ }
60
+ async function ask(call, agentId, owner) {
61
+ let response;
62
+ try {
63
+ response = await fetchImpl(`${url}/v1/${call}`, {
64
+ method: "POST",
65
+ headers: { authorization: `Bearer ${settings.token}`, "content-type": "application/json", accept: "application/json" },
66
+ body: JSON.stringify({ agentId, owner }),
67
+ signal: AbortSignal.timeout(hostTuning().credentialProvider.timeoutMs),
68
+ });
69
+ }
70
+ catch (error) {
71
+ const name = error instanceof Error ? error.name : "Error";
72
+ return fail(call, agentId, name === "TimeoutError" ? "timeout" : "unreachable", {
73
+ code: error?.cause?.code ?? name,
74
+ });
75
+ }
76
+ if (!response.ok) {
77
+ await response.body?.cancel().catch(() => undefined);
78
+ return fail(call, agentId, "status", { status: response.status });
79
+ }
80
+ let body;
81
+ try {
82
+ body = await response.json();
83
+ }
84
+ catch {
85
+ return fail(call, agentId, "not_json");
86
+ }
87
+ const list = body?.[call === "hosts" ? "hosts" : "variables"];
88
+ if (!Array.isArray(list))
89
+ return fail(call, agentId, "shape");
90
+ if (failing.delete(call))
91
+ logger.info("credential_provider_recovered", { call });
92
+ const items = list.filter((item) => typeof item === "string").slice(0, ANSWER_MAX);
93
+ const value = call === "hosts"
94
+ ? [...new Set(items.map((host) => host.toLowerCase().replace(/\.$/, "")).filter(isExactHost))]
95
+ : [...new Set(items.filter((name) => VARIABLE.test(name)))];
96
+ return { ok: true, value };
97
+ }
98
+ function answer(call, agentId, owner) {
99
+ if (owner === undefined || owner === "")
100
+ return Promise.resolve([]);
101
+ const key = `${call}\u0000${agentId}\u0000${owner}`;
102
+ const cached = cache.get(key);
103
+ if (cached && Date.now() < cached.until)
104
+ return Promise.resolve(cached.value);
105
+ const inFlight = pending.get(key);
106
+ if (inFlight)
107
+ return inFlight;
108
+ const tuning = hostTuning().credentialProvider;
109
+ const asked = ask(call, agentId, owner)
110
+ .catch(() => fail(call, agentId, "failed"))
111
+ .then(({ ok, value }) => {
112
+ // A failure is kept for `retryMs`, an answer for `cacheMs`.
113
+ const ttl = ok ? tuning.cacheMs : tuning.retryMs;
114
+ cache.delete(key);
115
+ cache.set(key, { until: Date.now() + ttl, value });
116
+ if (cache.size > tuning.cacheEntries)
117
+ cache.delete(cache.keys().next().value);
118
+ return value;
119
+ })
120
+ .finally(() => pending.delete(key));
121
+ pending.set(key, asked);
122
+ return asked;
123
+ }
124
+ return {
125
+ url,
126
+ environment: (agentId, owner) => answer("environment", agentId, owner),
127
+ hosts: (agentId, owner) => answer("hosts", agentId, owner),
128
+ forwardUrl: (path) => new URL(`${url}/v1/forward${path.startsWith("/") ? path : `/${path}`}`),
129
+ forwardHeaders: ({ host, agentId, owner }) => ({
130
+ [PROVIDER_HEADERS.host]: host,
131
+ [PROVIDER_HEADERS.agent]: agentId,
132
+ [PROVIDER_HEADERS.owner]: owner,
133
+ [PROVIDER_HEADERS.token]: settings.token,
134
+ }),
135
+ };
136
+ }
137
+ /** An exact host name (no `*.` pattern, no IP literal), as egress-gate serves. */
138
+ function isExactHost(host) {
139
+ return !host.startsWith("*.") && isSandboxHostPattern(host);
140
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Duplex } from "node:stream";
2
2
  import type { Logger } from "../tenant/types.js";
3
+ import { type CredentialProvider } from "./credential-provider.js";
3
4
  /** What the vault answers for one request to a host a credential may be bound to. */
4
5
  export type EgressCredentialRelease = {
5
6
  readonly status: "none";
@@ -34,6 +35,11 @@ export interface EgressTally {
34
35
  requests: number;
35
36
  readonly credentialIds: Set<string>;
36
37
  }
38
+ /** The Agent a provider host's requests are handed on for, and its owner. */
39
+ export interface ProviderRoute {
40
+ readonly agentId: string;
41
+ readonly owner: string;
42
+ }
37
43
  export interface CredentialTerminator {
38
44
  /**
39
45
  * Answers the CONNECT, terminates TLS for `host` and serves its requests, sending each to one
@@ -48,10 +54,16 @@ export interface CredentialTerminator {
48
54
  }[];
49
55
  /** Counted as requests are served, read when the connection closes. */
50
56
  readonly tally: EgressTally;
57
+ /** A host the credential provider lists: its requests go to the provider's forward route. */
58
+ readonly provider?: ProviderRoute;
51
59
  }): Promise<void>;
60
+ /** Closes the kept-alive connections to the credential provider. */
61
+ close(): void;
52
62
  }
53
63
  export declare function credentialTerminator(options: {
54
64
  readonly credentials: EgressCredentials;
55
65
  readonly logger: Logger;
56
66
  readonly idleMs: number;
67
+ /** The outside credential provider, for hosts it lists. */
68
+ readonly provider?: CredentialProvider;
57
69
  }): CredentialTerminator;
@@ -14,20 +14,29 @@
14
14
  * as an event; a value never is. Upgrades (WebSocket) and HTTP/2 are not served on this path.
15
15
  * Each connection keeps a tally for its `agent.egress` event (R7.3, D67): how many requests it
16
16
  * read and which credentials it set, by id.
17
+ *
18
+ * A host the outside credential provider lists (`credential-provider.ts`) and the vault does not
19
+ * bind is terminated the same way, but its requests go to the provider's forward route instead:
20
+ * `<provider>/v1/forward<path and query>`, with every header the sandbox sent (its own
21
+ * `Authorization` or inject header included, for the provider to replace) but `host` and any
22
+ * `nylorun-provider-*`, plus the engine's `nylorun-provider-host`, `-agent`, `-owner` and
23
+ * `-token`. Bodies stream both ways; the tally counts the requests, with no credential id.
17
24
  */
18
- import { createServer } from "node:http";
25
+ import { Agent as HttpAgent, createServer, request as httpRequest } from "node:http";
19
26
  import { Agent, request } from "node:https";
20
27
  import { createSecureContext, TLSSocket } from "node:tls";
21
28
  import { hostTuning } from "../tuning.js";
22
29
  import { HOP_BY_HOP_HEADERS } from "../vault/headers.js";
30
+ import { PROVIDER_HEADER_PREFIX } from "./credential-provider.js";
23
31
  /** The answer to a CONNECT the gate serves; a tunnel's byte counts leave it out. */
24
32
  export const CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
25
- /** `rawHeaders` without hop-by-hop headers and `drop`, as lowercase names to values. */
33
+ /** `rawHeaders` without hop-by-hop headers and those `drop` matches, as lowercase names to values. */
26
34
  function forwardable(raw, drop) {
27
35
  const headers = {};
36
+ const dropped = typeof drop === "function" ? drop : (name) => name === drop;
28
37
  for (let i = 0; i + 1 < raw.length; i += 2) {
29
38
  const name = raw[i].toLowerCase();
30
- if (HOP_BY_HOP_HEADERS.has(name) || name === drop)
39
+ if (HOP_BY_HOP_HEADERS.has(name) || dropped(name))
31
40
  continue;
32
41
  (headers[name] ??= []).push(raw[i + 1]);
33
42
  }
@@ -43,8 +52,14 @@ function reply(res, status, message) {
43
52
  res.end(body);
44
53
  }
45
54
  export function credentialTerminator(options) {
46
- const { credentials, logger } = options;
55
+ const { credentials, logger, provider } = options;
47
56
  const terminated = new WeakMap();
57
+ // One kept-alive pool to the provider, shared by every terminated connection.
58
+ const providerAgent = provider
59
+ ? new URL(provider.url).protocol === "https:"
60
+ ? new Agent({ keepAlive: true })
61
+ : new HttpAgent({ keepAlive: true })
62
+ : undefined;
48
63
  const http = createServer((req, res) => {
49
64
  forward(req, res).catch((error) => {
50
65
  logger.error("egress_forward_failed", { error: error instanceof Error ? error.message : String(error) });
@@ -69,6 +84,8 @@ export function credentialTerminator(options) {
69
84
  }
70
85
  const { sandboxId, host } = at;
71
86
  at.tally.requests += 1;
87
+ if (at.provider && provider)
88
+ return handOn(req, res, at, at.provider, provider);
72
89
  const release = await credentials.release(sandboxId, host);
73
90
  if (release.status === "refused") {
74
91
  logger.warn("egress_credential_refused", { status: 502, reason: release.reason, sandboxId, host });
@@ -106,7 +123,44 @@ export function credentialTerminator(options) {
106
123
  });
107
124
  req.pipe(upstream);
108
125
  }
126
+ /**
127
+ * One request of a provider host, handed to the provider's forward route as the sandbox sent
128
+ * it: its own headers stay (the provider replaces the bound one), `host` and any
129
+ * `nylorun-provider-*` it sent go, and the engine's provider headers are set.
130
+ */
131
+ function handOn(req, res, at, route, to) {
132
+ const { sandboxId, host } = at;
133
+ if (!req.url?.startsWith("/")) {
134
+ req.resume();
135
+ return reply(res, 400, "requests on this connection name a path");
136
+ }
137
+ const target = to.forwardUrl(req.url);
138
+ const headers = forwardable(req.rawHeaders, (name) => name === "host" || name.startsWith(PROVIDER_HEADER_PREFIX));
139
+ for (const [name, value] of Object.entries(to.forwardHeaders({ host, agentId: route.agentId, owner: route.owner })))
140
+ headers[name] = [value];
141
+ const upstream = target.protocol === "https:"
142
+ ? request(target, { method: req.method, headers, agent: providerAgent })
143
+ : httpRequest(target, { method: req.method, headers, agent: providerAgent });
144
+ upstream.on("response", (response) => {
145
+ res.writeHead(response.statusCode ?? 502, response.statusMessage, forwardable(response.rawHeaders));
146
+ response.pipe(res);
147
+ response.on("error", () => res.destroy());
148
+ });
149
+ upstream.on("error", (error) => {
150
+ logger.warn("egress_provider_failed", { status: 502, sandboxId, host, code: error.code });
151
+ reply(res, 502, `could not reach the credential provider for ${host}`);
152
+ });
153
+ req.on("error", () => upstream.destroy());
154
+ res.on("close", () => {
155
+ if (!res.writableFinished)
156
+ upstream.destroy();
157
+ });
158
+ req.pipe(upstream);
159
+ }
109
160
  return {
161
+ close() {
162
+ providerAgent?.destroy();
163
+ },
110
164
  async terminate(client, target) {
111
165
  const { sandboxId, host, addresses, tally } = target;
112
166
  const leaf = await credentials.leaf(host);
@@ -131,13 +185,13 @@ export function credentialTerminator(options) {
131
185
  secureContext: createSecureContext({ key: leaf.key, cert: leaf.cert }),
132
186
  ALPNProtocols: ["http/1.1"],
133
187
  });
134
- terminated.set(tls, { sandboxId, host, agent, tally });
188
+ terminated.set(tls, { sandboxId, host, agent, tally, ...(target.provider ? { provider: target.provider } : {}) });
135
189
  tls.on("error", () => tls.destroy());
136
190
  tls.on("close", () => {
137
191
  agent.destroy();
138
192
  client.destroy();
139
193
  });
140
- logger.info("egress_terminated", { sandboxId, host });
194
+ logger.info("egress_terminated", { sandboxId, host, ...(target.provider ? { via: "provider" } : {}) });
141
195
  http.emit("connection", tls);
142
196
  },
143
197
  };