@nylorun/runtime 0.21.0-beta → 0.22.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 (66) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +4 -2
  3. package/dist/adapters/execution/restate.js +26 -1
  4. package/dist/adapters/sandbox/local.js +1 -1
  5. package/dist/adapters/sandbox/virtual.js +1 -1
  6. package/dist/api/components.d.ts +42 -2
  7. package/dist/core/flow-host.d.ts +2 -3
  8. package/dist/core/flow-host.js +10 -7
  9. package/dist/execution/types.d.ts +13 -5
  10. package/dist/execution/types.js +13 -5
  11. package/dist/gates/egress-credentials.d.ts +48 -0
  12. package/dist/gates/egress-credentials.js +147 -0
  13. package/dist/gates/egress.d.ts +3 -0
  14. package/dist/gates/egress.js +14 -1
  15. package/dist/gates/tenant-vaults.d.ts +4 -0
  16. package/dist/gates/tenant-vaults.js +2 -0
  17. package/dist/harness/calls.d.ts +2 -0
  18. package/dist/harness/calls.js +1 -0
  19. package/dist/harness/executors.js +1 -0
  20. package/dist/harness/main.js +4 -1
  21. package/dist/harness/pod.d.ts +2 -0
  22. package/dist/harness/pod.js +36 -1
  23. package/dist/harness-api/start.d.ts +2 -0
  24. package/dist/harness-api/start.js +4 -2
  25. package/dist/harness-api/ws-server.js +2 -2
  26. package/dist/host/create-host.d.ts +1 -1
  27. package/dist/host/create-host.js +2 -1
  28. package/dist/host/gates.js +11 -0
  29. package/dist/host/http.d.ts +0 -1
  30. package/dist/host/http.js +0 -6
  31. package/dist/host/stack-config.d.ts +2 -0
  32. package/dist/host/stack-config.js +42 -73
  33. package/dist/keys/egress-ca.d.ts +15 -0
  34. package/dist/keys/egress-ca.js +66 -0
  35. package/dist/keys/keys.d.ts +8 -1
  36. package/dist/keys/keys.js +6 -0
  37. package/dist/keys/x509.d.ts +29 -0
  38. package/dist/keys/x509.js +160 -0
  39. package/dist/management-openapi.json +195 -9
  40. package/dist/openapi.json +2 -1
  41. package/dist/sandbox/join.d.ts +2 -0
  42. package/dist/sandbox/join.js +8 -3
  43. package/dist/sandbox/manager.d.ts +2 -0
  44. package/dist/sandbox/manager.js +1 -1
  45. package/dist/sandbox/tools.d.ts +3 -1
  46. package/dist/sandbox/tools.js +4 -2
  47. package/dist/sandbox/types.d.ts +5 -0
  48. package/dist/store/postgres/drizzle/0017_wake_outbox.sql +9 -0
  49. package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
  50. package/dist/store/postgres/schema.d.ts +105 -2
  51. package/dist/store/postgres/schema.js +17 -1
  52. package/dist/store/postgres/store.js +54 -4
  53. package/dist/store/types.d.ts +46 -5
  54. package/dist/tenant/advance.js +1 -4
  55. package/dist/tenant/commands.js +5 -5
  56. package/dist/tenant/context.d.ts +10 -7
  57. package/dist/tenant/runtime.js +2 -1
  58. package/dist/tenant/sessions.js +26 -0
  59. package/dist/tenant/sweep.d.ts +14 -0
  60. package/dist/tenant/sweep.js +69 -10
  61. package/dist/tenant/transcript.d.ts +8 -8
  62. package/dist/vault/service.d.ts +34 -0
  63. package/dist/vault/service.js +148 -15
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +3 -3
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The two certificates R2c needs (D50), built without a dependency: the installation's egress
3
+ * CA, and a short-lived leaf for one host name that egress-gate presents when it terminates TLS
4
+ * for a host an `environment_secret` is bound to. Node can parse X.509 but not build it, so this
5
+ * writes the DER by hand. Both use ECDSA P-256 with SHA-256.
6
+ *
7
+ * Only what clients check is written: a CA with basicConstraints CA:TRUE (critical), keyUsage
8
+ * keyCertSign and cRLSign (critical) and a subject key identifier; a leaf with the host as its
9
+ * only subjectAltName, keyUsage digitalSignature (critical), extKeyUsage serverAuth, and the
10
+ * CA's key identifier. Go (`gh`), OpenSSL (`curl`, `git`), Node and Python accept that shape.
11
+ */
12
+ import { isIP } from "node:net";
13
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, X509Certificate, } from "node:crypto";
14
+ const OID = {
15
+ ecdsaWithSha256: "1.2.840.10045.4.3.2",
16
+ commonName: "2.5.4.3",
17
+ organization: "2.5.4.10",
18
+ basicConstraints: "2.5.29.19",
19
+ keyUsage: "2.5.29.15",
20
+ extKeyUsage: "2.5.29.37",
21
+ serverAuth: "1.3.6.1.5.5.7.3.1",
22
+ subjectAltName: "2.5.29.17",
23
+ subjectKeyIdentifier: "2.5.29.14",
24
+ authorityKeyIdentifier: "2.5.29.35",
25
+ };
26
+ function length(n) {
27
+ if (n < 0x80)
28
+ return Buffer.from([n]);
29
+ const bytes = [];
30
+ for (let v = n; v > 0; v = Math.floor(v / 256))
31
+ bytes.unshift(v & 0xff);
32
+ return Buffer.from([0x80 | bytes.length, ...bytes]);
33
+ }
34
+ function tlv(tag, content) {
35
+ return Buffer.concat([Buffer.from([tag]), length(content.length), content]);
36
+ }
37
+ const seq = (...items) => tlv(0x30, Buffer.concat(items));
38
+ const set = (...items) => tlv(0x31, Buffer.concat(items));
39
+ const octets = (content) => tlv(0x04, content);
40
+ const bits = (content, unused = 0) => tlv(0x03, Buffer.concat([Buffer.from([unused]), content]));
41
+ const bool = (value) => tlv(0x01, Buffer.from([value ? 0xff : 0x00]));
42
+ const explicit = (n, content) => tlv(0xa0 + n, content);
43
+ function integer(value) {
44
+ let v = value;
45
+ while (v.length > 1 && v[0] === 0 && (v[1] & 0x80) === 0)
46
+ v = v.subarray(1);
47
+ if (v[0] & 0x80)
48
+ v = Buffer.concat([Buffer.from([0]), v]);
49
+ return tlv(0x02, v);
50
+ }
51
+ function oid(dotted) {
52
+ const parts = dotted.split(".").map(Number);
53
+ const out = [40 * parts[0] + parts[1]];
54
+ for (const part of parts.slice(2)) {
55
+ const chunk = [part & 0x7f];
56
+ for (let v = Math.floor(part / 128); v > 0; v = Math.floor(v / 128))
57
+ chunk.unshift((v & 0x7f) | 0x80);
58
+ out.push(...chunk);
59
+ }
60
+ return tlv(0x06, Buffer.from(out));
61
+ }
62
+ function name(fields) {
63
+ const rdns = [set(seq(oid(OID.commonName), tlv(0x0c, Buffer.from(fields.commonName, "utf8"))))];
64
+ if (fields.organization)
65
+ rdns.unshift(set(seq(oid(OID.organization), tlv(0x0c, Buffer.from(fields.organization, "utf8")))));
66
+ return seq(...rdns);
67
+ }
68
+ /** UTCTime before 2050, GeneralizedTime from then on (RFC 5280 §4.1.2.5). */
69
+ function time(at) {
70
+ const iso = at.toISOString().replace(/[-:T]/g, "").slice(0, 14) + "Z";
71
+ return at.getUTCFullYear() < 2050 ? tlv(0x17, Buffer.from(iso.slice(2), "ascii")) : tlv(0x18, Buffer.from(iso, "ascii"));
72
+ }
73
+ function extension(id, critical, value) {
74
+ return critical ? seq(oid(id), bool(true), octets(value)) : seq(oid(id), octets(value));
75
+ }
76
+ function spki(key) {
77
+ return key.export({ type: "spki", format: "der" });
78
+ }
79
+ /** SHA-1 of the subjectPublicKey bits (RFC 5280 §4.2.1.2, method 1). */
80
+ function keyIdentifier(publicKey) {
81
+ const der = spki(publicKey);
82
+ // The SPKI is SEQUENCE { AlgorithmIdentifier, BIT STRING }; for P-256 the key is its last 65 bytes.
83
+ return createHash("sha1").update(der.subarray(der.length - 65)).digest();
84
+ }
85
+ function serial() {
86
+ const value = randomBytes(16);
87
+ value[0] = value[0] & 0x7f;
88
+ return integer(value);
89
+ }
90
+ function certificate(tbs, issuerKey) {
91
+ const algorithm = seq(oid(OID.ecdsaWithSha256));
92
+ const signature = sign("sha256", tbs, issuerKey);
93
+ return seq(tbs, algorithm, bits(signature));
94
+ }
95
+ function pem(der) {
96
+ const body = der.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
97
+ return `-----BEGIN CERTIFICATE-----\n${body}\n-----END CERTIFICATE-----\n`;
98
+ }
99
+ /** A new P-256 CA, valid from an hour ago for `days` days. */
100
+ export function createCertificateAuthority(options) {
101
+ const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
102
+ const now = options.now ?? new Date();
103
+ const subject = name({ commonName: options.commonName, organization: "Nylorun" });
104
+ const ski = keyIdentifier(publicKey);
105
+ const tbs = seq(explicit(0, integer(Buffer.from([2]))), serial(), seq(oid(OID.ecdsaWithSha256)), subject, seq(time(new Date(now.getTime() - 3_600_000)), time(new Date(now.getTime() + (options.days ?? 3650) * 86_400_000))), subject, spki(publicKey), explicit(3, seq(extension(OID.basicConstraints, true, seq(bool(true), integer(Buffer.from([0])))), extension(OID.keyUsage, true, bits(Buffer.from([0x06]), 1)), extension(OID.subjectKeyIdentifier, false, octets(ski)))));
106
+ return {
107
+ certificate: pem(certificate(tbs, privateKey)),
108
+ privateKey: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
109
+ };
110
+ }
111
+ /**
112
+ * A leaf for `host` and the caller's `publicKey` (SPKI PEM), signed by the CA, valid from five
113
+ * minutes ago for `minutes` minutes. The caller keeps the leaf's private key: only the
114
+ * certificate comes back, so no private key leaves whoever holds it.
115
+ */
116
+ export function issueLeafCertificate(options) {
117
+ if (isIP(options.host) !== 0 || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(options.host))
118
+ throw new Error(`Not a host name: ${options.host}`);
119
+ const caKey = createPrivateKey(options.authority.privateKey);
120
+ const ca = new X509Certificate(options.authority.certificate);
121
+ const publicKey = createPublicKey(options.publicKey);
122
+ if (publicKey.asymmetricKeyType !== "ec" || publicKey.asymmetricKeyDetails?.namedCurve !== "prime256v1")
123
+ throw new Error("A leaf key must be ECDSA P-256");
124
+ const now = options.now ?? new Date();
125
+ const issuer = Buffer.from(ca.raw);
126
+ const tbs = seq(explicit(0, integer(Buffer.from([2]))), serial(), seq(oid(OID.ecdsaWithSha256)), subjectOf(issuer), seq(time(new Date(now.getTime() - 300_000)), time(new Date(now.getTime() + (options.minutes ?? 60) * 60_000))), name({ commonName: options.host }), spki(publicKey), explicit(3, seq(extension(OID.keyUsage, true, bits(Buffer.from([0x80]), 7)), extension(OID.extKeyUsage, false, seq(oid(OID.serverAuth))), extension(OID.subjectAltName, false, seq(tlv(0x82, Buffer.from(options.host, "ascii")))), extension(OID.authorityKeyIdentifier, false, seq(tlv(0x80, keyIdentifier(ca.publicKey)))))));
127
+ return pem(certificate(tbs, caKey));
128
+ }
129
+ /** A P-256 key pair for a leaf, as PEM: SPKI for the public key, PKCS#8 for the private key. */
130
+ export function leafKeyPair() {
131
+ const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
132
+ return {
133
+ publicKey: publicKey.export({ type: "spki", format: "pem" }).toString(),
134
+ privateKey: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
135
+ };
136
+ }
137
+ /** The subject Name of a DER certificate, byte for byte, to use as a leaf's issuer. */
138
+ function subjectOf(der) {
139
+ // Certificate ::= SEQUENCE { tbs SEQUENCE { [0] version, serial, sigAlg, issuer, validity, subject, … } }
140
+ const read = (buf, at) => {
141
+ let len = buf[at + 1];
142
+ let header = 2;
143
+ if (len & 0x80) {
144
+ const n = len & 0x7f;
145
+ len = 0;
146
+ for (let i = 0; i < n; i++)
147
+ len = len * 256 + buf[at + 2 + i];
148
+ header += n;
149
+ }
150
+ return { start: at, header, end: at + header + len };
151
+ };
152
+ const cert = read(der, 0);
153
+ const tbs = read(der, cert.start + cert.header);
154
+ // TBSCertificate fields: [0] version, serial, signature, issuer, validity, subject.
155
+ let at = tbs.start + tbs.header;
156
+ for (let field = 0; field < 5; field++)
157
+ at = read(der, at).end;
158
+ const subject = read(der, at);
159
+ return der.subarray(subject.start, subject.end);
160
+ }
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.2.0",
3
3
  "info": {
4
4
  "title": "Nylorun Management API",
5
- "version": "0.21.0-beta",
5
+ "version": "0.22.0-beta",
6
6
  "x-nylorun-protocol": {
7
7
  "min": 4,
8
8
  "max": 10,
@@ -97,6 +97,7 @@
97
97
  "definition_files_missing",
98
98
  "credential_rejected",
99
99
  "mcp_preview_failed",
100
+ "credential_conflict",
100
101
  "internal_error"
101
102
  ]
102
103
  },
