@nylorun/runtime 0.29.0-beta → 0.31.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 (49) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/api/components.d.ts +240 -2
  3. package/dist/api/components.js +3 -1
  4. package/dist/api/http/routes/agent-manifests.d.ts +2 -1
  5. package/dist/api/http/routes/agent-manifests.js +62 -10
  6. package/dist/api/openapi.js +2 -0
  7. package/dist/gates/egress-credentials.js +2 -13
  8. package/dist/gates/handler.js +2 -1
  9. package/dist/gates/in-process.d.ts +9 -0
  10. package/dist/gates/in-process.js +21 -0
  11. package/dist/host/http.js +3 -1
  12. package/dist/host/main.js +2 -0
  13. package/dist/host/stack-config.d.ts +10 -0
  14. package/dist/host/stack-config.js +36 -0
  15. package/dist/management-openapi.json +361 -5
  16. package/dist/mcp/connect.d.ts +34 -5
  17. package/dist/mcp/connect.js +75 -14
  18. package/dist/mcp/gateway-outcomes.d.ts +14 -0
  19. package/dist/mcp/gateway-outcomes.js +46 -0
  20. package/dist/mcp/pool.js +5 -0
  21. package/dist/model/call-headers.d.ts +15 -0
  22. package/dist/model/call-headers.js +9 -0
  23. package/dist/model/models.d.ts +7 -0
  24. package/dist/model/models.js +3 -0
  25. package/dist/model/pi-model.d.ts +6 -0
  26. package/dist/model/pi-model.js +29 -3
  27. package/dist/openapi.json +654 -30
  28. package/dist/store/postgres/drizzle/0038_t6_agent_manifest_source.sql +1 -0
  29. package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
  30. package/dist/store/postgres/schema.d.ts +21 -2
  31. package/dist/store/postgres/schema.js +7 -1
  32. package/dist/store/postgres/store.js +1 -0
  33. package/dist/store/types.d.ts +14 -0
  34. package/dist/tenant/agent-manifests.d.ts +18 -25
  35. package/dist/tenant/agent-manifests.js +136 -4
  36. package/dist/tenant/agents.js +7 -3
  37. package/dist/tenant/transcript.d.ts +8 -0
  38. package/dist/tenant/transcript.js +2 -0
  39. package/dist/vault/headers.d.ts +8 -0
  40. package/dist/vault/headers.js +25 -0
  41. package/dist/vault/host-model.js +8 -5
  42. package/dist/vault/service.d.ts +12 -5
  43. package/dist/vault/service.js +43 -6
  44. package/dist/vault/sources.js +22 -1
  45. package/dist/vault/workload-token.d.ts +16 -0
  46. package/dist/vault/workload-token.js +58 -0
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +3 -3
@@ -19,26 +19,15 @@ import { createServer } from "node:http";
19
19
  import { Agent, request } from "node:https";
20
20
  import { createSecureContext, TLSSocket } from "node:tls";
21
21
  import { hostTuning } from "../tuning.js";
22
+ import { HOP_BY_HOP_HEADERS } from "../vault/headers.js";
22
23
  /** The answer to a CONNECT the gate serves; a tunnel's byte counts leave it out. */
23
24
  export const CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
24
- /** Headers that describe one hop and are never forwarded. */
25
- const HOP_BY_HOP = new Set([
26
- "connection",
27
- "keep-alive",
28
- "proxy-authenticate",
29
- "proxy-authorization",
30
- "proxy-connection",
31
- "te",
32
- "trailer",
33
- "transfer-encoding",
34
- "upgrade",
35
- ]);
36
25
  /** `rawHeaders` without hop-by-hop headers and `drop`, as lowercase names to values. */
