@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 +6 -0
- package/dist/gates/credential-provider.d.ts +15 -3
- package/dist/gates/credential-provider.js +54 -13
- package/dist/gates/egress.js +8 -4
- package/dist/harness-api/start.js +9 -7
- package/dist/management-openapi.json +1 -1
- package/dist/openapi.json +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
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
|
}
|
package/dist/gates/egress.js
CHANGED
|
@@ -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
|
|
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,
|
|
231
|
-
*
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
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
|
|
125
|
+
const variables = await ctx.credentialProvider.environment(ask.agentId, ask.owner);
|
|
124
126
|
const vault = opened.sandbox.environment ?? {};
|
|
125
|
-
const added =
|
|
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,
|
|
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`. */
|
package/dist/openapi.json
CHANGED
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
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.
|
|
3
|
+
export const RUNTIME_VERSION = "0.35.0-beta";
|
package/package.json
CHANGED