Update logging rules, authentication process for computer, update pnpm

This commit is contained in:
Morgan Dean
2025-06-25 14:46:21 -07:00
parent 0f917e2966
commit 8f49e0e2bf
7 changed files with 146 additions and 66 deletions
@@ -4,7 +4,7 @@ import pino from 'pino';
import type { OSType } from '../../types';
import type { BaseComputerConfig, Display, VMProviderType } from '../types';
const logger = pino({ name: 'computer-base' });
const logger = pino({ name: 'computer.provider_base' });
/**
* Base Computer class with shared functionality
@@ -6,8 +6,6 @@ import {
import type { CloudComputerConfig, VMProviderType } from '../types';
import { BaseComputer } from './base';
const logger = pino({ name: 'computer-cloud' });
/**
* Cloud-specific computer implementation
*/
@@ -17,6 +15,8 @@ export class CloudComputer extends BaseComputer {
private iface?: BaseComputerInterface;
private initialized = false;
protected logger = pino({ name: 'computer.provider_cloud' });
constructor(config: CloudComputerConfig) {
super(config);
this.apiKey = config.apiKey;
@@ -31,14 +31,14 @@ export class CloudComputer extends BaseComputer {
*/
async run(): Promise<void> {
if (this.initialized) {
logger.info('Computer already initialized, skipping initialization');
this.logger.info('Computer already initialized, skipping initialization');
return;
}
try {
// For cloud provider, the VM is already running, we just need to connect
const ipAddress = this.ip;
logger.info(`Connecting to cloud VM at ${ipAddress}`);
this.logger.info(`Connecting to cloud VM at ${ipAddress}`);
// Create the interface with API key authentication
this.iface = InterfaceFactory.createInterfaceForOS(
@@ -49,13 +49,13 @@ export class CloudComputer extends BaseComputer {
);
// Wait for the interface to be ready
logger.info('Waiting for interface to be ready...');
this.logger.info('Waiting for interface to be ready...');
await this.iface.waitForReady();
this.initialized = true;
logger.info('Cloud computer ready');
this.logger.info('Cloud computer ready');
} catch (error) {
logger.error(`Failed to initialize cloud computer: ${error}`);
this.logger.error(`Failed to initialize cloud computer: ${error}`);
throw new Error(`Failed to initialize cloud computer: ${error}`);
}
}
@@ -64,7 +64,7 @@ export class CloudComputer extends BaseComputer {
* Stop the cloud computer (disconnect interface)
*/
async stop(): Promise<void> {
logger.info('Disconnecting from cloud computer...');
this.logger.info('Disconnecting from cloud computer...');
if (this.iface) {
this.iface.disconnect();
@@ -72,7 +72,7 @@ export class CloudComputer extends BaseComputer {
}
this.initialized = false;
logger.info('Disconnected from cloud computer');
this.logger.info('Disconnected from cloud computer');
}
/**
+66 -43
View File
@@ -40,7 +40,7 @@ export abstract class BaseComputerInterface {
protected apiKey?: string;
protected vmName?: string;
protected logger = pino({ name: 'interface-base' });
protected logger = pino({ name: 'computer.interface-base' });
constructor(
ipAddress: string,
@@ -108,48 +108,60 @@ export abstract class BaseComputerInterface {
throw new Error(`Interface not ready after ${timeout} seconds`);
}
/**
* Authenticate with the WebSocket server.
* This should be called immediately after the WebSocket connection is established.
*/
private async authenticate(): Promise<void> {
if (!this.apiKey || !this.vmName) {
// No authentication needed
return;
}
this.logger.info('Performing authentication handshake...');
const authMessage = {
command: 'authenticate',
params: {
api_key: this.apiKey,
container_name: this.vmName,
},
};
return new Promise<void>((resolve, reject) => {
const authHandler = (data: WebSocket.RawData) => {
try {
const authResult = JSON.parse(data.toString());
if (!authResult.success) {
const errorMsg = authResult.error || 'Authentication failed';
this.logger.error(`Authentication failed: ${errorMsg}`);
this.ws.close();
reject(new Error(`Authentication failed: ${errorMsg}`));
} else {
this.logger.info('Authentication successful');
this.ws.off('message', authHandler);
resolve();
}
} catch (error) {
this.ws.off('message', authHandler);
reject(error);
}
};
this.ws.on('message', authHandler);
this.ws.send(JSON.stringify(authMessage));
});
}
/**
* Connect to the WebSocket server.
*/
public async connect(): Promise<void> {
// If the WebSocket is already open, check if we need to authenticate
if (this.ws.readyState === WebSocket.OPEN) {
// send authentication message if needed
if (this.apiKey && this.vmName) {
this.logger.info('Performing authentication handshake...');
const authMessage = {
command: 'authenticate',
params: {
api_key: this.apiKey,
container_name: this.vmName,
},
};
return new Promise<void>((resolve, reject) => {
const authHandler = (data: WebSocket.RawData) => {
try {
const authResult = JSON.parse(data.toString());
if (!authResult.success) {
const errorMsg = authResult.error || 'Authentication failed';
this.logger.error(`Authentication failed: ${errorMsg}`);
this.ws.close();
reject(new Error(`Authentication failed: ${errorMsg}`));
} else {
this.logger.info('Authentication successful');
this.ws.off('message', authHandler);
resolve();
}
} catch (error) {
this.ws.off('message', authHandler);
reject(error);
}
};
this.ws.on('message', authHandler);
this.ws.send(JSON.stringify(authMessage));
});
}
return;
this.logger.info(
'Websocket is open, ensuring authentication is complete.'
);
return this.authenticate();
}
// If the WebSocket is closed or closing, reinitialize it
@@ -157,18 +169,31 @@ export abstract class BaseComputerInterface {
this.ws.readyState === WebSocket.CLOSED ||
this.ws.readyState === WebSocket.CLOSING
) {
this.logger.info('Websocket is closed. Reinitializing connection.');
const headers: { [key: string]: string } = {};
if (this.apiKey && this.vmName) {
headers['X-API-Key'] = this.apiKey;
headers['X-VM-Name'] = this.vmName;
}
this.ws = new WebSocket(this.wsUri, { headers });
return this.authenticate();
}
// Connect and authenticate
return new Promise((resolve, reject) => {
// If already connecting, wait for it to complete
const onOpen = async () => {
try {
// Always authenticate immediately after connection
await this.authenticate();
resolve();
} catch (error) {
reject(error);
}
};
// If already connecting, wait for it to complete then authenticate
if (this.ws.readyState === WebSocket.CONNECTING) {
this.ws.addEventListener('open', () => resolve(), { once: true });
this.ws.addEventListener('open', onOpen, { once: true });
this.ws.addEventListener('error', (error) => reject(error), {
once: true,
});
@@ -176,9 +201,7 @@ export abstract class BaseComputerInterface {
}
// Set up event handlers
this.ws.on('open', () => {
resolve();
});
this.ws.on('open', onOpen);
this.ws.on('error', (error: Error) => {
reject(error);
@@ -8,7 +8,6 @@ import * as path from 'node:path';
import { pino } from 'pino';
import { PostHog } from 'posthog-node';
import { v4 as uuidv4 } from 'uuid';
const logger = pino({ name: 'core.telemetry' });
// Controls how frequently telemetry will be sent (percentage)
export const TELEMETRY_SAMPLE_RATE = 100; // 100% sampling rate
@@ -37,6 +36,8 @@ export class PostHogTelemetryClient {
private posthogClient?: PostHog;
private counters: Record<string, number> = {};
private logger = pino({ name: 'core.telemetry' });
constructor() {
// set up config
this.config = {
@@ -63,11 +64,13 @@ export class PostHogTelemetryClient {
// Log telemetry status on startup
if (this.config.enabled) {
logger.info(`Telemetry enabled (sampling at ${this.config.sampleRate}%)`);
this.logger.info(
`Telemetry enabled (sampling at ${this.config.sampleRate}%)`
);
// Initialize PostHog client if config is available
this._initializePosthog();
} else {
logger.info('Telemetry disabled');
this.logger.info('Telemetry disabled');
}
}
@@ -84,7 +87,7 @@ export class PostHogTelemetryClient {
return fs.readFileSync(idFile, 'utf-8').trim();
}
} catch (error) {
logger.debug(`Failed to read installation ID: ${error}`);
this.logger.debug(`Failed to read installation ID: ${error}`);
}
// Create new ID if not exists
@@ -97,7 +100,7 @@ export class PostHogTelemetryClient {
fs.writeFileSync(idFile, newId);
return newId;
} catch (error) {
logger.debug(`Failed to write installation ID: ${error}`);
this.logger.debug(`Failed to write installation ID: ${error}`);
}
// Fallback to in-memory ID if file operations fail
@@ -119,13 +122,13 @@ export class PostHogTelemetryClient {
flushInterval: 30000, // Send events every 30 seconds
});
this.initialized = true;
logger.debug('PostHog client initialized successfully');
this.logger.debug('PostHog client initialized successfully');
// Process any queued events
this._processQueuedEvents();
return true;
} catch (error) {
logger.error(`Failed to initialize PostHog client: ${error}`);
this.logger.error(`Failed to initialize PostHog client: ${error}`);
return false;
}
}
@@ -171,7 +174,7 @@ export class PostHogTelemetryClient {
properties: eventProperties,
});
} catch (error) {
logger.debug(`Failed to capture event: ${error}`);
this.logger.debug(`Failed to capture event: ${error}`);
}
}
@@ -257,13 +260,13 @@ export class PostHogTelemetryClient {
}
await this.posthogClient.flush();
logger.debug('Telemetry flushed successfully');
this.logger.debug('Telemetry flushed successfully');
// Clear counters after sending
this.counters = {};
return true;
} catch (error) {
logger.debug(`Failed to flush telemetry: ${error}`);
this.logger.debug(`Failed to flush telemetry: ${error}`);
return false;
}
}
@@ -273,7 +276,7 @@ export class PostHogTelemetryClient {
* Enable telemetry collection.
*/
this.config.enabled = true;
logger.info('Telemetry enabled');
this.logger.info('Telemetry enabled');
if (!this.initialized) {
this._initializePosthog();
}
@@ -285,7 +288,7 @@ export class PostHogTelemetryClient {
*/
this.config.enabled = false;
await this.posthogClient?.disable();
logger.info('Telemetry disabled');
this.logger.info('Telemetry disabled');
}
get enabled(): boolean {
+1 -1
View File
@@ -16,7 +16,7 @@
"test": "pnpm -r test",
"typecheck": "pnpm -r typecheck"
},
"packageManager": "pnpm@10.6.5",
"packageManager": "pnpm@10.12.3",
"devDependencies": {
"@biomejs/biome": "^1.9.4"
},
+54
View File
@@ -95,6 +95,44 @@ importers:
specifier: ^3.1.3
version: 3.2.4(@types/node@22.15.33)(happy-dom@17.6.3)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0)
examples/byo-operator-ts:
dependencies:
'@cua/computer':
specifier: link:../../computer
version: link:../../computer
openai:
specifier: ^5.7.0
version: 5.7.0(ws@8.18.2)
devDependencies:
tsx:
specifier: ^4.20.3
version: 4.20.3
typescript:
specifier: ^5.8.3
version: 5.8.3
examples/cua-openai:
dependencies:
'@cua/computer':
specifier: link:../../computer
version: link:../../computer
dotenv:
specifier: ^16.5.0
version: 16.5.0
openai:
specifier: ^5.7.0
version: 5.7.0(ws@8.18.2)
devDependencies:
'@types/node':
specifier: ^22.15.33
version: 22.15.33
tsx:
specifier: ^4.20.3
version: 4.20.3
typescript:
specifier: ^5.8.3
version: 5.8.3
packages:
'@babel/generator@7.27.5':
@@ -769,6 +807,18 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
openai@5.7.0:
resolution: {integrity: sha512-zXWawZl6J/P5Wz57/nKzVT3kJQZvogfuyuNVCdEp4/XU2UNrjL7SsuNpWAyLZbo6HVymwmnfno9toVzBhelygA==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
peerDependenciesMeta:
ws:
optional: true
zod:
optional: true
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
@@ -1601,6 +1651,10 @@ snapshots:
on-exit-leak-free@2.1.2: {}
openai@5.7.0(ws@8.18.2):
optionalDependencies:
ws: 8.18.2
package-manager-detector@1.3.0: {}
pathe@2.0.3: {}