You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

deepclause-agentvm

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

deepclause-agentvm - npm Package Compare versions

Comparing version
0.0.2
to
0.0.3
+229
bin/cli.js
#!/usr/bin/env node
const { AgentVM } = require('../src/index.js');
const path = require('node:path');
function printUsage() {
console.log(`
AgentVM CLI - Interactive terminal into the VM
Usage: agentvm [options]
Options:
--network, -n Enable networking (default: enabled)
--no-network Disable networking
--mount, -m <path> Mount a host directory into the VM at /mnt/host
Can specify VM path with: --mount /host/path:/vm/path
--debug, -d Enable debug logging
--help, -h Show this help message
Examples:
agentvm # Start VM with networking enabled
agentvm --no-network # Start VM without networking
agentvm -m /home/user/project # Mount directory at /mnt/host
agentvm -m /tmp/data:/data # Mount /tmp/data at /data in VM
agentvm -n -m ./mydir # Network on, mount ./mydir
`);
}
function parseArgs(args) {
const options = {
network: true,
mounts: {},
debug: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--help':
case '-h':
printUsage();
process.exit(0);
break;
case '--network':
case '-n':
options.network = true;
break;
case '--no-network':
options.network = false;
break;
case '--mount':
case '-m':
const mountArg = args[++i];
if (!mountArg) {
console.error('Error: --mount requires a path argument');
process.exit(1);
}
// Support both "/host/path" and "/host/path:/vm/path" formats
if (mountArg.includes(':') && !mountArg.startsWith('/') || mountArg.split(':').length > 2) {
// Handle Windows-style paths or explicit VM path
const lastColon = mountArg.lastIndexOf(':');
if (lastColon > 0 && mountArg[lastColon - 1] !== '\\') {
const hostPath = mountArg.substring(0, lastColon);
const vmPath = mountArg.substring(lastColon + 1);
options.mounts[vmPath] = path.resolve(hostPath);
} else {
options.mounts['/mnt/host'] = path.resolve(mountArg);
}
} else if (mountArg.includes(':')) {
const [hostPath, vmPath] = mountArg.split(':');
options.mounts[vmPath] = path.resolve(hostPath);
} else {
options.mounts['/mnt/host'] = path.resolve(mountArg);
}
break;
case '--debug':
case '-d':
options.debug = true;
break;
default:
if (arg.startsWith('-')) {
console.error(`Unknown option: ${arg}`);
printUsage();
process.exit(1);
}
break;
}
}
return options;
}
async function main() {
const args = process.argv.slice(2);
const options = parseArgs(args);
// Print startup info to stderr so it doesn't interfere with VM output
process.stderr.write('AgentVM Terminal\n');
process.stderr.write('================\n');
process.stderr.write(`Network: ${options.network ? 'enabled' : 'disabled'}\n`);
if (Object.keys(options.mounts).length > 0) {
process.stderr.write('Mounts:\n');
for (const [vmPath, hostPath] of Object.entries(options.mounts)) {
process.stderr.write(` ${hostPath} -> ${vmPath}\n`);
}
}
process.stderr.write('\n');
const vm = new AgentVM({
network: options.network,
mounts: options.mounts,
debug: options.debug,
interactive: true, // Use interactive/raw mode
});
// Connect VM stdout/stderr directly to process stdout/stderr
vm.onStdout = (data) => {
process.stdout.write(data);
};
vm.onStderr = (data) => {
process.stderr.write(data);
};
// Handle VM exit (e.g., user types 'exit' in shell)
vm.onExit = async () => {
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.exit(0);
};
process.stderr.write('Booting VM...\n');
try {
await vm.start();
} catch (err) {
process.stderr.write(`Failed to start VM: ${err.message}\n`);
process.exit(1);
}
process.stderr.write('VM Ready. Press Ctrl+D to exit.\n\n');
// Put stdin in raw mode for true terminal experience
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
// Pipe stdin directly to VM
process.stdin.on('data', async (data) => {
// Check for Ctrl+D (EOF) - exit cleanly
if (data.length === 1 && data[0] === 0x04) {
process.stderr.write('\n\nShutting down VM...\n');
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
await vm.stop();
process.exit(0);
}
try {
await vm.writeToStdin(data);
} catch (err) {
if (options.debug) {
process.stderr.write(`Error writing to VM: ${err.message}\n`);
}
}
});
// Handle stdin close
process.stdin.on('end', async () => {
process.stderr.write('\n\nShutting down VM...\n');
await vm.stop();
process.exit(0);
});
// Handle Ctrl+C - pass it through to VM unless pressed twice
let ctrlCCount = 0;
let ctrlCTimer = null;
process.on('SIGINT', async () => {
ctrlCCount++;
if (ctrlCCount >= 2) {
process.stderr.write('\n\nForce shutting down VM...\n');
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
await vm.stop();
process.exit(0);
}
// Send Ctrl+C to VM
try {
await vm.writeToStdin('\x03');
} catch (err) {
// Ignore
}
// Reset counter after a delay
clearTimeout(ctrlCTimer);
ctrlCTimer = setTimeout(() => {
ctrlCCount = 0;
}, 500);
});
// Handle process termination
process.on('SIGTERM', async () => {
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
await vm.stop();
process.exit(0);
});
}
main().catch((err) => {
process.stderr.write(`Fatal error: ${err}\n`);
process.exit(1);
});
+17
-2
{
"name": "deepclause-agentvm",
"version": "0.0.2",
"version": "0.0.3",
"main": "src/index.js",
"bin": {
"agentvm": "./bin/cli.js"
},
"scripts": {
"test": "node test/basic.test.js && node test/mount.test.js && node test/network/http.test.js"
},
"keywords": ["vm", "wasm", "sandbox", "agent", "linux", "alpine", "container", "wasi"],
"keywords": [
"vm",
"wasm",
"sandbox",
"agent",
"linux",
"alpine",
"container",
"wasi"
],
"author": "DeepClause",

@@ -18,2 +30,3 @@ "license": "MIT",

"src/",
"bin/",
"agentvm-alpine-python.wasm",

@@ -27,5 +40,7 @@ "README.md"

"@ai-sdk/openai": "^3.0.12",
"0x": "^6.0.0",
"ai": "^6.0.39",
"clinic": "^13.0.0",
"dotenv": "^17.2.3"
}
}