@@ -2113,7 +2114,9 @@
2113
2114
  "type": "string",
2114
2115
  "enum": [
2115
2116
  "bearer",
2116
- "headers"
2117
+ "headers",
2118
+ "environment_secret",
2119
+ "environment_variable"
2117
2120
  ]
2118
2121
  },
2119
2122
  "binding": {
@@ -2121,11 +2124,41 @@
2121
2124
  "properties": {
2122
2125
  "url": {
2123
2126
  "type": "string"
2127
+ },
2128
+ "secretName": {
2129
+ "type": "string"
2130
+ },
2131
+ "allowedHosts": {
2132
+ "type": "array",
2133
+ "items": {
2134
+ "type": "string"
2135
+ }
2136
+ },
2137
+ "inject": {
2138
+ "type": "object",
2139
+ "properties": {
2140
+ "header": {
2141
+ "type": "string",
2142
+ "pattern": "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$"
2143
+ },
2144
+ "format": {
2145
+ "type": "string",
2146
+ "minLength": 1
2147
+ }
2148
+ },
2149
+ "required": [
2150
+ "header",
2151
+ "format"
2152
+ ],
2153
+ "additionalProperties": false
2154
+ },
2155
+ "variableName": {
2156
+ "type": "string"
2157
+ },
2158
+ "variableValue": {
2159
+ "type": "string"
2124
2160
  }
2125
2161
  },
2126
- "required": [
2127
- "url"
2128
- ],
2129
2162
  "additionalProperties": false
2130
2163
  },
