deepclause-agentvm
Advanced tools
+1
-1
| { | ||
| "name": "deepclause-agentvm", | ||
| "version": "0.0.3", | ||
| "version": "0.0.4", | ||
| "main": "src/index.js", | ||
@@ -5,0 +5,0 @@ "bin": { |
+2
-0
@@ -16,2 +16,4 @@ # DeepClause - AgentVM | ||
| Latest version on npm: 0.0.3 | ||
| ## Installation | ||
@@ -18,0 +20,0 @@ |
+1
-1
@@ -31,3 +31,3 @@ const { Worker, MessageChannel, SHARE_ENV } = require('node:worker_threads'); | ||
| // Rate limit: 256KB/s default to avoid overwhelming VM filesystem writes | ||
| this.networkRateLimit = options.networkRateLimit !== undefined ? options.networkRateLimit : 1024 * 1024 * 1024; | ||
| this.networkRateLimit = options.networkRateLimit !== undefined ? options.networkRateLimit : 256 * 1024; | ||
| this.sharedBuffer = new SharedArrayBuffer(SHARED_BUFFER_SIZE); | ||
@@ -34,0 +34,0 @@ this.inputInt32 = new Int32Array(this.sharedBuffer); |
+239
-19
@@ -120,9 +120,26 @@ const { parentPort, workerData, receiveMessageOnPort } = require('node:worker_threads'); | ||
| // Check if this is a preopened directory | ||
| const preopenInfo = fdToHostPath.get(actualFd); | ||
| // Check if this is a preopened directory OR one of our custom directory handles | ||
| let preopenInfo = fdToHostPath.get(actualFd); | ||
| // Also check if fd is one of our custom directory handles | ||
| if (!preopenInfo && customFdHandles.has(fd)) { | ||
| const handle = customFdHandles.get(fd); | ||
| if (handle.type === 'directory') { | ||
| preopenInfo = { hostPath: handle.hostPath, wasiPath: handle.wasiPath }; | ||
| } | ||
| } | ||
| // Fix: Node.js WASI doesn't handle path_open(fd, ".") properly for preopens | ||
| if (pathStr === '.' && (oflags & 0x2) !== 0 && preopenInfo) { | ||
| const fakeFd = nextFakeFd++; | ||
| fakeFdMap.set(fakeFd, actualFd); | ||
| // For custom directory handles, map to a new custom handle instead of fakeFdMap | ||
| if (customFdHandles.has(fd)) { | ||
| customFdHandles.set(fakeFd, { | ||
| type: 'directory', | ||
| hostPath: preopenInfo.hostPath, | ||
| wasiPath: preopenInfo.wasiPath | ||
| }); | ||
| } else { | ||
| fakeFdMap.set(fakeFd, actualFd); | ||
| } | ||
| if (instance) { | ||
@@ -141,6 +158,5 @@ const view = new DataView(instance.exports.memory.buffer); | ||
| // For files inside preopened directories, implement our own file opening | ||
| // because Node.js WASI has bugs with 64-bit rights validation | ||
| if (preopenInfo && pathStr !== '.' && (oflags & 0x2) === 0) { | ||
| // This is trying to open a file (not a directory) inside a preopen | ||
| // For files OR subdirectories inside preopened directories, implement our own handling | ||
| // because Node.js WASI has bugs with 64-bit rights validation and subdirectory access | ||
| if (preopenInfo && pathStr !== '.') { | ||
| const hostFilePath = require('path').join(preopenInfo.hostPath, pathStr); | ||
@@ -150,2 +166,47 @@ | ||
| const O_CREAT = 1, O_DIRECTORY = 2, O_EXCL = 4, O_TRUNC = 8; | ||
| // Handle opening subdirectories | ||
| if (oflags & O_DIRECTORY) { | ||
| try { | ||
| const stat = fs.statSync(hostFilePath); | ||
| if (!stat.isDirectory()) { | ||
| return 54; // WASI_ERRNO_NOTDIR | ||
| } | ||
| // Create a fake fd that maps to this subdirectory path | ||
| const newFd = nextFakeFd++; | ||
| // Store subdirectory info so future path_open calls relative to this fd work | ||
| customFdHandles.set(newFd, { | ||
| type: 'directory', | ||
| hostPath: hostFilePath, | ||
| wasiPath: require('path').join(preopenInfo.wasiPath, pathStr) | ||
| }); | ||
| if (instance) { | ||
| const view = new DataView(instance.exports.memory.buffer); | ||
| view.setUint32(opened_fd_ptr, newFd, true); | ||
| } | ||
| if (process.env.DEBUG_WASI_PATH === '1') { | ||
| parentPort.postMessage({ | ||
| type: 'debug', | ||
| msg: `WASI path_open CUSTOM DIR: fd=${fd}, path="${pathStr}" => 0 (custom fd ${newFd})` | ||
| }); | ||
| } | ||
| return 0; | ||
| } catch (err) { | ||
| if (process.env.DEBUG_WASI_PATH === '1') { | ||
| parentPort.postMessage({ | ||
| type: 'debug', | ||
| msg: `WASI path_open CUSTOM DIR FAIL: fd=${fd}, path="${pathStr}" => ${err.message}` | ||
| }); | ||
| } | ||
| if (err.code === 'ENOENT') return 44; // WASI_ERRNO_NOENT | ||
| if (err.code === 'EACCES') return 2; // WASI_ERRNO_ACCES | ||
| if (err.code === 'ENOTDIR') return 54; // WASI_ERRNO_NOTDIR | ||
| return 28; // WASI_ERRNO_INVAL | ||
| } | ||
| } | ||
| // This is trying to open a file (not a directory) inside a preopen | ||
| // WASI fdflags | ||
@@ -495,5 +556,8 @@ const FDFLAG_APPEND = 1, FDFLAG_DSYNC = 2, FDFLAG_NONBLOCK = 4, FDFLAG_RSYNC = 8, FDFLAG_SYNC = 16; | ||
| const handle = customFdHandles.get(fd); | ||
| try { | ||
| fs.closeSync(handle.nodeFd); | ||
| } catch (err) { /* ignore */ } | ||
| // Only close if it's a file handle with a nodeFd (directories don't have one) | ||
| if (handle.type === 'file' && handle.nodeFd !== undefined) { | ||
| try { | ||
| fs.closeSync(handle.nodeFd); | ||
| } catch (err) { /* ignore */ } | ||
| } | ||
| customFdHandles.delete(fd); | ||
@@ -529,4 +593,94 @@ return 0; | ||
| // Wrap fd operations to handle our fake directory fds (not fd_read, we handle that above) | ||
| wasiImport.fd_readdir = wrapFdOp('fd_readdir', wasiImport.fd_readdir); | ||
| // Custom fd_readdir for our directory handles | ||
| const origFdReaddir = wasiImport.fd_readdir; | ||
| wasiImport.fd_readdir = (fd, buf_ptr, buf_len, cookie, bufused_ptr) => { | ||
| const hasIt = customFdHandles.has(fd); | ||
| // Check if this is one of our custom directory handles | ||
| if (hasIt) { | ||
| const handle = customFdHandles.get(fd); | ||
| if (!handle || handle.type !== 'directory') { | ||
| return handle ? 54 : 8; // WASI_ERRNO_NOTDIR or WASI_ERRNO_BADF | ||
| } | ||
| if (!instance) { | ||
| return 8; // WASI_ERRNO_BADF | ||
| } | ||
| const view = new DataView(instance.exports.memory.buffer); | ||
| const mem = new Uint8Array(instance.exports.memory.buffer); | ||
| try { | ||
| const entries = fs.readdirSync(handle.hostPath, { withFileTypes: true }); | ||
| const startIdx = typeof cookie === 'bigint' ? Number(cookie) : cookie; | ||
| let offset = 0; | ||
| for (let i = startIdx; i < entries.length; i++) { | ||
| const entry = entries[i]; | ||
| const name = entry.name; | ||
| const nameBytes = new TextEncoder().encode(name); | ||
| // WASI dirent structure: | ||
| // d_next: u64 (8 bytes) at offset 0 | ||
| // d_ino: u64 (8 bytes) at offset 8 | ||
| // d_namlen: u32 (4 bytes) at offset 16 | ||
| // d_type: u8 (1 byte) at offset 20 | ||
| // padding: 3 bytes at offset 21-23 | ||
| // name: starts at offset 24 | ||
| const DIRENT_HEADER_SIZE = 24; | ||
| const direntSize = DIRENT_HEADER_SIZE + nameBytes.length; | ||
| if (offset + direntSize > buf_len) { | ||
| break; | ||
| } | ||
| const base = buf_ptr + offset; | ||
| // d_next is the cookie for the next fd_readdir call (entry index + 1) | ||
| view.setBigUint64(base, BigInt(i + 1), true); | ||
| // d_ino - compute a hash of the path as inode | ||
| const fullPath = require('path').join(handle.hostPath, name); | ||
| let ino = 0; | ||
| for (let j = 0; j < fullPath.length; j++) { | ||
| ino = ((ino << 5) - ino + fullPath.charCodeAt(j)) | 0; | ||
| } | ||
| view.setBigUint64(base + 8, BigInt(Math.abs(ino)), true); | ||
| // d_namlen | ||
| view.setUint32(base + 16, nameBytes.length, true); | ||
| // d_type: 0=unknown, 3=dir, 4=file, 7=symlink | ||
| let dtype = 0; | ||
| if (entry.isFile()) dtype = 4; | ||
| else if (entry.isDirectory()) dtype = 3; | ||
| else if (entry.isSymbolicLink()) dtype = 7; | ||
| view.setUint8(base + 20, dtype); | ||
| // 3 bytes padding at offset 21-23 | ||
| view.setUint8(base + 21, 0); | ||
| view.setUint8(base + 22, 0); | ||
| view.setUint8(base + 23, 0); | ||
| // Name starts at offset 24 | ||
| mem.set(nameBytes, base + 24); | ||
| offset += direntSize; | ||
| } | ||
| view.setUint32(bufused_ptr, offset, true); | ||
| return 0; | ||
| } catch (err) { | ||
| if (err.code === 'ENOENT') return 44; // WASI_ERRNO_NOENT | ||
| if (err.code === 'EACCES') return 2; // WASI_ERRNO_ACCES | ||
| return 29; // WASI_ERRNO_IO | ||
| } | ||
| } | ||
| if (fakeFdMap.has(fd)) { | ||
| return origFdReaddir(fakeFdMap.get(fd), buf_ptr, buf_len, cookie, bufused_ptr); | ||
| } | ||
| console.error(`[fd_readdir] FALLBACK`); | ||
| return origFdReaddir(fd, buf_ptr, buf_len, cookie, bufused_ptr); | ||
| }; | ||
@@ -544,7 +698,8 @@ // Wrap fd_fdstat_get for custom file handles and debugging | ||
| // filetype: 4 = REGULAR_FILE, 3 = DIRECTORY | ||
| view.setUint8(fdstat_ptr, 4); // FILETYPE_REGULAR_FILE | ||
| const fileType = handle.type === 'directory' ? 3 : 4; | ||
| view.setUint8(fdstat_ptr, fileType); | ||
| view.setUint8(fdstat_ptr + 1, 0); // padding | ||
| view.setUint16(fdstat_ptr + 2, 0, true); // fs_flags | ||
| view.setUint32(fdstat_ptr + 4, 0, true); // padding | ||
| // rights_base (full rights for file) | ||
| // rights_base (full rights) | ||
| view.setUint32(fdstat_ptr + 8, 0x1FFFFFFF, true); // low 32 bits | ||
@@ -583,4 +738,65 @@ view.setUint32(fdstat_ptr + 12, 0, true); // high 32 bits | ||
| wasiImport.fd_filestat_get = wrapFdOp('fd_filestat_get', wasiImport.fd_filestat_get); | ||
| wasiImport.path_filestat_get = wrapFdOp('path_filestat_get', wasiImport.path_filestat_get); | ||
| // Custom path_filestat_get for our directory handles | ||
| const origPathFilestatGet = wasiImport.path_filestat_get; | ||
| wasiImport.path_filestat_get = (fd, flags, path_ptr, path_len, filestat_ptr) => { | ||
| // Check if fd is one of our custom directory handles | ||
| if (customFdHandles.has(fd)) { | ||
| const handle = customFdHandles.get(fd); | ||
| if (handle.type === 'directory' && instance) { | ||
| const view = new DataView(instance.exports.memory.buffer); | ||
| const mem = new Uint8Array(instance.exports.memory.buffer); | ||
| const pathBytes = mem.slice(path_ptr, path_ptr + path_len); | ||
| const pathStr = new TextDecoder().decode(pathBytes); | ||
| const fullPath = require('path').join(handle.hostPath, pathStr); | ||
| require('fs').writeSync(2, `[path_filestat_get] custom fd=${fd}, path="${pathStr}", fullPath="${fullPath}"\n`); | ||
| try { | ||
| const stat = fs.statSync(fullPath); | ||
| // WASI filestat structure (64 bytes total): | ||
| // 0: dev (u64) | ||
| // 8: ino (u64) | ||
| // 16: filetype (u8) | ||
| // 17-23: padding | ||
| // 24: nlink (u64) | ||
| // 32: size (u64) | ||
| // 40: atim (u64) | ||
| // 48: mtim (u64) | ||
| // 56: ctim (u64) | ||
| view.setBigUint64(filestat_ptr, BigInt(0), true); // dev | ||
| view.setBigUint64(filestat_ptr + 8, BigInt(stat.ino || 0), true); // ino | ||
| // filetype: 0=unknown, 1=block, 2=char, 3=dir, 4=file, 5=socket_dgram, 6=socket_stream, 7=symlink | ||
| let filetype = 0; | ||
| if (stat.isFile()) filetype = 4; | ||
| else if (stat.isDirectory()) filetype = 3; | ||
| else if (stat.isSymbolicLink()) filetype = 7; | ||
| view.setUint8(filestat_ptr + 16, filetype); | ||
| view.setBigUint64(filestat_ptr + 24, BigInt(stat.nlink || 1), true); // nlink | ||
| view.setBigUint64(filestat_ptr + 32, BigInt(stat.size), true); // size | ||
| view.setBigUint64(filestat_ptr + 40, BigInt(Math.floor(stat.atimeMs * 1000000)), true); // atim (ns) | ||
| view.setBigUint64(filestat_ptr + 48, BigInt(Math.floor(stat.mtimeMs * 1000000)), true); // mtim (ns) | ||
| view.setBigUint64(filestat_ptr + 56, BigInt(Math.floor(stat.ctimeMs * 1000000)), true); // ctim (ns) | ||
| return 0; | ||
| } catch (err) { | ||
| require('fs').writeSync(2, `[path_filestat_get] ERROR: ${err.message}\n`); | ||
| if (err.code === 'ENOENT') return 44; // WASI_ERRNO_NOENT | ||
| if (err.code === 'EACCES') return 2; // WASI_ERRNO_ACCES | ||
| return 29; // WASI_ERRNO_IO | ||
| } | ||
| } | ||
| } | ||
| // Fall back to wrapped original | ||
| if (fakeFdMap.has(fd)) { | ||
| return origPathFilestatGet(fakeFdMap.get(fd), flags, path_ptr, path_len, filestat_ptr); | ||
| } | ||
| return origPathFilestatGet(fd, flags, path_ptr, path_len, filestat_ptr); | ||
| }; | ||
| // Debug: trace path operations for mount debugging | ||
@@ -814,3 +1030,3 @@ const DEBUG_WASI_PATH = process.env.DEBUG_WASI_PATH === '1'; | ||
| // 2. Check Immediate Status | ||
| const netReadable = sockRecvBuffer.length > 0 || netStack.hasPendingData() || netStack.hasReceivedFin(); | ||
| const netReadable = netStack.hasPendingData() || netStack.hasReceivedFin(); | ||
| const netWritable = true; // Always writable | ||
@@ -875,3 +1091,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 = sockRecvBuffer.length > 0 || netStack.hasPendingData() || netStack.hasReceivedFin(); | ||
| const postNetReadable = netStack.hasPendingData() || netStack.hasReceivedFin(); | ||
@@ -900,3 +1116,3 @@ for(let i=0; i<nsubscriptions; i++) { | ||
| evType = 1; | ||
| nbytes = sockRecvBuffer.length + netStack.txBuffer.length; | ||
| nbytes = netStack.txBuffer.length; | ||
| // // parentPort.postMessage({ type: 'debug', msg: `poll: conn fd readable, ${nbytes} bytes` }); | ||
@@ -965,3 +1181,3 @@ } | ||
| parentPort.postMessage({ type: 'debug', msg: `sock_recv(${fd}) called, buffered=${sockRecvBuffer.length}, pending=${netStack.hasPendingData()}, fin=${netStack.hasReceivedFin()}` }); | ||
| parentPort.postMessage({ type: 'debug', msg: `sock_recv(${fd}) called, sockRecvBuf=${sockRecvBuffer.length}, txBuf=${netStack.txBuffer.length}, fin=${netStack.hasReceivedFin()}` }); | ||
@@ -979,2 +1195,3 @@ if (fd !== NET_CONN_FD) { | ||
| const data = netStack.readFromNetwork(4096); | ||
| parentPort.postMessage({ type: 'debug', msg: `sock_recv readFromNetwork returned ${data ? data.length : 'null'} bytes, txBuf now=${netStack.txBuffer.length}` }); | ||
| if (!data || data.length === 0) { | ||
@@ -1006,2 +1223,5 @@ // Check if FIN was received - if so, return EOF (0 bytes) instead of EAGAIN | ||
| // Debug: show how much we wrote vs how much we had | ||
| parentPort.postMessage({ type: 'debug', msg: `sock_recv wrote ${bytesWritten}/${sockRecvBuffer.length} bytes to ${ri_data_len} iovecs` }); | ||
| // Keep any unwritten data for the next call | ||
@@ -1008,0 +1228,0 @@ sockRecvBuffer = sockRecvBuffer.subarray(bytesWritten); |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
180849452
0.01%2460
8.23%155
1.31%36
28.57%