@@ -19,2 +19,5 @@ const { Worker, MessageChannel, SHARE_ENV } = require('node:worker_threads');

* @param {string} [options.mac] - MAC address for the VM (default: 02:00:00:00:00:01).
* @param {number} [options.networkRateLimit] - Network rate limit in bytes/sec (default: 256KB/s). Set to 0 for unlimited.
* @param {boolean} [options.debug] - Enable debug logging.
* @param {boolean} [options.interactive] - Interactive/raw mode - skip shell setup for direct terminal access.
*/

@@ -26,2 +29,6 @@ constructor(options = {}) {

this.mac = options.mac || '02:00:00:00:00:01';
this.debug = options.debug || false;
this.interactive = options.interactive || false;
// Rate limit: 256KB/s default to avoid overwhelming VM filesystem writes
this.networkRateLimit = options.networkRateLimit !== undefined ? options.networkRateLimit : 1024 * 1024 * 1024;
this.sharedBuffer = new SharedArrayBuffer(SHARED_BUFFER_SIZE);

@@ -36,5 +43,10 @@ this.inputInt32 = new Int32Array(this.sharedBuffer);

// Callbacks for raw/interactive mode
this.onStdout = null;
this.onStderr = null;
this.onExit = null;
// NAT: Main thread handles sockets, worker polls for responses via MessageChannel
this.udpSessions = new Map(); // key -> { socket, lastActive }
this.tcpSessions = new Map(); // key -> { socket, state, ... }
this.tcpSessions = new Map(); // key -> { socket, state, bytesThisSecond, lastReset, paused, pendingData }
this.netChannel = null;

@@ -83,3 +95,10 @@ }