2131
2164
  "headerNames": {
@@ -2269,6 +2302,85 @@
2269
2302
  "headers"
2270
2303
  ],
2271
2304
  "additionalProperties": false
2305
+ },
2306
+ {
2307
+ "type": "object",
2308
+ "properties": {
2309
+ "type": {
2310
+ "type": "string",
2311
+ "enum": [
2312
+ "environment_secret"
2313
+ ]
2314
+ },
2315
+ "secretName": {
2316
+ "type": "string",
2317
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$"
2318
+ },
2319
+ "secretValue": {
2320
+ "type": "string",
2321
+ "minLength": 1,
2322
+ "maxLength": 8192
2323
+ },
2324
+ "allowedHosts": {
2325
+ "type": "array",
2326
+ "items": {
2327
+ "type": "string"
2328
+ },
2329
+ "minItems": 1,
2330
+ "maxItems": 32
2331
+ },
2332
+ "inject": {
2333
+ "type": "object",
2334
+ "properties": {
2335
+ "header": {
2336
+ "type": "string",
2337
+ "pattern": "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$"
2338
+ },
2339
+ "format": {
2340
+ "type": "string",
2341
+ "minLength": 1
2342
+ }
2343
+ },
2344
+ "required": [
2345
+ "header",
2346
+ "format"
2347
+ ],
2348
+ "additionalProperties": false
2349
+ }
2350
+ },
2351
+ "required": [
2352
+ "type",
2353
+ "secretName",
2354
+ "secretValue",
2355
+ "allowedHosts"
2356
+ ],
2357
+ "additionalProperties": false
2358
+ },
2359
+ {
2360
+ "type": "object",
2361
+ "properties": {
2362
+ "type": {
2363
+ "type": "string",
2364
+ "enum": [
2365
+ "environment_variable"
2366
+ ]
2367
+ },
2368
+ "variableName": {
2369
+ "type": "string",
2370
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,127}$"
2371
+ },
2372
+ "variableValue": {
2373
+ "type": "string",
2374
+ "minLength": 1,
2375
+ "maxLength": 8192
2376
+ }
2377
+ },
2378
+ "required": [
2379
+ "type",
2380
+ "variableName",
2381
+ "variableValue"
2382
+ ],
2383
+ "additionalProperties": false
2272
2384
  }
