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
353 changes: 324 additions & 29 deletions crates/execution/assets/runners/python-runner.mjs

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions crates/execution/src/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pub enum PythonVfsRpcMethod {
Stat,
ReadDir,
Mkdir,
Unlink,
Rmdir,
Rename,
HttpRequest,
DnsLookup,
SubprocessRun,
Expand All @@ -63,6 +66,9 @@ impl PythonVfsRpcMethod {
"fsStat" => Some(Self::Stat),
"fsReaddir" => Some(Self::ReadDir),
"fsMkdir" => Some(Self::Mkdir),
"fsUnlink" => Some(Self::Unlink),
"fsRmdir" => Some(Self::Rmdir),
"fsRename" => Some(Self::Rename),
"httpRequest" => Some(Self::HttpRequest),
"dnsLookup" => Some(Self::DnsLookup),
"subprocessRun" => Some(Self::SubprocessRun),
Expand All @@ -76,6 +82,8 @@ pub struct PythonVfsRpcRequest {
pub id: u64,
pub method: PythonVfsRpcMethod,
pub path: String,
/// Second path for `Rename` (the destination); `None` for other methods.
pub destination: Option<String>,
pub content_base64: Option<String>,
pub recursive: bool,
pub url: Option<String>,
Expand Down Expand Up @@ -137,6 +145,8 @@ struct PythonVfsBridgeRequestWire {
#[serde(default)]
path: String,
#[serde(default)]
destination: Option<String>,
#[serde(default)]
content_base64: Option<String>,
#[serde(default)]
recursive: bool,
Expand Down Expand Up @@ -1176,6 +1186,7 @@ fn parse_python_bridge_sync_rpc_request(
id: request.id,
method,
path: wire.path,
destination: wire.destination,
content_base64: wire.content_base64,
recursive: wire.recursive,
url: wire.url,
Expand Down
349 changes: 331 additions & 18 deletions crates/sidecar/src/execution.rs

Large diffs are not rendered by default.

47 changes: 38 additions & 9 deletions crates/sidecar/src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,38 @@ where
.mkdir(&path, request.recursive)
.map(|()| PythonVfsRpcResponsePayload::Empty)
.map_err(kernel_error),
// Mirror the delete/rename into the host-side shadow too, the
// same way the wire `GuestFilesystemOperation` handlers do —
// otherwise a later shadow→kernel sync would resurrect the
// entry the guest just removed.
PythonVfsRpcMethod::Unlink => {
match vm.kernel.remove_file(&path).map_err(kernel_error) {
Ok(()) => remove_guest_shadow_path(vm, &path)
.map(|()| PythonVfsRpcResponsePayload::Empty),
Err(error) => Err(error),
}
}
PythonVfsRpcMethod::Rmdir => {
match vm.kernel.remove_dir(&path).map_err(kernel_error) {
Ok(()) => remove_guest_shadow_path(vm, &path)
.map(|()| PythonVfsRpcResponsePayload::Empty),
Err(error) => Err(error),
}
}
PythonVfsRpcMethod::Rename => {
let destination = request.destination.as_deref().ok_or_else(|| {
SidecarError::InvalidState(format!(
"python VFS fsRename for {} requires destination",
path
))
})?;
let destination = normalize_python_vfs_rpc_path(destination)?;
match vm.kernel.rename(&path, &destination).map_err(kernel_error) {
Ok(()) => rename_guest_shadow_path(vm, &path, &destination)
.map(|()| PythonVfsRpcResponsePayload::Empty),
Err(error) => Err(error),
}
}
PythonVfsRpcMethod::HttpRequest
| PythonVfsRpcMethod::DnsLookup
| PythonVfsRpcMethod::SubprocessRun => {
Expand Down Expand Up @@ -860,16 +892,13 @@ pub(crate) fn normalize_python_vfs_rpc_path(path: &str) -> Result<String, Sideca
)));
}

// Root is `/`: Python may address the whole guest VFS. Textual `..` segments
// are resolved by `normalize_path`, and the kernel enforces fs permissions
// plus mount-confinement (openat2 RESOLVE_BENEATH refuses escaping symlinks)
// on every op — so confinement is the kernel's job, not a prefix check here.
let normalized = normalize_path(path);
if normalized == PYTHON_VFS_RPC_GUEST_ROOT
|| normalized.starts_with(&format!("{PYTHON_VFS_RPC_GUEST_ROOT}/"))
{
Ok(normalized)
} else {
Err(SidecarError::InvalidState(format!(
"python VFS RPC path {normalized} escapes guest workspace root {PYTHON_VFS_RPC_GUEST_ROOT}"
)))
}
debug_assert_eq!(PYTHON_VFS_RPC_GUEST_ROOT, "/");
Ok(normalized)
}

/// Kernel-VFS-backed reader for the module resolver. The resolution algorithm
Expand Down
6 changes: 5 additions & 1 deletion crates/sidecar/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ pub(crate) const EXECUTION_DRIVER_NAME: &str = "secure-exec-sidecar-execution";
pub(crate) const JAVASCRIPT_COMMAND: &str = "node";
pub(crate) const PYTHON_COMMAND: &str = "python";
pub(crate) const WASM_COMMAND: &str = "wasm";
pub(crate) const PYTHON_VFS_RPC_GUEST_ROOT: &str = "/workspace";
// The Python runtime addresses the whole guest VFS (the kernel enforces fs
// permissions and mount-confinement on every op, identical to what the JS/WASM
// runtimes and `vm.readFile()` see), so the VFS-RPC root is `/`, not a single
// workspace dir.
pub(crate) const PYTHON_VFS_RPC_GUEST_ROOT: &str = "/";
pub(crate) const EXECUTION_SANDBOX_ROOT_ENV: &str = "AGENTOS_SANDBOX_ROOT";
pub(crate) const WASM_STDIO_SYNC_RPC_ENV: &str = "AGENTOS_WASI_STDIO_SYNC_RPC";
#[cfg(test)]
Expand Down
9 changes: 8 additions & 1 deletion crates/sidecar/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const SHADOW_ROOT_BOOTSTRAP_DIRS: &[(&str, u32)] = &[

pub(crate) const DEFAULT_GUEST_PATH_ENV: &str =
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
#[cfg(test)]
const KERNEL_COMMAND_STUB: &[u8] = b"#!/bin/sh\n# kernel command stub\n";
pub(crate) const MAX_VM_LAYERS: usize = 256;

Expand Down Expand Up @@ -222,6 +223,10 @@ where
let mut execution_commands = vec![
String::from(JAVASCRIPT_COMMAND),
String::from(PYTHON_COMMAND),
// `python3` resolves to the same Pyodide runtime; register it so the
// guest shell can find `/bin/python3` on PATH (the command resolver
// already rewrites the alias to `python`).
String::from("python3"),
String::from(WASM_COMMAND),
];
execution_commands.extend(command_guest_paths.keys().cloned());
Expand All @@ -231,7 +236,6 @@ where
execution_commands,
))
.map_err(kernel_error)?;
prune_kernel_command_stub(&mut kernel, "/bin/python")?;
if let Some(root) = kernel.root_filesystem_mut() {
root.finish_bootstrap();
}
Expand Down Expand Up @@ -2043,6 +2047,9 @@ pub(crate) fn normalize_dns_hostname(hostname: &str) -> Result<String, SidecarEr
Ok(normalized)
}

// Retained for the native-root command-stub test; `python` is now a real
// command so production no longer prunes `/bin/python`.
#[cfg(test)]
fn prune_kernel_command_stub(
kernel: &mut KernelVm<secure_exec_kernel::mount_table::MountTable>,
path: &str,
Expand Down
Loading
Loading