@nylorun/runtime 0.34.0-beta → 0.35.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 1376d3e: The outside credential provider can name host patterns, stand-ins and plain values (no protocol change). `POST /v1/hosts` may now answer `*.<suffix>` entries, such as `*.googleapis.com`, beside exact host names: egress-gate matches them as it matches `network.allow`, so `run.googleapis.com` and `a.b.googleapis.com` pass and `googleapis.com` itself does not. They still pass on 443 only, inside the Tenant's egress ceiling, a secret of the vault bound to the same host still wins, and `nylorun-provider-host` is still the host the sandbox addressed; an IP literal or a malformed entry is dropped. `POST /v1/environment` may now answer, for each variable, `{ name, standIn }`, which sets it to that stand-in (one shaped like the vendor's keys, for a CLI that checks a key's format), or `{ name, value }`, which sets it to that plain value (a setting beside a key, never a secret), beside a bare name, which is still `nylorun_managed`. A `standIn` or `value` is 1 to 1,024 printable characters with no control character or line break; any other entry is dropped, and the first entry for a name wins. The vault's own variables still win, and an older provider's names-only answer works as before.
8
+
3
9
  ## 0.34.0-beta
4
10
 
5
11
  ### Minor Changes
@@ -12,6 +12,12 @@ export declare const PROVIDER_HEADERS: {
12
12
  };
13
13
  /** Every header of this prefix a sandbox sends is dropped before the engine sets its own. */
14
14
  export declare const PROVIDER_HEADER_PREFIX = "nylorun-provider-";
15
+ /** One variable a pod Agent's segments set: its name and the value the sandbox sees. */
16
+ export interface ProviderVariable {
17
+ readonly name: string;
18
+ /** The stand-in (`nylorun_managed` unless the provider names another) or a plain value. */
19
+ readonly value: string;
20
+ }
15
21
  /** Where the provider is and its token (`StackConfig.credentialProvider`). */
16
22
  export interface CredentialProviderSettings {
17
23
  /** `NYLORUN_CREDENTIAL_PROVIDER_URL`, without a trailing slash. */
@@ -22,9 +28,15 @@ export interface CredentialProviderSettings {
22
28
  export interface CredentialProvider {
23
29
  /** The provider's base URL, without a trailing slash. */
24
30
  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. */
31
+ /**
32
+ * The variables `agentId`'s sessions set, each with its stand-in or plain value; none without
33
+ * an owner.
34
+ */
35
+ environment(agentId: string, owner: string | undefined): Promise<readonly ProviderVariable[]>;
36
+ /**
37
+ * The hosts `owner`'s keys are bound to (lowercase): exact names and `*.<suffix>` patterns, to
38
+ * match as `network.allow`'s do (`egressAllows`); none without an owner.
39
+ */
28
40
  hosts(agentId: string, owner: string | undefined): Promise<readonly string[]>;
29
41
  /** Where one request to `path` (origin form, `/v2/projects?x=1`) is handed. */
30
42
  forwardUrl(path: string): URL;
@@ -6,11 +6,19 @@
6
6
  * here runs.
7
7
  *
8
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;
9
+ * - `POST <url>/v1/hosts` `{ agentId, owner }` → `{ hosts: string[] }`: the hosts the owner's keys
10
+ * are bound to, each an exact host name or a `*.<suffix>` pattern (`*.googleapis.com`, v0.26
11
+ * TM-E1). A pattern matches as `network.allow`'s do: `run.googleapis.com` and
12
+ * `a.b.googleapis.com`, never `googleapis.com` itself. An IP literal or a malformed entry is
13
+ * dropped. egress-gate lets a pod Agent's sandbox reach them (inside the Tenant's egress
14
+ * ceiling), terminates TLS for them on 443 and hands each request on;
15
+ * - `POST <url>/v1/environment` `{ agentId, owner }` → `{ variables: Entry[] }`: the variables each
16
+ * of a pod Agent's segments sets, under the vault's own (v0.26 TM-E2). An entry is a name (`"X"`,
17
+ * set to the stand-in `nylorun_managed`, as before v0.26), `{ name, standIn }` (set to that
18
+ * stand-in, shaped like the vendor's keys) or `{ name, value }` (set to that plain value, a
19
+ * setting beside a key and never a secret). A `standIn` or `value` is 1 to 1,024 printable
20
+ * characters, with no control character or line break; an entry with anything else, or a name
21
+ * a shell can't set, is dropped, and the first entry for a name wins;
14
22
  * - `ANY <url>/v1/forward/<path and query>`: one request of the sandbox's, with its headers as it
15
23
  * sent them (its own `Authorization` or inject header included, which the provider replaces),
16
24
  * plus `nylorun-provider-host`, `-agent`, `-owner` and, in place of a bearer the sandbox's own
@@ -23,6 +31,7 @@
23
31
  * answers nothing, kept for `retryMs`, so the gate refuses what it refused before; it is logged
24
32
  * once until a call succeeds again. The token is never logged.
25
33
  */
34
+ import { ENVIRONMENT_SECRET_SENTINEL } from "@nylorun/core/contracts";
26
35
  import { isSandboxHostPattern } from "@nylorun/core/define";
27
36
  import { hostTuning } from "../tuning.js";
28
37
  /** The headers the engine adds to a request it hands to the provider's forward route. */
@@ -42,6 +51,8 @@ export const PROVIDER_HEADER_PREFIX = "nylorun-provider-";
42
51
  const ANSWER_MAX = 256;
43
52
  /** A variable name a shell can set. */
44
53
  const VARIABLE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
54
+ /** A stand-in or plain value: 1 to 1,024 printable characters, no control character or line break. */
55
+ const VALUE = /^[^\p{Cc}\p{Cs}\p{Zl}\p{Zp}]{1,1024}$/u;
45
56
  export function createCredentialProvider(settings, options) {
46
57
  const url = settings.url.replace(/\/+$/, "");
47
58
  const { logger } = options;
@@ -89,11 +100,7 @@ export function createCredentialProvider(settings, options) {
89
100
  return fail(call, agentId, "shape");
90
101
  if (failing.delete(call))
91
102
  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 };
103
+ return { ok: true, value: call === "hosts" ? hostsOf(list) : variablesOf(list) };
97
104
  }
98
105
  function answer(call, agentId, owner) {
99
106
  if (owner === undefined || owner === "")
@@ -134,7 +141,41 @@ export function createCredentialProvider(settings, options) {
134
141
  }),
135
142
  };
136
143
  }