37
26
  function forwardable(raw, drop) {
38
27
  const headers = {};
39
28
  for (let i = 0; i + 1 < raw.length; i += 2) {
40
29
  const name = raw[i].toLowerCase();
41
- if (HOP_BY_HOP.has(name) || name === drop)
30
+ if (HOP_BY_HOP_HEADERS.has(name) || name === drop)
42
31
  continue;
43
32
  (headers[name] ??= []).push(raw[i + 1]);
44
33
  }
@@ -6,7 +6,7 @@
6
6
  * the Tenant's ledger. Logs one line per call, never the prompt, the output or a credential.
7
7
  */
8
8
  import { classifyThrown } from "../model/classify.js";
9
- import { callFiles, inProcessModelGate, modelCallSettings, serve } from "./in-process.js";
9
+ import { callCaller, callFiles, inProcessModelGate, modelCallSettings, serve } from "./in-process.js";
10
10
  import { createMeter } from "./meter.js";
11
11
  import { GateRefusal } from "./tenant-vaults.js";
12
12
  export function createModelCallHandler(options) {
@@ -28,6 +28,7 @@ export function createModelCallHandler(options) {
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) } : {}),
31
+ caller: callCaller(vault.store),
31
32
  });
32
33
  outcome = await meter.call(vault.store, call, () => serve(vault.store, gate, call, signal));
33
34
  }
@@ -11,6 +11,7 @@ import type { HostModelSecret } from "../vault/service.js";
11
11
  import type { ModelGate, ModelGateRequest } from "./model-gate.js";
12
12
  import { type FileResolver } from "../artifacts/files.js";
13
13
  import type { BlobStore } from "../blob/index.js";