2273
2385
  ]
2274
2386
  }
@@ -2302,7 +2414,9 @@
2302
2414
  "type": "string",
2303
2415
  "enum": [
2304
2416
  "bearer",
2305
- "headers"
2417
+ "headers",
2418
+ "environment_secret",
2419
+ "environment_variable"
2306
2420
  ]
2307
2421
  },
2308
2422
  "binding": {
@@ -2310,11 +2424,41 @@
2310
2424
  "properties": {
2311
2425
  "url": {
2312
2426
  "type": "string"
2427
+ },
2428
+ "secretName": {
2429
+ "type": "string"
2430
+ },
2431
+ "allowedHosts": {
2432
+ "type": "array",
2433
+ "items": {
2434
+ "type": "string"
2435
+ }
2436
+ },
2437
+ "inject": {
2438
+ "type": "object",
2439
+ "properties": {
2440
+ "header": {
2441
+ "type": "string",
2442
+ "pattern": "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$"
2443
+ },
2444
+ "format": {
2445
+ "type": "string",
2446
+ "minLength": 1
2447
+ }
2448
+ },
2449
+ "required": [
2450
+ "header",
2451
+ "format"
2452
+ ],
2453
+ "additionalProperties": false
2454
+ },
2455
+ "variableName": {
2456
+ "type": "string"
2457
+ },
2458
+ "variableValue": {
2459
+ "type": "string"
2313
2460
  }
2314
2461
  },
2315
- "required": [
2316
- "url"
2317
- ],
2318
2462
  "additionalProperties": false
2319
2463
  },
