feat(devshell): add provider-backed VM dev sandbox

This commit is contained in:
Drew Newberry
2026-05-27 00:19:04 -07:00
parent fa84e437ab
commit 5bb970a7cc
12 changed files with 956 additions and 53 deletions
+7
View File
@@ -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
```
@@ -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
+33 -16
View File
@@ -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<String, Status> {
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<PreparedImageDisk, Status> {
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 },
},
)
+73 -5
View File
@@ -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<u64, String> {
Ok(round_up_to_mib(size))
}
fn e2fsck_status_is_successful(code: Option<i32>) -> 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<u64, String> {
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()
+77 -14
View File
@@ -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<PathBuf>,
identity_cache: &BinaryIdentityCache,
bin_path: &Path,
owner_pid: u32,
) -> std::result::Result<Vec<PathBuf>, 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/<entrypoint_pid>/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).
+39 -14
View File
@@ -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
+303
View File
@@ -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 "$@"
+277
View File
@@ -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 -- <command...> 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 "$@"
+43
View File
@@ -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
@@ -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/**
+4 -3
View File
@@ -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[@]}"} \
+19 -1
View File
@@ -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