Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/bridge/bridge-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@
"_sqliteStatementFinalizeRaw",
"_kernelStdioWriteRaw",
"_kernelPollRaw",
"_kernelIsattyRaw",
"_kernelTtySizeRaw",
"_ptySetRawMode"
]
},
Expand Down
12 changes: 12 additions & 0 deletions crates/execution/assets/v8-bridge.source.js
Original file line number Diff line number Diff line change
Expand Up @@ -3943,6 +3943,16 @@ var __bridge = (() => {
classification: "hardened",
rationale: "Host kernel poll bridge reference for multi-fd readiness waits."
},
{
name: "_kernelIsattyRaw",
classification: "hardened",
rationale: "Host kernel TTY detection bridge reference for WASM terminal commands."
},
{
name: "_kernelTtySizeRaw",
classification: "hardened",
rationale: "Host kernel TTY size bridge reference for WASM terminal commands."
},
{
name: "_ptySetRawMode",
classification: "hardened",
Expand Down Expand Up @@ -6409,6 +6419,8 @@ var __bridge = (() => {
var _processResourceUsage = createBridgeSyncFacade("process.resourceUsage");
var _processVersions = createBridgeSyncFacade("process.versions");
var _kernelPollRaw = createBridgeSyncFacade("_kernelPollRaw");
var _kernelIsattyRaw = createBridgeSyncFacade("_kernelIsattyRaw");
var _kernelTtySizeRaw = createBridgeSyncFacade("_kernelTtySizeRaw");
function decodeBridgeJson(value) {
return typeof value === "string" ? JSON.parse(value) : value;
}
Expand Down
7 changes: 7 additions & 0 deletions crates/execution/src/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const V8_WALL_CLOCK_LIMIT_MS_ENV: &str = "AGENTOS_V8_WALL_CLOCK_LIMIT_MS";
const NODE_SYNC_RPC_DEFAULT_DATA_BYTES: usize = 4 * 1024 * 1024;
const NODE_SYNC_RPC_DEFAULT_WAIT_TIMEOUT_MS: u64 = 30_000;
const NODE_SYNC_RPC_RESPONSE_QUEUE_CAPACITY: usize = 1;
const FORWARD_KERNEL_STDIN_RPC_ENV: &str = "AGENTOS_FORWARD_KERNEL_STDIN_RPC";
// Defense-in-depth headroom: a transient burst of guest events (e.g. a chatty
// tool/skill turn) should be absorbed by the buffer, so the producer only ever
// hits backpressure under a genuinely stuck consumer rather than on every spike.
Expand Down Expand Up @@ -481,6 +482,7 @@ struct LocalBridgeState {
next_timer_id: u64,
timers: Arc<Mutex<HashMap<u64, LocalTimerEntry>>>,
kernel_stdin: Arc<LocalKernelStdinBridge>,
forward_kernel_stdin_rpc: bool,
v8_session: Option<V8SessionHandle>,
/// Optional read-only reader over the mounted `node_modules` VFS, supplied by
/// the sidecar. When present, the bridge thread resolves module-resolution
Expand Down Expand Up @@ -1888,6 +1890,10 @@ impl JavascriptExecutionEngine {
local_bridge.v8_session = Some(v8_session.clone());
local_bridge.module_reader = module_reader;
local_bridge.module_resolution = GuestModuleResolution::from_env(&request.env);
local_bridge.forward_kernel_stdin_rpc = request
.env
.get(FORWARD_KERNEL_STDIN_RPC_ENV)
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
let events = spawn_v8_event_bridge(
frame_receiver,
pending_sync_rpc.clone(),
Expand Down Expand Up @@ -3031,6 +3037,7 @@ impl LocalBridgeState {
bytes[15],
))))
}
"_kernelStdinRead" | "_kernelStdinReadRaw" if self.forward_kernel_stdin_rpc => None,
"_kernelStdinRead" | "_kernelStdinReadRaw" => Some(LocalBridgeCallResult::Immediate(
self.kernel_stdin.read(args),
)),
Expand Down
88 changes: 81 additions & 7 deletions crates/execution/src/node_import_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT_MS_ENV: &str =
"AGENTOS_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT_MS";
const NODE_IMPORT_CACHE_SCHEMA_VERSION: &str = "1";
const NODE_IMPORT_CACHE_LOADER_VERSION: &str = "8";
const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "71";
const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "78";
const NODE_IMPORT_CACHE_DIR_PREFIX: &str = "agentos-node-import-cache";
const DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT: Duration = Duration::from_secs(30);
const PYODIDE_DIST_DIR: &str = "pyodide-dist";
Expand Down Expand Up @@ -9228,17 +9228,24 @@ function decodeBase64ToUint8Array(value) {
return bytes;
}

function readKernelStdinChunk(maxBytes) {
function readKernelStdinChunk(maxBytes, timeoutMs = 10) {
const requestedLength = Math.max(1, Number(maxBytes) >>> 0);
const numericTimeoutMs = Number(timeoutMs);
const blocking = !Number.isFinite(numericTimeoutMs) || numericTimeoutMs >= 0xffffffff;
const deadline = blocking ? 0 : Date.now() + Math.max(0, numericTimeoutMs);
while (true) {
const response = callSyncRpc('__kernel_stdin_read', [requestedLength, 10]);
const waitMs = blocking ? 10 : Math.max(0, Math.min(10, deadline - Date.now()));
const response = callSyncRpc('__kernel_stdin_read', [requestedLength, waitMs]);
if (response && typeof response.dataBase64 === 'string') {
return Buffer.from(response.dataBase64, 'base64');
}
if (response && response.done === true) {
return null;
}
Atomics.wait(syntheticWaitArray, 0, 0, 10);
if (!blocking && Date.now() >= deadline) {
return Buffer.alloc(0);
}
Atomics.wait(syntheticWaitArray, 0, 0, blocking ? 10 : Math.max(0, Math.min(10, deadline - Date.now())));
}
}

Expand Down Expand Up @@ -12380,7 +12387,12 @@ const hostUserImport = {
},
isatty(fd, retBoolPtr) {
const descriptor = Number(fd) >>> 0;
const isTerminal = descriptor <= 2 ? 0 : 0;
let isTerminal = 0;
try {
isTerminal = callSyncRpc('__kernel_isatty', [descriptor]) === true ? 1 : 0;
} catch {
isTerminal = 0;
}
return writeGuestUint32(retBoolPtr, isTerminal);
},
getpwuid(uid, bufPtr, bufLen, retLenPtr) {
Expand All @@ -12393,6 +12405,67 @@ const hostUserImport = {
},
};

const hostTtyImport = {
isatty(fd) {
try {
const result = callSyncRpc('__kernel_isatty', [Number(fd) >>> 0]);
return result === true || result === 1 ? 1 : 0;
} catch (error) {
process?.stderr?.write?.(`WARN host_tty.isatty failed: ${error?.stack || error}\n`);
return 0;
}
},
get_size(fd, colsPtr, rowsPtr) {
try {
if (!(instanceMemory instanceof WebAssembly.Memory)) {
return WASI_ERRNO_FAULT;
}
const size = callSyncRpc('__kernel_tty_size', [Number(fd) >>> 0]);
if (!size || typeof size !== 'object') {
return WASI_ERRNO_FAULT;
}
const colsValue = Array.isArray(size) ? size[0] : size.cols;
const rowsValue = Array.isArray(size) ? size[1] : size.rows;
const cols = Math.max(0, Math.min(0xffff, Number(colsValue) >>> 0));
const rows = Math.max(0, Math.min(0xffff, Number(rowsValue) >>> 0));
const view = new DataView(instanceMemory.buffer);
view.setUint16(Number(colsPtr) >>> 0, cols, true);
view.setUint16(Number(rowsPtr) >>> 0, rows, true);
return WASI_ERRNO_SUCCESS;
} catch (error) {
process?.stderr?.write?.(`WARN host_tty.get_size failed: ${error?.stack || error}\n`);
return WASI_ERRNO_FAULT;
}
},
set_raw_mode(enabled) {
try {
callSyncRpc('__pty_set_raw_mode', [Number(enabled) !== 0]);
return WASI_ERRNO_SUCCESS;
} catch (error) {
process?.stderr?.write?.(`WARN host_tty.set_raw_mode failed: ${error?.stack || error}\n`);
return WASI_ERRNO_FAULT;
}
},
read(ptr, len, timeoutMs = 10) {
try {
if (!(instanceMemory instanceof WebAssembly.Memory)) {
return 0;
}
const requestedLength = Math.max(1, Number(len) >>> 0);
const chunk = readKernelStdinChunk(requestedLength, Number(timeoutMs) >>> 0);
if (!chunk || chunk.length === 0) {
return 0;
}
const written = Math.min(chunk.length, requestedLength);
new Uint8Array(instanceMemory.buffer).set(chunk.subarray(0, written), Number(ptr) >>> 0);
return written >>> 0;
} catch (error) {
process?.stderr?.write?.(`WARN host_tty.read failed: ${error?.stack || error}\n`);
return 0;
}
},
};

const HOST_FS_MODE_REGULAR = 0o100644;
const HOST_FS_MODE_CHARACTER = 0o020666;
const HOST_FS_MODE_FIFO = 0o010600;
Expand Down Expand Up @@ -12874,7 +12947,7 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => {
const sidecarManagedProcess =
typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' &&
process.env.AGENTOS_SANDBOX_ROOT.length > 0;
if (sidecarManagedProcess || KERNEL_STDIO_SYNC_RPC) {
if (typeof callSyncRpc === 'function') {
try {
const requestedLength = (() => {
if (!(instanceMemory instanceof WebAssembly.Memory)) {
Expand All @@ -12888,7 +12961,7 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => {
}
return total >>> 0;
})();
const chunk = readKernelStdinChunk(requestedLength);
const chunk = readKernelStdinChunk(requestedLength, 0xffffffff);
if (!chunk || chunk.length === 0) {
return writeGuestUint32(nreadPtr, 0);
}
Expand Down Expand Up @@ -13637,6 +13710,7 @@ const instance = new WebAssembly.Instance(module, {
: limitedHostProcessImport,
host_net: permissionTier === 'full' ? hostNetImport : undefined,
host_user: hostUserImport,
host_tty: hostTtyImport,
host_fs: hostFsImport,
});

Expand Down
3 changes: 3 additions & 0 deletions crates/execution/src/v8_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ pub fn map_bridge_method(method: &str) -> (&str, bool) {
"_kernelStdinReadRaw" => ("__kernel_stdin_read", false),
"_kernelStdioWriteRaw" => ("__kernel_stdio_write", false),
"_kernelPollRaw" => ("__kernel_poll", false),
"_kernelIsattyRaw" => ("__kernel_isatty", false),
"_kernelTtySizeRaw" => ("__kernel_tty_size", false),

// Network operations
"_networkHttpServerListenRaw" => ("net.http_listen", false),
Expand Down Expand Up @@ -577,6 +579,7 @@ mod tests {
"_netSocketSetNoDelayRaw",
"_kernelStdioWriteRaw",
"_kernelPollRaw",
"_kernelTtySizeRaw",
"_netSocketUpgradeTlsRaw",
"_tlsGetCiphersRaw",
"_dgramSocketAddressRaw",
Expand Down
29 changes: 24 additions & 5 deletions crates/execution/src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2157,11 +2157,17 @@ fn build_wasm_internal_env(
.filter(|(key, _)| key.starts_with("AGENTOS_"))
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<BTreeMap<_, _>>();

if let Some(value) = request.env.get("SECURE_EXEC_KEEP_STDIN_OPEN") {
internal_env.insert(String::from("SECURE_EXEC_KEEP_STDIN_OPEN"), value.clone());
}
internal_env.insert(
WASM_MODULE_PATH_ENV.to_string(),
resolved_module.specifier.clone(),
);
internal_env.insert(
String::from("AGENTOS_FORWARD_KERNEL_STDIN_RPC"),
String::from("1"),
);
if let Ok(module_bytes) = fs::read(&resolved_module.resolved_path) {
internal_env.insert(
String::from("AGENTOS_WASM_MODULE_BASE64"),
Expand Down Expand Up @@ -2199,8 +2205,6 @@ fn build_wasm_internal_env(
} else {
internal_env.remove(WASM_PREWARM_ONLY_ENV);
}
internal_env.remove("SECURE_EXEC_KEEP_STDIN_OPEN");

internal_env
}

Expand Down Expand Up @@ -2282,7 +2286,7 @@ const __agentOSRequireBuiltin = (specifier) => {{
}}
throw new Error(`secure-exec WASM bootstrap cannot load ${{specifier}}`);
}};
if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule === "undefined") {{
if (typeof globalThis !== "undefined") {{
const __agentOSFs = () => __agentOSRequireBuiltin("node:fs");
const __agentOSPath = () => __agentOSRequireBuiltin("node:path");
const __agentOSCrypto = () => __agentOSRequireBuiltin("node:crypto");
Expand Down Expand Up @@ -3444,7 +3448,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule =
const sidecarManagedProcess =
typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" &&
process.env.AGENTOS_SANDBOX_ROOT.length > 0;
if (syncRpc && (sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled())) {{
if (syncRpc) {{
try {{
let chunk = null;
while (true) {{
Expand Down Expand Up @@ -4367,6 +4371,21 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSSyncRpc ===
throw new Error("secure-exec WASM kernel poll bridge is unavailable");
}}
return _kernelPollRaw.applySync(void 0, args);
case "__kernel_isatty":
if (typeof _kernelIsattyRaw === "undefined") {{
throw new Error("secure-exec WASM kernel isatty bridge is unavailable");
}}
return _kernelIsattyRaw.applySync(void 0, args);
case "__kernel_tty_size":
if (typeof _kernelTtySizeRaw === "undefined") {{
throw new Error("secure-exec WASM kernel tty size bridge is unavailable");
}}
return _kernelTtySizeRaw.applySync(void 0, args);
case "__pty_set_raw_mode":
if (typeof _ptySetRawMode === "undefined") {{
throw new Error("secure-exec WASM PTY raw-mode bridge is unavailable");
}}
return _ptySetRawMode.applySync(void 0, args);
case "child_process.spawn": {{
if (typeof _childProcessSpawnStart === "undefined") {{
throw new Error("secure-exec WASM child_process bridge is unavailable");
Expand Down
14 changes: 13 additions & 1 deletion crates/kernel/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ use crate::process_table::{
ProcessTableError, ProcessWaitResult, SigmaskHow, SignalSet, DEFAULT_PROCESS_UMASK, SIGCONT,
SIGPIPE, SIGSTOP, SIGTSTP, SIGWINCH,
};
use crate::pty::{LineDisciplineConfig, PartialTermios, PtyError, PtyManager, Termios};
use crate::pty::{
LineDisciplineConfig, PartialTermios, PtyError, PtyManager, PtyWindowSize, Termios,
};
use crate::resource_accounting::{
measure_filesystem_usage, FileSystemUsage, ResourceAccountant, ResourceError, ResourceLimits,
ResourceSnapshot, DEFAULT_MAX_OPEN_FDS,
Expand Down Expand Up @@ -2488,6 +2490,16 @@ impl<F: VirtualFileSystem + 'static> KernelVm<F> {
Ok(self.ptys.is_slave(entry.description.id()))
}

pub fn pty_window_size(
&self,
requester_driver: &str,
pid: u32,
fd: u32,
) -> KernelResult<PtyWindowSize> {
let description = self.description_for_fd(requester_driver, pid, fd)?;
Ok(self.ptys.window_size(description.id())?)
}

pub fn pty_set_discipline(
&self,
requester_driver: &str,
Expand Down
Loading
Loading