2320
2464
  "headerNames": {
@@ -2463,6 +2607,48 @@
2463
2607
  "headers"
2464
2608
  ],
2465
2609
  "additionalProperties": false
2610
+ },
2611
+ {
2612
+ "type": "object",
2613
+ "properties": {
2614
+ "type": {
2615
+ "type": "string",
2616
+ "enum": [
2617
+ "environment_secret"
2618
+ ]
2619
+ },
2620
+ "secretValue": {
2621
+ "type": "string",
2622
+ "minLength": 1,
2623
+ "maxLength": 8192
2624
+ }
2625
+ },
2626
+ "required": [
2627
+ "type",
2628
+ "secretValue"
2629
+ ],
2630
+ "additionalProperties": false
2631
+ },
2632
+ {
2633
+ "type": "object",
2634
+ "properties": {
2635
+ "type": {
2636
+ "type": "string",
2637
+ "enum": [
2638
+ "environment_variable"
2639
+ ]
2640
+ },
2641
+ "variableValue": {
2642
+ "type": "string",
2643
+ "minLength": 1,
2644
+ "maxLength": 8192
2645
+ }
2646
+ },
2647
+ "required": [
2648
+ "type",
2649
+ "variableValue"
2650
+ ],
2651
+ "additionalProperties": false
2466
2652
  }
