mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-17 05:31:16 +08:00
* feat(podman): add Podman compute driver for rootless sandbox management Adds openshell-driver-podman, a new compute driver that manages OpenShell sandboxes as rootless Podman containers via the Podman REST API over a Unix socket. Enables local workstation sandboxes without Kubernetes. Driver features: - Bridge networking with ephemeral host-port mapping for rootless SSH reachability - Named volumes for workspace storage, Podman native health checks, GPU via CDI - Supervisor binary sideloaded via image volume mount (BYOC-compatible) - SSH handshake secret injected via Podman secrets API (not plaintext env) - Typed ContainerSpec structs, input validation, and path-traversal guards - Cgroups v2 required; fails fast on v1 hosts - Bounded event stream buffer; watch stream reconnection handled by server watch_loop - Graceful shutdown and standalone driver binary with gRPC bridge Rootless-specific fixes: - Skip drop_privileges when user namespace lacks SETUID/SETGID/DAC_READ_SEARCH caps - Add /run/netns tmpfs mount for ip netns in rootless containers - Use secret_env map (not secrets array) for env-var injection in libpod API - Resolve SSH endpoint to 127.0.0.1:<host_port> instead of unreachable bridge IP Server/sandbox hardening: - Split loopback and link-local SSRF gates; Podman/VM drivers allow loopback - Close SSRF bypass in SSH tunnel Host path by resolving DNS before connecting - Prevent OPENSHELL_* env var override by user-supplied spec environment maps - Disable SQLite pool idle_timeout/max_lifetime for in-memory databases - Emit deleted_event on 404-during-inspect instead of regressing sandbox phase - Key delete cleanup by stable sandbox_id to survive container label drift CLI fixes: - Restore --name as a named flag on sandbox create (not positional) - Fix exec command arg parsing to not consume sandboxed-command flags - Propagate SSH verbosity via OPENSHELL_SSH_LOG_LEVEL Build tooling: - Add tasks/scripts/container-engine.sh: auto-detects Podman or Docker, exposes unified ce_* helpers; all build/cluster/VM scripts updated to use it - Add docker:build:supervisor mise task for standalone supervisor image - Add openshell-driver-podman to Dockerfile.images pre-fetch/build stages - Add e2e/rust/e2e-podman.sh and e2e:podman mise task for full lifecycle testing Signed-off-by: Adam Miller <admiller@redhat.com> * fix(driver-podman): derive grpc endpoint from server bind port When a user starts the gateway on a non-default port (e.g. --port 8081), sandbox containers were receiving OPENSHELL_ENDPOINT pointing at the default port 8080. The driver's auto-detection fallback read OPENSHELL_BIND_ADDRESS from the environment, which was stale or unset, and fell back to DEFAULT_SERVER_PORT. Add gateway_port to PodmanComputeConfig and thread config.bind_address.port() from the server into the driver so the fallback uses the actual listening port. Remove the OPENSHELL_BIND_ADDRESS env var read and the extract_port_from_bind_address helper which are no longer needed. Add --gateway-port / OPENSHELL_GATEWAY_PORT to the standalone driver binary for parity when the driver is run outside the embedded server path. Signed-off-by: Adam Miller <admiller@redhat.com> * fix(driver-podman): address PR feedback on env test safety and cluster DNS docs Replace hand-rolled unsafe TempEnvVar RAII guard with temp_env::with_vars and a static ENV_LOCK mutex, fixing a data race in parallel test execution. The prior safety comment incorrectly claimed Cargo runs tests single-threaded. Update debug-openshell-cluster skill to accurately document the DNS proxy strategy (setup_dns_proxy + public DNS fallback) and clarify the separation between cluster DNS and sandbox agent DNS enforcement. Signed-off-by: Adam Miller <admiller@redhat.com> * fix(e2e): resolve CI failures in auth timeout, test harness, and formatting - Short-circuit browser_auth_flow when OPENSHELL_NO_BROWSER=1 instead of waiting the full 120s AUTH_TIMEOUT for a callback that never arrives - Add timeout to SandboxGuard::create() and create_with_upload() to prevent indefinite hangs (matches create_keep() which already had one) - Add missing '--' separator in no_proxy test before command args - Add #![cfg(feature = "e2e")] gate to sandbox_lifecycle.rs - Run cargo fmt on openshell-driver-podman - Refine cluster DNS docs for Podman in debug-openshell-cluster skill Signed-off-by: Adam Miller <admiller@redhat.com> * refactor(server): remove allows_loopback_endpoints from ComputeRuntime SSRF protection is now handled at the network and proxy layers (openshell-core net.rs, openshell-sandbox proxy.rs) rather than requiring per-driver flags on ComputeRuntime. Update architecture docs to reflect supervisor relay SSH transport and add rootless networking deep-dive. Signed-off-by: Adam Miller <admiller@redhat.com> --------- Signed-off-by: Adam Miller <admiller@redhat.com>
272 lines
8.2 KiB
TOML
272 lines
8.2 KiB
TOML
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# Python development, build, lint, and format tasks
|
|
|
|
["python:dev"]
|
|
description = "Install Python package in development mode (builds CLI binary)"
|
|
depends = ["python:proto"]
|
|
run = "uv sync --group dev && uv pip install ."
|
|
hide = true
|
|
|
|
["build:python:wheel"]
|
|
description = "Build Python wheel with CLI binary (native)"
|
|
depends = ["python:proto"]
|
|
run = "uv run maturin build --release"
|
|
hide = true
|
|
|
|
["python:build"]
|
|
description = "Alias for build:python:wheel"
|
|
depends = ["build:python:wheel"]
|
|
hide = true
|
|
|
|
["build:python:wheel:linux"]
|
|
description = "Build a Linux Python wheel natively on the current host"
|
|
depends = ["python:proto"]
|
|
run = '''
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
WHEEL_OUTPUT_DIR=${WHEEL_OUTPUT_DIR:?Set WHEEL_OUTPUT_DIR to a per-platform wheel output directory}
|
|
EXPECTED_HOST_ARCH=${EXPECTED_HOST_ARCH:?Set EXPECTED_HOST_ARCH to amd64 or arm64}
|
|
ORIGINAL_CARGO_TOML=""
|
|
|
|
normalize_arch() {
|
|
case "$1" in
|
|
x86_64|amd64) echo amd64 ;;
|
|
aarch64|arm64) echo arm64 ;;
|
|
*) echo "$1" ;;
|
|
esac
|
|
}
|
|
|
|
cleanup() {
|
|
if [ -n "$ORIGINAL_CARGO_TOML" ] && [ -f "$ORIGINAL_CARGO_TOML" ]; then
|
|
cp "$ORIGINAL_CARGO_TOML" Cargo.toml
|
|
rm -f "$ORIGINAL_CARGO_TOML"
|
|
fi
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
if [ "$(uname -s)" != "Linux" ]; then
|
|
echo "build:python:wheel:linux requires a Linux host" >&2
|
|
exit 1
|
|
fi
|
|
|
|
HOST_ARCH=$(normalize_arch "$(uname -m)")
|
|
EXPECTED_HOST_ARCH=$(normalize_arch "$EXPECTED_HOST_ARCH")
|
|
if [ "$HOST_ARCH" != "$EXPECTED_HOST_ARCH" ]; then
|
|
echo "build:python:wheel:linux expected ${EXPECTED_HOST_ARCH} host, got ${HOST_ARCH}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Avoid failures when RUSTC_WRAPPER is set to an unresolved `sccache` binary.
|
|
if command -v sccache >/dev/null 2>&1; then
|
|
export RUSTC_WRAPPER="$(command -v sccache)"
|
|
else
|
|
unset RUSTC_WRAPPER || true
|
|
fi
|
|
|
|
if [ -n "${OPENSHELL_CARGO_VERSION:-}" ]; then
|
|
ORIGINAL_CARGO_TOML=$(mktemp)
|
|
cp Cargo.toml "$ORIGINAL_CARGO_TOML"
|
|
sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${OPENSHELL_CARGO_VERSION}"'"/}' Cargo.toml
|
|
fi
|
|
|
|
rm -rf "$WHEEL_OUTPUT_DIR"
|
|
mkdir -p "$WHEEL_OUTPUT_DIR"
|
|
|
|
uv run maturin build --release --features bundled-z3 --out "$WHEEL_OUTPUT_DIR"
|
|
|
|
ls -la "$WHEEL_OUTPUT_DIR"/*.whl
|
|
'''
|
|
hide = true
|
|
|
|
["build:python:wheel:linux:amd64"]
|
|
description = "Build Python wheel for Linux amd64 natively"
|
|
depends = ["EXPECTED_HOST_ARCH=amd64 WHEEL_OUTPUT_DIR=target/wheels/linux-amd64 build:python:wheel:linux"]
|
|
hide = true
|
|
|
|
["python:build:linux:amd64"]
|
|
description = "Alias for build:python:wheel:linux:amd64"
|
|
depends = ["build:python:wheel:linux:amd64"]
|
|
hide = true
|
|
|
|
["build:python:wheel:linux:arm64"]
|
|
description = "Build Python wheel for Linux arm64 natively"
|
|
depends = ["EXPECTED_HOST_ARCH=arm64 WHEEL_OUTPUT_DIR=target/wheels/linux-arm64 build:python:wheel:linux"]
|
|
hide = true
|
|
|
|
["python:build:linux:arm64"]
|
|
description = "Alias for build:python:wheel:linux:arm64"
|
|
depends = ["build:python:wheel:linux:arm64"]
|
|
hide = true
|
|
|
|
["build:python:wheel:macos"]
|
|
description = "Build Python wheel for macOS arm64"
|
|
depends = ["python:proto"]
|
|
run = """
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
if [ "$(uname -s)" = "Darwin" ] && [ -z "${OPENSHELL_CARGO_VERSION:-}" ]; then
|
|
# Avoid failures when RUSTC_WRAPPER is set to an unresolved `sccache` binary.
|
|
if command -v sccache >/dev/null 2>&1; then
|
|
export RUSTC_WRAPPER="$(command -v sccache)"
|
|
else
|
|
unset RUSTC_WRAPPER || true
|
|
fi
|
|
|
|
mkdir -p target/wheels
|
|
|
|
rustup target add aarch64-apple-darwin
|
|
|
|
uv run maturin build --release --target aarch64-apple-darwin --out target/wheels
|
|
else
|
|
echo "Building macOS wheel via Docker cross-toolchain"
|
|
mise run build:python:wheel:macos:docker
|
|
fi
|
|
|
|
ls -la target/wheels/*.whl
|
|
"""
|
|
hide = true
|
|
|
|
["python:build:macos"]
|
|
description = "Alias for build:python:wheel:macos"
|
|
depends = ["build:python:wheel:macos"]
|
|
hide = true
|
|
|
|
["build:python:wheel:macos:docker"]
|
|
description = "Build Python wheel for macOS arm64 from Docker"
|
|
depends = ["python:proto"]
|
|
run = """
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
source tasks/scripts/container-engine.sh
|
|
|
|
sha256_16() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$1" | awk '{print substr($1, 1, 16)}'
|
|
else
|
|
shasum -a 256 "$1" | awk '{print substr($1, 1, 16)}'
|
|
fi
|
|
}
|
|
|
|
sha256_16_stdin() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum | awk '{print substr($1, 1, 16)}'
|
|
else
|
|
shasum -a 256 | awk '{print substr($1, 1, 16)}'
|
|
fi
|
|
}
|
|
|
|
CARGO_VERSION=${OPENSHELL_CARGO_VERSION:-}
|
|
OSXCROSS_IMAGE_REF=${OSXCROSS_IMAGE:-crazymax/osxcross:latest}
|
|
if [ -z "$CARGO_VERSION" ] && [ -n "${CI:-}" ]; then
|
|
CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo)
|
|
fi
|
|
|
|
LOCK_HASH=$(sha256_16 Cargo.lock)
|
|
RUST_SCOPE=${RUST_TOOLCHAIN_SCOPE:-rustup-stable}
|
|
CACHE_SCOPE_INPUT="v1|python-wheels-macos|base|${LOCK_HASH}|${RUST_SCOPE}"
|
|
CARGO_TARGET_CACHE_SCOPE=$(printf '%s' "$CACHE_SCOPE_INPUT" | sha256_16_stdin)
|
|
|
|
mkdir -p target/wheels
|
|
|
|
ce build \
|
|
-f deploy/docker/Dockerfile.python-wheels-macos \
|
|
--target wheels \
|
|
--build-arg "OSXCROSS_IMAGE=${OSXCROSS_IMAGE_REF}" \
|
|
--build-arg "CARGO_TARGET_CACHE_SCOPE=${CARGO_TARGET_CACHE_SCOPE}" \
|
|
${CARGO_VERSION:+--build-arg "OPENSHELL_CARGO_VERSION=${CARGO_VERSION}"} \
|
|
${OPENSHELL_IMAGE_TAG:+--build-arg "OPENSHELL_IMAGE_TAG=${OPENSHELL_IMAGE_TAG}"} \
|
|
--output type=local,dest=target/wheels \
|
|
.
|
|
|
|
ls -la target/wheels/*macosx*arm64.whl
|
|
"""
|
|
hide = true
|
|
|
|
["python:build:macos:docker"]
|
|
description = "Alias for build:python:wheel:macos:docker"
|
|
depends = ["build:python:wheel:macos:docker"]
|
|
hide = true
|
|
|
|
["python:lint"]
|
|
description = "Lint Python code with ruff"
|
|
depends = ["python:proto"]
|
|
env = { UV_NO_SYNC = "1" }
|
|
run = "uv run ruff check {{vars.python_paths}}"
|
|
hide = true
|
|
|
|
["python:format"]
|
|
description = "Format Python code with ruff"
|
|
run = "uv run ruff format {{vars.python_paths}}"
|
|
hide = true
|
|
|
|
["python:format:check"]
|
|
description = "Check Python formatting with ruff"
|
|
run = "uv run ruff format --check {{vars.python_paths}}"
|
|
hide = true
|
|
|
|
["python:typecheck"]
|
|
description = "Type check Python code with ty"
|
|
depends = ["python:proto"]
|
|
run = "uv run ty check {{vars.python_paths}}"
|
|
hide = true
|
|
|
|
["python:proto"]
|
|
description = "Generate Python protobuf stubs from .proto files"
|
|
env = { UV_NO_SYNC = "1" }
|
|
run = """
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
uv run python -m grpc_tools.protoc \
|
|
-Iproto \
|
|
--python_out=python/openshell/_proto \
|
|
--pyi_out=python/openshell/_proto \
|
|
--grpc_python_out=python/openshell/_proto \
|
|
proto/inference.proto \
|
|
proto/openshell.proto \
|
|
proto/datamodel.proto \
|
|
proto/sandbox.proto
|
|
# Fix absolute imports in generated stubs to use package-relative imports
|
|
uv run python - <<'PY'
|
|
from pathlib import Path
|
|
import re
|
|
|
|
line_rewrites = {
|
|
"python/openshell/_proto/inference_pb2_grpc.py": [
|
|
(r"^import inference_pb2 as inference__pb2$", "from . import inference_pb2 as inference__pb2"),
|
|
],
|
|
"python/openshell/_proto/openshell_pb2_grpc.py": [
|
|
(r"^import openshell_pb2 as openshell__pb2$", "from . import openshell_pb2 as openshell__pb2"),
|
|
(r"^import sandbox_pb2 as sandbox__pb2$", "from . import sandbox_pb2 as sandbox__pb2"),
|
|
],
|
|
"python/openshell/_proto/openshell_pb2.py": [
|
|
(r"^import datamodel_pb2 as datamodel__pb2$", "from . import datamodel_pb2 as datamodel__pb2"),
|
|
(r"^import sandbox_pb2 as sandbox__pb2$", "from . import sandbox_pb2 as sandbox__pb2"),
|
|
],
|
|
"python/openshell/_proto/datamodel_pb2.py": [
|
|
(r"^import sandbox_pb2 as sandbox__pb2$", "from . import sandbox_pb2 as sandbox__pb2"),
|
|
],
|
|
"python/openshell/_proto/datamodel_pb2_grpc.py": [
|
|
(r"^import datamodel_pb2 as datamodel__pb2$", "from . import datamodel_pb2 as datamodel__pb2"),
|
|
],
|
|
"python/openshell/_proto/sandbox_pb2_grpc.py": [
|
|
(r"^import sandbox_pb2 as sandbox__pb2$", "from . import sandbox_pb2 as sandbox__pb2"),
|
|
],
|
|
}
|
|
|
|
for path, rules in line_rewrites.items():
|
|
file_path = Path(path)
|
|
text = file_path.read_text()
|
|
text = text.replace("from . from . import", "from . import")
|
|
for pattern, replacement in rules:
|
|
text = re.sub(pattern, replacement, text, flags=re.MULTILINE)
|
|
file_path.write_text(text)
|
|
PY
|
|
"""
|
|
hide = true
|