@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.
@@ -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 };