mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-16 05:20:22 +08:00
feat(devshell): add provider-backed VM dev sandbox
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Executable
+303
@@ -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 "$@"
|
||||
Executable
+277
@@ -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 "$@"
|
||||
@@ -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/**
|
||||
@@ -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[@]}"} \
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user