mirror of
https://github.com/trycua/computer.git
synced 2026-04-30 20:19:31 -05:00
Update logging rules, authentication process for computer, update pnpm
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+54
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user