@yeaft/webchat-agent 1.0.385 → 1.0.387
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/Dockerfile +26 -0
- package/cli.js +5 -4
- package/connection/index.js +2 -12
- package/container-cli.js +86 -0
- package/container-manager.js +165 -0
- package/index.js +5 -5
- package/local-runtime/server/config.js +8 -37
- package/local-runtime/server/container-agent-service.js +78 -0
- package/local-runtime/server/database.js +0 -1
- package/local-runtime/server/db/connection.js +5 -6
- package/local-runtime/server/db/user-db.js +8 -95
- package/local-runtime/server/index.js +13 -40
- package/local-runtime/server/routes/sandbox-routes.js +28 -95
- package/local-runtime/server/routes/user-routes.js +4 -0
- package/local-runtime/server/ws-agent.js +11 -27
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +96 -103
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +4 -1
- package/local-runtime/server/db/sandbox-db.js +0 -673
- package/local-runtime/server/sandbox-agent-auth.js +0 -66
- package/local-runtime/server/sandbox-attestation-listener.js +0 -128
- package/local-runtime/server/sandbox-config.js +0 -42
- package/local-runtime/server/sandbox-host-attestation.js +0 -175
- package/local-runtime/server/sandbox-reconciler.js +0 -355
- package/managed-sandbox/agent-runtime.js +0 -73
- package/managed-sandbox/controller.js +0 -118
- package/managed-sandbox/helper.js +0 -437
- package/managed-sandbox/identity-store.js +0 -9
- package/managed-sandbox/runtime-executor.js +0 -387
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID, sign, verify } from 'crypto';
|
|
2
|
-
import { request as httpsRequest } from 'https';
|
|
3
|
-
import { sandboxDb } from './database.js';
|
|
4
|
-
import { isSandboxAgentReady } from './sandbox-agent-auth.js';
|
|
5
|
-
import {
|
|
6
|
-
validateSandboxCleanupConfig,
|
|
7
|
-
validateSandboxDeploymentConfig
|
|
8
|
-
} from './sandbox-config.js';
|
|
9
|
-
|
|
10
|
-
function canonicalEnvelope(envelope) {
|
|
11
|
-
return JSON.stringify({
|
|
12
|
-
protocolVersion: envelope.protocolVersion,
|
|
13
|
-
operationId: envelope.operationId,
|
|
14
|
-
hostId: envelope.hostId,
|
|
15
|
-
sandboxId: envelope.sandboxId,
|
|
16
|
-
action: envelope.action,
|
|
17
|
-
requestDigest: envelope.requestDigest,
|
|
18
|
-
generation: envelope.generation,
|
|
19
|
-
hostEpoch: envelope.hostEpoch,
|
|
20
|
-
instanceId: envelope.instanceId,
|
|
21
|
-
imageDigest: envelope.imageDigest,
|
|
22
|
-
desiredState: envelope.desiredState,
|
|
23
|
-
issuedAt: envelope.issuedAt,
|
|
24
|
-
expiresAt: envelope.expiresAt,
|
|
25
|
-
nonce: envelope.nonce,
|
|
26
|
-
bootstrap: envelope.bootstrap || null,
|
|
27
|
-
resources: envelope.resources
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function signEnvelope(envelope, privateKey) {
|
|
32
|
-
return sign(null, Buffer.from(canonicalEnvelope(envelope)), privateKey).toString('base64url');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function activationDigest(hostId, epoch) {
|
|
36
|
-
return createHash('sha256')
|
|
37
|
-
.update(JSON.stringify({ protocolVersion: 1, action: 'ACTIVATE_EPOCH', hostId, epoch }))
|
|
38
|
-
.digest('hex');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function canonicalHelperAttestation(attestation) {
|
|
42
|
-
return JSON.stringify({
|
|
43
|
-
protocolVersion: attestation.protocolVersion,
|
|
44
|
-
operationId: attestation.operationId,
|
|
45
|
-
hostId: attestation.hostId,
|
|
46
|
-
sandboxId: attestation.sandboxId,
|
|
47
|
-
action: attestation.action,
|
|
48
|
-
requestDigest: attestation.requestDigest,
|
|
49
|
-
generation: attestation.generation,
|
|
50
|
-
hostEpoch: attestation.hostEpoch,
|
|
51
|
-
requestNonce: attestation.requestNonce,
|
|
52
|
-
issuedAt: attestation.issuedAt,
|
|
53
|
-
imageDigest: attestation.imageDigest || null,
|
|
54
|
-
readinessProof: attestation.readinessProof || null,
|
|
55
|
-
absenceProof: attestation.absenceProof || null,
|
|
56
|
-
resourceInspection: attestation.resourceInspection || null
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function canonicalControllerResult(result) {
|
|
61
|
-
return JSON.stringify({
|
|
62
|
-
operationId: result.operationId,
|
|
63
|
-
action: result.action,
|
|
64
|
-
hostId: result.hostId,
|
|
65
|
-
sandboxId: result.sandboxId,
|
|
66
|
-
requestDigest: result.requestDigest,
|
|
67
|
-
generation: result.generation,
|
|
68
|
-
hostEpoch: result.hostEpoch,
|
|
69
|
-
requestNonce: result.requestNonce,
|
|
70
|
-
issuedAt: result.issuedAt,
|
|
71
|
-
success: result.success,
|
|
72
|
-
imageDigest: result.imageDigest || null,
|
|
73
|
-
helperAttestation: result.helperAttestation || null,
|
|
74
|
-
errorCode: result.errorCode || null
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function verifyHelperAttestation(result, operation, config, now) {
|
|
79
|
-
const attestation = result.helperAttestation;
|
|
80
|
-
if (!attestation || attestation.protocolVersion !== 1
|
|
81
|
-
|| attestation.operationId !== operation.id
|
|
82
|
-
|| attestation.hostId !== operation.host_id
|
|
83
|
-
|| attestation.sandboxId !== operation.sandbox_id
|
|
84
|
-
|| attestation.action !== operation.kind
|
|
85
|
-
|| attestation.requestDigest !== operation.request_digest
|
|
86
|
-
|| attestation.generation !== operation.generation
|
|
87
|
-
|| attestation.hostEpoch !== operation.host_epoch
|
|
88
|
-
|| attestation.requestNonce !== operation.requestNonce
|
|
89
|
-
|| !Number.isFinite(attestation.issuedAt)
|
|
90
|
-
|| Math.abs(now - attestation.issuedAt) > (config.controllerProtocolMaxSkewMs || 30_000)) {
|
|
91
|
-
throw new Error('Controller returned a mismatched Helper attestation');
|
|
92
|
-
}
|
|
93
|
-
let signatureValid = false;
|
|
94
|
-
try {
|
|
95
|
-
signatureValid = verify(
|
|
96
|
-
null,
|
|
97
|
-
Buffer.from(canonicalHelperAttestation(attestation)),
|
|
98
|
-
config.helperAttestationPublicKey,
|
|
99
|
-
Buffer.from(String(attestation.signature || ''), 'base64url')
|
|
100
|
-
);
|
|
101
|
-
} catch {
|
|
102
|
-
signatureValid = false;
|
|
103
|
-
}
|
|
104
|
-
if (!signatureValid) throw new Error('Controller returned an invalid Helper attestation signature');
|
|
105
|
-
return attestation;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function verifyResourceInspection(attestation, operation) {
|
|
109
|
-
if (['remove', 'ACTIVATE_EPOCH'].includes(operation.kind)) return;
|
|
110
|
-
const inspection = attestation.resourceInspection;
|
|
111
|
-
const expected = {
|
|
112
|
-
cpuMillis: operation.cpu_millis,
|
|
113
|
-
memoryMiB: operation.memory_mib,
|
|
114
|
-
diskGiB: operation.disk_gib
|
|
115
|
-
};
|
|
116
|
-
if (!inspection
|
|
117
|
-
|| inspection.cpuMillis !== expected.cpuMillis
|
|
118
|
-
|| inspection.memoryMiB !== expected.memoryMiB
|
|
119
|
-
|| inspection.diskGiB !== expected.diskGiB
|
|
120
|
-
|| !Number.isInteger(inspection.pidsLimit) || inspection.pidsLimit <= 0
|
|
121
|
-
|| !Number.isInteger(inspection.ioWeight) || inspection.ioWeight <= 0
|
|
122
|
-
|| inspection.quotaHard !== true
|
|
123
|
-
|| inspection.networkPolicy !== 'public-egress-isolated') {
|
|
124
|
-
throw new Error('Helper attestation does not prove the requested Sandbox resource policy');
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function verifyControllerResult(result, operation, config, now = Date.now()) {
|
|
129
|
-
if (result.operationId !== operation.id
|
|
130
|
-
|| result.action !== operation.kind
|
|
131
|
-
|| result.hostId !== operation.host_id
|
|
132
|
-
|| result.sandboxId !== operation.sandbox_id
|
|
133
|
-
|| result.requestDigest !== operation.request_digest
|
|
134
|
-
|| result.generation !== operation.generation
|
|
135
|
-
|| result.hostEpoch !== operation.host_epoch
|
|
136
|
-
|| result.requestNonce !== operation.requestNonce) {
|
|
137
|
-
throw new Error('Controller returned a mismatched operation result');
|
|
138
|
-
}
|
|
139
|
-
if (!Number.isFinite(result.issuedAt)
|
|
140
|
-
|| Math.abs(now - result.issuedAt) > (config.controllerProtocolMaxSkewMs || 30_000)) {
|
|
141
|
-
throw new Error('Controller returned a stale operation result');
|
|
142
|
-
}
|
|
143
|
-
let signatureValid = false;
|
|
144
|
-
try {
|
|
145
|
-
signatureValid = verify(
|
|
146
|
-
null,
|
|
147
|
-
Buffer.from(canonicalControllerResult(result)),
|
|
148
|
-
config.controllerResultPublicKey,
|
|
149
|
-
Buffer.from(String(result.signature || ''), 'base64url')
|
|
150
|
-
);
|
|
151
|
-
} catch {
|
|
152
|
-
signatureValid = false;
|
|
153
|
-
}
|
|
154
|
-
if (!signatureValid) throw new Error('Controller returned an invalid operation result signature');
|
|
155
|
-
const attestation = verifyHelperAttestation(result, operation, config, now);
|
|
156
|
-
verifyResourceInspection(attestation, operation);
|
|
157
|
-
return attestation;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function validateControllerConfig(config) {
|
|
161
|
-
return validateSandboxDeploymentConfig(config);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function requestController(url, options) {
|
|
165
|
-
return new Promise((resolve, reject) => {
|
|
166
|
-
const request = httpsRequest(url, {
|
|
167
|
-
method: 'POST',
|
|
168
|
-
headers: options.headers,
|
|
169
|
-
cert: options.cert,
|
|
170
|
-
key: options.key,
|
|
171
|
-
ca: options.ca,
|
|
172
|
-
rejectUnauthorized: true,
|
|
173
|
-
timeout: options.timeout
|
|
174
|
-
}, response => {
|
|
175
|
-
const chunks = [];
|
|
176
|
-
response.on('data', chunk => chunks.push(chunk));
|
|
177
|
-
response.on('end', () => resolve({
|
|
178
|
-
ok: response.statusCode >= 200 && response.statusCode < 300,
|
|
179
|
-
status: response.statusCode,
|
|
180
|
-
json: async () => JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
181
|
-
}));
|
|
182
|
-
});
|
|
183
|
-
request.on('timeout', () => request.destroy(new Error('Controller request timed out')));
|
|
184
|
-
request.on('error', reject);
|
|
185
|
-
request.end(options.body);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function createSandboxReconciler({
|
|
190
|
-
config,
|
|
191
|
-
store = sandboxDb,
|
|
192
|
-
fetchImpl = requestController,
|
|
193
|
-
logger = console
|
|
194
|
-
}) {
|
|
195
|
-
let running = false;
|
|
196
|
-
const epochActivations = new Map();
|
|
197
|
-
|
|
198
|
-
async function ensureEpochActivated(operation, { allowActivation }) {
|
|
199
|
-
if (!store.isEpochActivated || !store.recordEpochActivation) return allowActivation;
|
|
200
|
-
const epoch = Number(operation.host_epoch);
|
|
201
|
-
if (!Number.isSafeInteger(epoch) || epoch < 1) throw new Error('Invalid Host epoch');
|
|
202
|
-
const digest = activationDigest(operation.host_id, epoch);
|
|
203
|
-
if (store.isEpochActivated(operation.host_id, epoch, digest)) return true;
|
|
204
|
-
if (!allowActivation) return false;
|
|
205
|
-
const key = `${operation.host_id}:${epoch}`;
|
|
206
|
-
if (epochActivations.has(key)) return epochActivations.get(key);
|
|
207
|
-
const activationPromise = (async () => {
|
|
208
|
-
const issuedAt = Date.now();
|
|
209
|
-
const activation = {
|
|
210
|
-
protocolVersion: 1,
|
|
211
|
-
operationId: `activate:${operation.host_id}:${epoch}`,
|
|
212
|
-
hostId: operation.host_id,
|
|
213
|
-
sandboxId: null,
|
|
214
|
-
action: 'ACTIVATE_EPOCH',
|
|
215
|
-
requestDigest: digest,
|
|
216
|
-
generation: null,
|
|
217
|
-
hostEpoch: epoch,
|
|
218
|
-
instanceId: null,
|
|
219
|
-
imageDigest: null,
|
|
220
|
-
desiredState: null,
|
|
221
|
-
issuedAt,
|
|
222
|
-
expiresAt: issuedAt + (config.controllerRequestTimeoutMs || 10_000),
|
|
223
|
-
nonce: randomUUID(),
|
|
224
|
-
bootstrap: null,
|
|
225
|
-
resources: null
|
|
226
|
-
};
|
|
227
|
-
const response = await fetchImpl(new URL('/v1/operations', config.controllerUrl), {
|
|
228
|
-
method: 'POST',
|
|
229
|
-
headers: { authorization: `Bearer ${config.controllerToken}`, 'content-type': 'application/json' },
|
|
230
|
-
body: JSON.stringify({ ...activation, signature: signEnvelope(activation, config.operationSigningPrivateKey) }),
|
|
231
|
-
cert: config.controllerClientCert,
|
|
232
|
-
key: config.controllerClientKey,
|
|
233
|
-
ca: config.controllerCaCert,
|
|
234
|
-
timeout: config.controllerRequestTimeoutMs || 10_000
|
|
235
|
-
});
|
|
236
|
-
if (!response.ok) throw new Error(`Controller returned HTTP ${response.status}`);
|
|
237
|
-
const result = await response.json();
|
|
238
|
-
verifyControllerResult(result, {
|
|
239
|
-
id: activation.operationId,
|
|
240
|
-
kind: activation.action,
|
|
241
|
-
host_id: activation.hostId,
|
|
242
|
-
sandbox_id: activation.sandboxId,
|
|
243
|
-
request_digest: activation.requestDigest,
|
|
244
|
-
generation: activation.generation,
|
|
245
|
-
host_epoch: activation.hostEpoch,
|
|
246
|
-
requestNonce: activation.nonce,
|
|
247
|
-
cpu_millis: null,
|
|
248
|
-
memory_mib: null,
|
|
249
|
-
disk_gib: null
|
|
250
|
-
}, config);
|
|
251
|
-
store.recordEpochActivation(operation.host_id, epoch, digest, Date.now());
|
|
252
|
-
return true;
|
|
253
|
-
})();
|
|
254
|
-
epochActivations.set(key, activationPromise);
|
|
255
|
-
try {
|
|
256
|
-
return await activationPromise;
|
|
257
|
-
} finally {
|
|
258
|
-
epochActivations.delete(key);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function dispatch(pendingOperation, { allowActivation }) {
|
|
263
|
-
if (pendingOperation.host_id !== config.controllerHostId) return;
|
|
264
|
-
if (!await ensureEpochActivated(pendingOperation, { allowActivation })) return;
|
|
265
|
-
const operation = store.admitPendingOperation?.(pendingOperation.id, config, Date.now())
|
|
266
|
-
|| (store.admitPendingOperation ? null : pendingOperation);
|
|
267
|
-
if (!operation) return;
|
|
268
|
-
const issuedAt = Date.now();
|
|
269
|
-
const envelope = {
|
|
270
|
-
protocolVersion: 1,
|
|
271
|
-
operationId: operation.id,
|
|
272
|
-
hostId: operation.host_id,
|
|
273
|
-
sandboxId: operation.sandbox_id,
|
|
274
|
-
action: operation.kind,
|
|
275
|
-
requestDigest: operation.request_digest,
|
|
276
|
-
generation: operation.generation,
|
|
277
|
-
hostEpoch: operation.host_epoch,
|
|
278
|
-
instanceId: operation.instance_id,
|
|
279
|
-
imageDigest: operation.image_digest,
|
|
280
|
-
desiredState: operation.desired_state,
|
|
281
|
-
issuedAt,
|
|
282
|
-
expiresAt: issuedAt + (config.controllerRequestTimeoutMs || 10_000),
|
|
283
|
-
nonce: randomUUID(),
|
|
284
|
-
...(['create', 'start', 'retry'].includes(operation.kind)
|
|
285
|
-
? { bootstrap: store.issueBootstrap(
|
|
286
|
-
operation.id,
|
|
287
|
-
config.bootstrapTtlMs,
|
|
288
|
-
config.bootstrapSigningKey
|
|
289
|
-
) }
|
|
290
|
-
: {}),
|
|
291
|
-
resources: {
|
|
292
|
-
cpuMillis: operation.cpu_millis,
|
|
293
|
-
memoryMiB: operation.memory_mib,
|
|
294
|
-
diskGiB: operation.disk_gib
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
const response = await fetchImpl(new URL('/v1/operations', config.controllerUrl), {
|
|
298
|
-
method: 'POST',
|
|
299
|
-
headers: {
|
|
300
|
-
authorization: `Bearer ${config.controllerToken}`,
|
|
301
|
-
'content-type': 'application/json'
|
|
302
|
-
},
|
|
303
|
-
body: JSON.stringify({ ...envelope, signature: signEnvelope(envelope, config.operationSigningPrivateKey) }),
|
|
304
|
-
cert: config.controllerClientCert,
|
|
305
|
-
key: config.controllerClientKey,
|
|
306
|
-
ca: config.controllerCaCert,
|
|
307
|
-
timeout: config.controllerRequestTimeoutMs || 10_000
|
|
308
|
-
});
|
|
309
|
-
if (!response.ok) throw new Error(`Controller returned HTTP ${response.status}`);
|
|
310
|
-
const result = await response.json();
|
|
311
|
-
const helperAttestation = verifyControllerResult(
|
|
312
|
-
result,
|
|
313
|
-
{ ...operation, requestNonce: envelope.nonce },
|
|
314
|
-
config
|
|
315
|
-
);
|
|
316
|
-
store.applyControllerResult({
|
|
317
|
-
...result,
|
|
318
|
-
imageDigest: helperAttestation.imageDigest,
|
|
319
|
-
readinessProof: helperAttestation.readinessProof,
|
|
320
|
-
absenceProof: helperAttestation.absenceProof,
|
|
321
|
-
resourceInspection: helperAttestation.resourceInspection
|
|
322
|
-
}, config, { isAgentReady: isSandboxAgentReady });
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function tick(now = Date.now()) {
|
|
326
|
-
if (running || !config) return;
|
|
327
|
-
running = true;
|
|
328
|
-
try {
|
|
329
|
-
store.reconcileRuntimeState?.(now, config, { isAgentReady: isSandboxAgentReady });
|
|
330
|
-
const operations = store.listPendingOperations(now);
|
|
331
|
-
const deploymentReady = validateControllerConfig(config);
|
|
332
|
-
const cleanupReady = validateSandboxCleanupConfig(config);
|
|
333
|
-
const dispatchable = operations.filter(operation => (
|
|
334
|
-
deploymentReady || (cleanupReady && operation.kind === 'remove')
|
|
335
|
-
));
|
|
336
|
-
await Promise.all(dispatchable.map(operation => dispatch(operation, {
|
|
337
|
-
allowActivation: deploymentReady
|
|
338
|
-
}).catch(error => {
|
|
339
|
-
logger.warn(`[Sandbox] Controller dispatch failed for ${operation.id}: ${error.message}`);
|
|
340
|
-
})));
|
|
341
|
-
} finally {
|
|
342
|
-
running = false;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function start() {
|
|
347
|
-
if (!config) return null;
|
|
348
|
-
const timer = setInterval(() => void tick(), config.reconcileIntervalMs || 5_000);
|
|
349
|
-
timer.unref?.();
|
|
350
|
-
void tick();
|
|
351
|
-
return timer;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return { start, tick };
|
|
355
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { setManagedSandboxIdentity } from './identity-store.js';
|
|
4
|
-
|
|
5
|
-
function assertIdentity(value) {
|
|
6
|
-
const claims = value?.claims;
|
|
7
|
-
if (!value?.serverUrl || !claims?.sandboxId || !claims?.instanceId
|
|
8
|
-
|| !Number.isInteger(claims.generation) || !claims.imageDigest) {
|
|
9
|
-
throw new Error('Managed Sandbox Agent rejected an invalid identity');
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function readJson(path) {
|
|
14
|
-
return JSON.parse(await readFile(path, 'utf8'));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function persistCredential(path, identity) {
|
|
18
|
-
const temporary = join(dirname(path), `.managed-agent-credential-${process.pid}.tmp`);
|
|
19
|
-
await writeFile(temporary, JSON.stringify(identity), { mode: 0o600 });
|
|
20
|
-
await rename(temporary, path);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function exchangeUrl(serverUrl) {
|
|
24
|
-
const url = new URL(serverUrl);
|
|
25
|
-
if (url.protocol === 'wss:') url.protocol = 'https:';
|
|
26
|
-
else if (url.protocol === 'ws:') url.protocol = 'http:';
|
|
27
|
-
url.pathname = '/api/sandbox/bootstrap/exchange';
|
|
28
|
-
url.search = '';
|
|
29
|
-
return url;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function loadManagedSandboxIdentity({ bootstrapFile, credentialFile, fetchImpl = fetch }) {
|
|
33
|
-
try {
|
|
34
|
-
const saved = await readJson(credentialFile);
|
|
35
|
-
assertIdentity(saved);
|
|
36
|
-
if (!saved.credentialId || !saved.secret) throw new Error('Managed Sandbox Agent credential is incomplete');
|
|
37
|
-
return saved;
|
|
38
|
-
} catch (error) {
|
|
39
|
-
if (error.code !== 'ENOENT') throw error;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const bootstrap = await readJson(bootstrapFile);
|
|
43
|
-
assertIdentity(bootstrap);
|
|
44
|
-
if (!bootstrap.token) throw new Error('Managed Sandbox Agent bootstrap token is missing');
|
|
45
|
-
const response = await fetchImpl(exchangeUrl(bootstrap.serverUrl), {
|
|
46
|
-
method: 'POST',
|
|
47
|
-
headers: { 'content-type': 'application/json' },
|
|
48
|
-
body: JSON.stringify({ token: bootstrap.token, claims: bootstrap.claims })
|
|
49
|
-
});
|
|
50
|
-
if (!response.ok) throw new Error(`Managed Sandbox Agent bootstrap exchange failed (${response.status})`);
|
|
51
|
-
const credential = await response.json();
|
|
52
|
-
const identity = { ...bootstrap, token: undefined, ...credential };
|
|
53
|
-
if (!identity.credentialId || !identity.secret) throw new Error('Managed Sandbox Agent bootstrap returned no credential');
|
|
54
|
-
await persistCredential(credentialFile, identity);
|
|
55
|
-
await unlink(bootstrapFile);
|
|
56
|
-
return identity;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function runManagedSandboxAgent(args, options = {}) {
|
|
60
|
-
const bootstrapIndex = args.indexOf('--bootstrap-file');
|
|
61
|
-
if (bootstrapIndex < 0 || !args[bootstrapIndex + 1]) {
|
|
62
|
-
throw new Error('managed-sandbox requires --bootstrap-file');
|
|
63
|
-
}
|
|
64
|
-
const bootstrapFile = args[bootstrapIndex + 1];
|
|
65
|
-
const credentialFile = options.credentialFile || '/home/yeaft/.yeaft/managed-agent-credential';
|
|
66
|
-
const identity = await loadManagedSandboxIdentity({ bootstrapFile, credentialFile, fetchImpl: options.fetchImpl });
|
|
67
|
-
setManagedSandboxIdentity(identity);
|
|
68
|
-
process.env.SERVER_URL = identity.serverUrl;
|
|
69
|
-
process.env.AGENT_NAME = identity.claims.sandboxId;
|
|
70
|
-
process.env.YEAFT_AGENT_INSTANCE = identity.claims.instanceId;
|
|
71
|
-
process.env.WORK_DIR = '/workspace';
|
|
72
|
-
await (options.startAgent || (() => import('../index.js')))();
|
|
73
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { createServer as createHttpsServer } from 'node:https';
|
|
2
|
-
import { sign, timingSafeEqual } from 'node:crypto';
|
|
3
|
-
|
|
4
|
-
const MAX_REQUEST_BYTES = 64 * 1024;
|
|
5
|
-
|
|
6
|
-
function canonicalControllerResult(result) {
|
|
7
|
-
return JSON.stringify({
|
|
8
|
-
operationId: result.operationId,
|
|
9
|
-
action: result.action,
|
|
10
|
-
hostId: result.hostId,
|
|
11
|
-
sandboxId: result.sandboxId,
|
|
12
|
-
requestDigest: result.requestDigest,
|
|
13
|
-
generation: result.generation,
|
|
14
|
-
hostEpoch: result.hostEpoch,
|
|
15
|
-
requestNonce: result.requestNonce,
|
|
16
|
-
issuedAt: result.issuedAt,
|
|
17
|
-
success: result.success,
|
|
18
|
-
imageDigest: result.imageDigest || null,
|
|
19
|
-
helperAttestation: result.helperAttestation || null,
|
|
20
|
-
errorCode: result.errorCode || null
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function tokenMatches(header, expectedToken) {
|
|
25
|
-
const prefix = 'Bearer ';
|
|
26
|
-
if (typeof header !== 'string' || !header.startsWith(prefix)) return false;
|
|
27
|
-
const actual = Buffer.from(header.slice(prefix.length));
|
|
28
|
-
const expected = Buffer.from(expectedToken);
|
|
29
|
-
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function writeJson(response, status, body) {
|
|
33
|
-
response.writeHead(status, { 'content-type': 'application/json' });
|
|
34
|
-
response.end(JSON.stringify(body));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Dedicated-Host Controller boundary. Privileged work remains exclusively in
|
|
39
|
-
* the Helper; the Controller only authenticates Server requests and signs the
|
|
40
|
-
* Helper's durable result for transport back to the control plane.
|
|
41
|
-
*/
|
|
42
|
-
export function createSandboxController({ config, helper, now = Date.now, createServer = createHttpsServer }) {
|
|
43
|
-
if (!config?.hostId || !config.token || !config.tlsCert || !config.tlsKey || !config.clientCa
|
|
44
|
-
|| !config.resultSigningPrivateKey || !helper?.execute) {
|
|
45
|
-
throw new Error('Sandbox Controller requires complete dedicated Host configuration');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function execute(operation) {
|
|
49
|
-
if (!operation || operation.hostId !== config.hostId) {
|
|
50
|
-
throw new Error('Sandbox Controller rejected an operation for another Host');
|
|
51
|
-
}
|
|
52
|
-
const helperResult = await helper.execute(operation);
|
|
53
|
-
const result = {
|
|
54
|
-
operationId: operation.operationId,
|
|
55
|
-
action: operation.action,
|
|
56
|
-
hostId: operation.hostId,
|
|
57
|
-
sandboxId: operation.sandboxId || null,
|
|
58
|
-
requestDigest: operation.requestDigest,
|
|
59
|
-
generation: operation.generation || null,
|
|
60
|
-
hostEpoch: operation.hostEpoch,
|
|
61
|
-
requestNonce: operation.nonce,
|
|
62
|
-
issuedAt: now(),
|
|
63
|
-
success: helperResult.success === true,
|
|
64
|
-
imageDigest: helperResult.helperAttestation?.imageDigest || null,
|
|
65
|
-
helperAttestation: helperResult.helperAttestation || null,
|
|
66
|
-
errorCode: helperResult.errorCode || null
|
|
67
|
-
};
|
|
68
|
-
result.signature = sign(
|
|
69
|
-
null,
|
|
70
|
-
Buffer.from(canonicalControllerResult(result)),
|
|
71
|
-
config.resultSigningPrivateKey
|
|
72
|
-
).toString('base64url');
|
|
73
|
-
return result;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const server = createServer({
|
|
77
|
-
cert: config.tlsCert,
|
|
78
|
-
key: config.tlsKey,
|
|
79
|
-
ca: config.clientCa,
|
|
80
|
-
requestCert: true,
|
|
81
|
-
rejectUnauthorized: true
|
|
82
|
-
}, (request, response) => {
|
|
83
|
-
if (request.method !== 'POST' || request.url !== '/v1/operations') {
|
|
84
|
-
writeJson(response, 404, { code: 'NOT_FOUND' });
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (!request.socket.authorized || !tokenMatches(request.headers.authorization, config.token)) {
|
|
88
|
-
writeJson(response, 401, { code: 'UNAUTHORIZED' });
|
|
89
|
-
request.resume();
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const chunks = [];
|
|
94
|
-
let size = 0;
|
|
95
|
-
request.on('data', chunk => {
|
|
96
|
-
size += chunk.length;
|
|
97
|
-
if (size > MAX_REQUEST_BYTES) request.destroy();
|
|
98
|
-
else chunks.push(chunk);
|
|
99
|
-
});
|
|
100
|
-
request.on('end', async () => {
|
|
101
|
-
if (size > MAX_REQUEST_BYTES) return;
|
|
102
|
-
try {
|
|
103
|
-
const operation = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
104
|
-
writeJson(response, 200, await execute(operation));
|
|
105
|
-
} catch {
|
|
106
|
-
writeJson(response, 400, { code: 'SANDBOX_OPERATION_REJECTED' });
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
execute,
|
|
113
|
-
listen: (...args) => server.listen(...args),
|
|
114
|
-
close: () => new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export { canonicalControllerResult };
|