this.isReady = true;
// Run setup commands
// In interactive mode, skip shell setup and resolve immediately
if (this.interactive) {
resolve();
return;
}
// Run setup commands for exec() mode
this.exec("stty -echo; export PS1=''").then(async () => {

@@ -104,7 +123,12 @@ // Auto-setup network if enabled

} else if (msg.type === 'debug') {
// console.log('[Worker Debug]', msg.msg);
// Worker debug messages (disabled by default for performance)
// if (this.debug) console.log('[Worker]', msg.msg);
} else if (msg.type === 'exit') {
if (!this.destroyed) {
if (!this.destroyed && !this.interactive) {
console.error('VM Exited unexpectedly:', msg.error);
}
// In interactive mode, trigger onExit callback
if (this.interactive && this.onExit) {
this.onExit(msg.error);
}
}

@@ -240,5 +264,17 @@ });

const socket = new net.Socket();
const session = { socket, srcIP, srcPort, dstIP, dstPort };
const session = {
socket, srcIP, srcPort, dstIP, dstPort,
// Rate limiting state
bytesThisSecond: 0,
lastReset: Date.now(),
rateLimitPaused: false,
flowControlPaused: false, // Track flow control state
pendingResume: null
};
this.tcpSessions.set(key, session);
if (this.debug) {
console.log(`[TCP] Connecting to ${connectIP}:${dstPort}, key=${key}`);
}
socket.connect(dstPort, connectIP, () => {

@@ -254,9 +290,63 @@ if (!this.netChannel) return;

if (!this.netChannel) return;
// Rate limiting: track bytes per second
if (this.networkRateLimit > 0) {
const now = Date.now();
// Reset counter every second
if (now - session.lastReset >= 1000) {
session.bytesThisSecond = 0;
session.lastReset = now;
}
session.bytesThisSecond += data.length;
// If we've exceeded rate limit, pause the socket
if (session.bytesThisSecond >= this.networkRateLimit && !session.rateLimitPaused) {
session.rateLimitPaused = true;
socket.pause();
if (this.debug) {
console.log(`[RateLimit] Pausing ${key}, sent ${session.bytesThisSecond} bytes this second`);
}
// Schedule resume at start of next second
const timeUntilNextSecond = 1000 - (now - session.lastReset);
session.pendingResume = setTimeout(() => {
// Always clear the rate limit pause flag and reset counters
session.rateLimitPaused = false;
session.bytesThisSecond = 0;
session.lastReset = Date.now();
session.pendingResume = null;
// Only actually resume if flow control also allows it
if (!session.flowControlPaused) {
socket.resume();
if (this.debug) {
console.log(`[RateLimit] Resuming ${key}`);
}
} else if (this.debug) {
console.log(`[RateLimit] Rate limit cleared for ${key}, but flow control still paused`);
}
}, timeUntilNextSecond);
}
}
// Use transferable Uint8Array for efficiency with large data
const uint8 = new Uint8Array(data.buffer, data.byteOffset, data.length);
this.netChannel.port2.postMessage({
type: 'tcp-data',
key,
data: uint8
}, [uint8.buffer]);
// NOTE: We must copy the data because socket buffers may share ArrayBuffer
try {
const copy = new Uint8Array(data.length);
copy.set(data);
this.netChannel.port2.postMessage({
type: 'tcp-data',
key,
data: copy
}, [copy.buffer]);
} catch (e) {
console.error('[TCP] Failed to post data:', e.message);
// Fallback: send without transfer
this.netChannel.port2.postMessage({
type: 'tcp-data',
key,
data: new Uint8Array(data)
});
}
});

@@ -273,2 +363,7 @@

