@nylorun/runtime 0.21.1-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.
- package/CHANGELOG.md +19 -0
- package/dist/adapters/sandbox/local.js +1 -1
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/api/components.d.ts +42 -2
- package/dist/gates/egress-credentials.d.ts +48 -0
- package/dist/gates/egress-credentials.js +147 -0
- package/dist/gates/egress.d.ts +3 -0
- package/dist/gates/egress.js +14 -1
- package/dist/gates/tenant-vaults.d.ts +4 -0
- package/dist/gates/tenant-vaults.js +2 -0
- package/dist/harness/calls.d.ts +2 -0
- package/dist/harness/calls.js +1 -0
- package/dist/harness/executors.js +1 -0
- package/dist/harness/main.js +4 -1
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/pod.js +36 -1
- package/dist/harness-api/start.d.ts +2 -0
- package/dist/harness-api/start.js +4 -2
- package/dist/harness-api/ws-server.js +2 -2
- package/dist/host/gates.js +11 -0
- package/dist/keys/egress-ca.d.ts +15 -0
- package/dist/keys/egress-ca.js +66 -0
- package/dist/keys/keys.d.ts +8 -1
- package/dist/keys/keys.js +6 -0
- package/dist/keys/x509.d.ts +29 -0
- package/dist/keys/x509.js +160 -0
- package/dist/management-openapi.json +195 -9
- package/dist/openapi.json +2 -1
- package/dist/sandbox/join.d.ts +2 -0
- package/dist/sandbox/join.js +8 -3
- package/dist/sandbox/manager.d.ts +2 -0
- package/dist/sandbox/manager.js +1 -1
- package/dist/sandbox/tools.d.ts +3 -1
- package/dist/sandbox/tools.js +4 -2
- package/dist/sandbox/types.d.ts +5 -0
- package/dist/store/postgres/schema.d.ts +2 -2
- package/dist/store/postgres/schema.js +1 -1
- package/dist/tenant/sessions.js +26 -0
- package/dist/tenant/transcript.d.ts +8 -8
- package/dist/vault/service.d.ts +34 -0
- package/dist/vault/service.js +148 -15
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The installation's egress CA (R2c, D50, E12): one long-lived certificate authority whose
|
|
3
|
+
* certificate every pod sandbox trusts, and whose key signs the short-lived leaf egress-gate
|
|
4
|
+
* presents when it terminates TLS for a host an `environment_secret` is bound to. Only keys holds
|
|
5
|
+
* the key: it lives in the Tenant setting `egress.ca`, sealed under the vault KEK like a signing
|
|
6
|
+
* key, beside the certificate in the clear. Created on first use under the signing keys' lock.
|
|
7
|
+
* No rotation yet (R8, with the KeyRoot seam).
|
|
8
|
+
*/
|
|
9
|
+
import { X509Certificate } from "node:crypto";
|
|
10
|
+
import { decryptSecret, encryptSecret } from "../vault/crypto.js";
|
|
11
|
+
import { createCertificateAuthority, issueLeafCertificate } from "./x509.js";
|
|
12
|
+
const SETTING = "egress.ca";
|
|
13
|
+
/** How long a leaf egress-gate presents is valid: one connection rarely lasts this long. */
|
|
14
|
+
export const EGRESS_LEAF_MINUTES = 60;
|
|
15
|
+
function aad(certificate) {
|
|
16
|
+
return Buffer.from(`nylorun.egress-ca:${new X509Certificate(certificate).fingerprint256}`);
|
|
17
|
+
}
|
|
18
|
+
export class EgressAuthority {
|
|
19
|
+
store;
|
|
20
|
+
kek;
|
|
21
|
+
/** The CA with its key, decrypted once per process, as signing keys are. */
|
|
22
|
+
authority;
|
|
23
|
+
constructor(store, kek) {
|
|
24
|
+
this.store = store;
|
|
25
|
+
this.kek = kek;
|
|
26
|
+
}
|
|
27
|
+
/** The CA certificate (PEM), creating the CA when the Tenant has none. */
|
|
28
|
+
async certificate() {
|
|
29
|
+
return (await this.load()).certificate;
|
|
30
|
+
}
|
|
31
|
+
/** A leaf for `host` and the caller's P-256 public key (SPKI PEM); the caller keeps its key. */
|
|
32
|
+
async signLeaf(host, publicKey) {
|
|
33
|
+
return issueLeafCertificate({ authority: await this.load(), host, publicKey, minutes: EGRESS_LEAF_MINUTES });
|
|
34
|
+
}
|
|
35
|
+
async load() {
|
|
36
|
+
if (this.authority)
|
|
37
|
+
return this.authority;
|
|
38
|
+
const kek = this.kek();
|
|
39
|
+
const stored = await this.store.tx(async (t) => (await read(t)) ?? (await create(t, kek)));
|
|
40
|
+
const privateKey = decryptSecret(kek, aad(stored.certificate), Buffer.from(stored.nonce, "base64"), Buffer.from(stored.ciphertext, "base64"), Buffer.from(stored.wrappedDek, "base64"), stored.kekId).toString("utf8");
|
|
41
|
+
this.authority = { certificate: stored.certificate, privateKey };
|
|
42
|
+
return this.authority;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function read(t) {
|
|
46
|
+
const value = await t.getSetting(SETTING);
|
|
47
|
+
return value === undefined ? undefined : JSON.parse(value);
|
|
48
|
+
}
|
|
49
|
+
async function create(t, kek) {
|
|
50
|
+
// Concurrent first uses would both create a CA: lock, then read what the winner made.
|
|
51
|
+
await t.lockSigningKeys();
|
|
52
|
+
const existing = await read(t);
|
|
53
|
+
if (existing)
|
|
54
|
+
return existing;
|
|
55
|
+
const authority = createCertificateAuthority({ commonName: "Nylorun egress CA" });
|
|
56
|
+
const sealed = encryptSecret(kek, aad(authority.certificate), Buffer.from(authority.privateKey, "utf8"));
|
|
57
|
+
const stored = {
|
|
58
|
+
certificate: authority.certificate,
|
|
59
|
+
kekId: sealed.kekId,
|
|
60
|
+
nonce: sealed.nonce.toString("base64"),
|
|
61
|
+
ciphertext: sealed.ciphertext.toString("base64"),
|
|
62
|
+
wrappedDek: sealed.wrappedDek.toString("base64"),
|
|
63
|
+
};
|
|
64
|
+
await t.putSetting(SETTING, JSON.stringify(stored));
|
|
65
|
+
return stored;
|
|
66
|
+
}
|
package/dist/keys/keys.d.ts
CHANGED
|
@@ -32,9 +32,16 @@ export interface Keys {
|
|
|
32
32
|
* (R2b C12, `mcp/preview.ts`); never calls a tool.
|
|
33
33
|
*/
|
|
34
34
|
previewMcp(body: McpPreviewRequest): Promise<McpPreview>;
|
|
35
|
+
/** The egress CA's certificate (PEM), creating the CA when the Tenant has none (R2c, D50). */
|
|
36
|
+
egressCa(): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* A short-lived leaf for `host` and the caller's P-256 public key (SPKI PEM), signed by the
|
|
39
|
+
* egress CA (R2c). The leaf's private key never reaches keys; the CA's never leaves it.
|
|
40
|
+
*/
|
|
41
|
+
signEgressLeaf(host: string, publicKey: string): Promise<string>;
|
|
35
42
|
}
|
|
36
43
|
/** The operations, by the name the HTTP route carries. */
|
|
37
|
-
export declare const KEYS_OPERATIONS: readonly ["createCredential", "rotateCredential", "putHostModel", "selectHostModel", "sign", "rotateSigningKeys", "ensureSigningKeys", "previewMcp"];
|
|
44
|
+
export declare const KEYS_OPERATIONS: readonly ["createCredential", "rotateCredential", "putHostModel", "selectHostModel", "sign", "rotateSigningKeys", "ensureSigningKeys", "previewMcp", "egressCa", "signEgressLeaf"];
|
|
38
45
|
export type KeysOperation = (typeof KEYS_OPERATIONS)[number];
|
|
39
46
|
export interface InProcessKeysOptions {
|
|
40
47
|
readonly store: SessionStore;
|
package/dist/keys/keys.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* service itself, which serves the HTTP routes with it), and `httpKeys` (`client.ts`), the
|
|
13
13
|
* runtime container's client of the `keys` service in the gateway.
|
|
14
14
|
*/
|
|
15
|
+
import { EgressAuthority } from "./egress-ca.js";
|
|
15
16
|
import { SignJWT } from "jose";
|
|
16
17
|
import { previewMcpServer } from "../mcp/preview.js";
|
|
17
18
|
import { signingKeyView } from "../tenant/signing-keys.js";
|
|
@@ -25,9 +26,12 @@ export const KEYS_OPERATIONS = [
|
|
|
25
26
|
"rotateSigningKeys",
|
|
26
27
|
"ensureSigningKeys",
|
|
27
28
|
"previewMcp",
|
|
29
|
+
"egressCa",
|
|
30
|
+
"signEgressLeaf",
|
|
28
31
|
];
|
|
29
32
|
export function inProcessKeys(options) {
|
|
30
33
|
const { store, vault, signingKeys, kek } = options;
|
|
34
|
+
const egress = new EgressAuthority(store, kek);
|
|
31
35
|
return {
|
|
32
36
|
createCredential: (vaultId, body) => vault.createCredential(vaultId, body),
|
|
33
37
|
rotateCredential: (vaultId, id, body) => vault.rotateCredential(vaultId, id, body),
|
|
@@ -52,5 +56,7 @@ export function inProcessKeys(options) {
|
|
|
52
56
|
await store.tx((t) => signingKeys.ensure(t, key));
|
|
53
57
|
},
|
|
54
58
|
previewMcp: (body) => previewMcpServer(body, { vault, policy: options.policy ?? {} }),
|
|
59
|
+
egressCa: () => egress.certificate(),
|
|
60
|
+
signEgressLeaf: (host, publicKey) => egress.signLeaf(host, publicKey),
|
|
55
61
|
};
|
|
56
62
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface CertificateAuthority {
|
|
2
|
+
/** PEM of the CA certificate: the only part a sandbox ever receives. */
|
|
3
|
+
readonly certificate: string;
|
|
4
|
+
/** PKCS#8 PEM of the CA key: sealed in the vault, used only by keys. */
|
|
5
|
+
readonly privateKey: string;
|
|
6
|
+
}
|
|
7
|
+
/** A new P-256 CA, valid from an hour ago for `days` days. */
|
|
8
|
+
export declare function createCertificateAuthority(options: {
|
|
9
|
+
readonly commonName: string;
|
|
10
|
+
readonly days?: number;
|
|
11
|
+
readonly now?: Date;
|
|
12
|
+
}): CertificateAuthority;
|
|
13
|
+
/**
|
|
14
|
+
* A leaf for `host` and the caller's `publicKey` (SPKI PEM), signed by the CA, valid from five
|
|
15
|
+
* minutes ago for `minutes` minutes. The caller keeps the leaf's private key: only the
|
|
16
|
+
* certificate comes back, so no private key leaves whoever holds it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function issueLeafCertificate(options: {
|
|
19
|
+
readonly authority: CertificateAuthority;
|
|
20
|
+
readonly host: string;
|
|
21
|
+
readonly publicKey: string;
|
|
22
|
+
readonly minutes?: number;
|
|
23
|
+
readonly now?: Date;
|
|
24
|
+
}): string;
|
|
25
|
+
/** A P-256 key pair for a leaf, as PEM: SPKI for the public key, PKCS#8 for the private key. */
|
|
26
|
+
export declare function leafKeyPair(): {
|
|
27
|
+
readonly publicKey: string;
|
|
28
|
+
readonly privateKey: string;
|
|
29
|
+
};
|
|
@@ -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.
|
|
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.
|
|
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
|
},
|
package/dist/sandbox/join.d.ts
CHANGED
|
@@ -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 {
|
package/dist/sandbox/join.js
CHANGED
|
@@ -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).
|
package/dist/sandbox/manager.js
CHANGED
|
@@ -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,
|
package/dist/sandbox/tools.d.ts
CHANGED
|
@@ -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
|
|
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>;
|