mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-31 07:44:57 +08:00
feat(devshell): add provider-backed VM dev sandbox
This commit is contained in:
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/**
|
||||
Reference in New Issue
Block a user