From 5bb970a7cc19ff60ed466a080cb56d7de383bf8f Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 27 May 2026 00:19:04 -0700 Subject: [PATCH] feat(devshell): add provider-backed VM dev sandbox --- crates/openshell-driver-vm/README.md | 7 + .../scripts/openshell-vm-sandbox-init.sh | 4 + crates/openshell-driver-vm/src/driver.rs | 49 ++- crates/openshell-driver-vm/src/rootfs.rs | 78 ++++- crates/openshell-sandbox/src/proxy.rs | 91 +++++- deploy/docker/Dockerfile.ci | 53 ++- deploy/docker/devshell-bootstrap.sh | 303 ++++++++++++++++++ scripts/devshell | 277 ++++++++++++++++ scripts/devshell.profiles/codex.yaml | 43 +++ .../openshell-dev-toolchains.yaml | 77 +++++ tasks/scripts/docker-build-ci.sh | 7 +- tasks/scripts/gateway-vm.sh | 20 +- 12 files changed, 956 insertions(+), 53 deletions(-) create mode 100755 deploy/docker/devshell-bootstrap.sh create mode 100755 scripts/devshell create mode 100644 scripts/devshell.profiles/codex.yaml create mode 100644 scripts/devshell.profiles/openshell-dev-toolchains.yaml diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index 8da0b96a4..574c0ee3f 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -43,6 +43,7 @@ By default `mise run gateway:vm`: - Registers the CLI gateway `vm-dev` by writing `~/.config/openshell/gateways/vm-dev/metadata.json`. It does not modify the workspace `.env`. - Persists the gateway SQLite DB under `.cache/gateway-vm/gateway.db`. - Places the VM driver state (per-sandbox `overlay.ext4`, image cache, and `run/compute-driver.sock`) under `/tmp/openshell-vm-driver-$USER-vm-dev/` so the AF_UNIX socket path stays under macOS `SUN_LEN`. +- Starts development sandboxes with 4 vCPUs, 8192 MiB RAM, and a 32768 MiB sparse writable overlay disk. - Writes `.cache/gateway-vm/gateway.toml` with `[openshell.drivers.vm].driver_dir = "$PWD/target/debug"` so the freshly built `openshell-driver-vm` is used instead of an older installed copy from `~/.local/libexec/openshell`, `/usr/libexec/openshell`, or `/usr/local/libexec`. For GPU passthrough (VFIO), pass `-- --gpu` and run with root privileges: @@ -76,6 +77,12 @@ mise run gateway:vm # custom sandbox image OPENSHELL_SANDBOX_IMAGE=ghcr.io/example/sandbox:latest mise run gateway:vm +# custom sandbox VM size +OPENSHELL_VM_DRIVER_VCPUS=6 \ +OPENSHELL_VM_DRIVER_MEM_MIB=12288 \ +OPENSHELL_VM_OVERLAY_DISK_MIB=32768 \ +mise run gateway:vm + # custom bootstrap image for the VM runtime used to prepare/boot target images OPENSHELL_VM_BOOTSTRAP_IMAGE=ghcr.io/example/bootstrap:latest mise run gateway:vm ``` diff --git a/crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh b/crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh index 8725984f9..ee5dc6d22 100644 --- a/crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh +++ b/crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh @@ -243,6 +243,10 @@ setup_overlay_root() { if [ "${OPENSHELL_VM_INIT_MODE:-sandbox}" = "image-prep" ]; then prepare_guest_image_rootfs sync + if ! umount /overlay 2>/dev/null; then + ts "WARNING: failed to unmount image-prep disk cleanly" + fi + sync ts "image-prep complete" exit 0 fi diff --git a/crates/openshell-driver-vm/src/driver.rs b/crates/openshell-driver-vm/src/driver.rs index f09f1ebc3..2d13abee4 100644 --- a/crates/openshell-driver-vm/src/driver.rs +++ b/crates/openshell-driver-vm/src/driver.rs @@ -6,7 +6,8 @@ use crate::gpu::{ }; use crate::rootfs::{ clone_or_copy_sparse_file, create_ext4_image_from_dir_with_size, create_rootfs_image_from_dir, - extract_rootfs_archive_to, prepare_sandbox_rootfs_from_image_root, sandbox_guest_init_path, + embedded_rootfs_payload_identity, extract_rootfs_archive_to, + prepare_sandbox_rootfs_from_image_root, repair_ext4_image, sandbox_guest_init_path, set_rootfs_image_file_mode, write_rootfs_image_file, }; use bollard::Docker; @@ -1317,7 +1318,10 @@ impl VmDriver { image_identity = %source_image_identity, "vm driver: manifest digest resolved" ); - let image_identity = bootstrap_image_cache_identity(&source_image_identity); + let image_identity = bootstrap_image_cache_identity( + &source_image_identity, + &embedded_rootfs_payload_identity(), + ); let image_path = image_cache_rootfs_image(&self.config.state_dir, &image_identity); // Emit a driver progress hint for cache hits too and immediately @@ -1498,7 +1502,8 @@ impl VmDriver { docker: &Docker, image_identity: &str, ) -> Result { - let cache_identity = bootstrap_image_cache_identity(image_identity); + let cache_identity = + bootstrap_image_cache_identity(image_identity, &embedded_rootfs_payload_identity()); let image_path = image_cache_rootfs_image(&self.config.state_dir, &cache_identity); self.publish_platform_event( @@ -1605,7 +1610,8 @@ impl VmDriver { image_identity: &str, bootstrap_root_disk: &Path, ) -> Result { - let cache_identity = prepared_image_cache_identity(image_identity); + let cache_identity = + prepared_image_cache_identity(image_identity, &embedded_rootfs_payload_identity()); let image_path = image_cache_rootfs_image(&self.config.state_dir, &cache_identity); if tokio::fs::metadata(&image_path).await.is_ok() { @@ -1714,7 +1720,10 @@ impl VmDriver { "failed to resolve vm sandbox image '{image_ref}': {err}" )) })?; - let cache_identity = prepared_image_cache_identity(&source_image_identity); + let cache_identity = prepared_image_cache_identity( + &source_image_identity, + &embedded_rootfs_payload_identity(), + ); let image_path = image_cache_rootfs_image(&self.config.state_dir, &cache_identity); if tokio::fs::metadata(&image_path).await.is_ok() { @@ -1904,6 +1913,11 @@ impl VmDriver { let _ = tokio::fs::remove_dir_all(staging_dir).await; return Err(err); } + let prepared_image_for_repair = prepared_image.clone(); + tokio::task::spawn_blocking(move || repair_ext4_image(&prepared_image_for_repair)) + .await + .map_err(|err| Status::internal(format!("prepared image repair panicked: {err}")))? + .map_err(Status::failed_precondition)?; if tokio::fs::metadata(&image_path).await.is_ok() { let _ = tokio::fs::remove_dir_all(staging_dir).await; @@ -3724,12 +3738,12 @@ fn write_oci_layout_for_manifest( Ok(()) } -fn bootstrap_image_cache_identity(image_identity: &str) -> String { - format!("{BOOTSTRAP_IMAGE_CACHE_LAYOUT_VERSION}:{image_identity}") +fn bootstrap_image_cache_identity(image_identity: &str, rootfs_payload_identity: &str) -> String { + format!("{BOOTSTRAP_IMAGE_CACHE_LAYOUT_VERSION}:{rootfs_payload_identity}:{image_identity}") } -fn prepared_image_cache_identity(image_identity: &str) -> String { - format!("{PREPARED_IMAGE_CACHE_LAYOUT_VERSION}:{image_identity}") +fn prepared_image_cache_identity(image_identity: &str, rootfs_payload_identity: &str) -> String { + format!("{PREPARED_IMAGE_CACHE_LAYOUT_VERSION}:{rootfs_payload_identity}:{image_identity}") } fn registry_layer_download_concurrency() -> usize { @@ -5517,18 +5531,18 @@ mod tests { } #[test] - fn prepared_image_cache_identity_includes_rootfs_layout_version() { + fn prepared_image_cache_identity_includes_rootfs_layout_and_payload_version() { assert_eq!( - prepared_image_cache_identity("sha256:local-image"), - "sandbox-prepared-rootfs-ext4-umoci-v2:sha256:local-image" + prepared_image_cache_identity("sha256:local-image", "runtime-sha256:abc"), + "sandbox-prepared-rootfs-ext4-umoci-v2:runtime-sha256:abc:sha256:local-image" ); } #[test] - fn bootstrap_image_cache_identity_includes_rootfs_layout_version() { + fn bootstrap_image_cache_identity_includes_rootfs_layout_and_payload_version() { assert_eq!( - bootstrap_image_cache_identity("sha256:bootstrap-image"), - "sandbox-bootstrap-rootfs-ext4-v2:sha256:bootstrap-image" + bootstrap_image_cache_identity("sha256:bootstrap-image", "runtime-sha256:def"), + "sandbox-bootstrap-rootfs-ext4-v2:runtime-sha256:def:sha256:bootstrap-image" ); } @@ -5551,7 +5565,10 @@ mod tests { &staging_dir, &GuestImagePayload { image_ref: "ghcr.io/example/app:latest".to_string(), - image_identity: prepared_image_cache_identity("sha256:abc"), + image_identity: prepared_image_cache_identity( + "sha256:abc", + "runtime-sha256:payload", + ), source: GuestImagePayloadSource::RegistryOciLayout { layout_dir }, }, ) diff --git a/crates/openshell-driver-vm/src/rootfs.rs b/crates/openshell-driver-vm/src/rootfs.rs index 904ed8cd3..1cccd7673 100644 --- a/crates/openshell-driver-vm/src/rootfs.rs +++ b/crates/openshell-driver-vm/src/rootfs.rs @@ -10,8 +10,11 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::atomic::{AtomicU64, Ordering}; +use sha2::{Digest, Sha256}; + const SUPERVISOR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/openshell-sandbox.zst")); const UMOCI: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/umoci.zst")); +const GUEST_INIT_SCRIPT: &str = include_str!("../scripts/openshell-vm-sandbox-init.sh"); const ROOTFS_VARIANT_MARKER: &str = ".openshell-rootfs-variant"; const SANDBOX_GUEST_INIT_PATH: &str = "/srv/openshell-vm-sandbox-init.sh"; const SANDBOX_SUPERVISOR_PATH: &str = "/opt/openshell/bin/openshell-sandbox"; @@ -26,6 +29,18 @@ pub const fn sandbox_guest_init_path() -> &'static str { SANDBOX_GUEST_INIT_PATH } +pub fn embedded_rootfs_payload_identity() -> String { + let mut hasher = Sha256::new(); + hasher.update(b"openshell-vm-rootfs-payload-v1\0"); + hasher.update(b"init\0"); + hasher.update(GUEST_INIT_SCRIPT.as_bytes()); + hasher.update(b"supervisor\0"); + hasher.update(SUPERVISOR); + hasher.update(b"umoci\0"); + hasher.update(UMOCI); + format!("runtime-sha256:{:x}", hasher.finalize()) +} + pub fn prepare_sandbox_rootfs_from_image_root( rootfs: &Path, image_identity: &str, @@ -125,6 +140,44 @@ pub fn create_ext4_image_from_dir_with_size( Ok(()) } +pub fn repair_ext4_image(image_path: &Path) -> Result<(), String> { + let mut last_error = None; + for tool in ["e2fsck", "fsck.ext4"] { + for candidate in e2fs_tool_candidates(tool) { + let label = candidate.display().to_string(); + let output = Command::new(&candidate) + .arg("-f") + .arg("-p") + .arg(image_path) + .output(); + match output { + Ok(output) if e2fsck_status_is_successful(output.status.code()) => { + return Ok(()); + } + Ok(output) => { + last_error = Some(format!( + "{label} failed with status {}\nstdout: {}\nstderr: {}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + last_error = Some(format!("{label} not found")); + } + Err(err) => { + last_error = Some(format!("run {label}: {err}")); + } + } + } + } + Err(format!( + "failed to repair ext4 image {}: {}. Install e2fsprogs (e2fsck/fsck.ext4) and retry", + image_path.display(), + last_error.unwrap_or_else(|| "e2fsck not found".to_string()) + )) +} + pub fn clone_or_copy_sparse_file(source: &Path, dest: &Path) -> Result<(), String> { if let Some(parent) = dest.parent() { fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?; @@ -357,11 +410,8 @@ fn prepare_sandbox_rootfs(rootfs: &Path) -> Result<(), String> { if let Some(parent) = init_path.parent() { fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?; } - fs::write( - &init_path, - include_str!("../scripts/openshell-vm-sandbox-init.sh"), - ) - .map_err(|e| format!("write {}: {e}", init_path.display()))?; + fs::write(&init_path, GUEST_INIT_SCRIPT) + .map_err(|e| format!("write {}: {e}", init_path.display()))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt as _; @@ -429,6 +479,13 @@ fn rootfs_image_size_bytes(source: &Path) -> Result { Ok(round_up_to_mib(size)) } +fn e2fsck_status_is_successful(code: Option) -> bool { + // e2fsck uses a bitmask exit status. 0 means clean, 1 means filesystem + // errors were corrected, and 2 requests a reboot for mounted filesystems. + // For offline image files, any combination of those bits is usable. + matches!(code, Some(code) if code >= 0 && (code & !0b11) == 0) +} + fn ext4_image_min_size_bytes(source: &Path) -> Result { let used = directory_size_bytes(source)?; Ok(round_up_to_mib(used + EXT4_IMAGE_MIN_HEADROOM_BYTES)) @@ -1121,6 +1178,17 @@ mod tests { assert_eq!(debugfs_quote_argument("/tmp/bad\npath"), None); } + #[test] + fn e2fsck_status_accepts_clean_and_corrected_images() { + for code in [0, 1, 2, 3] { + assert!(e2fsck_status_is_successful(Some(code))); + } + for code in [4, 8, 16, 32, 128] { + assert!(!e2fsck_status_is_successful(Some(code))); + } + assert!(!e2fsck_status_is_successful(None)); + } + fn unique_temp_dir() -> PathBuf { static COUNTER: AtomicU64 = AtomicU64::new(0); let nanos = SystemTime::now() diff --git a/crates/openshell-sandbox/src/proxy.rs b/crates/openshell-sandbox/src/proxy.rs index 037ecfc78..bb4c68b23 100644 --- a/crates/openshell-sandbox/src/proxy.rs +++ b/crates/openshell-sandbox/src/proxy.rs @@ -18,6 +18,8 @@ use openshell_ocsf::{ NetworkActivityBuilder, Process, SeverityId, StatusId, Url as OcsfUrl, ocsf_emit, }; use std::net::{IpAddr, SocketAddr}; +#[cfg(target_os = "linux")] +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; @@ -1137,20 +1139,7 @@ fn resolve_owner_identity( })?; let ancestors = crate::procfs::collect_ancestor_binaries(owner_pid, entrypoint_pid); - - for ancestor in &ancestors { - identity_cache - .verify_or_cache(ancestor) - .map_err(|e| IdentityError { - reason: format!( - "ancestor integrity check failed for {}: {e}", - ancestor.display() - ), - binary: Some(bin_path.clone()), - binary_pid: Some(owner_pid), - ancestors: ancestors.clone(), - })?; - } + let ancestors = verify_existing_ancestors(ancestors, identity_cache, &bin_path, owner_pid)?; let mut exclude = ancestors.clone(); exclude.push(bin_path.clone()); @@ -1165,6 +1154,56 @@ fn resolve_owner_identity( }) } +#[cfg(target_os = "linux")] +fn verify_existing_ancestors( + ancestors: Vec, + identity_cache: &BinaryIdentityCache, + bin_path: &Path, + owner_pid: u32, +) -> std::result::Result, IdentityError> { + let mut verified = Vec::with_capacity(ancestors.len()); + + for ancestor in ancestors { + match std::fs::metadata(&ancestor) { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + debug!( + ancestor = %ancestor.display(), + "Skipping missing process ancestor during identity verification" + ); + continue; + } + Err(error) => { + return Err(IdentityError { + reason: format!( + "ancestor integrity check failed for {}: Failed to stat {}: {error}", + ancestor.display(), + ancestor.display() + ), + binary: Some(bin_path.to_path_buf()), + binary_pid: Some(owner_pid), + ancestors: verified, + }); + } + } + + identity_cache + .verify_or_cache(&ancestor) + .map_err(|e| IdentityError { + reason: format!( + "ancestor integrity check failed for {}: {e}", + ancestor.display() + ), + binary: Some(bin_path.to_path_buf()), + binary_pid: Some(owner_pid), + ancestors: verified.clone(), + })?; + verified.push(ancestor); + } + + Ok(verified) +} + /// Resolve the identity of the process owning a TCP peer connection. /// /// Walks `/proc//net/tcp` to find the socket inode, locates @@ -6351,6 +6390,30 @@ network_policies: assert_eq!(resp_str[body_start..].len(), cl); } + #[cfg(target_os = "linux")] + #[test] + fn verify_existing_ancestors_skips_missing_paths() { + use crate::identity::BinaryIdentityCache; + use std::io::Write; + + let mut existing = tempfile::NamedTempFile::new().unwrap(); + existing.write_all(b"ancestor").unwrap(); + existing.flush().unwrap(); + + let missing = existing.path().with_file_name("missing-ancestor"); + let cache = BinaryIdentityCache::new(); + + let verified = verify_existing_ancestors( + vec![existing.path().to_path_buf(), missing], + &cache, + Path::new("/usr/bin/curl"), + 123, + ) + .expect("missing ancestors should be ignored"); + + assert_eq!(verified, vec![existing.path().to_path_buf()]); + } + /// End-to-end regression for the `docker cp` hot-swap hazard that /// motivated `binary_path()` stripping the kernel's `" (deleted)"` /// suffix (PR #844). diff --git a/deploy/docker/Dockerfile.ci b/deploy/docker/Dockerfile.ci index 77a8c94e2..6e7e025ff 100644 --- a/deploy/docker/Dockerfile.ci +++ b/deploy/docker/Dockerfile.ci @@ -14,9 +14,9 @@ ARG NPM_VERSION=11.13.0 ARG TARGETARCH ENV DEBIAN_FRONTEND=noninteractive -ENV MISE_DATA_DIR=/opt/mise -ENV MISE_CACHE_DIR=/opt/mise/cache -ENV PATH="/opt/mise/shims:/root/.cargo/bin:/root/.local/bin:$PATH" +ENV MISE_DATA_DIR=/usr/local/share/openshell/mise +ENV RUSTUP_HOME=/usr/local/share/openshell/mise/rustup +ENV PATH="/usr/local/share/openshell/mise/shims:/usr/local/bin:/usr/local/share/openshell/mise/cargo/bin:/root/.local/bin:$PATH" # Install system dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -26,6 +26,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ clang \ libclang-dev \ + libnss-wrapper \ libz3-dev \ pkg-config \ libssl-dev \ @@ -38,7 +39,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ unzip \ xz-utils \ jq \ + iptables \ + iproute2 \ + nftables \ rsync \ + util-linux \ zstd \ && apt-get install -y --only-upgrade gpgv python3 \ && rm -rf /var/lib/apt/lists/* @@ -68,22 +73,42 @@ RUN case "$TARGETARCH" in \ # Install mise (NOTE: keep this version in sync with mise.toml) ARG MISE_VERSION=v2026.4.25 -RUN curl https://mise.run | MISE_VERSION=$MISE_VERSION sh +RUN curl https://mise.run | MISE_VERSION=$MISE_VERSION sh \ + && install -D -m 0755 /root/.local/bin/mise /usr/local/bin/mise + +# The VM sandbox runs as the unprivileged `sandbox` user and does not preserve +# Docker image ENV metadata, so expose mise through system paths and profiles. +RUN printf '%s\n' \ + 'export MISE_DATA_DIR="${MISE_DATA_DIR:-/usr/local/share/openshell/mise}"' \ + 'export MISE_CACHE_DIR="${MISE_CACHE_DIR:-${HOME:-/tmp}/.cache/mise}"' \ + 'export CARGO_HOME="${CARGO_HOME:-${HOME:-/tmp}/.cargo}"' \ + 'export RUSTUP_HOME="${RUSTUP_HOME:-/usr/local/share/openshell/mise/rustup}"' \ + 'case ":${PATH}:" in *:/usr/local/share/openshell/mise/shims:*) ;; *) export PATH="/usr/local/share/openshell/mise/shims:${PATH}" ;; esac' \ + 'case ":${PATH}:" in *:/usr/local/share/openshell/mise/cargo/bin:*) ;; *) export PATH="/usr/local/share/openshell/mise/cargo/bin:${PATH}" ;; esac' \ + 'case ":${PATH}:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:${PATH}" ;; esac' \ + > /etc/profile.d/openshell-mise.sh \ + && printf '%s\n' '' '[ -r /etc/profile.d/openshell-mise.sh ] && . /etc/profile.d/openshell-mise.sh' >> /etc/bash.bashrc + +COPY deploy/docker/devshell-bootstrap.sh /usr/local/bin/openshell-devshell-bootstrap +RUN chmod 0755 /usr/local/bin/openshell-devshell-bootstrap # Copy mise.toml and task includes, then install all tools via mise -COPY mise.toml /opt/mise/mise.toml -COPY mise.lock /opt/mise/mise.lock -COPY tasks/ /opt/mise/tasks/ -WORKDIR /opt/mise +COPY mise.toml /usr/local/share/openshell/mise/mise.toml +COPY mise.lock /usr/local/share/openshell/mise/mise.lock +COPY tasks/ /usr/local/share/openshell/mise/tasks/ +WORKDIR /usr/local/share/openshell/mise RUN --mount=type=secret,id=MISE_GITHUB_TOKEN \ export MISE_GITHUB_TOKEN="$(cat /run/secrets/MISE_GITHUB_TOKEN 2>/dev/null || true)" && \ - mise trust /opt/mise/mise.toml && \ - env -u RUSTC_WRAPPER mise install --locked && \ - mise reshim && \ + export CARGO_HOME=/usr/local/share/openshell/mise/cargo && \ + export RUSTUP_HOME=/usr/local/share/openshell/mise/rustup && \ + /usr/local/bin/mise trust /usr/local/share/openshell/mise/mise.toml && \ + env -u RUSTC_WRAPPER /usr/local/bin/mise install --locked && \ + /usr/local/bin/mise reshim && \ npm install -g "npm@${NPM_VERSION}" && \ - mise reshim && \ - (/root/.cargo/bin/rustup component remove rust-docs || true) && \ - rm -rf /root/.rustup/toolchains/*/share/doc /root/.rustup/toolchains/*/share/man && \ + /usr/local/bin/mise reshim && \ + find /usr/local/share/openshell/mise/shims -type l -exec ln -sfn /usr/local/bin/mise {} \; && \ + (/usr/local/share/openshell/mise/cargo/bin/rustup component remove rust-docs || true) && \ + rm -rf /usr/local/share/openshell/mise/rustup/toolchains/*/share/doc /usr/local/share/openshell/mise/rustup/toolchains/*/share/man && \ helm plugin install https://github.com/helm-unittest/helm-unittest --verify=false # Set working directory for CI jobs diff --git a/deploy/docker/devshell-bootstrap.sh b/deploy/docker/devshell-bootstrap.sh new file mode 100755 index 000000000..a3e667f61 --- /dev/null +++ b/deploy/docker/devshell-bootstrap.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Bootstrap an OpenShell development checkout inside a sandbox image. + +set -euo pipefail + +REPO_SLUG="${OPENSHELL_DEVSHELL_REPO_SLUG:-NVIDIA/OpenShell}" + +export HOME=/sandbox +export USER=sandbox +export MISE_CACHE_DIR=/sandbox/.cache/mise +export CARGO_HOME=/sandbox/.cargo + +path_prepend() { + case ":${PATH}:" in + *":$1:"*) ;; + *) PATH="$1:${PATH}" ;; + esac +} + +ld_prepend() { + case ":${LD_LIBRARY_PATH:-}:" in + *":$1:"*) ;; + *) LD_LIBRARY_PATH="$1:${LD_LIBRARY_PATH:-}" ;; + esac +} + +preload_prepend() { + case ":${LD_PRELOAD:-}:" in + *":$1:"*) ;; + *) LD_PRELOAD="$1${LD_PRELOAD:+:${LD_PRELOAD}}" ;; + esac +} + +find_libclang_dir() { + local dir + for dir in \ + /sandbox/.deps/usr/lib/llvm-*/lib \ + /usr/lib/llvm-*/lib \ + /sandbox/.deps/usr/lib/*-linux-gnu \ + /usr/lib/*-linux-gnu \ + /usr/local/lib \ + /usr/lib64; do + [ -d "$dir" ] || continue + if [ -e "$dir/libclang.so" ] \ + || compgen -G "$dir/libclang-*.so" >/dev/null \ + || compgen -G "$dir/libclang.so.*" >/dev/null; then + printf '%s\n' "$dir" + return 0 + fi + done + return 1 +} + +find_clang_resource_include() { + local dir + for dir in \ + /sandbox/.deps/usr/lib/llvm-*/lib/clang/*/include \ + /usr/lib/llvm-*/lib/clang/*/include; do + if [ -f "$dir/stddef.h" ]; then + printf '%s\n' "$dir" + return 0 + fi + done + return 1 +} + +find_z3_header() { + local header + for header in /sandbox/.deps/usr/include/z3.h /usr/local/include/z3.h /usr/include/z3.h; do + if [ -f "$header" ]; then + printf '%s\n' "$header" + return 0 + fi + done + return 1 +} + +find_z3_libdir() { + local dir + for dir in /sandbox/.deps/usr/lib/*-linux-gnu /usr/local/lib /usr/lib/*-linux-gnu /usr/lib64; do + [ -d "$dir" ] || continue + if [ -e "$dir/libz3.so" ] || [ -e "$dir/libz3.a" ]; then + printf '%s\n' "$dir" + return 0 + fi + done + return 1 +} + +find_nss_wrapper() { + local lib + for lib in \ + /sandbox/.deps/usr/lib/*-linux-gnu/libnss_wrapper.so \ + /usr/lib/*-linux-gnu/libnss_wrapper.so \ + /usr/lib64/libnss_wrapper.so; do + if [ -f "$lib" ]; then + printf '%s\n' "$lib" + return 0 + fi + done + return 1 +} + +etc_hosts_has_localhost() { + grep -E '^(127\.0\.0\.1|::1)[[:space:]].*localhost' /etc/hosts >/dev/null 2>&1 +} + +native_deps_ready() { + find_libclang_dir >/dev/null \ + && find_clang_resource_include >/dev/null \ + && find_z3_header >/dev/null \ + && find_z3_libdir >/dev/null \ + && { etc_hosts_has_localhost || find_nss_wrapper >/dev/null; } +} + +ensure_native_deps() { + native_deps_ready && return 0 + command -v apt-get >/dev/null 2>&1 || return 0 + command -v dpkg-deb >/dev/null 2>&1 || return 0 + [ -f /etc/apt/sources.list.d/ubuntu.sources ] || return 0 + + local apt_root=/sandbox/.apt + mkdir -p \ + "$apt_root/apt.conf.d" \ + "$apt_root/sources" \ + "$apt_root/lists/partial" \ + "$apt_root/cache/archives/partial" \ + /sandbox/.deps + cp /etc/apt/sources.list.d/ubuntu.sources "$apt_root/sources/ubuntu.sources" + + local apt_opts=( + -o Dir::Etc::main=/dev/null + -o "Dir::Etc::parts=$apt_root/apt.conf.d" + -o Dir::Etc::sourcelist=/dev/null + -o "Dir::Etc::sourceparts=$apt_root/sources" + -o "Dir::State::Lists=$apt_root/lists" + -o Dir::State::status=/var/lib/dpkg/status + -o "Dir::Cache=$apt_root/cache" + -o "Dir::Cache::archives=$apt_root/cache/archives" + -o Debug::NoLocking=1 + -o Acquire::Languages=none + ) + + if ! compgen -G "$apt_root/lists/*_Packages*" >/dev/null; then + apt-get "${apt_opts[@]}" update + fi + if ! compgen -G "$apt_root/cache/archives/libclang-*.deb" >/dev/null \ + || ! compgen -G "$apt_root/cache/archives/libz3-dev_*.deb" >/dev/null; then + apt-get "${apt_opts[@]}" --download-only --reinstall -y install \ + libclang-dev \ + libz3-dev \ + libnss-wrapper \ + pkg-config + fi + + local deb + for deb in "$apt_root"/cache/archives/*.deb; do + [ -f "$deb" ] || continue + dpkg-deb -x "$deb" /sandbox/.deps + done +} + +configure_native_deps_env() { + local libclang_dir clang_include z3_header z3_libdir nss_wrapper + + if [ -d /sandbox/.deps/usr/bin ]; then + path_prepend /sandbox/.deps/usr/bin + fi + + if libclang_dir="$(find_libclang_dir)"; then + export LIBCLANG_PATH="$libclang_dir" + ld_prepend "$libclang_dir" + fi + if clang_include="$(find_clang_resource_include)"; then + export BINDGEN_EXTRA_CLANG_ARGS="-I${clang_include}${BINDGEN_EXTRA_CLANG_ARGS:+ ${BINDGEN_EXTRA_CLANG_ARGS}}" + fi + if z3_header="$(find_z3_header)"; then + export Z3_SYS_Z3_HEADER="$z3_header" + fi + if z3_libdir="$(find_z3_libdir)"; then + export Z3_LIBRARY_PATH_OVERRIDE="$z3_libdir" + ld_prepend "$z3_libdir" + fi + if [ -d /sandbox/.deps/usr/lib/aarch64-linux-gnu/pkgconfig ]; then + export PKG_CONFIG_PATH="/sandbox/.deps/usr/lib/aarch64-linux-gnu/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" + export PKG_CONFIG_SYSROOT_DIR=/sandbox/.deps + fi + + if ! etc_hosts_has_localhost && nss_wrapper="$(find_nss_wrapper)"; then + { + printf '127.0.0.1 localhost localhost.localdomain\n' + printf '::1 localhost ip6-localhost ip6-loopback\n' + printf '8.8.8.8 dns.google\n' + grep -v -E '^(127\.0\.0\.1|::1)[[:space:]]' /etc/hosts || true + } > /sandbox/.nss-hosts + cat /etc/passwd > /sandbox/.nss-passwd + if ! grep -q -E "^$(id -un):" /sandbox/.nss-passwd; then + printf '%s:x:%s:%s::%s:%s\n' \ + "$(id -un)" "$(id -u)" "$(id -g)" "$HOME" "${SHELL:-/bin/bash}" \ + >> /sandbox/.nss-passwd + fi + cat /etc/group > /sandbox/.nss-group + if ! grep -q -E "^$(id -gn):" /sandbox/.nss-group; then + printf '%s:x:%s:%s\n' "$(id -gn)" "$(id -g)" "$(id -un)" \ + >> /sandbox/.nss-group + fi + export NSS_WRAPPER_HOSTS=/sandbox/.nss-hosts + export NSS_WRAPPER_PASSWD=/sandbox/.nss-passwd + export NSS_WRAPPER_GROUP=/sandbox/.nss-group + export RUST_TEST_THREADS="${RUST_TEST_THREADS:-1}" + preload_prepend "$nss_wrapper" + fi + + export PATH LD_LIBRARY_PATH LD_PRELOAD +} + +write_shell_profile() { + { + printf 'export HOME=%q\n' "$HOME" + printf 'export USER=%q\n' "$USER" + printf 'export MISE_CACHE_DIR=%q\n' "$MISE_CACHE_DIR" + printf 'export CARGO_HOME=%q\n' "$CARGO_HOME" + printf 'export MISE_DATA_DIR=%q\n' "$MISE_DATA_DIR" + printf 'export RUSTUP_HOME=%q\n' "$RUSTUP_HOME" + printf 'export PATH=%q\n' "$PATH" + [ -z "${LD_LIBRARY_PATH:-}" ] || printf 'export LD_LIBRARY_PATH=%q\n' "$LD_LIBRARY_PATH" + [ -z "${LD_PRELOAD:-}" ] || printf 'export LD_PRELOAD=%q\n' "$LD_PRELOAD" + [ -z "${LIBCLANG_PATH:-}" ] || printf 'export LIBCLANG_PATH=%q\n' "$LIBCLANG_PATH" + [ -z "${BINDGEN_EXTRA_CLANG_ARGS:-}" ] || printf 'export BINDGEN_EXTRA_CLANG_ARGS=%q\n' "$BINDGEN_EXTRA_CLANG_ARGS" + [ -z "${PKG_CONFIG_PATH:-}" ] || printf 'export PKG_CONFIG_PATH=%q\n' "$PKG_CONFIG_PATH" + [ -z "${PKG_CONFIG_SYSROOT_DIR:-}" ] || printf 'export PKG_CONFIG_SYSROOT_DIR=%q\n' "$PKG_CONFIG_SYSROOT_DIR" + [ -z "${Z3_SYS_Z3_HEADER:-}" ] || printf 'export Z3_SYS_Z3_HEADER=%q\n' "$Z3_SYS_Z3_HEADER" + [ -z "${Z3_LIBRARY_PATH_OVERRIDE:-}" ] || printf 'export Z3_LIBRARY_PATH_OVERRIDE=%q\n' "$Z3_LIBRARY_PATH_OVERRIDE" + [ -z "${NSS_WRAPPER_HOSTS:-}" ] || printf 'export NSS_WRAPPER_HOSTS=%q\n' "$NSS_WRAPPER_HOSTS" + [ -z "${NSS_WRAPPER_PASSWD:-}" ] || printf 'export NSS_WRAPPER_PASSWD=%q\n' "$NSS_WRAPPER_PASSWD" + [ -z "${NSS_WRAPPER_GROUP:-}" ] || printf 'export NSS_WRAPPER_GROUP=%q\n' "$NSS_WRAPPER_GROUP" + [ -z "${RUST_TEST_THREADS:-}" ] || printf 'export RUST_TEST_THREADS=%q\n' "$RUST_TEST_THREADS" + } > /sandbox/.profile + cp /sandbox/.profile /sandbox/.bash_profile + printf 'source /sandbox/.profile\n' > /sandbox/.bashrc +} + +configure_mise() { + if command -v mise >/dev/null 2>&1 && [ -d /usr/local/share/openshell/mise ]; then + export MISE_DATA_DIR=/usr/local/share/openshell/mise + export RUSTUP_HOME=/usr/local/share/openshell/mise/rustup + path_prepend /usr/local/share/openshell/mise/cargo/bin + path_prepend /usr/local/bin + path_prepend /usr/local/share/openshell/mise/shims + return + fi + + if command -v mise >/dev/null 2>&1 && [ -d /opt/mise ]; then + export MISE_DATA_DIR=/opt/mise + export RUSTUP_HOME=/opt/mise/rustup + path_prepend /opt/mise/cargo/bin + path_prepend /usr/local/bin + path_prepend /opt/mise/shims + return + fi + + export MISE_DATA_DIR=/sandbox/.local/share/mise + export RUSTUP_HOME=/sandbox/.rustup + path_prepend /sandbox/.cargo/bin + path_prepend /sandbox/.local/bin + path_prepend /sandbox/.local/share/mise/shims + if ! command -v mise >/dev/null 2>&1; then + mkdir -p /sandbox/.local/bin + curl -fsSL https://mise.run | sh + fi +} + +clone_repo() { + if [ -d /sandbox/openshell/.git ]; then + return + fi + + if command -v gh >/dev/null 2>&1; then + gh repo clone "$REPO_SLUG" /sandbox/openshell \ + || git clone "https://github.com/${REPO_SLUG}.git" /sandbox/openshell + else + git clone "https://github.com/${REPO_SLUG}.git" /sandbox/openshell + fi +} + +mkdir -p /sandbox /sandbox/.cargo /sandbox/.cache/mise +configure_mise +ensure_native_deps +configure_native_deps_env +write_shell_profile +clone_repo + +cd /sandbox/openshell +mise trust >/dev/null +MISE_YES=1 mise install +if [ -f pyproject.toml ] && ! .venv/bin/python -c 'import grpc_tools.protoc' >/dev/null 2>&1; then + uv sync --locked +fi +exec "$@" diff --git a/scripts/devshell b/scripts/devshell new file mode 100755 index 000000000..f6b6ccc4e --- /dev/null +++ b/scripts/devshell @@ -0,0 +1,277 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Create or reconnect to an OpenShell development sandbox on the active gateway. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OPENSHELL_BIN="${OPENSHELL_BIN:-${ROOT}/scripts/bin/openshell}" +SANDBOX_NAME="${OPENSHELL_DEVSHELL_SANDBOX:-openshell-dev}" +SANDBOX_IMAGE="${OPENSHELL_DEVSHELL_IMAGE:-openshell/ci:dev}" +BOOTSTRAP_FILE="${OPENSHELL_DEVSHELL_BOOTSTRAP:-${ROOT}/deploy/docker/devshell-bootstrap.sh}" +PROFILE_DIR="${OPENSHELL_DEVSHELL_PROFILE_DIR:-${ROOT}/scripts/devshell.profiles}" +POLICY_FILE="${OPENSHELL_DEVSHELL_POLICY:-}" +DEVSHELL_PROVIDER="${OPENSHELL_DEVSHELL_TOOLCHAIN_PROVIDER:-openshell-dev-toolchains}" +PROVIDERS="${OPENSHELL_DEVSHELL_PROVIDERS-github codex claude ${DEVSHELL_PROVIDER}}" +REPO="${OPENSHELL_DEVSHELL_REPO:-}" + +usage() { + cat <<'USAGE' +Usage: + scripts/devshell Create or connect to the dev sandbox + scripts/devshell -- Run a command in the dev sandbox + scripts/devshell recreate Delete and recreate the dev sandbox + scripts/devshell delete Delete the dev sandbox + scripts/devshell policy Print the optional policy override path + scripts/devshell profiles Print the dev provider profile directory + +Environment: + OPENSHELL_DEVSHELL_IMAGE Sandbox image (default: openshell/ci:dev) + OPENSHELL_DEVSHELL_REPO GitHub repo slug or URL (default: origin remote) + OPENSHELL_DEVSHELL_PROVIDERS Provider names/types to attach + (default: github codex claude openshell-dev-toolchains) + OPENSHELL_DEVSHELL_POLICY Optional policy file override (default: none) + OPENSHELL_DEVSHELL_PROFILE_DIR Provider profiles to import for Providers v2 + OPENSHELL_DEVSHELL_BOOTSTRAP Fallback bootstrap helper for images that do not include one +USAGE +} + +fail() { + printf 'ERROR: %s\n' "$*" >&2 + exit 1 +} + +openshell() { + "$OPENSHELL_BIN" "$@" +} + +normalize_repo() { + local input=$1 + input="${input%.git}" + case "$input" in + https://github.com/*/*) input="${input#https://github.com/}" ;; + http://github.com/*/*) input="${input#http://github.com/}" ;; + git@github.com:*) input="${input#git@github.com:}" ;; + ssh://git@github.com/*/*) input="${input#ssh://git@github.com/}" ;; + esac + printf '%s\n' "$input" +} + +repo_slug() { + local repo=$REPO + if [[ -z "$repo" ]]; then + repo="$(git -C "$ROOT" config --get remote.origin.url 2>/dev/null || true)" + fi + repo="$(normalize_repo "$repo")" + [[ -n "$repo" ]] || repo="NVIDIA/OpenShell" + [[ "$repo" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] \ + || fail "OPENSHELL_DEVSHELL_REPO must be a GitHub owner/repo slug or URL, got '$repo'" + printf '%s\n' "$repo" +} + +provider_args() { + local provider + for provider in $PROVIDERS; do + printf '%s\n' "--provider" + printf '%s\n' "$provider" + done +} + +policy_args() { + if [[ -n "$POLICY_FILE" ]]; then + printf '%s\n' "--policy" + printf '%s\n' "$POLICY_FILE" + fi +} + +profile_id_from_file() { + sed -n -E 's/^id:[[:space:]]*"?([^"#]+)"?[[:space:]]*$/\1/p' "$1" | head -n 1 +} + +ensure_provider_profile() { + local file=$1 + local id + id="$(profile_id_from_file "$file")" + [[ -n "$id" ]] || fail "provider profile '$file' is missing an id" + if openshell provider profile export "$id" -o json >/dev/null 2>&1; then + return + fi + openshell provider profile import -f "$file" >/dev/null +} + +ensure_provider_profiles_v2() { + local file + for file in "$PROFILE_DIR"/*.yaml "$PROFILE_DIR"/*.yml "$PROFILE_DIR"/*.json; do + [[ -f "$file" ]] || continue + ensure_provider_profile "$file" + done + openshell settings set --global --key providers_v2_enabled --value true --yes >/dev/null +} + +ensure_devshell_provider() { + if openshell provider get "$DEVSHELL_PROVIDER" >/dev/null 2>&1; then + return + fi + openshell provider create \ + --name "$DEVSHELL_PROVIDER" \ + --type "$DEVSHELL_PROVIDER" \ + --credential OPENSHELL_DEVSHELL_ACCESS=1 \ + >/dev/null +} + +attach_devshell_provider() { + openshell sandbox provider attach "$SANDBOX_NAME" "$DEVSHELL_PROVIDER" >/dev/null +} + +quote_command() { + local quoted=() + local item q + for item in "$@"; do + printf -v q '%q' "$item" + quoted+=("$q") + done + printf '%s\n' "${quoted[*]}" +} + +base64_file() { + base64 < "$1" | tr -d '\n' +} + +bootstrap_command() { + local repo=$1 + local command_string=$2 + local bootstrap_b64 + bootstrap_b64="$(base64_file "$BOOTSTRAP_FILE")" + { + printf 'set -euo pipefail\n' + printf 'export OPENSHELL_DEVSHELL_REPO_SLUG=%q\n' "$repo" + printf 'set -- %s\n' "$command_string" + cat <<'EOF' +if command -v openshell-devshell-bootstrap >/dev/null 2>&1; then + exec openshell-devshell-bootstrap "$@" +fi +mkdir -p /sandbox +EOF + printf 'printf %%s %q | base64 -d > /sandbox/.openshell-devshell-bootstrap\n' "$bootstrap_b64" + cat <<'EOF' +chmod +x /sandbox/.openshell-devshell-bootstrap +exec /sandbox/.openshell-devshell-bootstrap "$@" +EOF + } +} + +bash_script_arg() { + local script=$1 + local encoded + encoded="$(printf '%s' "$script" | base64 | tr -d '\n')" + # shellcheck disable=SC2016 + printf 'eval "$(printf %%s %q | base64 -d)"\n' "$encoded" +} + +run_in_sandbox() { + local repo command_string script + repo="$(repo_slug)" + command_string="$(quote_command "$@")" + script="$(bootstrap_command "$repo" "$command_string")" + script="$(bash_script_arg "$script")" + openshell sandbox exec -n "$SANDBOX_NAME" --workdir /sandbox --tty -- /bin/bash -lc "$script" +} + +main() { + [[ -x "$OPENSHELL_BIN" ]] || command -v "$OPENSHELL_BIN" >/dev/null 2>&1 \ + || fail "openshell binary not found: $OPENSHELL_BIN" + [[ -z "$POLICY_FILE" || -f "$POLICY_FILE" ]] || fail "policy file not found: $POLICY_FILE" + [[ -f "$BOOTSTRAP_FILE" ]] || fail "bootstrap helper not found: $BOOTSTRAP_FILE" + [[ -d "$PROFILE_DIR" ]] || fail "provider profile directory not found: $PROFILE_DIR" + + local subcommand=connect + if [[ $# -gt 0 ]]; then + case "$1" in + -h|--help|help) + usage + exit 0 + ;; + connect|recreate|delete|policy|profiles) + subcommand=$1 + shift + ;; + --) + shift + ;; + esac + fi + if [[ $# -gt 0 && "$1" == "--" ]]; then + shift + fi + + case "$subcommand" in + policy) + if [[ -n "$POLICY_FILE" ]]; then + printf '%s\n' "$POLICY_FILE" + else + printf 'no policy override configured; using Providers v2 profiles from %s\n' "$PROFILE_DIR" + fi + exit 0 + ;; + profiles) + printf '%s\n' "$PROFILE_DIR" + exit 0 + ;; + delete) + openshell sandbox delete "$SANDBOX_NAME" + exit $? + ;; + esac + + local command=(/bin/bash -l) + if [[ $# -gt 0 ]]; then + command=("$@") + fi + + ensure_provider_profiles_v2 + ensure_devshell_provider + + if openshell sandbox get "$SANDBOX_NAME" >/dev/null 2>&1; then + if [[ "$subcommand" == recreate ]]; then + openshell sandbox delete "$SANDBOX_NAME" + else + attach_devshell_provider + run_in_sandbox "${command[@]}" + exit $? + fi + fi + + if [[ -z "${GITHUB_TOKEN:-}" && -z "${GH_TOKEN:-}" ]] && command -v gh >/dev/null 2>&1; then + GH_TOKEN="$(gh auth token 2>/dev/null || true)" + export GH_TOKEN + fi + + local providers=() + while IFS= read -r arg; do + providers+=("$arg") + done < <(provider_args) + local policy=() + while IFS= read -r arg; do + policy+=("$arg") + done < <(policy_args) + + local repo command_string script + repo="$(repo_slug)" + command_string="$(quote_command "${command[@]}")" + script="$(bootstrap_command "$repo" "$command_string")" + script="$(bash_script_arg "$script")" + + openshell sandbox create \ + --name "$SANDBOX_NAME" \ + --from "$SANDBOX_IMAGE" \ + "${policy[@]}" \ + "${providers[@]}" \ + --auto-providers \ + --tty \ + -- /bin/bash -lc "$script" +} + +main "$@" diff --git a/scripts/devshell.profiles/codex.yaml b/scripts/devshell.profiles/codex.yaml new file mode 100644 index 000000000..2e0b841a4 --- /dev/null +++ b/scripts/devshell.profiles/codex.yaml @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +id: codex +display_name: Codex +description: Codex CLI access for OpenShell development sandboxes +category: agent +inference_capable: true +credentials: + - name: api_key + description: OpenAI API key used by Codex + env_vars: [OPENAI_API_KEY] + required: false + auth_style: bearer + header_name: authorization +discovery: + credentials: [api_key] +endpoints: + - host: api.openai.com + port: 443 + protocol: rest + access: full + enforcement: enforce + - host: auth.openai.com + port: 443 + protocol: rest + access: full + enforcement: enforce + - host: chatgpt.com + port: 443 + protocol: rest + access: full + enforcement: enforce + - host: ab.chatgpt.com + port: 443 + protocol: rest + access: full + enforcement: enforce +binaries: + - /usr/bin/codex + - /usr/local/bin/codex + - /usr/bin/node + - /usr/local/bin/node diff --git a/scripts/devshell.profiles/openshell-dev-toolchains.yaml b/scripts/devshell.profiles/openshell-dev-toolchains.yaml new file mode 100644 index 000000000..74d5cc6b6 --- /dev/null +++ b/scripts/devshell.profiles/openshell-dev-toolchains.yaml @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +id: openshell-dev-toolchains +display_name: OpenShell Dev Toolchains +description: Network access for OpenShell development toolchain bootstrap +category: other +credentials: + - name: activation + description: Non-secret marker credential used to attach this policy bundle + env_vars: [OPENSHELL_DEVSHELL_ACCESS] + required: false +discovery: + credentials: [activation] +endpoints: + - { host: github.com, port: 443 } + - { host: api.github.com, port: 443 } + - { host: raw.githubusercontent.com, port: 443 } + - { host: objects.githubusercontent.com, port: 443 } + - { host: codeload.github.com, port: 443 } + - { host: release-assets.githubusercontent.com, port: 443 } + - { host: crates.io, port: 443 } + - { host: index.crates.io, port: 443 } + - { host: static.crates.io, port: 443 } + - { host: static.rust-lang.org, port: 443 } + - { host: sh.rustup.rs, port: 443 } + - { host: pypi.org, port: 443 } + - { host: files.pythonhosted.org, port: 443 } + - { host: astral.sh, port: 443 } + - host: registry.npmjs.org + port: 443 + protocol: rest + enforcement: enforce + access: read-only + allow_encoded_slash: true + - { host: nodejs.org, port: 443 } + - { host: dl.k8s.io, port: 443 } + - { host: get.helm.sh, port: 443 } + - { host: storage.googleapis.com, port: 443 } + - { host: ziglang.org, port: 443 } + - { host: zig.linus.dev, port: 443 } + - { host: zig.tilok.dev, port: 443 } + - { host: zig.chainsafe.dev, port: 443 } + - { host: mise.run, port: 443 } + - { host: mise.en.dev, port: 443 } + - { host: mise.jdx.dev, port: 443 } + - { host: mise-versions.jdx.dev, port: 443 } + - { host: fulcio.sigstore.dev, port: 443 } + - { host: rekor.sigstore.dev, port: 443 } + - { host: tuf-repo-cdn.sigstore.dev, port: 443 } + - { host: oauth2.sigstore.dev, port: 443 } + - { host: ports.ubuntu.com, port: 80 } + - { host: ports.ubuntu.com, port: 443 } + - { host: archive.ubuntu.com, port: 80 } + - { host: archive.ubuntu.com, port: 443 } + - { host: security.ubuntu.com, port: 80 } + - { host: security.ubuntu.com, port: 443 } +binaries: + - /usr/local/bin/mise + - /usr/local/share/openshell/mise/** + - /sandbox/.local/bin/mise + - /sandbox/.local/share/mise/** + - /sandbox/.cargo/** + - /usr/bin/git + - /usr/local/bin/git + - /usr/lib/git-core/git-remote-http + - /usr/bin/curl + - /usr/local/bin/curl + - /usr/bin/python3 + - /usr/local/bin/python3 + - /usr/bin/node + - /usr/local/bin/node + - /usr/bin/npm + - /usr/local/bin/npm + - /usr/bin/apt-get + - /usr/bin/apt-cache + - /usr/lib/apt/** diff --git a/tasks/scripts/docker-build-ci.sh b/tasks/scripts/docker-build-ci.sh index 56ff7148f..14fb72f7d 100755 --- a/tasks/scripts/docker-build-ci.sh +++ b/tasks/scripts/docker-build-ci.sh @@ -9,6 +9,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 source "${SCRIPT_DIR}/container-engine.sh" OUTPUT_ARGS=(--load) @@ -20,12 +21,12 @@ fi SECRET_ARGS=() if [[ -n "${MISE_GITHUB_TOKEN:-}" ]]; then - SECRET_ARGS=(--secret id=MISE_GITHUB_TOKEN,env=MISE_GITHUB_TOKEN) + SECRET_ARGS=(--secret "id=MISE_GITHUB_TOKEN,env=MISE_GITHUB_TOKEN") elif [[ -n "${GITHUB_TOKEN:-}" ]]; then - SECRET_ARGS=(--secret id=MISE_GITHUB_TOKEN,env=GITHUB_TOKEN) + SECRET_ARGS=(--secret "id=MISE_GITHUB_TOKEN,env=GITHUB_TOKEN") fi -exec ce_build \ +ce_build \ ${DOCKER_BUILDER:+--builder ${DOCKER_BUILDER}} \ ${DOCKER_PLATFORM:+--platform ${DOCKER_PLATFORM}} \ ${SECRET_ARGS[@]+"${SECRET_ARGS[@]}"} \ diff --git a/tasks/scripts/gateway-vm.sh b/tasks/scripts/gateway-vm.sh index 22ba1b039..8d25693eb 100755 --- a/tasks/scripts/gateway-vm.sh +++ b/tasks/scripts/gateway-vm.sh @@ -22,6 +22,8 @@ # OPENSHELL_VM_GATEWAY_NAME=my-vm-gateway mise run gateway:vm # OPENSHELL_SANDBOX_NAMESPACE=my-ns mise run gateway:vm # OPENSHELL_SANDBOX_IMAGE=ghcr.io/... mise run gateway:vm +# OPENSHELL_VM_DRIVER_VCPUS=6 OPENSHELL_VM_DRIVER_MEM_MIB=12288 \ +# OPENSHELL_VM_OVERLAY_DISK_MIB=32768 mise run gateway:vm # mise run gateway:vm -- --gpu # # This script also writes ~/.config/openshell/active_gateway so the @@ -39,7 +41,6 @@ STATE_DIR="${OPENSHELL_VM_GATEWAY_STATE_DIR:-${ROOT}/.cache/gateway-vm}" SANDBOX_NAMESPACE="${OPENSHELL_SANDBOX_NAMESPACE:-vm-dev}" SANDBOX_IMAGE="${OPENSHELL_SANDBOX_IMAGE:-${COMMUNITY_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-community/sandboxes/base:latest}}" VM_BOOTSTRAP_IMAGE="${OPENSHELL_VM_BOOTSTRAP_IMAGE:-}" -SANDBOX_IMAGE_PULL_POLICY="${OPENSHELL_SANDBOX_IMAGE_PULL_POLICY:-IfNotPresent}" LOG_LEVEL="${OPENSHELL_LOG_LEVEL:-info}" GATEWAY_BIN="${ROOT}/target/debug/openshell-gateway" DRIVER_DIR_DEFAULT="${ROOT}/target/debug" @@ -70,6 +71,16 @@ normalize_bool() { esac } +positive_int() { + local name=$1 + local val=$2 + if [[ ! "${val}" =~ ^[1-9][0-9]*$ ]]; then + echo "ERROR: ${name} must be a positive integer, got '${val}'" >&2 + exit 2 + fi + printf '%s\n' "${val}" +} + port_is_in_use() { local port=$1 if command -v lsof >/dev/null 2>&1; then @@ -209,6 +220,9 @@ check_supervisor_cross_toolchain() { } VM_GPU="$(normalize_bool "${OPENSHELL_VM_GPU:-false}")" +VM_DRIVER_VCPUS="$(positive_int OPENSHELL_VM_DRIVER_VCPUS "${OPENSHELL_VM_DRIVER_VCPUS:-4}")" +VM_DRIVER_MEM_MIB="$(positive_int OPENSHELL_VM_DRIVER_MEM_MIB "${OPENSHELL_VM_DRIVER_MEM_MIB:-8192}")" +VM_OVERLAY_DISK_MIB="$(positive_int OPENSHELL_VM_OVERLAY_DISK_MIB "${OPENSHELL_VM_OVERLAY_DISK_MIB:-32768}")" while [ "$#" -gt 0 ]; do case "$1" in @@ -343,6 +357,9 @@ bootstrap_image = "${VM_BOOTSTRAP_IMAGE}" grpc_endpoint = "${GRPC_ENDPOINT}" driver_dir = "${DRIVER_DIR}" state_dir = "${VM_DRIVER_STATE_DIR}" +vcpus = ${VM_DRIVER_VCPUS} +mem_mib = ${VM_DRIVER_MEM_MIB} +overlay_disk_mib = ${VM_OVERLAY_DISK_MIB} EOF GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" @@ -356,6 +373,7 @@ echo " namespace: ${SANDBOX_NAMESPACE}" echo " state dir: ${STATE_DIR}" echo " driver: ${DRIVER_DIR}/openshell-driver-vm" echo " driver dir: ${VM_DRIVER_STATE_DIR}" +echo " vm size: ${VM_DRIVER_VCPUS} vCPU / ${VM_DRIVER_MEM_MIB} MiB RAM / ${VM_OVERLAY_DISK_MIB} MiB overlay" echo " gpu: ${VM_GPU}" echo " image: ${SANDBOX_IMAGE}" echo