2467
2653
  ]
2468
2654
  }
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.21.0-beta",
5
+ "version": "0.22.0-beta",
6
6
  "x-nylorun-protocol": {
7
7
  "min": 4,
8
8
  "max": 10,
@@ -6338,6 +6338,7 @@
6338
6338
  "definition_files_missing",
6339
6339
  "credential_rejected",
6340
6340
  "mcp_preview_failed",
6341
+ "credential_conflict",
6341
6342
  "internal_error"
6342
6343
  ]
6343
6344
  },
@@ -15,6 +15,8 @@ export interface HostJoinAnswer {
15
15
  readonly epoch: number;
16
16
  /** ISO time the tokens expire. */
17
17
  readonly expiresAt: string;
18
+ /** The egress CA certificate (PEM) the sandbox trusts (R2c). */
19
+ readonly caCertificate: string;
18
20
  }
19
21
  /** A refused join, renewal or host connection. */
20
22
  export declare class HostAuthError extends Error {
@@ -24,6 +24,10 @@
24
24
  * and the pod (`tenant/host-token.ts`). `renew` mints a new pair at the same epoch while the
25
25
  * host is current; it is refused once the epoch moved.
26
26
  *
27
+ * The answer also carries the installation's egress CA certificate (R2c, D50): the engine adds it
28
+ * to the trust bundle every command gets, so a host egress-gate terminates TLS for (one an
29
+ * `environment_secret` is bound to) is trusted. Only the certificate: its key stays in keys.
30
+ *
27
31
  * Every refusal answers the same 401 (the reason is logged), so a caller learns nothing about
28
32
  * which check failed.
29
33
  */
@@ -53,13 +57,14 @@ function sameHash(a, b) {
53
57
  const right = Buffer.from(b, "hex");
54
58
  return left.length === right.length && left.length > 0 && timingSafeEqual(left, right);
55
59
  }
56
- function answerOf(grant) {
60
+ function answerOf(grant, caCertificate) {
57
61
  return {
58
62
  hostToken: grant.hostToken,
59
63
  egressToken: grant.egressToken,
60
64
  sandboxId: grant.claims.sandboxId,
61
65
  epoch: grant.claims.epoch,
62
66
  expiresAt: new Date(grant.claims.expiresAt).toISOString(),
67
+ caCertificate,
63
68
  };
64
69
  }
65
70
  export function hostAuthority(ctx) {
@@ -154,11 +159,11 @@ export function hostAuthority(ctx) {
154
159
  // The previous host's connection (an older epoch) serves nothing more.
155
160
  ctx.harness.revokeHost(sandboxId, joined);
156
161
  ctx.config.logger.info("sandbox host joined", { sandboxId, epoch: joined });
157
- return answerOf(await mintHostTokens(ctx, { sandboxId, epoch: joined, podUid }));
162
+ return answerOf(await mintHostTokens(ctx, { sandboxId, epoch: joined, podUid }), await ctx.keys.egressCa());
158
163
  },
159
164
  async renew(hostToken) {
160
165
  const claims = await verified(hostToken);
161
- return answerOf(await mintHostTokens(ctx, claims));
166
+ return answerOf(await mintHostTokens(ctx, claims), await ctx.keys.egressCa());
162
167
  },
163
168
  verify: verified,
164
169
  };
@@ -34,6 +34,8 @@ export interface SandboxSessionRef {
34
34
  * so every session attached to it shares the files; absent, the workspace is the session's.
35
35
  */
36
36
  readonly sandboxId?: string;
37
+ /** Variables every command of the call gets (R2c): secret names set to the sentinel, plain values. */
38
+ readonly environment?: Readonly<Record<string, string>>;
37
39
  /**
38
40
  * Who the call's events are claimed for, passed back to `emit`: in a harness, the run that
39
41
  * made the call (its run id).
@@ -285,7 +285,7 @@ export class SandboxManager {
285
285
  try {
286
286
  const outcome = await runSandboxTool(live.handle, toolName, (input ?? {}), signal, (value) => {
287
287
  report = value;
288
- });
288
+ }, session.environment);
289
289
  await this.options.emit(session.id, session.activeTurnId, "sandbox.exec", {
290
290
  tool: toolName,
291
291
  ...report,
@@ -20,4 +20,6 @@ export declare function truncateMiddle(value: string, limit?: number): {
20
20
  text: string;
21
21
  truncated: boolean;
22
22
  };
23
- export declare function runSandboxTool(handle: SandboxHandle, name: SandboxToolName, input: Record<string, any>, signal: AbortSignal, report: (value: SandboxToolReport) => void): Promise<SandboxToolOutcome>;
23
+ export declare function runSandboxTool(handle: SandboxHandle, name: SandboxToolName, input: Record<string, any>, signal: AbortSignal, report: (value: SandboxToolReport) => void,
24
+ /** Variables every command gets (R2c): secret names set to the sentinel, plain values. */
25
+ environment?: Readonly<Record<string, string>>): Promise<SandboxToolOutcome>;
@@ -39,10 +39,12 @@ const failed = (code, message) => ({
39
39
  code,
40
40
  message,
41
41
  });
42
- export async function runSandboxTool(handle, name, input, signal, report) {
42
+ export async function runSandboxTool(handle, name, input, signal, report,
43
+ /** Variables every command gets (R2c): secret names set to the sentinel, plain values. */
44
+ environment) {
43
45
  const workspace = handle.workspace ?? SANDBOX_WORKSPACE;
44
46
  const resolvePath = (path) => resolveSandboxPath(path, workspace);
45
- const exec = (command, timeoutMs = 60_000) => handle.exec({ command, cwd: workspace, timeoutMs }, signal);
47
+ const exec = (command, timeoutMs = 60_000) => handle.exec({ command, cwd: workspace, timeoutMs, ...(environment ? { env: environment } : {}) }, signal);
46
48
  switch (name) {
47
49
  case "bash": {
48
50
  const command = String(input.command);
@@ -36,6 +36,11 @@ export interface ExecRequest {
36
36
  readonly command: string;
37
37
  readonly cwd: string;
38
38
  readonly timeoutMs: number;
39
+ /**
40
+ * Variables this command gets on top of the backend's own (R2c): never a secret. The backend's
41
+ * proxy and trust variables win over them.
42
+ */
43
+ readonly env?: Readonly<Record<string, string>>;
39
44
  }
40
45
  export interface ExecResult {
41
46
  readonly exitCode: number;
@@ -0,0 +1,9 @@
1
+ CREATE TABLE "nylorun"."wakes" (
2
+ "id" text COLLATE "C" PRIMARY KEY NOT NULL,
3
+ "session_id" text COLLATE "C" NOT NULL,
4
+ "reason" text NOT NULL,
5
+ "dedupe_key" text,
6
+ "created_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL
7
+ );
8
+ --> statement-breakpoint
9
+ CREATE INDEX "wakes_created_at" ON "nylorun"."wakes" USING btree ("created_at");
@@ -120,6 +120,13 @@
120
120
  "when": 1791395759051,
121
121
  "tag": "0016_mcp_oauth_removed",
122
122
  "breakpoints": true
123
+ },
124
+ {
125
+ "idx": 17,
126
+ "version": "7",
127
+ "when": 1791455573196,
128
+ "tag": "0017_wake_outbox",
129
+ "breakpoints": true
123
130
  }
124
131
  ]
125
132
  }