14
+ import type { ModelCaller } from "../model/call-headers.js";
14
15
  export interface InProcessModelGateOptions {
15
16
  /** The Tenant's home: `piModel` redacts secrets it finds there from failure messages. */
16
17
  readonly root?: string;
@@ -20,6 +21,8 @@ export interface InProcessModelGateOptions {
20
21
  readonly settings?: ModelCallSettings;
21
22
  /** Reads the files a call's prompt names (protocol 6): artifacts, for model-gate. */
22
23
  readonly files?: (request: ModelGateRequest) => FileResolver;
24
+ /** Reads who makes a call, for a custom endpoint's per-call headers (#493). */
25
+ readonly caller?: (request: ModelGateRequest) => Promise<ModelCaller | undefined>;
23
26
  }
24
27
  /**
25
28
  * The files a call names, from the Tenant's artifacts, limited to the call's session: in
@@ -27,6 +30,12 @@ export interface InProcessModelGateOptions {
27
30
  * gate's route puts on the request (F5). A request without a session reads no file at all.
28
31
  */
29
32
  export declare function callFiles(store: SessionStore, blobs: BlobStore): (request: Pick<ModelGateRequest, "sessionId">) => FileResolver;
33
+ /**
34
+ * Who makes a call, from the Tenant's store: the Agent its session runs on, with its labels, and
35
+ * the session's owner when it is a person. The session is the loop's own in process, and the run
36
+ * token's over HTTP; the request's `agentId` names the manifest's agent, not the Agent.
37
+ */
38
+ export declare function callCaller(store: SessionStore): (request: Pick<ModelGateRequest, "sessionId">) => Promise<ModelCaller | undefined>;
30
39
  /**
31
40
  * The in-process gate of an open Tenant: reads its host model from its own vault and records
32
41
  * each call in its usage ledger. Only a Runtime without the gates service (embedding, the
@@ -5,6 +5,7 @@ import { createMeter } from "./meter.js";
5
5
  import { artifactFiles } from "../artifacts/files.js";
6
6
  import { usesFixtureModel } from "../tenant/model-setting.js";
7
7
  import { fixtureAnswer } from "./fixture-model.js";
8
+ import { isSubject } from "@nylorun/core/contracts";
8
9
  /**
9
10
  * The files a call names, from the Tenant's artifacts, limited to the call's session: in
10
11
  * process, the loop's own request; over HTTP, the session of the run token's claims, which the
@@ -13,6 +14,24 @@ import { fixtureAnswer } from "./fixture-model.js";
13
14
  export function callFiles(store, blobs) {
14
15
  return (request) => artifactFiles({ store, blobs, sessionId: request.sessionId });
15
16
  }
17
+ /**
18
+ * Who makes a call, from the Tenant's store: the Agent its session runs on, with its labels, and
19
+ * the session's owner when it is a person. The session is the loop's own in process, and the run
20
+ * token's over HTTP; the request's `agentId` names the manifest's agent, not the Agent.
21
+ */
22
+ export function callCaller(store) {
23
+ return (request) => store.tx(async (t) => {
24
+ const session = await t.get("sessions", request.sessionId);
25
+ const agent = session && (await t.environment(session.agentId));
26
+ if (!session || !agent)
27
+ return undefined;
28
+ return {
29
+ agentId: agent.id,
30
+ labels: agent.labels,
31
+ ...(isSubject(session.ownerUserId) ? { owner: session.ownerUserId } : {}),
32
+ };
33
+ });
34
+ }
16
35
  /**
17
36
  * The in-process gate of an open Tenant: reads its host model from its own vault and records
18
37
  * each call in its usage ledger. Only a Runtime without the gates service (embedding, the
@@ -30,6 +49,7 @@ export function tenantModelGate(options) {
30
49
  writeHostCredential: (credential) => vault.updateHostCredential(credential),
31
50
  settings: await modelCallSettings(options.store),
32
51
  ...(options.blobs ? { files: callFiles(options.store, options.blobs) } : {}),
52
+ caller: callCaller(options.store),
33
53
  });
34
54
  return serve(options.store, gate, request, signal);
35
55
  }),
@@ -58,6 +78,7 @@ export function inProcessModelGate(options) {
58
78
  writeHostCredential: options.writeHostCredential,
59
79
  ...(options.settings ? { settings: options.settings } : {}),
60
80
  ...(options.files ? { files: options.files(request) } : {}),
81
+ ...(options.caller ? { caller: () => options.caller(request) } : {}),
61
82
  });
62
83
  return adapter(request.call, {
63
84
  // The Runtime never journals the provider request (harness `durable.ts`), and
package/dist/host/http.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, timingSafeEqual } from "node:crypto";
2
2
  import { jsonResponse } from "../api/http/respond.js";
3
3
  import { HOST_PROTOCOL, } from "@nylorun/core/compatibility";
4
+ import { AGENT_FOLDER_MEDIA_TYPE } from "@nylorun/core/contracts";
4
5
  export const OPAQUE_NOT_FOUND = {
5
6
  status: "rejected",
6
7
  code: "not_found",
@@ -113,7 +114,8 @@ export function isJsonContentType(value) {
113
114
  if (value === undefined)
114
115
  return false;
115
116
  const media = value.split(";")[0].trim().toLowerCase();
116
- return media === "application/json";
117
+ // An agent folder by hash (track T6) is JSON too.
118
+ return media === "application/json" || media === AGENT_FOLDER_MEDIA_TYPE;
117
119
  }
118
120
  /** True when the request carries a body (Content-Length > 0 or chunked). */
119
121
  export function requestHasBody(request) {
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 { useWorkloadToken } from "../vault/workload-token.js";
68
69
  const nodeRequire = createRequire(import.meta.url);
69
70
  function coreVersion() {
70
71
  try {
@@ -218,6 +219,7 @@ export async function main() {
218
219
  // Read once, before any service starts: each reads the parts it uses where it uses them.
219
220
  if (stack.tuning)
220
221
  useHostTuning(stack.tuning.values, stack.tuning.overrides);
222
+ useWorkloadToken(stack.workloadToken ?? {});
221
223
  if (stack.services.has("gates") || stack.services.has("keys") || stack.services.has("egress"))
222
224
  return runGates(stack);
223
225
  if (stack.services.has("harness"))
@@ -178,6 +178,16 @@ export interface HarnessServiceConfig {
178
178
  export interface StackConfig {
179
179
  /** The Runtime services this process runs. */
180
180
  services: RuntimeServices;
181
+ /**
182
+ * `NYLORUN_WORKLOAD_TOKEN_FILE`, the file of the Runtime's workload token, which a platform
183
+ * rotates and the Runtime reads at each use, and `NYLORUN_WORKLOAD_TOKEN_ORIGINS`, the
184
+ * comma-separated origins a `workload` credential's `via` may send it to (#496,
185
+ * `vault/workload-token.ts`). Absent: no workload token.
186
+ */
187
+ workloadToken?: {
188
+ file?: string;
189
+ origins?: string[];
190
+ };
181
191
  /** Present when the process runs gates. */
182
192
  gates?: GatesConfig;
183
193
  /** Present when the process runs egress. */
@@ -47,6 +47,7 @@
47
47
  import { DEFAULT_SERVICE_NAME } from "../traces/otlp.js";
48
48
  import { TRACE_CONTENT_KINDS } from "../traces/map.js";
49
49
  import { isTenantId } from "@nylorun/core/compatibility";
50
+ import { LOOPBACK_HOSTS } from "@nylorun/core/contracts";
50
51
  import { blockedAddresses } from "../sandbox/pods/network-gate.js";
51
52
  import { HostTuningError, parseHostTuning } from "../tuning.js";
52
53
  export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys", "egress", "harness"];
@@ -325,6 +326,7 @@ export function parseStackConfig(env, argv) {
325
326
  throw new StackConfigError("NYLORUN_HARNESS_LISTEN_PORT must differ from NYLORUN_LISTEN_PORT");
326
327
  const delivery = parseDelivery(env);
327
328
  const tenant = servesApi ? parseTenant(env) : undefined;
329
+ const workloadToken = parseWorkloadToken(env);
328
330
  const objectStore = parseObjectStore(env);
329
331
  const traces = services.has("core") ? parseTraces(env) : undefined;
330
332
  const removedResolverVariables = Object.keys(env)
@@ -347,6 +349,7 @@ export function parseStackConfig(env, argv) {
347
349
  ...(sandboxes ? { sandboxes } : {}),
348
350
  ...(delivery ? { delivery } : {}),
349
351
  ...(tenant ? { tenant } : {}),
352
+ ...(workloadToken ? { workloadToken } : {}),
350
353
  ...(objectStore ? { objectStore } : {}),
351
354
  ...(traces ? { traces } : {}),
352
355
  ...(tuning ? { tuning } : {}),
@@ -454,6 +457,39 @@ function parseTenant(env) {
454
457
  ...(managementKeyFile ? { managementKeyFile } : {}),
455
458
  };
456
459
  }
460
+ /**
461
+ * `StackConfig.workloadToken` (#496): the Runtime's workload token, which the gates read for a
462
+ * custom model and for `workload` credentials, and the in-process Tenant does without them.
463
+ */
464
+ function parseWorkloadToken(env) {
465
+ const file = read(env, "NYLORUN_WORKLOAD_TOKEN_FILE");
466
+ const origins = parseOrigins(read(env, "NYLORUN_WORKLOAD_TOKEN_ORIGINS"));
467
+ if (file === undefined && origins === undefined)
468
+ return undefined;
469
+ return { ...(file ? { file } : {}), ...(origins ? { origins } : {}) };
470
+ }
471
+ /** `NYLORUN_WORKLOAD_TOKEN_ORIGINS`: each an `https` origin (`http` only to a loopback host). */
472
+ function parseOrigins(raw) {
473
+ if (raw === undefined)
474
+ return undefined;
475
+ return raw
476
+ .split(",")
477
+ .map((item) => item.trim())
478
+ .filter((item) => item !== "")
479
+ .map((item) => {
480
+ let url;
481
+ try {
482
+ url = new URL(item);
483
+ }
484
+ catch {
485
+ throw new StackConfigError(`NYLORUN_WORKLOAD_TOKEN_ORIGINS: ${item} is not a URL`);
486
+ }
487
+ const loopback = url.protocol === "http:" && LOOPBACK_HOSTS.test(url.hostname);
488
+ if ((url.protocol !== "https:" && !loopback) || url.origin !== item.replace(/\/$/, ""))
489
+ throw new StackConfigError(`NYLORUN_WORKLOAD_TOKEN_ORIGINS: ${item} must be an https origin, such as https://gateway.example.com`);
490
+ return url.origin;
491
+ });
492
+ }
457
493
  const GATES_TOKEN = /^[0-9a-f]{64,}$/i;
458
494
  /** `NYLORUN_HARNESS`: `in-process` (the default when unset) or `remote`. */
459
495
  function parseHarnessMode(env) {