socket.on('close', () => {
// Clean up rate limit timer
if (session.pendingResume) {
clearTimeout(session.pendingResume);
session.pendingResume = null;
}
if (this.netChannel) {

@@ -284,2 +379,7 @@ this.netChannel.port2.postMessage({

socket.on('error', (err) => {
// Clean up rate limit timer
if (session.pendingResume) {
clearTimeout(session.pendingResume);
session.pendingResume = null;
}
if (this.netChannel) {

@@ -332,2 +432,3 @@ this.netChannel.port2.postMessage({

if (session && session.socket) {
session.flowControlPaused = true;
session.socket.pause();

@@ -345,3 +446,7 @@ }

if (session && session.socket) {
session.socket.resume();
session.flowControlPaused = false;
// Only resume if not also rate-limit paused
if (!session.rateLimitPaused) {
session.socket.resume();
}
}

@@ -409,2 +514,12 @@ }

// In interactive mode, call callbacks directly
if (this.interactive) {
if (type === 'stdout' && this.onStdout) {
this.onStdout(dataUint8);
} else if (type === 'stderr' && this.onStderr) {
this.onStderr(dataUint8);
}
return;
}
if (!this.pendingCommand) return;

@@ -411,0 +526,0 @@

+0
-2

@@ -147,4 +147,2 @@ const EventEmitter = require('events');

// MTU is 1500, IP header is 20, TCP header is 20

@@ -151,0 +149,0 @@ // Maximum Segment Size (MSS) = 1500 - 20 - 20 = 1460

@@ -680,2 +680,5 @@ const { parentPort, workerData, receiveMessageOnPort } = require('node:worker_threads');

// Poll for network responses before reading
netStack.pollNetResponses();
const data = netStack.readFromNetwork(4096);

@@ -799,4 +802,9 @@ if (!data || data.length === 0) {

// IMPORTANT: Always poll for network responses first!
// This ensures TCP data from main thread is received even when
// we're polling for both read and write (common during TLS handshake).
netStack.pollNetResponses();
// 2. Check Immediate Status
const netReadable = netStack.hasPendingData() || netStack.hasReceivedFin();
const netReadable = sockRecvBuffer.length > 0 || netStack.hasPendingData() || netStack.hasReceivedFin();
const netWritable = true; // Always writable

@@ -861,3 +869,3 @@ const stdinReadable = localBuffer.length > 0 || Atomics.load(inputInt32, INPUT_FLAG_INDEX) !== 0;

const postStdinReadable = localBuffer.length > 0 || Atomics.load(inputInt32, INPUT_FLAG_INDEX) !== 0;
const postNetReadable = netStack.hasPendingData() || netStack.hasReceivedFin();
const postNetReadable = sockRecvBuffer.length > 0 || netStack.hasPendingData() || netStack.hasReceivedFin();

@@ -886,3 +894,3 @@ for(let i=0; i<nsubscriptions; i++) {

evType = 1;
nbytes = netStack.txBuffer.length;
nbytes = sockRecvBuffer.length + netStack.txBuffer.length;
// // parentPort.postMessage({ type: 'debug', msg: `poll: conn fd readable, ${nbytes} bytes` });

@@ -925,2 +933,5 @@ }

sock_accept: (fd, flags, result_fd_ptr) => {
// Poll for network responses first - TCP-connected messages may be pending
netStack.pollNetResponses();
if (fd !== NET_FD) {

@@ -946,2 +957,5 @@ // parentPort.postMessage({ type: 'debug', msg: `sock_accept(${fd}) - wrong fd` });

sock_recv: (fd, ri_data_ptr, ri_data_len, ri_flags, ro_datalen_ptr, ro_flags_ptr) => {
// Poll for network responses first
netStack.pollNetResponses();
parentPort.postMessage({ type: 'debug', msg: `sock_recv(${fd}) called, buffered=${sockRecvBuffer.length}, pending=${netStack.hasPendingData()}, fin=${netStack.hasReceivedFin()}` });

@@ -948,0 +962,0 @@

Sorry, the diff of this file is not supported yet