@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.
- package/CHANGELOG.md +36 -0
- package/dist/api/ag-ui/routes.js +4 -2
- package/dist/api/ag-ui/translate.js +3 -1
- package/dist/api/components.d.ts +35 -0
- package/dist/api/components.js +3 -1
- package/dist/api/http/routes/sessions.js +63 -2
- package/dist/api/openapi.js +3 -0
- package/dist/gates/credential-provider.d.ts +42 -0
- package/dist/gates/credential-provider.js +140 -0
- package/dist/gates/egress-credentials.d.ts +12 -0
- package/dist/gates/egress-credentials.js +60 -6
- package/dist/gates/egress.d.ts +14 -1
- package/dist/gates/egress.js +67 -19
- package/dist/gates/handler.js +2 -2
- package/dist/gates/in-process.d.ts +4 -3
- package/dist/gates/in-process.js +11 -5
- package/dist/gates/tenant-vaults.d.ts +4 -3
- package/dist/gates/tenant-vaults.js +2 -2
- package/dist/harness-api/start.js +30 -3
- package/dist/host/gates.d.ts +8 -1
- package/dist/host/gates.js +6 -1
- package/dist/host/main.js +17 -1
- package/dist/host/stack-config.d.ts +17 -1
- package/dist/host/stack-config.js +47 -1
- package/dist/management-openapi.json +61 -5
- package/dist/model/descriptor.d.ts +5 -4
- package/dist/model/descriptor.js +4 -3
- package/dist/model/pi-model.js +17 -1
- package/dist/openapi.json +516 -2
- package/dist/tenant/context.d.ts +12 -1
- package/dist/tenant/runtime.d.ts +6 -0
- package/dist/tenant/runtime.js +1 -0
- package/dist/tenant/session-model.d.ts +16 -0
- package/dist/tenant/session-model.js +46 -0
- package/dist/tuning.d.ts +6 -0
- package/dist/tuning.js +14 -0
- package/dist/vault/host-identity.d.ts +24 -0
- package/dist/vault/host-identity.js +35 -0
- package/dist/vault/host-model.d.ts +9 -5
- package/dist/vault/host-model.js +30 -19
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +52 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +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
|
package/dist/api/ag-ui/routes.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/api/components.d.ts
CHANGED
|
@@ -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<{
|
package/dist/api/components.js
CHANGED
|
@@ -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",
|
package/dist/api/openapi.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
};
|