137
- /** An exact host name (no `*.` pattern, no IP literal), as egress-gate serves. */
138
- function isExactHost(host) {
139
- return !host.startsWith("*.") && isSandboxHostPattern(host);
144
+ /**
145
+ * The hosts of a `/v1/hosts` answer: exact host names and `*.<suffix>` patterns, lowercase, without
146
+ * a trailing root dot, once each; an IP literal or a malformed entry is dropped.
147
+ */
148
+ function hostsOf(list) {
149
+ const items = list.filter((item) => typeof item === "string").slice(0, ANSWER_MAX);
150
+ return [...new Set(items.map((host) => host.toLowerCase().replace(/\.$/, "")).filter(isSandboxHostPattern))];
151
+ }
152
+ /**
153
+ * The variables of a `/v1/environment` answer, each with the value the sandbox sees: a bare name is
154
+ * the stand-in `nylorun_managed`, `{ name, standIn }` that stand-in, `{ name, value }` that value.
155
+ * Any other entry is dropped, and the first entry for a name wins.
156
+ */
157
+ function variablesOf(list) {
158
+ const variables = new Map();
159
+ for (const item of list) {
160
+ if (variables.size >= ANSWER_MAX)
161
+ break;
162
+ const variable = variableOf(item);
163
+ if (variable && !variables.has(variable.name))
164
+ variables.set(variable.name, variable);
165
+ }
166
+ return [...variables.values()];
167
+ }
168
+ function variableOf(item) {
169
+ if (typeof item === "string")
170
+ return VARIABLE.test(item) ? { name: item, value: ENVIRONMENT_SECRET_SENTINEL } : undefined;
171
+ if (typeof item !== "object" || item === null || Array.isArray(item))
172
+ return undefined;
173
+ const { name, standIn, value } = item;
174
+ if (typeof name !== "string" || !VARIABLE.test(name))
175
+ return undefined;
176
+ // Exactly one of the two: an entry naming both, or neither, is not one the engine can read.
177
+ if ((standIn === undefined) === (value === undefined))
178
+ return undefined;
179
+ const set = standIn ?? value;
180
+ return typeof set === "string" && VALUE.test(set) ? { name, value: set } : undefined;
140
181
  }
@@ -12,7 +12,8 @@
12
12
  * else 407;
13
13
  * 3. the port is 443 or 80, the target is a host name and no IP literal, and it matches the spec's
14
14
  * `network.allow` exactly or by `*.suffix`, or (443 only) the outside credential provider lists
15
- * it for the Agent's owner and the Tenant's egress ceiling holds it, else 403;
15
+ * it for the Agent's owner, exactly or by `*.suffix` likewise, and the Tenant's egress ceiling
16
+ * holds it, else 403;
16
17
  * 4. the sandbox has fewer than `maxPerSandbox` tunnels open, else 429;
17
18
  * 5. the name resolves, and to at least one address that is not private, loopback, link-local
18
19
  * (cloud metadata included), multicast or reserved, else 502 or 403;
@@ -227,15 +228,18 @@ export async function startEgressGate(options) {
227
228
  : undefined;
228
229
  const provider = terminator ? options.provider : undefined;
229
230
  /**
230
- * Whether the credential provider lists `host` for the sandbox's owner, inside the Tenant's
231
- * ceiling: only on 443, where the gate terminates TLS, and never for an Agent without an owner.
231
+ * Whether the credential provider lists `host` for the sandbox's owner, exactly or by a
232
+ * `*.suffix` pattern (`*.x.com` holds `a.x.com` and `b.a.x.com`, not `x.com`), inside the
233
+ * Tenant's ceiling: only on 443, where the gate terminates TLS, and never for an Agent without an
234
+ * owner. The request still names the host the sandbox addressed (`nylorun-provider-host`).
232
235
  */
233
236
  async function providerLists(sandboxId, sandbox, host, port) {
234
237
  if (!provider || port !== 443 || sandbox.owner === undefined)
235
238
  return false;
236
239
  if (sandbox.ceiling && !withinCeiling(host, sandbox.ceiling))
237
240
  return false;
238
- return (await provider.hosts(sandboxId, sandbox.owner)).includes(host);
241
+ // An exact name or a `*.suffix` pattern, matched as `network.allow`'s are (v0.26 TM-E1).
242
+ return egressAllows(host, await provider.hosts(sandboxId, sandbox.owner));
239
243
  }
240
244
  const open = new Map();
241
245
  const tunnels = new Set();
@@ -7,10 +7,11 @@
7
7
  *
8
8
  * A pod Agent's sandbox variables are its vault's (R2c), then, with an outside credential
9
9
  * provider (`gates/credential-provider.ts`) and an owner on the Agent, each variable the provider
10
- * names for that owner, set to the stand-in `nylorun_managed` and never over a vault variable.
10
+ * names for that owner, set to the value it gives (the stand-in `nylorun_managed`, a stand-in of
11
+ * its own or a plain value) and never over a vault variable.
11
12
  * The provider is asked after the transaction, and one that fails adds nothing.
12
13
  */
13
- import { ENVIRONMENT_SECRET_SENTINEL, harnessLimitsOf } from "@nylorun/core/contracts";
14
+ import { harnessLimitsOf } from "@nylorun/core/contracts";
14
15
  import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
15
16
  import { isWorkflowManifest } from "../core/flow-host.js";
16
17
  import { sandboxWorkspaceOf } from "../sandbox/share.js";
@@ -114,18 +115,19 @@ options = {}) {
114
115
  return { ...started, transcript, cursor };
115
116
  }
116
117
  /**
117
- * `opened` with the credential provider's variables for `ask` added to its sandbox's environment
118
- * as the stand-in, under the vault's own; `opened` itself when there are none.
118
+ * `opened` with the credential provider's variables for `ask` added to its sandbox's environment,
119
+ * each with the value the provider gives (a stand-in or a plain value), under the vault's own;
120
+ * `opened` itself when there are none.
119
121
  */
120
122
  async function withProviderVariables(ctx, opened, ask) {
121
123
  if (!ask || !ctx.credentialProvider)
122
124
  return opened;
123
- const names = await ctx.credentialProvider.environment(ask.agentId, ask.owner);
125
+ const variables = await ctx.credentialProvider.environment(ask.agentId, ask.owner);
124
126
  const vault = opened.sandbox.environment ?? {};
125
- const added = names.filter((name) => !Object.hasOwn(vault, name));
127
+ const added = variables.filter(({ name }) => !Object.hasOwn(vault, name));
126
128
  if (added.length === 0)
127
129
  return opened;
128
- const environment = { ...Object.fromEntries(added.map((name) => [name, ENVIRONMENT_SECRET_SENTINEL])), ...vault };
130
+ const environment = { ...Object.fromEntries(added.map(({ name, value }) => [name, value])), ...vault };
129
131
  return { ...opened, sandbox: { ...opened.sandbox, environment } };
130
132
  }
131
133
  /** The run's `turn.start`. */
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.2.0",
3
3
  "info": {
4
4
  "title": "Nylorun Management API",
5
- "version": "0.34.0-beta",
5
+ "version": "0.35.0-beta",
6
6
  "x-nylorun-protocol": {
7
7
  "min": 16,
8
8
  "max": 18,
package/dist/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.2.0",
3
3
  "info": {
4
4
  "title": "Nylorun Runtime API",
5
- "version": "0.34.0-beta",
5
+ "version": "0.35.0-beta",
6
6
  "x-nylorun-protocol": {
7
7
  "min": 16,
8
8
  "max": 18,
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const RUNTIME_VERSION = "0.34.0-beta";
1
+ export declare const RUNTIME_VERSION = "0.35.0-beta";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in step with package.json by runtime/server/scripts/check-package.mjs, which runs in npm run check.
2
2
  // release:prepare also rewrites this when it bumps @nylorun/runtime.
3
- export const RUNTIME_VERSION = "0.34.0-beta";
3
+ export const RUNTIME_VERSION = "0.35.0-beta";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/runtime",
3
- "version": "0.34.0-beta",
3
+ "version": "0.35.0-beta",
4
4
  "description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",