From 7b068898579b3377c236e452a54a3e7c7a697e98 Mon Sep 17 00:00:00 2001 From: Jim Meyer Date: Tue, 1 Sep 2026 16:54:09 -0700 Subject: [PATCH 01/16] fix(build): raise file limit for native gateway builds Signed-off-by: Jim Meyer --- architecture/build.md | 13 ++++++------- tasks/gateway.toml | 2 +- tasks/scripts/build-env.sh | 24 ++++++++++-------------- tasks/scripts/gateway-docker.sh | 4 ++++ tasks/scripts/gateway-podman.sh | 4 ++++ tasks/scripts/gateway-vm.sh | 4 ++++ tasks/scripts/gateway.sh | 4 ++++ tasks/scripts/test-build-env.sh | 22 ++++------------------ 8 files changed, 37 insertions(+), 40 deletions(-) diff --git a/architecture/build.md b/architecture/build.md index 393eb9e468..18f1561f50 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -120,13 +120,12 @@ and the supervisor image from `deploy/docker/Dockerfile.supervisor`. Neither Dockerfile compiles Rust — both copy a staged binary out of `deploy/docker/.build/prebuilt-binaries//` into the final image. -Local binary staging is driven by `tasks/scripts/stage-prebuilt-binaries.sh`. Because -staging cross-compiles on the host, it sources `tasks/scripts/build-env.sh` and -raises the per-process open-file limit before invoking `cargo zigbuild` on -macOS — the static musl link opens hundreds of `.rlib` files at once and would -otherwise fail with `ProcessFdQuotaExceeded` under macOS's default soft limit of -256. The guard is a no-op on Linux and when `cargo-zigbuild` is absent. Gateway -binaries use `cargo zigbuild` with GNU targets pinned to glibc 2.28, including +Local binary staging is driven by `tasks/scripts/stage-prebuilt-binaries.sh`. +It and the local gateway tasks source `tasks/scripts/build-env.sh` before host +Rust builds on macOS. The helper raises the per-process open-file limit because +sccache and static musl linking can each open hundreds of files, exceeding the +default soft limit of 256. The guard is a no-op on Linux. Gateway binaries use +`cargo zigbuild` with GNU targets pinned to glibc 2.28, including native-architecture builds, so the gateway image, standalone tarballs, and Linux packages share the same host portability floor. The gateway build enables `bundled-z3`. Linux VM driver release artifacts use the same glibc floor so diff --git a/tasks/gateway.toml b/tasks/gateway.toml index bbf309c459..89a42eebc3 100644 --- a/tasks/gateway.toml +++ b/tasks/gateway.toml @@ -5,7 +5,7 @@ ["build:gateway"] description = "Build the standalone openshell-gateway binary" -run = "cargo build -p openshell-gateway --bin openshell-gateway" +run = "bash -c 'source tasks/scripts/build-env.sh && ensure_build_nofile_limit && cargo build -p openshell-gateway --bin openshell-gateway'" hide = true ["gateway"] diff --git a/tasks/scripts/build-env.sh b/tasks/scripts/build-env.sh index e2396d68f6..74da6009bd 100644 --- a/tasks/scripts/build-env.sh +++ b/tasks/scripts/build-env.sh @@ -3,21 +3,19 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Shared build-environment helpers for host-side cross-compilation. +# Shared build-environment helpers for host-side Rust builds. # # Source this file (do not execute it) and call the helpers before invoking -# cargo-zigbuild on the host. +# Cargo on the host. # ensure_build_nofile_limit raises the per-process open-file limit before a -# host cargo-zigbuild cross-compile. The static *-unknown-linux-musl link opens -# hundreds of .rlib files simultaneously, which exceeds macOS's default soft -# limit of 256 and fails with ProcessFdQuotaExceeded. The raised limit -# propagates to the cargo/zig children the caller spawns. +# host Rust build. Sccache and static *-unknown-linux-musl links can each open +# hundreds of files simultaneously, exceeding macOS's default soft limit of +# 256. The raised limit propagates to the Cargo children the caller spawns. # # The limit is read from OPENSHELL_BUILD_NOFILE_LIMIT (default 8192), honoring -# the legacy OPENSHELL_VM_BUILD_NOFILE_LIMIT for back-compat. This is a no-op on -# Linux and when cargo-zigbuild is not installed (native builds, CI Linux -# runners), so it must be safe to call unconditionally. +# the legacy OPENSHELL_VM_BUILD_NOFILE_LIMIT for back-compat. This is a no-op +# on Linux, so it must be safe to call unconditionally. ensure_build_nofile_limit() { local desired="${OPENSHELL_BUILD_NOFILE_LIMIT:-${OPENSHELL_VM_BUILD_NOFILE_LIMIT:-8192}}" local minimum=1024 @@ -26,8 +24,6 @@ ensure_build_nofile_limit() { local target="" [ "$(uname -s)" = "Darwin" ] || return 0 - command -v cargo-zigbuild >/dev/null 2>&1 || return 0 - current="$(ulimit -n 2>/dev/null || echo "")" case "${current}" in ''|*[!0-9]*) @@ -54,7 +50,7 @@ ensure_build_nofile_limit() { esac if [ "${target}" -gt "${current}" ] && ulimit -n "${target}" 2>/dev/null; then - echo "==> Raised open file limit for cargo-zigbuild: ${current} -> $(ulimit -n)" + echo "==> Raised open file limit for host Cargo build: ${current} -> $(ulimit -n)" fi current="$(ulimit -n 2>/dev/null || echo "${current}")" @@ -65,11 +61,11 @@ ensure_build_nofile_limit() { esac if [ "${current}" -lt "${desired}" ]; then - echo "WARNING: Open file limit is ${current}; cargo-zigbuild is more reliable at ${desired}+ on macOS." + echo "WARNING: Open file limit is ${current}; host Cargo builds are more reliable at ${desired}+ on macOS." fi if [ "${current}" -lt "${minimum}" ]; then - echo "ERROR: Open file limit (${current}) is too low for cargo-zigbuild on macOS." >&2 + echo "ERROR: Open file limit (${current}) is too low for host Cargo builds on macOS." >&2 echo " Run: ulimit -n ${desired}" >&2 echo " Then re-run this script." >&2 exit 1 diff --git a/tasks/scripts/gateway-docker.sh b/tasks/scripts/gateway-docker.sh index 6826829fd8..e561e3a906 100644 --- a/tasks/scripts/gateway-docker.sh +++ b/tasks/scripts/gateway-docker.sh @@ -24,6 +24,10 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=tasks/scripts/build-env.sh +source "${ROOT}/tasks/scripts/build-env.sh" +ensure_build_nofile_limit + PORT="${OPENSHELL_SERVER_PORT:-18080}" GATEWAY_NAME="${OPENSHELL_DOCKER_GATEWAY_NAME:-docker-dev}" STATE_DIR="${OPENSHELL_DOCKER_GATEWAY_STATE_DIR:-${ROOT}/.cache/gateway-docker}" diff --git a/tasks/scripts/gateway-podman.sh b/tasks/scripts/gateway-podman.sh index ab166865ef..3acd6b7089 100644 --- a/tasks/scripts/gateway-podman.sh +++ b/tasks/scripts/gateway-podman.sh @@ -21,6 +21,10 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=tasks/scripts/build-env.sh +source "${ROOT}/tasks/scripts/build-env.sh" +ensure_build_nofile_limit + PORT="${OPENSHELL_SERVER_PORT:-18080}" GATEWAY_NAME="${OPENSHELL_PODMAN_GATEWAY_NAME:-podman-dev}" STATE_DIR="${OPENSHELL_PODMAN_GATEWAY_STATE_DIR:-${OPENSHELL_GATEWAY_STATE_DIR:-${ROOT}/.cache/gateway-podman}}" diff --git a/tasks/scripts/gateway-vm.sh b/tasks/scripts/gateway-vm.sh index 3818dca364..f8020a238f 100755 --- a/tasks/scripts/gateway-vm.sh +++ b/tasks/scripts/gateway-vm.sh @@ -33,6 +33,10 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=tasks/scripts/build-env.sh +source "${ROOT}/tasks/scripts/build-env.sh" +ensure_build_nofile_limit + PORT="${OPENSHELL_SERVER_PORT:-18081}" GATEWAY_NAME="${OPENSHELL_VM_GATEWAY_NAME:-vm-dev}" STATE_DIR="${OPENSHELL_VM_GATEWAY_STATE_DIR:-${ROOT}/.cache/gateway-vm}" diff --git a/tasks/scripts/gateway.sh b/tasks/scripts/gateway.sh index 019d1b1b63..b1ad650e0b 100644 --- a/tasks/scripts/gateway.sh +++ b/tasks/scripts/gateway.sh @@ -18,6 +18,10 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" GATEWAY_BIN="${ROOT}/target/debug/openshell-gateway" +# shellcheck source=tasks/scripts/build-env.sh +source "${ROOT}/tasks/scripts/build-env.sh" +ensure_build_nofile_limit + usage() { cat <<'EOF' Usage: mise run gateway [-- --driver DRIVER] diff --git a/tasks/scripts/test-build-env.sh b/tasks/scripts/test-build-env.sh index 049ad369d1..be989a70e9 100755 --- a/tasks/scripts/test-build-env.sh +++ b/tasks/scripts/test-build-env.sh @@ -38,28 +38,14 @@ if [ "${os}" != "Darwin" ]; then exit 0 fi -# Darwin below. -if ! command -v cargo-zigbuild >/dev/null 2>&1; then - # Without cargo-zigbuild the helper must be a no-op even on macOS. - ( - ulimit -Sn 256 2>/dev/null || true - before="$(ulimit -n)" - ensure_build_nofile_limit >/dev/null - after="$(ulimit -n)" - [ "${before}" = "${after}" ] || fail "limit changed on macOS without cargo-zigbuild (${before} -> ${after})" - ) - pass "no-op on macOS without cargo-zigbuild" - echo "All build-env tests passed." - exit 0 -fi - -# Darwin + cargo-zigbuild: the helper should raise a low soft limit. +# Darwin native Cargo builds need the same protection even when +# cargo-zigbuild is unavailable. ( if ! ulimit -Sn 256 2>/dev/null; then echo "SKIP: unable to lower soft limit to 256 for test" exit 0 fi - ensure_build_nofile_limit >/dev/null + PATH="/usr/bin:/bin" ensure_build_nofile_limit >/dev/null after="$(ulimit -n)" hard="$(ulimit -Hn 2>/dev/null || echo unlimited)" case "${hard}" in @@ -76,7 +62,7 @@ fi ;; esac ) -pass "raises low soft limit on macOS with cargo-zigbuild" +pass "raises low soft limit for native Cargo builds on macOS" # Idempotent: when the current limit already meets the desired value, the helper # leaves it unchanged (drive this via the env override so it holds regardless of From c098eabdb3072908d915c7908e957d5093fb8254 Mon Sep 17 00:00:00 2001 From: Jim Meyer Date: Wed, 2 Sep 2026 16:44:17 -0700 Subject: [PATCH 02/16] Explicitly get and set soft limits --- tasks/scripts/build-env.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tasks/scripts/build-env.sh b/tasks/scripts/build-env.sh index 74da6009bd..cb9a462e1b 100644 --- a/tasks/scripts/build-env.sh +++ b/tasks/scripts/build-env.sh @@ -24,7 +24,7 @@ ensure_build_nofile_limit() { local target="" [ "$(uname -s)" = "Darwin" ] || return 0 - current="$(ulimit -n 2>/dev/null || echo "")" + current="$(ulimit -Sn 2>/dev/null || echo "")" case "${current}" in ''|*[!0-9]*) return 0 @@ -49,11 +49,11 @@ ensure_build_nofile_limit() { ;; esac - if [ "${target}" -gt "${current}" ] && ulimit -n "${target}" 2>/dev/null; then - echo "==> Raised open file limit for host Cargo build: ${current} -> $(ulimit -n)" + if [ "${target}" -gt "${current}" ] && ulimit -Sn "${target}" 2>/dev/null; then + echo "==> Raised open file limit for host Cargo build: ${current} -> $(ulimit -Sn)" fi - current="$(ulimit -n 2>/dev/null || echo "${current}")" + current="$(ulimit -Sn 2>/dev/null || echo "${current}")" case "${current}" in ''|*[!0-9]*) return 0 @@ -66,7 +66,7 @@ ensure_build_nofile_limit() { if [ "${current}" -lt "${minimum}" ]; then echo "ERROR: Open file limit (${current}) is too low for host Cargo builds on macOS." >&2 - echo " Run: ulimit -n ${desired}" >&2 + echo " Run: ulimit -Sn ${desired}" >&2 echo " Then re-run this script." >&2 exit 1 fi From 607a32c24fae7caf8408fd71df08216cf7c374ee Mon Sep 17 00:00:00 2001 From: Dhiraj Bokde Date: Wed, 2 Sep 2026 00:23:39 +0000 Subject: [PATCH 03/16] feat(helm): split gateway and workspace charts (#2643) * feat(helm): split gateway and workspace charts Signed-off-by: Dhiraj Bokde * fix(helm): preserve split chart upgrade compatibility Keep workspace manifests valid after value validation and default legacy reused values to the combined resource topology. * fix(ci): preserve VM runtime for E2E The Rust cache restores target/ after VM runtime artifacts are staged, overwriting target/vm-runtime-compressed before openshell-driver-vm is built. Stage the compressed runtime outside target and pass that location through OPENSHELL_VM_RUNTIME_COMPRESSED_DIR so build.rs can embed the supervisor. Also locate the Helm split-ownership test repository root from the script path rather than git rev-parse. The test runs in a container where the GitHub checkout can be owned by a different UID and rejected as dubious ownership. Signed-off-by: Dhiraj Bokde * fix(ci): install yq for Helm ownership test The split-chart ownership regression uses yq to inspect rendered YAML, but the Helm CI container installs only tools declared in mise. Declare and lock yq so mise install --locked provides the test dependency. Signed-off-by: Dhiraj Bokde --------- Signed-off-by: Dhiraj Bokde --- .../skills/debug-openshell-cluster/SKILL.md | 20 ++++++ .github/actions/release-helm-oci/action.yml | 63 +++++++++++------- architecture/compute-runtimes.md | 11 ++++ deploy/helm/openshell-workspace/Chart.yaml | 9 +++ deploy/helm/openshell-workspace/README.md | 47 +++++++++++++ .../helm/openshell-workspace/README.md.gotmpl | 33 ++++++++++ .../templates/_helpers.tpl | 44 +++++++++++++ .../templates/networkpolicy.yaml | 30 +++++++++ .../openshell-workspace/templates/role.yaml | 39 +++++++++++ .../templates/rolebinding.yaml | 19 ++++++ .../templates/serviceaccount.yaml | 17 +++++ .../tests/workspace_test.yaml | 61 +++++++++++++++++ deploy/helm/openshell-workspace/values.yaml | 31 +++++++++ deploy/helm/openshell/README.md | 9 +++ deploy/helm/openshell/README.md.gotmpl | 8 +++ deploy/helm/openshell/templates/_helpers.tpl | 13 ++++ .../openshell/templates/networkpolicy.yaml | 2 +- deploy/helm/openshell/templates/role.yaml | 2 +- .../helm/openshell/templates/rolebinding.yaml | 2 +- .../openshell/templates/serviceaccount.yaml | 4 +- .../tests/sandbox_namespace_test.yaml | 18 +++++ .../tests/sandbox_service_account_test.yaml | 10 +++ deploy/helm/openshell/values.yaml | 8 +++ deploy/helm/test-split-ownership.sh | 66 +++++++++++++++++++ docs/kubernetes/setup.mdx | 31 +++++++++ mise.lock | 28 ++++++++ mise.toml | 1 + tasks/helm.toml | 29 +++++--- 28 files changed, 620 insertions(+), 35 deletions(-) create mode 100644 deploy/helm/openshell-workspace/Chart.yaml create mode 100644 deploy/helm/openshell-workspace/README.md create mode 100644 deploy/helm/openshell-workspace/README.md.gotmpl create mode 100644 deploy/helm/openshell-workspace/templates/_helpers.tpl create mode 100644 deploy/helm/openshell-workspace/templates/networkpolicy.yaml create mode 100644 deploy/helm/openshell-workspace/templates/role.yaml create mode 100644 deploy/helm/openshell-workspace/templates/rolebinding.yaml create mode 100644 deploy/helm/openshell-workspace/templates/serviceaccount.yaml create mode 100644 deploy/helm/openshell-workspace/tests/workspace_test.yaml create mode 100644 deploy/helm/openshell-workspace/values.yaml create mode 100755 deploy/helm/test-split-ownership.sh diff --git a/.agents/skills/debug-openshell-cluster/SKILL.md b/.agents/skills/debug-openshell-cluster/SKILL.md index b73d152fc7..47588b1aeb 100644 --- a/.agents/skills/debug-openshell-cluster/SKILL.md +++ b/.agents/skills/debug-openshell-cluster/SKILL.md @@ -476,6 +476,26 @@ helm -n openshell get values openshell | grep sandboxNamespace Then inspect sandbox resources in that namespace. +For a split release, the gateway values should have +`workspaceResources.enabled=false`, and the target namespace should contain a +separate `openshell-workspace` release: + +```bash +helm -n openshell get values openshell | grep -A2 workspaceResources +helm -n status openshell-workspace +kubectl -n get serviceaccount,role,rolebinding,networkpolicy \ + -l app.kubernetes.io/instance=openshell-workspace +kubectl auth can-i create sandboxes.agents.x-k8s.io \ + --namespace \ + --as system:serviceaccount:openshell:openshell +``` + +If the gateway cannot create or watch sandboxes, verify the workspace +RoleBinding subject matches the gateway ServiceAccount name and namespace. +If SSH relay connections fail, verify the workspace NetworkPolicy selects the +gateway's actual `app.kubernetes.io/name` and +`app.kubernetes.io/instance` labels. + Check the configured sandbox service account when TokenReview bootstrap or sandbox registration fails. Helm creates a dedicated sandbox service account by default and writes it to `[openshell.drivers.kubernetes].service_account_name`; diff --git a/.github/actions/release-helm-oci/action.yml b/.github/actions/release-helm-oci/action.yml index d20691ad0f..46649e0cb0 100644 --- a/.github/actions/release-helm-oci/action.yml +++ b/.github/actions/release-helm-oci/action.yml @@ -4,7 +4,7 @@ name: Release Helm OCI description: > Patch chart version/appVersion, refuse duplicate OCI versions on public - releases, package the chart, and push to GHCR OCI. + releases, package the gateway and workspace charts, and push them to GHCR OCI. inputs: chart-version: @@ -51,11 +51,16 @@ runs: shell: bash run: | set -euo pipefail - CHART_DIR="${RUNNER_TEMP}/chart-build" - cp -a deploy/helm/openshell/. "${CHART_DIR}" - sed -i "s/^version:.*/version: ${CHART_VERSION}/" "${CHART_DIR}/Chart.yaml" - sed -i "s/^appVersion:.*/appVersion: \"${APP_VERSION}\"/" "${CHART_DIR}/Chart.yaml" - echo "chart_dir=${CHART_DIR}" >> "$GITHUB_OUTPUT" + GATEWAY_CHART_DIR="${RUNNER_TEMP}/gateway-chart-build" + WORKSPACE_CHART_DIR="${RUNNER_TEMP}/workspace-chart-build" + cp -a deploy/helm/openshell/. "${GATEWAY_CHART_DIR}" + cp -a deploy/helm/openshell-workspace/. "${WORKSPACE_CHART_DIR}" + for chart_dir in "${GATEWAY_CHART_DIR}" "${WORKSPACE_CHART_DIR}"; do + sed -i "s/^version:.*/version: ${CHART_VERSION}/" "${chart_dir}/Chart.yaml" + sed -i "s/^appVersion:.*/appVersion: \"${APP_VERSION}\"/" "${chart_dir}/Chart.yaml" + done + echo "gateway_chart_dir=${GATEWAY_CHART_DIR}" >> "$GITHUB_OUTPUT" + echo "workspace_chart_dir=${WORKSPACE_CHART_DIR}" >> "$GITHUB_OUTPUT" echo "chart_version=${CHART_VERSION}" >> "$GITHUB_OUTPUT" - name: Refuse duplicate chart version @@ -65,38 +70,52 @@ runs: shell: bash run: | set -euo pipefail - OCI_CHART="oci://ghcr.io/nvidia/openshell/helm-chart" - if helm show chart "${OCI_CHART}" --version "${CHART_VERSION}" >/dev/null 2>&1; then - echo "::error::Chart ${CHART_VERSION} is already published. Use a new tag or delete the existing package first." - exit 1 - fi + for chart in helm-chart openshell-workspace; do + OCI_CHART="oci://ghcr.io/nvidia/openshell/${chart}" + if helm show chart "${OCI_CHART}" --version "${CHART_VERSION}" >/dev/null 2>&1; then + echo "::error::Chart ${chart}:${CHART_VERSION} is already published. Use a new tag or delete the existing package first." + exit 1 + fi + done - name: Package Helm chart env: - CHART_DIR: ${{ steps.prep.outputs.chart_dir }} + GATEWAY_CHART_DIR: ${{ steps.prep.outputs.gateway_chart_dir }} + WORKSPACE_CHART_DIR: ${{ steps.prep.outputs.workspace_chart_dir }} shell: bash run: | set -euo pipefail - helm package "${CHART_DIR}" --destination /tmp - ls /tmp/helm-chart-*.tgz + mkdir -p /tmp/helm-charts + helm package "${GATEWAY_CHART_DIR}" --destination /tmp/helm-charts + helm package "${WORKSPACE_CHART_DIR}" --destination /tmp/helm-charts + ls /tmp/helm-charts/*.tgz - name: Push Helm chart to GHCR OCI shell: bash run: | set -euo pipefail - helm push /tmp/helm-chart-*.tgz oci://ghcr.io/nvidia/openshell + for archive in /tmp/helm-charts/*.tgz; do + helm push "${archive}" oci://ghcr.io/nvidia/openshell + done - name: Push SHA-pinned chart if: inputs.pin-sha != '' env: PIN_SHA: ${{ inputs.pin-sha }} - CHART_DIR: ${{ steps.prep.outputs.chart_dir }} + GATEWAY_CHART_DIR: ${{ steps.prep.outputs.gateway_chart_dir }} + WORKSPACE_CHART_DIR: ${{ steps.prep.outputs.workspace_chart_dir }} shell: bash run: | set -euo pipefail - SHA_CHART_DIR="${RUNNER_TEMP}/chart-build-sha" - cp -a "${CHART_DIR}/." "${SHA_CHART_DIR}" - sed -i "s/^version:.*/version: 0.0.0-dev.${PIN_SHA}/" "${SHA_CHART_DIR}/Chart.yaml" - sed -i "s/^appVersion:.*/appVersion: \"${PIN_SHA}\"/" "${SHA_CHART_DIR}/Chart.yaml" - helm package "${SHA_CHART_DIR}" --destination /tmp/sha-pin - helm push /tmp/sha-pin/helm-chart-*.tgz oci://ghcr.io/nvidia/openshell + mkdir -p /tmp/sha-pin + for source_dir in "${GATEWAY_CHART_DIR}" "${WORKSPACE_CHART_DIR}"; do + chart_name="$(basename "${source_dir}")" + sha_chart_dir="${RUNNER_TEMP}/${chart_name}-sha" + cp -a "${source_dir}/." "${sha_chart_dir}" + sed -i "s/^version:.*/version: 0.0.0-dev.${PIN_SHA}/" "${sha_chart_dir}/Chart.yaml" + sed -i "s/^appVersion:.*/appVersion: \"${PIN_SHA}\"/" "${sha_chart_dir}/Chart.yaml" + helm package "${sha_chart_dir}" --destination /tmp/sha-pin + done + for archive in /tmp/sha-pin/*.tgz; do + helm push "${archive}" oci://ghcr.io/nvidia/openshell + done diff --git a/architecture/compute-runtimes.md b/architecture/compute-runtimes.md index aa97fbcb78..9f5b9230bc 100644 --- a/architecture/compute-runtimes.md +++ b/architecture/compute-runtimes.md @@ -283,6 +283,17 @@ through the driver configuration. The Helm chart defaults sandbox agents to `Unconfined` so runtime/default AppArmor profiles do not block supervisor network namespace setup on AppArmor-enabled nodes. +The Kubernetes deployment packaging has two ownership boundaries. The gateway +chart owns the gateway workload, configuration, Services, PKI, and +cluster-scoped gateway resources. It can retain the legacy combined behavior, +or omit workspace resources. The workspace chart is installed into a +pre-provisioned sandbox namespace and owns only the sandbox ServiceAccount, +namespaced RBAC, and sandbox ingress NetworkPolicy. Its RoleBinding names the +gateway ServiceAccount and namespace explicitly, so the two releases have +disjoint lifecycle ownership. A shared-mode gateway can target one external +namespace, while operator mode maps workspace names to multiple +platform-provisioned namespaces. + Resource requirements enter the driver layer through `SandboxSpec.resource_requirements`. This includes a set of GPU requirements, where a user can request a specific number of GPUs or the driver-specific default behaviour. For all in-tree drivers, this is equivalent to selecting a single GPU. diff --git a/deploy/helm/openshell-workspace/Chart.yaml b/deploy/helm/openshell-workspace/Chart.yaml new file mode 100644 index 0000000000..03a3919d46 --- /dev/null +++ b/deploy/helm/openshell-workspace/Chart.yaml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v2 +name: openshell-workspace +description: Namespace-scoped prerequisites for OpenShell Kubernetes sandboxes +type: application +version: 0.0.0 +appVersion: "0.0.0" diff --git a/deploy/helm/openshell-workspace/README.md b/deploy/helm/openshell-workspace/README.md new file mode 100644 index 0000000000..96ae4ca6e4 --- /dev/null +++ b/deploy/helm/openshell-workspace/README.md @@ -0,0 +1,47 @@ +# OpenShell Workspace Helm Chart + + + +> **Experimental** - the shared-gateway, multi-namespace deployment path is +> under active design. + +This chart installs the namespace-scoped ServiceAccount, RBAC, and NetworkPolicy +needed for OpenShell Kubernetes sandboxes. Install it once in every +platform-managed workspace namespace. It does not create a namespace or deploy +an OpenShell gateway. + +Install the gateway chart with `workspaceResources.enabled=false`, then install +this chart with the gateway ServiceAccount identity. Configure the gateway's +Kubernetes driver in `operator` workspace mode when it serves more than one +pre-provisioned workspace namespace: + +```shell +helm install openshell-workspace ./deploy/helm/openshell-workspace \ + --namespace app-a \ + --set gateway.serviceAccount.name=openshell \ + --set gateway.serviceAccount.namespace=openshell +``` + +Keep `sandboxServiceAccount.name` aligned with the gateway chart's +`sandboxServiceAccount.name`. The defaults for both charts are +`openshell-sandbox`. + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| fullnameOverride | string | `""` | Override the full generated resource name. | +| gateway.networkPolicy.podSelector | object | `{"app.kubernetes.io/instance":"openshell","app.kubernetes.io/name":"openshell"}` | Labels selecting gateway pods allowed to reach sandbox SSH. | +| gateway.serviceAccount.name | string | `"openshell"` | Name of the shared gateway ServiceAccount. | +| gateway.serviceAccount.namespace | string | `"openshell"` | Namespace containing the shared gateway ServiceAccount. | +| nameOverride | string | `""` | Override the chart name used in generated resource names. | +| networkPolicy.enabled | bool | `true` | Restrict sandbox SSH ingress to the shared gateway pods. | +| sandboxServiceAccount.annotations | object | `{}` | Annotations added to the generated sandbox ServiceAccount. | +| sandboxServiceAccount.create | bool | `true` | Create the ServiceAccount assigned to sandbox pods. | +| sandboxServiceAccount.name | string | `"openshell-sandbox"` | Sandbox ServiceAccount name. | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/deploy/helm/openshell-workspace/README.md.gotmpl b/deploy/helm/openshell-workspace/README.md.gotmpl new file mode 100644 index 0000000000..a233eef165 --- /dev/null +++ b/deploy/helm/openshell-workspace/README.md.gotmpl @@ -0,0 +1,33 @@ +# OpenShell Workspace Helm Chart + + + +> **Experimental** - the shared-gateway, multi-namespace deployment path is +> under active design. + +This chart installs the namespace-scoped ServiceAccount, RBAC, and NetworkPolicy +needed for OpenShell Kubernetes sandboxes. Install it once in every +platform-managed workspace namespace. It does not create a namespace or deploy +an OpenShell gateway. + +Install the gateway chart with `workspaceResources.enabled=false`, then install +this chart with the gateway ServiceAccount identity. Configure the gateway's +Kubernetes driver in `operator` workspace mode when it serves more than one +pre-provisioned workspace namespace: + +```shell +helm install openshell-workspace ./deploy/helm/openshell-workspace \ + --namespace app-a \ + --set gateway.serviceAccount.name=openshell \ + --set gateway.serviceAccount.namespace=openshell +``` + +Keep `sandboxServiceAccount.name` aligned with the gateway chart's +`sandboxServiceAccount.name`. The defaults for both charts are +`openshell-sandbox`. + +{{ template "chart.valuesSection" . }} +{{ template "helm-docs.versionFooter" . }} diff --git a/deploy/helm/openshell-workspace/templates/_helpers.tpl b/deploy/helm/openshell-workspace/templates/_helpers.tpl new file mode 100644 index 0000000000..eb948b9907 --- /dev/null +++ b/deploy/helm/openshell-workspace/templates/_helpers.tpl @@ -0,0 +1,44 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "openshell-workspace.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "openshell-workspace.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Common labels. +*/}} +{{- define "openshell-workspace.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +app.kubernetes.io/name: {{ include "openshell-workspace.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Validate required cross-namespace gateway identity values. +*/}} +{{- define "openshell-workspace.validateValues" -}} +{{- $gatewayServiceAccountName := required "gateway.serviceAccount.name is required" .Values.gateway.serviceAccount.name -}} +{{- $gatewayServiceAccountNamespace := required "gateway.serviceAccount.namespace is required" .Values.gateway.serviceAccount.namespace -}} +{{- $sandboxServiceAccountName := required "sandboxServiceAccount.name is required" .Values.sandboxServiceAccount.name -}} +{{- end }} diff --git a/deploy/helm/openshell-workspace/templates/networkpolicy.yaml b/deploy/helm/openshell-workspace/templates/networkpolicy.yaml new file mode 100644 index 0000000000..f88cf84d04 --- /dev/null +++ b/deploy/helm/openshell-workspace/templates/networkpolicy.yaml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{{- include "openshell-workspace.validateValues" . }} +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "openshell-workspace.fullname" . }}-sandbox-ssh + namespace: {{ .Release.Namespace }} + labels: + {{- include "openshell-workspace.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + openshell.ai/managed-by: openshell + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.gateway.serviceAccount.namespace }} + podSelector: + matchLabels: + {{- toYaml .Values.gateway.networkPolicy.podSelector | nindent 14 }} + ports: + - protocol: TCP + port: 2222 +{{- end }} diff --git a/deploy/helm/openshell-workspace/templates/role.yaml b/deploy/helm/openshell-workspace/templates/role.yaml new file mode 100644 index 0000000000..45b3beb831 --- /dev/null +++ b/deploy/helm/openshell-workspace/templates/role.yaml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{{- include "openshell-workspace.validateValues" . }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "openshell-workspace.fullname" . }}-sandbox + namespace: {{ .Release.Namespace }} + labels: + {{- include "openshell-workspace.labels" . | nindent 4 }} +rules: + - apiGroups: + - agents.x-k8s.io + resources: + - sandboxes + - sandboxes/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - get diff --git a/deploy/helm/openshell-workspace/templates/rolebinding.yaml b/deploy/helm/openshell-workspace/templates/rolebinding.yaml new file mode 100644 index 0000000000..669ec47628 --- /dev/null +++ b/deploy/helm/openshell-workspace/templates/rolebinding.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{{- include "openshell-workspace.validateValues" . }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "openshell-workspace.fullname" . }}-sandbox + namespace: {{ .Release.Namespace }} + labels: + {{- include "openshell-workspace.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "openshell-workspace.fullname" . }}-sandbox +subjects: + - kind: ServiceAccount + name: {{ .Values.gateway.serviceAccount.name }} + namespace: {{ .Values.gateway.serviceAccount.namespace }} diff --git a/deploy/helm/openshell-workspace/templates/serviceaccount.yaml b/deploy/helm/openshell-workspace/templates/serviceaccount.yaml new file mode 100644 index 0000000000..20bb263b54 --- /dev/null +++ b/deploy/helm/openshell-workspace/templates/serviceaccount.yaml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{{- include "openshell-workspace.validateValues" . }} +{{- if .Values.sandboxServiceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.sandboxServiceAccount.name }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "openshell-workspace.labels" . | nindent 4 }} + {{- with .Values.sandboxServiceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/openshell-workspace/tests/workspace_test.yaml b/deploy/helm/openshell-workspace/tests/workspace_test.yaml new file mode 100644 index 0000000000..2f71920eaf --- /dev/null +++ b/deploy/helm/openshell-workspace/tests/workspace_test.yaml @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +suite: workspace namespace resources +templates: + - templates/serviceaccount.yaml + - templates/role.yaml + - templates/rolebinding.yaml + - templates/networkpolicy.yaml +release: + name: tenant-a + namespace: app-a + +tests: + - it: creates the workspace role in the release namespace + template: templates/role.yaml + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.namespace + value: app-a + + - it: binds the shared gateway service account + template: templates/rolebinding.yaml + set: + gateway.serviceAccount.name: shared-gateway + gateway.serviceAccount.namespace: openshell-system + asserts: + - equal: + path: metadata.namespace + value: app-a + - equal: + path: subjects[0].name + value: shared-gateway + - equal: + path: subjects[0].namespace + value: openshell-system + + - it: selects gateway pods in the gateway namespace + template: templates/networkpolicy.yaml + set: + gateway.serviceAccount.namespace: openshell-system + gateway.networkPolicy.podSelector: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: central + asserts: + - equal: + path: spec.ingress[0].from[0].namespaceSelector.matchLabels["kubernetes.io/metadata.name"] + value: openshell-system + - equal: + path: spec.ingress[0].from[0].podSelector.matchLabels["app.kubernetes.io/instance"] + value: central + + - it: supports a pre-existing sandbox service account + template: templates/serviceaccount.yaml + set: + sandboxServiceAccount.create: false + asserts: + - hasDocuments: + count: 0 diff --git a/deploy/helm/openshell-workspace/values.yaml b/deploy/helm/openshell-workspace/values.yaml new file mode 100644 index 0000000000..2c52b0460d --- /dev/null +++ b/deploy/helm/openshell-workspace/values.yaml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# -- Override the chart name used in generated resource names. +nameOverride: "" +# -- Override the full generated resource name. +fullnameOverride: "" + +gateway: + serviceAccount: + # -- Name of the shared gateway ServiceAccount. + name: openshell + # -- Namespace containing the shared gateway ServiceAccount. + namespace: openshell + networkPolicy: + # -- Labels selecting gateway pods allowed to reach sandbox SSH. + podSelector: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell + +sandboxServiceAccount: + # -- Create the ServiceAccount assigned to sandbox pods. + create: true + # -- Sandbox ServiceAccount name. + name: openshell-sandbox + # -- Annotations added to the generated sandbox ServiceAccount. + annotations: {} + +networkPolicy: + # -- Restrict sandbox SSH ingress to the shared gateway pods. + enabled: true diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 9d1b9cd7dd..7dbd7964d0 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -9,6 +9,14 @@ Edit README.md.gotmpl and values.yaml, then run `mise run helm:docs`. This chart deploys the OpenShell gateway into a Kubernetes cluster. It is published as an OCI artifact to GHCR at `oci://ghcr.io/nvidia/openshell/helm-chart`. +By default, this chart also creates the namespace-scoped resources needed by +sandboxes. For a shared-gateway deployment, install it with +`workspaceResources.enabled=false`, then install the +`deploy/helm/openshell-workspace` chart in every pre-provisioned workspace +namespace. The gateway and workspace releases can then be upgraded and removed +independently. Use Kubernetes `operator` workspace mode when one gateway serves +multiple pre-provisioned workspace namespaces. + ## Prerequisites The Kubernetes Agent Sandbox CRDs and controller must be installed on the cluster before deploying OpenShell. Install them with: @@ -306,6 +314,7 @@ discovery endpoint or its TLS CA. | upstreamProxy.url | string | `""` | HTTP proxy URL in http://host:port form. HTTPS-to-proxy is not supported. | | workload.allowMultiReplicaStatefulSet | bool | `false` | Allow replicaCount > 1 while rendering a StatefulSet. Prefer workload.kind=deployment for external database-backed multi-replica gateways; this override exists for operators who explicitly require StatefulSet identity or storage semantics. | | workload.kind | string | `"statefulset"` | Gateway workload controller kind. Use `statefulset` for the default SQLite database, or `deployment` when server.externalDbSecret points at an external database. | +| workspaceResources.enabled | bool | `true` | Create the sandbox ServiceAccount, Role, RoleBinding, and NetworkPolicy from this chart. Disable for a gateway-only release. | ---------------------------------------------- Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/deploy/helm/openshell/README.md.gotmpl b/deploy/helm/openshell/README.md.gotmpl index 73ebb39c88..cf8677741e 100644 --- a/deploy/helm/openshell/README.md.gotmpl +++ b/deploy/helm/openshell/README.md.gotmpl @@ -9,6 +9,14 @@ Edit README.md.gotmpl and values.yaml, then run `mise run helm:docs`. This chart deploys the OpenShell gateway into a Kubernetes cluster. It is published as an OCI artifact to GHCR at `oci://ghcr.io/nvidia/openshell/helm-chart`. +By default, this chart also creates the namespace-scoped resources needed by +sandboxes. For a shared-gateway deployment, install it with +`workspaceResources.enabled=false`, then install the +`deploy/helm/openshell-workspace` chart in every pre-provisioned workspace +namespace. The gateway and workspace releases can then be upgraded and removed +independently. Use Kubernetes `operator` workspace mode when one gateway serves +multiple pre-provisioned workspace namespaces. + ## Prerequisites The Kubernetes Agent Sandbox CRDs and controller must be installed on the cluster before deploying OpenShell. Install them with: diff --git a/deploy/helm/openshell/templates/_helpers.tpl b/deploy/helm/openshell/templates/_helpers.tpl index 5b67c018c4..3d9f2f3e0b 100644 --- a/deploy/helm/openshell/templates/_helpers.tpl +++ b/deploy/helm/openshell/templates/_helpers.tpl @@ -70,6 +70,19 @@ Create the name of the service account assigned to sandbox pods {{- end }} {{- end }} +{{/* +Whether this chart owns workspace-scoped resources. Missing legacy values +default to enabled so upgrades with --reuse-values preserve the old topology. +*/}} +{{- define "openshell.workspaceResourcesEnabled" -}} +{{- $workspaceResources := .Values.workspaceResources | default dict -}} +{{- $enabled := true -}} +{{- if hasKey $workspaceResources "enabled" -}} +{{- $enabled = get $workspaceResources "enabled" -}} +{{- end -}} +{{- if $enabled -}}true{{- end -}} +{{- end }} + {{/* Gateway image reference. Uses image.tag when set; falls back to .Chart.AppVersion so a released chart automatically pulls the matching image without extra overrides. diff --git a/deploy/helm/openshell/templates/networkpolicy.yaml b/deploy/helm/openshell/templates/networkpolicy.yaml index e85571e5f5..c9e4a760e3 100644 --- a/deploy/helm/openshell/templates/networkpolicy.yaml +++ b/deploy/helm/openshell/templates/networkpolicy.yaml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -{{- if .Values.networkPolicy.enabled }} +{{- if and (include "openshell.workspaceResourcesEnabled" .) .Values.networkPolicy.enabled }} # NetworkPolicy restricting SSH ingress on sandbox pods to the gateway pod. # Sandbox pods are dynamically created by the server and labelled with # openshell.ai/managed-by=openshell. This policy ensures only the gateway diff --git a/deploy/helm/openshell/templates/role.yaml b/deploy/helm/openshell/templates/role.yaml index 8def13f310..dfd6423615 100644 --- a/deploy/helm/openshell/templates/role.yaml +++ b/deploy/helm/openshell/templates/role.yaml @@ -1,5 +1,5 @@ {{- $workspaceMode := .Values.server.drivers.kubernetes.workspaceMode | default "shared" -}} -{{- if eq $workspaceMode "shared" }} +{{- if and (eq $workspaceMode "shared") (include "openshell.workspaceResourcesEnabled" .) }} # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 apiVersion: rbac.authorization.k8s.io/v1 diff --git a/deploy/helm/openshell/templates/rolebinding.yaml b/deploy/helm/openshell/templates/rolebinding.yaml index 381473a58b..32f11644bf 100644 --- a/deploy/helm/openshell/templates/rolebinding.yaml +++ b/deploy/helm/openshell/templates/rolebinding.yaml @@ -1,5 +1,5 @@ {{- $workspaceMode := .Values.server.drivers.kubernetes.workspaceMode | default "shared" -}} -{{- if eq $workspaceMode "shared" }} +{{- if and (eq $workspaceMode "shared") (include "openshell.workspaceResourcesEnabled" .) }} # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 apiVersion: rbac.authorization.k8s.io/v1 diff --git a/deploy/helm/openshell/templates/serviceaccount.yaml b/deploy/helm/openshell/templates/serviceaccount.yaml index a98ad5363e..1a9245d4dc 100644 --- a/deploy/helm/openshell/templates/serviceaccount.yaml +++ b/deploy/helm/openshell/templates/serviceaccount.yaml @@ -13,10 +13,10 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} {{- end }} -{{- if and .Values.serviceAccount.create .Values.sandboxServiceAccount.create }} +{{- if and .Values.serviceAccount.create (include "openshell.workspaceResourcesEnabled" .) .Values.sandboxServiceAccount.create }} --- {{- end }} -{{- if .Values.sandboxServiceAccount.create }} +{{- if and (include "openshell.workspaceResourcesEnabled" .) .Values.sandboxServiceAccount.create }} apiVersion: v1 kind: ServiceAccount metadata: diff --git a/deploy/helm/openshell/tests/sandbox_namespace_test.yaml b/deploy/helm/openshell/tests/sandbox_namespace_test.yaml index ee89fce53d..864e3a8512 100644 --- a/deploy/helm/openshell/tests/sandbox_namespace_test.yaml +++ b/deploy/helm/openshell/tests/sandbox_namespace_test.yaml @@ -75,3 +75,21 @@ tests: path: metadata.namespace value: other-ns documentIndex: 1 + + - it: omits workspace resources in gateway-only mode + set: + workspaceResources.enabled: false + networkPolicy.enabled: true + asserts: + - hasDocuments: + count: 1 + template: templates/gateway-config.yaml + - hasDocuments: + count: 0 + template: templates/networkpolicy.yaml + - hasDocuments: + count: 0 + template: templates/role.yaml + - hasDocuments: + count: 0 + template: templates/rolebinding.yaml diff --git a/deploy/helm/openshell/tests/sandbox_service_account_test.yaml b/deploy/helm/openshell/tests/sandbox_service_account_test.yaml index c426415823..c9f10868fe 100644 --- a/deploy/helm/openshell/tests/sandbox_service_account_test.yaml +++ b/deploy/helm/openshell/tests/sandbox_service_account_test.yaml @@ -29,3 +29,13 @@ tests: asserts: - hasDocuments: count: 1 + + - it: renders only the gateway service account in gateway-only mode + set: + workspaceResources.enabled: false + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: openshell diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 25236eb474..8f5b6fa51a 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -102,6 +102,14 @@ sandboxServiceAccount: # -- Existing service account name for sandbox pods when sandboxServiceAccount.create is false. name: "" +# Namespace-scoped resources needed to run sandboxes. Disable this when the +# gateway and workspace prerequisites are managed as separate Helm releases +# using the openshell-workspace chart. +workspaceResources: + # -- Create the sandbox ServiceAccount, Role, RoleBinding, and NetworkPolicy + # from this chart. Disable for a gateway-only release. + enabled: true + # -- Extra annotations to add to the gateway pod. podAnnotations: {} # -- Extra labels to add to the gateway pod. diff --git a/deploy/helm/test-split-ownership.sh b/deploy/helm/test-split-ownership.sh new file mode 100755 index 0000000000..30fd7360d4 --- /dev/null +++ b/deploy/helm/test-split-ownership.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +work_dir="$(mktemp -d)" +trap 'rm -rf "${work_dir}"' EXIT + +helm template openshell "${repo_root}/deploy/helm/openshell" \ + --namespace openshell \ + --set agentSandbox.preflight.enabled=false \ + --set workspaceResources.enabled=false \ + >"${work_dir}/gateway.yaml" + +if yq ea -e \ + 'select(.kind == "Role" and .metadata.name == "openshell-sandbox")' \ + "${work_dir}/gateway.yaml" >/dev/null 2>&1; then + echo "gateway chart rendered workspace resources despite workspaceResources.enabled=false" >&2 + exit 1 +fi + +helm template openshell-workspace "${repo_root}/deploy/helm/openshell-workspace" \ + --namespace app-a \ + --set gateway.serviceAccount.name=openshell \ + --set gateway.serviceAccount.namespace=openshell \ + >"${work_dir}/workspace.yaml" + +invalid_workspace_docs="$( + yq ea -N -r \ + 'select(. != null and (.apiVersion == null or .kind == null)) | document_index' \ + "${work_dir}/workspace.yaml" +)" +if [[ -n "${invalid_workspace_docs}" ]]; then + echo "workspace chart rendered documents without apiVersion or kind: ${invalid_workspace_docs}" >&2 + exit 1 +fi + +helm template openshell "${repo_root}/deploy/helm/openshell" \ + --namespace openshell \ + --set agentSandbox.preflight.enabled=false \ + --set-json workspaceResources=null \ + >"${work_dir}/legacy-reuse-values.yaml" + +yq ea -e \ + 'select(.kind == "Role" and .metadata.name == "openshell-sandbox") | + .apiVersion == "rbac.authorization.k8s.io/v1"' \ + "${work_dir}/legacy-reuse-values.yaml" >/dev/null + +yq ea -N -r \ + 'select(.kind != null) | [.apiVersion, .kind, (.metadata.namespace // "openshell"), .metadata.name] | @tsv' \ + "${work_dir}/gateway.yaml" | sort -u >"${work_dir}/gateway.objects" +yq ea -N -r \ + 'select(.kind != null) | [.apiVersion, .kind, (.metadata.namespace // "app-a"), .metadata.name] | @tsv' \ + "${work_dir}/workspace.yaml" | sort -u >"${work_dir}/workspace.objects" + +comm -12 "${work_dir}/gateway.objects" "${work_dir}/workspace.objects" \ + >"${work_dir}/overlap.objects" +if [[ -s "${work_dir}/overlap.objects" ]]; then + echo "gateway and workspace charts claim the same Kubernetes objects:" >&2 + cat "${work_dir}/overlap.objects" >&2 + exit 1 +fi + +echo "gateway and workspace chart object ownership is disjoint" diff --git a/docs/kubernetes/setup.mdx b/docs/kubernetes/setup.mdx index 221f935eb6..fb7881af2d 100644 --- a/docs/kubernetes/setup.mdx +++ b/docs/kubernetes/setup.mdx @@ -99,6 +99,36 @@ helm upgrade --install openshell \ The chart automatically generates PKI secrets on first install using pre-install Helm hooks. No manual secret creation is required. +### Split gateway and workspace releases + +For a platform-managed namespace, install the gateway without namespace-scoped +sandbox resources, then install the workspace chart in the sandbox namespace: + +```shell +helm upgrade --install openshell \ + oci://ghcr.io/nvidia/openshell/helm-chart \ + --version \ + --namespace openshell \ + --set workspaceResources.enabled=false \ + --set server.sandboxNamespace=app-a + +helm upgrade --install openshell-workspace \ + oci://ghcr.io/nvidia/openshell/openshell-workspace \ + --version \ + --namespace app-a \ + --set gateway.serviceAccount.name=openshell \ + --set gateway.serviceAccount.namespace=openshell +``` + +The workspace chart does not create the namespace or deploy a gateway. It owns +only the sandbox ServiceAccount, Role, RoleBinding, and NetworkPolicy in its +release namespace. For one pre-provisioned namespace, keep +`server.drivers.kubernetes.workspaceMode=shared` and set +`server.sandboxNamespace=app-a`. To map multiple workspaces to separately +provisioned namespaces, use `workspaceMode=operator`, configure exactly one of +`operatorNamespaceLabel` or `operatorNamespaceFile`, and install the workspace +chart in every allowlisted namespace. + ## Wait for the gateway to be ready ```shell @@ -163,6 +193,7 @@ The most commonly changed values are: | `workload.kind` | Gateway workload controller. Use `statefulset` for SQLite or `deployment` with `server.externalDbSecret`. | | `workload.allowMultiReplicaStatefulSet` | Allow `replicaCount > 1` with `workload.kind=statefulset`. Prefer Deployment for external database-backed multi-replica gateways. | | `server.sandboxNamespace` | Namespace where sandbox pods are created. Defaults to the Helm release namespace when left empty. | +| `workspaceResources.enabled` | Create namespace-scoped sandbox prerequisites from the gateway chart. Disable when installing the workspace chart separately. | | `server.externalDbSecret` | Secret containing a PostgreSQL connection URI in the `uri` key. Use when the database is managed outside the chart. | | `server.telemetryEnabled` | Enable anonymous OpenShell telemetry from the gateway and its sandbox supervisors. Set to `false` to opt out. | | `server.sandboxImage` | Default sandbox image used when a sandbox does not specify one. | diff --git a/mise.lock b/mise.lock index 23ce8b24e0..3a57cbf0e0 100644 --- a/mise.lock +++ b/mise.lock @@ -438,6 +438,34 @@ url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-x86_64-pc-wi url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/377491992" provenance = "github-attestations" +[[tools.yq]] +version = "4.53.6" +backend = "aqua:mikefarah/yq" + +[tools.yq."platforms.linux-arm64"] +checksum = "sha256:88a1016bc1d657375a35864e4f44b6f333df8ff97b559f51bba0adcb2169df09" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_linux_arm64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028007" +provenance = "cosign" + +[tools.yq."platforms.linux-x64"] +checksum = "sha256:c5f056448f973ae7d39b5401949648a78f2dc1947d6a8eb65be60d5c504b9385" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_linux_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028022" +provenance = "cosign" + +[tools.yq."platforms.macos-arm64"] +checksum = "sha256:cceb0b8d71ea5294334121f8429f33f92b920e7217d904a2f9f35443968ac424" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_darwin_arm64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028033" +provenance = "cosign" + +[tools.yq."platforms.windows-x64"] +checksum = "sha256:ece3dd8bb50d39f93610506273ea262feb91e5c486bbddbb10abf91b2a6c0f14" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_windows_amd64.exe" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522027974" +provenance = "cosign" + [[tools.zig]] version = "0.14.1" backend = "core:zig" diff --git a/mise.toml b/mise.toml index ec643ed08e..bd36408b80 100644 --- a/mise.toml +++ b/mise.toml @@ -33,6 +33,7 @@ go = "1.26" buf = "1.72.0" helm = { version = "4.2.0", version_prefix = "v" } helm-docs = "1.14.2" +yq = "4.53.6" skaffold = { version = "2.20.0", os = ["linux", "macos"], version_prefix = "v" } # Keep k3d out of Linux CI images until upstream ships a release rebuilt with # patched Go/container dependencies. Linux Kubernetes E2E uses kind or an diff --git a/tasks/helm.toml b/tasks/helm.toml index f7db8f9c57..525e9c2e51 100644 --- a/tasks/helm.toml +++ b/tasks/helm.toml @@ -4,27 +4,36 @@ # Helm chart tasks ["helm:docs"] -description = "Generate the openshell Helm chart README from Chart.yaml, values.yaml, and README.md.gotmpl" -run = "helm-docs --chart-search-root deploy/helm/openshell" +description = "Generate the OpenShell gateway and workspace Helm chart READMEs" +run = """ + helm-docs --chart-search-root deploy/helm/openshell + helm-docs --chart-search-root deploy/helm/openshell-workspace +""" ["helm:docs:check"] description = "Verify the openshell Helm chart README is generated and up to date" run = """ set -e - tmp="$(mktemp)" - trap 'rm -f "$tmp"' EXIT + gateway_tmp="$(mktemp)" + workspace_tmp="$(mktemp)" + trap 'rm -f "$gateway_tmp" "$workspace_tmp"' EXIT - helm-docs --chart-search-root deploy/helm/openshell --dry-run > "$tmp" - if ! diff -u deploy/helm/openshell/README.md "$tmp"; then + helm-docs --chart-search-root deploy/helm/openshell --dry-run > "$gateway_tmp" + if ! diff -u deploy/helm/openshell/README.md "$gateway_tmp"; then echo "Helm chart README is out of sync. Run: mise run helm:docs" >&2 exit 1 fi + helm-docs --chart-search-root deploy/helm/openshell-workspace --dry-run > "$workspace_tmp" + if ! diff -u deploy/helm/openshell-workspace/README.md "$workspace_tmp"; then + echo "Workspace Helm chart README is out of sync. Run: mise run helm:docs" >&2 + exit 1 + fi """ run_windows = "echo Skipping helm:docs:check: Helm validation is not part of the native Windows lane." hide = true ["helm:lint"] -description = "Lint the openshell Helm chart (defaults + all CI configuration variants)" +description = "Lint the OpenShell gateway and workspace Helm charts" run = """ set -e helm dependency build deploy/helm/openshell @@ -37,12 +46,14 @@ run = """ echo "values files: deploy/helm/openshell/values.yaml, $f" helm lint deploy/helm/openshell -f "$f" --set agentSandbox.preflight.enabled=false done + echo "--- helm lint: workspace defaults ---" + helm lint deploy/helm/openshell-workspace echo "All variants passed." """ run_windows = "echo Skipping helm:lint: Helm validation is not part of the native Windows lane." ["helm:test"] -description = "Run Helm chart unit tests" +description = "Run gateway and workspace Helm chart unit tests" run = """ set -e if ! helm plugin list | grep -q unittest; then @@ -50,6 +61,8 @@ run = """ fi helm dependency build deploy/helm/openshell helm unittest deploy/helm/openshell + helm unittest deploy/helm/openshell-workspace + deploy/helm/test-split-ownership.sh """ run_windows = "echo Skipping helm:test: Helm validation is not part of the native Windows lane." From da351384caff828072df770021c8e40040ade47e Mon Sep 17 00:00:00 2001 From: "John T. Myers" <9696606+johntmyers@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:47:29 +0000 Subject: [PATCH 04/16] fix(tui): expose workspace switching from providers (#3115) Closes #3112 Reuse the dashboard workspace cycle action from the providers pane, advertise the shortcut, and cover the state transition and rendered hint. Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- .agents/skills/tui-development/SKILL.md | 4 +-- crates/openshell-tui/src/app.rs | 29 ++++++++++++++++ crates/openshell-tui/src/ui/mod.rs | 46 +++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/.agents/skills/tui-development/SKILL.md b/.agents/skills/tui-development/SKILL.md index 5d61af6ecd..f570f283f0 100644 --- a/.agents/skills/tui-development/SKILL.md +++ b/.agents/skills/tui-development/SKILL.md @@ -384,7 +384,7 @@ All actions are accessible via keyboard shortcuts displayed in the nav bar. The `[Tab] Switch Panel [Enter] Select [j/k] Navigate │ [:] Command [q] Quit` **Dashboard (Providers focus):** -`[Tab] Switch Panel [h/l] Switch Tab [j/k] Navigate [Enter] Detail [c] Create [u] Update [d] Delete │ [:] Command [q] Quit` +`[Tab] Switch Panel [h/l] Switch Tab [j/k] Navigate [Enter] Detail [c] Create [u] Update [d] Delete [w] Workspace │ [:] Command [q] Quit` **Dashboard (Global Settings focus):** `[Tab] Switch Panel [h/l] Switch Tab [j/k] Navigate [Enter] Edit [d] Delete │ [:] Command [q] Quit` @@ -535,7 +535,7 @@ On launch, before the event loop starts: ### Workspace switching lifecycle -1. User presses `[w]` on the sandboxes panel → `cycle_workspace()` advances through discovered workspace names, then "all" +1. User presses `[w]` on the providers or sandboxes panel → `cycle_workspace()` advances through discovered workspace names, then "all" 2. `pending_workspace_refresh = true` is set, cursor indices are reset 3. Event loop calls `refresh_providers()` and `refresh_sandboxes()` with the new workspace scope diff --git a/crates/openshell-tui/src/app.rs b/crates/openshell-tui/src/app.rs index 13a00aaca3..cf7464fcd8 100644 --- a/crates/openshell-tui/src/app.rs +++ b/crates/openshell-tui/src/app.rs @@ -1407,6 +1407,9 @@ impl App { self.input_mode = InputMode::Command; self.command_input.clear(); } + KeyCode::Char('w') => { + self.cycle_workspace(); + } KeyCode::Char('j') | KeyCode::Down => { if self.provider_count > 0 && self.provider_selected < self.provider_count - 1 { self.provider_selected += 1; @@ -3825,6 +3828,32 @@ mod tests { assert_eq!(gateway.source_label(), "unknown"); } + #[tokio::test] + async fn providers_workspace_shortcut_cycles_scope_and_resets_selections() { + let mut app = test_app(); + app.screen = Screen::Dashboard; + app.focus = Focus::Providers; + app.workspace_names = vec!["default".to_string(), "team-b".to_string()]; + app.provider_selected = 3; + app.sandbox_selected = 4; + + app.handle_key(key(KeyCode::Char('w'))); + + assert_eq!(app.current_workspace, "team-b"); + assert!(!app.all_workspaces); + assert_eq!(app.provider_selected, 0); + assert_eq!(app.sandbox_selected, 0); + assert!(app.pending_workspace_refresh); + + app.handle_key(key(KeyCode::Char('w'))); + assert!(app.all_workspaces); + assert_eq!(app.workspace_display(), "all"); + + app.handle_key(key(KeyCode::Char('w'))); + assert!(!app.all_workspaces); + assert_eq!(app.current_workspace, "default"); + } + // -- selected_sandbox_workspace ---------------------------------------- #[test] diff --git a/crates/openshell-tui/src/ui/mod.rs b/crates/openshell-tui/src/ui/mod.rs index 7ad1589186..01171fdb5d 100644 --- a/crates/openshell-tui/src/ui/mod.rs +++ b/crates/openshell-tui/src/ui/mod.rs @@ -223,6 +223,9 @@ fn draw_nav_bar(frame: &mut Frame<'_>, app: &App, area: Rect) { Span::styled(" ", t.text), Span::styled("[c/u/d]", t.key_hint), Span::styled(" Create/Update/Delete", t.text), + Span::styled(" ", t.text), + Span::styled("[w]", t.key_hint), + Span::styled(" Workspace", t.text), Span::styled(" | ", t.border), Span::styled("[:]", t.muted), Span::styled(" Command ", t.muted), @@ -662,3 +665,46 @@ pub fn centered_popup(percent_x: u16, height: u16, area: Rect) -> Rect { ]) .split(vert[1])[1] } + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::auth::EdgeAuthInterceptor; + use openshell_core::proto::open_shell_client::OpenShellClient; + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + fn test_app() -> App { + let channel = tonic::transport::Endpoint::from_static("http://127.0.0.1:1").connect_lazy(); + let client = OpenShellClient::with_interceptor(channel, EdgeAuthInterceptor::noop()); + let mut app = App::new( + client, + "test".to_string(), + "http://127.0.0.1:1".to_string(), + "default".to_string(), + Theme::dark(), + ); + app.screen = Screen::Dashboard; + app.focus = Focus::Providers; + app + } + + #[tokio::test] + async fn providers_navigation_advertises_workspace_shortcut() { + let app = test_app(); + let mut terminal = Terminal::new(TestBackend::new(180, 1)).unwrap(); + + terminal + .draw(|frame| draw_nav_bar(frame, &app, frame.size())) + .unwrap(); + + let text: String = terminal + .backend() + .buffer() + .content() + .iter() + .map(ratatui::buffer::Cell::symbol) + .collect(); + assert!(text.contains("[w] Workspace"), "nav bar was: {text:?}"); + } +} From ad23fe463944527d8b30ee90f4fbac1782d00d49 Mon Sep 17 00:00:00 2001 From: "John T. Myers" <9696606+johntmyers@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:48:01 +0000 Subject: [PATCH 05/16] chore(gator): adopt authoritative provider profiles (#3108) Signed-off-by: John Myers Co-authored-by: John Myers --- scripts/agents/gator/agent.yaml | 2 +- scripts/agents/gator/skills/gator-gate/SKILL.md | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/agents/gator/agent.yaml b/scripts/agents/gator/agent.yaml index 363cea207f..dc225669c9 100644 --- a/scripts/agents/gator/agent.yaml +++ b/scripts/agents/gator/agent.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 id: gator -payload_version: 7 +payload_version: 8 display_name: Gator Gate Agent description: Validate and monitor OpenShell GitHub issues and pull requests through the gator state machine. diff --git a/scripts/agents/gator/skills/gator-gate/SKILL.md b/scripts/agents/gator/skills/gator-gate/SKILL.md index 99ab8304f5..d66d5029ea 100644 --- a/scripts/agents/gator/skills/gator-gate/SKILL.md +++ b/scripts/agents/gator/skills/gator-gate/SKILL.md @@ -531,9 +531,9 @@ Validate small and concentrated work when it has clear motivation and one of the Documentation changes from non-maintainers must not reorder ToC items, change fundamental hierarchy, or restructure docs without a clear maintainer-approved reason. -### Provider V2 and Credential Support +### Provider Profiles and Credential Support -Provider V2 work is a supported high-traction area, but require all of the following: +Provider-profile work is a supported high-traction area, but requires all of the following: - Clear UX path for how users configure and use the provider feature in OpenShell - Clear statement of why the change is important @@ -541,7 +541,13 @@ Provider V2 work is a supported high-traction area, but require all of the follo - Security boundary analysis for credential handling - Explanation of whether secrets remain hidden from the sandbox agent -Provider additions and updates must use providers v2 through provider profiles. Treat any new or modified legacy `ProviderDiscoverySpec` entries as a blocking review finding unless a maintainer explicitly requests the legacy path. Do not ask contributors to update both systems for compatibility; the provider profile is the source of truth for new provider network policy, credentials, discovery, and refresh metadata. +Provider profiles are the authoritative provider model. Provider additions and +updates must define their network policy, credentials, discovery, refresh +metadata, and user-facing semantics through a built-in or imported profile. +Treat any new or modified legacy `ProviderDiscoverySpec` entries as a blocking +review finding unless a maintainer explicitly requests the legacy path. Do not +ask contributors to update both systems for compatibility or to enable a +gateway feature switch; profile-backed behavior is always active. Be skeptical of changes that expose raw credentials to agents or weaken the credential proxy model, even if the user story is clear. From 7a33a6625feee75092bc177c2687a368d40fe0b8 Mon Sep 17 00:00:00 2001 From: grs Date: Wed, 2 Sep 2026 00:55:29 +0000 Subject: [PATCH 06/16] feat(server): add sandbox templates (#2833) * feat(server): add sandbox workload templates Signed-off-by: Gordon Sim * feat(go-sdk): add sandbox workload template support Signed-off-by: Gordon Sim * feat(rust-sdk): add sandbox workload template support Signed-off-by: Gordon Sim * feat(python-sdk): add sandbox workload template support Signed-off-by: Gordon Sim * feat(typescript-sdk): add sandbox workload template support Signed-off-by: Gordon Sim * docs(agents): document sandbox workload templates Signed-off-by: Gordon Sim * fix(cli): support default GPU requests in sandbox templates Signed-off-by: Gordon Sim * fix(server): cap sandbox templates per workspace Signed-off-by: Gordon Sim * docs(architecture): document sandbox workload template boundaries Signed-off-by: Gordon Sim * feat(cli+sdk): expose sandbox workload template provenance Signed-off-by: Gordon Sim * fix(cli): include sandbox template annotations in output Signed-off-by: Gordon Sim * feat(sandbox): add label selectors to template listing Signed-off-by: Gordon Sim * test(e2e): cover sandbox template failure paths Signed-off-by: Gordon Sim * fix(server): preserve command and ttl when creating sandbox from template Signed-off-by: Gordon Sim * test(server): add field coverage test for template merge Signed-off-by: Gordon Sim * fix(go-sdk): add pagination support to fake client Signed-off-by: Gordon Sim * fix(cli): warn on env vars that looks like secrets Signed-off-by: Gordon Sim * fix(sdk-ts): propagate sandbox workspace through lifecycle calls Signed-off-by: Gordon Sim * fix(docs): update workspace management docs Signed-off-by: Gordon Sim * fix(ts-sdk): support command and tty when creating from template Signed-off-by: Gordon Sim * fix(go-sdk): support command and tty when creating from template Signed-off-by: Gordon Sim * test(python-sdk): verify command and tty handling when creating from template Signed-off-by: Gordon Sim * fix(go-sdk): update docs and ClientInterface Signed-off-by: Gordon Sim * fix(server): validate sandbox create specs before I/O Signed-off-by: Gordon Sim * fix(go-sdk): guard empty DNS-1123 label validation Signed-off-by: Gordon Sim * fix(python-sdk): allow empty template builder mappings Signed-off-by: Gordon Sim * fix(cli): align template GPU JSON default output Signed-off-by: Gordon Sim --------- Signed-off-by: Gordon Sim --- .agents/skills/openshell-cli/SKILL.md | 44 +- .agents/skills/openshell-cli/cli-reference.md | 65 +- architecture/compute-runtimes.md | 10 + architecture/gateway.md | 44 +- architecture/sandbox-limits.md | 9 + architecture/sandbox.md | 6 + crates/openshell-cli/src/main.rs | 428 ++ crates/openshell-cli/src/run.rs | 788 +++- .../tests/ensure_providers_integration.rs | 2 + crates/openshell-cli/tests/helpers/mod.rs | 89 + .../openshell-cli/tests/mtls_integration.rs | 2 + .../tests/provider_commands_integration.rs | 3 + .../sandbox_create_lifecycle_integration.rs | 476 ++- .../sandbox_name_fallback_integration.rs | 2 + crates/openshell-core/src/metadata.rs | 48 +- crates/openshell-core/src/telemetry.rs | 2 + .../src/proto_json.rs | 1 + .../src/runtime.rs | 1 + crates/openshell-sdk/README.md | 52 +- crates/openshell-sdk/src/client.rs | 209 +- crates/openshell-sdk/src/lib.rs | 9 +- crates/openshell-sdk/src/raw.rs | 14 +- crates/openshell-sdk/src/types.rs | 71 + crates/openshell-sdk/tests/client_mock.rs | 269 +- crates/openshell-server/src/compute/mod.rs | 8 +- crates/openshell-server/src/grpc/mod.rs | 76 +- crates/openshell-server/src/grpc/provider.rs | 2 + crates/openshell-server/src/grpc/sandbox.rs | 1536 +++++++- .../openshell-server/src/grpc/validation.rs | 63 +- crates/openshell-server/src/grpc/workspace.rs | 73 +- .../openshell-server/src/persistence/mod.rs | 33 + .../src/persistence/postgres.rs | 67 + .../src/persistence/sqlite.rs | 61 + .../openshell-server/src/persistence/tests.rs | 4 + crates/openshell-server/tests/common/mod.rs | 28 + .../tests/supervisor_relay_integration.rs | 28 + crates/openshell-tui/src/lib.rs | 1 + docs/sandboxes/manage-sandboxes.mdx | 71 + docs/sandboxes/manage-workspaces.mdx | 17 +- e2e/rust/Cargo.toml | 5 + e2e/rust/tests/sandbox_templates.rs | 275 ++ proto/openshell.proto | 149 +- python/openshell/__init__.py | 4 + python/openshell/openshell_test.py | 4 + python/openshell/sandbox.py | 248 +- python/openshell/sandbox_test.py | 402 ++ sdk/go/docs/src/SUMMARY.md | 1 + sdk/go/docs/src/api/client.md | 7 +- sdk/go/docs/src/api/fake.md | 11 +- sdk/go/docs/src/api/overview.md | 5 +- sdk/go/docs/src/api/sandbox-templates.md | 127 + sdk/go/docs/src/api/sandboxes.md | 26 + sdk/go/docs/src/introduction.md | 2 +- sdk/go/openshell/v1/client.go | 43 +- sdk/go/openshell/v1/fake/fake.go | 53 +- sdk/go/openshell/v1/fake/sandbox.go | 134 +- sdk/go/openshell/v1/fake/sandbox_template.go | 301 ++ .../v1/fake/sandbox_template_test.go | 481 +++ sdk/go/openshell/v1/fake/sandbox_test.go | 41 +- .../v1/internal/converter/coverage_test.go | 56 + .../v1/internal/converter/sandbox.go | 236 +- .../v1/internal/converter/sandbox_test.go | 158 + sdk/go/openshell/v1/sandbox.go | 6 + sdk/go/openshell/v1/sandbox_client.go | 41 + sdk/go/openshell/v1/sandbox_client_test.go | 66 + sdk/go/openshell/v1/sandbox_template.go | 45 + .../openshell/v1/sandbox_template_client.go | 94 + .../v1/sandbox_template_client_test.go | 265 ++ sdk/go/openshell/v1/types/sandbox.go | 84 +- sdk/go/proto/openshellv1/openshell.pb.go | 3444 +++++++++++------ sdk/go/proto/openshellv1/openshell_grpc.pb.go | 160 + sdk/typescript/README.md | 43 + sdk/typescript/src/client.test.ts | 366 +- sdk/typescript/src/client.ts | 269 +- sdk/typescript/src/index.ts | 12 +- 75 files changed, 10793 insertions(+), 1583 deletions(-) create mode 100644 e2e/rust/tests/sandbox_templates.rs create mode 100644 sdk/go/docs/src/api/sandbox-templates.md create mode 100644 sdk/go/openshell/v1/fake/sandbox_template.go create mode 100644 sdk/go/openshell/v1/fake/sandbox_template_test.go create mode 100644 sdk/go/openshell/v1/sandbox_template.go create mode 100644 sdk/go/openshell/v1/sandbox_template_client.go create mode 100644 sdk/go/openshell/v1/sandbox_template_client_test.go diff --git a/.agents/skills/openshell-cli/SKILL.md b/.agents/skills/openshell-cli/SKILL.md index c0be7fc730..f268527956 100644 --- a/.agents/skills/openshell-cli/SKILL.md +++ b/.agents/skills/openshell-cli/SKILL.md @@ -242,6 +242,7 @@ Key flags: - `--gpu [COUNT]`: Request the driver's default GPU selection or a specific GPU count - `--cpu`, `--memory`: Set per-sandbox compute sizing. Docker/Podman apply limits; Kubernetes applies matching requests and limits. - `--driver-config-json`: Pass experimental driver-specific sandbox configuration +- `--template NAME`: Create from a named sandbox workload template. Conflicts with inline workload flags such as `--from`, `--gpu`, `--cpu`, `--memory`, `--env`, and `--driver-config-json`. - `--label KEY=VALUE`: Add labels for later selection (repeatable) - `--env KEY=VALUE`: Set non-secret sandbox environment variables (repeatable); use `--provider` for credentials - `--tty`: Allocate a retained PTY for the canonical main process @@ -262,6 +263,45 @@ Do not combine `--upload` with a trailing main command. Uploads currently finish after the canonical process starts; create a scratch sandbox and use `sandbox exec`, or build the files into the image. +Create from a reusable workload template when several sandboxes should share +image, environment, sizing, or driver-specific configuration: + +```bash +openshell sandbox template create gpu-kata \ + --image ghcr.io/nvidia/openshell-community/sandboxes/python:latest \ + --cpu 2 \ + --memory 4Gi \ + --gpu 1 \ + --driver-config-json '{"kubernetes":{"pod":{"node_selector":{"pool":"gpu"}}}}' + +openshell sandbox create --name my-sandbox --template gpu-kata --provider my-github -- claude +``` + +Direct `sandbox create --driver-config-json` remains valid for one-off +creates. Put driver config on a template only when it should be reused. + +### Manage sandbox workload templates + +```bash +openshell sandbox template create gpu-kata \ + --image ghcr.io/nvidia/openshell-community/sandboxes/python:latest \ + --cpu 2 \ + --memory 4Gi \ + --gpu 1 \ + --label team=runtime \ + --env FEATURE_FLAG=on +openshell sandbox template list +openshell sandbox template list --label-selector team=runtime +openshell sandbox template list --all-workspaces --output json +openshell sandbox template get gpu-kata +openshell sandbox template delete gpu-kata +``` + +Template `--image` accepts an OCI image reference. If omitted, the gateway +applies its default sandbox image when creating a sandbox from the template. +Create-time policy, providers, labels, uploads, forwarding, editor launch, and +the initial command stay on `sandbox create`. + ### List and inspect sandboxes ```bash @@ -772,7 +812,7 @@ The CLI help is always authoritative. If the help output contradicts this skill, ```bash $ openshell sandbox --help -# Shows: create, get, list, stop, start, delete, exec, connect, upload, download, ssh-config, provider +# Shows: create, get, list, stop, start, delete, exec, connect, upload, download, ssh-config, provider, template $ openshell sandbox upload --help # Shows: positional arguments (name, path, dest), usage examples @@ -793,6 +833,8 @@ $ openshell sandbox upload --help | Create sandbox with tool | `openshell sandbox create -- claude` | | Create sandbox with GPUs | `openshell sandbox create --gpu 1` | | Create with custom policy | `openshell sandbox create --policy ./p.yaml` | +| Create from template | `openshell sandbox create --template gpu-kata` | +| Create workload template | `openshell sandbox template create gpu-kata --image python:3.12` | | Connect to sandbox | `openshell sandbox connect ` | | Stop sandbox compute | `openshell sandbox stop [name]` | | Start sandbox compute | `openshell sandbox start [name]` | diff --git a/.agents/skills/openshell-cli/cli-reference.md b/.agents/skills/openshell-cli/cli-reference.md index a3dc91c958..2f1065c234 100644 --- a/.agents/skills/openshell-cli/cli-reference.md +++ b/.agents/skills/openshell-cli/cli-reference.md @@ -60,10 +60,15 @@ openshell │ ├── upload [dest] │ ├── download [dest] │ ├── ssh-config [name] -│ └── provider -│ ├── list [name] [-o table|yaml|json] -│ ├── attach -│ └── detach +│ ├── provider +│ │ ├── list [name] [-o table|yaml|json] +│ │ ├── attach +│ │ └── detach +│ └── template +│ ├── create [opts] +│ ├── get +│ ├── list [opts] +│ └── delete ... ├── forward │ ├── start [name] [-d] │ ├── stop [name] @@ -243,6 +248,7 @@ process result immediately when it exits. Foreground creation declares one expected main-process SSH attachment; cleanup finalizes after that connection drains and closes naturally. | `--driver-config-json ` | Experimental driver-keyed configuration object | +| `--template ` | Create from a named sandbox workload template | | `--provider ` | Provider to attach (repeatable) | | `--policy ` | Custom policy YAML; overrides the built-in default and `OPENSHELL_SANDBOX_POLICY` | | `--forward <[BIND:]PORT>` | Start a local port forward and keep the sandbox alive | @@ -261,6 +267,57 @@ currently complete after the canonical process starts. Create the default scratch sandbox, upload files, then use `sandbox exec`, or build the files into the image. +`--template` uses the named template workload, so it conflicts with inline +workload flags: `--from`, `--gpu`, `--cpu`, `--memory`, `--env`, and +`--driver-config-json`. Direct `sandbox create --driver-config-json` remains +valid when `--template` is not set. + +### `openshell sandbox template create NAME [OPTIONS]` + +Create a reusable sandbox workload template. Templates hold image, +environment, resource, startup, and driver-specific configuration for later +`sandbox create --template NAME` calls. + +| Flag | Description | +|------|-------------| +| `--image ` | OCI image reference; when omitted, the gateway default image is applied at sandbox create time | +| `--env ` | Set a non-secret template workload environment variable (repeatable) | +| `--cpu ` | CPU limit for sandboxes created from the template | +| `--memory ` | Memory limit for sandboxes created from the template | +| `--gpu [COUNT]` | Request the driver's default GPU selection or a specific GPU count for sandboxes created from the template | +| `--driver-config-json ` | Experimental driver-keyed configuration object owned by the template | +| `--ready-within ` | Target startup readiness duration, for example `30s`, `5m`, or `1h` | +| `--max-burst ` | Maximum startup burst associated with this template | +| `--label ` | Attach a template label (repeatable) | +| `--annotation ` | Attach a template annotation (repeatable) | +| `--output table|yaml|json` | Output format | + +### `openshell sandbox template get NAME` + +Show a sandbox workload template. + +| Flag | Description | +|------|-------------| +| `--output table|yaml|json` | Output format | + +### `openshell sandbox template list` + +List sandbox workload templates. + +| Flag | Default | Description | +|------|---------|-------------| +| `--limit ` | 100 | Maximum templates | +| `--offset ` | 0 | Pagination offset | +| `--label-selector ` | empty | Filter templates by labels | +| `--names` | false | Print only template names | +| `--all-workspaces` | false | List templates across all workspaces; requires platform-admin permissions | +| `--output table|yaml|json` | `table` | Output format | + +### `openshell sandbox template delete NAME...` + +Delete one or more sandbox workload templates by name. Existing sandboxes +created from a template are not deleted. + ### `openshell sandbox get [name]` Show sandbox details and the active policy. Metadata identifies sandbox or global policy source and the corresponding revision. The name defaults to the last-used sandbox. diff --git a/architecture/compute-runtimes.md b/architecture/compute-runtimes.md index 9f5b9230bc..e1e731a0ce 100644 --- a/architecture/compute-runtimes.md +++ b/architecture/compute-runtimes.md @@ -258,6 +258,16 @@ template resource limits. Docker and Podman apply them as runtime limits. Kubernetes mirrors each limit into the matching request. VM accepts the fields but currently ignores them. +Reusable sandbox workload templates are resolved before the compute-driver +boundary. Drivers do not receive a separate template resource; the gateway +lowers the selected `SandboxWorkloadTemplate` into the existing sandbox spec +and validates that spec before calling `ValidateSandboxCreate` or +`CreateSandbox`. Template CPU and memory become the same typed resource limits +described above. Template GPU settings become `ResourceRequirements`, preserving +the driver's default GPU assignment when the count is omitted. Template +`driver_config` remains a driver-keyed envelope until the compute layer selects +the active driver block and forwards only that block to the driver. + Docker and Podman also accept per-sandbox driver-config mounts for existing runtime-managed named volumes and tmpfs mounts. Podman additionally accepts image mounts through its image-volume API. User-supplied bind and volume mounts diff --git a/architecture/gateway.md b/architecture/gateway.md index 0430d95159..f86411511b 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -344,23 +344,33 @@ default WAL journal mode), which mirror the same sensitive contents. Persisted state includes sandboxes, providers, provider credential refresh state, SSH sessions, policy revisions, settings, inference configuration, and -deployment records. Provider refresh state is stored as a separate object -scoped to the provider instance through `objects.scope`. Its non-secret -configuration remains inline, while refresh tokens, client secrets, private -keys, and other secret source material are stored through the active credential -driver and represented by opaque handles. The provider record keeps only the -current injectable credential handles and optional per-credential expiry -timestamps. A refresh normally mints one credential, but a strategy may -co-mint several (AWS STS mints the access key, secret key, and session token in -one call); the refresh state pins the resolved set of env keys it owns so -collision checks reserve all of them before the first mint. Provider records -keep inline credential values only for legacy records created before credential -driver storage. New provider and refresh-material writes keep driver-owned -credential handles. When no external credential driver is configured, gateways -use server-owned encrypted database credential storage for defense in depth. -Multi-replica deployments can use that default with a shared database and -shared key-encryption key, or opt into an external backend such as Vault or -Kubernetes Secrets. +deployment records, and reusable sandbox workload templates. Provider refresh +state is stored as a separate object scoped to the provider instance through +`objects.scope`. Its non-secret configuration remains inline, while refresh +tokens, client secrets, private keys, and other secret source material are +stored through the active credential driver and represented by opaque handles. +The provider record keeps only the current injectable credential handles and +optional per-credential expiry timestamps. A refresh normally mints one +credential, but a strategy may co-mint several (AWS STS mints the access key, +secret key, and session token in one call); the refresh state pins the resolved +set of env keys it owns so collision checks reserve all of them before the +first mint. Provider records keep inline credential values only for legacy +records created before credential driver storage. New provider and +refresh-material writes keep driver-owned credential handles. When no external +credential driver is configured, gateways use server-owned encrypted database +credential storage for defense in depth. Multi-replica deployments can use that +default with a shared database and shared key-encryption key, or opt into an +external backend such as Vault or Kubernetes Secrets. + +Sandbox workload templates are workspace-scoped gateway resources. Workspace +admins create and delete them; workspace users can read and list them. A +template owns reusable workload intent: image, environment, CPU and memory +limits, GPU request, driver-specific config, and service-level hints. A sandbox +created from a template resolves that resource once and persists an ordinary +`SandboxSpec` snapshot. The create request still owns per-sandbox governance: +name, labels, annotations, provider attachments, and policy. The sandbox stores +template provenance as the template name and resource version used for the +snapshot, so later template edits or deletes do not mutate existing sandboxes. OAuth refresh failures retain a gateway-owned recovery classification alongside the refresh state. The gateway reads only a bounded error response and maps diff --git a/architecture/sandbox-limits.md b/architecture/sandbox-limits.md index 49f1ab7ce9..9635bc1c30 100644 --- a/architecture/sandbox-limits.md +++ b/architecture/sandbox-limits.md @@ -40,6 +40,15 @@ New limits should follow these rules: query parameters, or external free-form diagnostics. - Test time bounds with simulated time and test shared budgets under saturation. +## Gateway Sandbox Resources + +Gateway-owned sandbox resources also carry admission limits before they can +produce supervisor work. Reusable workload templates are capped at 1000 per +workspace. Template payloads reuse sandbox spec validation for environment +entry count and size, image and resource field sizes, driver-config serialized +size, and GPU count. Template names use the same DNS-style resource-name rules +as other named gateway resources. + ## Middleware Middleware limits are process-wide per sandbox. Registry replacement preserves diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 6365ed0631..6e4e020536 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -259,6 +259,12 @@ own DNS view, e.g. DoH tunneled via CONNECT, is a possible future enhancement and out of scope.) The workload child's proxy variables are unaffected — they are always rewritten to point at the local policy proxy. +Template environment is treated like user-provided sandbox environment. It can +shape the workload child, but it cannot override driver-controlled identity, +gateway callback, TLS, relay socket, proxy, provider, or supervisor coordination +variables. Drivers and the supervisor rewrite those reserved values after image +and template environment are considered. + The configuration is fail-closed: a setting that is present but invalid — an empty value, an unsupported or malformed proxy URL, an unreadable auth file or CA bundle, a malformed credential, or an auth file, `NO_PROXY` list, or CA diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index 7c042296f4..befac54759 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -1351,6 +1351,10 @@ enum SandboxCommands { #[arg(long, add = ArgValueCompleter::new(completers::complete_sandbox_names))] name: Option, + /// Create the sandbox from a named sandbox template. + #[arg(long, conflicts_with_all = ["from", "gpu", "cpu", "memory", "driver_config_json", "envs"])] + template: Option, + /// Sandbox source: a community sandbox name (e.g., `ollama`), a path /// to a Dockerfile or directory containing one, or a full container /// image reference (e.g., `myregistry.com/img:tag`). @@ -1693,6 +1697,10 @@ enum SandboxCommands { /// Manage providers attached to a sandbox. #[command(subcommand)] Provider(SandboxProviderCommands), + + /// Manage reusable sandbox workload templates. + #[command(subcommand)] + Template(SandboxTemplateCommands), } #[derive(Subcommand, Debug)] @@ -1734,6 +1742,121 @@ enum SandboxProviderCommands { }, } +#[derive(Subcommand, Debug)] +// `Create` carries several optional strings and repeated key-value flags. This +// enum is only used for clap parsing, so boxing fields would add friction +// without a meaningful runtime win. +#[allow(clippy::large_enum_variant)] +enum SandboxTemplateCommands { + /// Create a reusable sandbox workload template. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Create { + /// Template name. + name: String, + + /// Container image for sandboxes created from this template. + /// When omitted, the gateway's default sandbox image is used at create time. + #[arg(long)] + image: Option, + + /// CPU limit for sandboxes created from this template (for example: 500m, 1, 2.5). + #[arg(long)] + cpu: Option, + + /// Memory limit for sandboxes created from this template (for example: 512Mi, 4Gi, 8G). + #[arg(long)] + memory: Option, + + /// Request GPU resources for sandboxes created from this template. + /// + /// Omit COUNT for the driver's default GPU selection, or pass COUNT + /// to request a specific number of GPUs. + #[arg(long, num_args = 0..=1, value_name = "COUNT", default_missing_value = "", value_parser = parse_gpu_request)] + gpu: Option, + + /// Experimental driver-keyed JSON object for driver-specific sandbox settings. + #[arg(long, value_name = "JSON")] + driver_config_json: Option, + + /// Target startup readiness duration for this template (for example: 30s, 5m, 1h). + #[arg(long, value_name = "DURATION")] + ready_within: Option, + + /// Maximum startup burst associated with this template. + #[arg(long, value_name = "COUNT", value_parser = clap::value_parser!(u32).range(1..))] + max_burst: Option, + + /// Attach labels to the template (key=value format, repeatable). + #[arg(long = "label", value_name = "KEY=VALUE")] + labels: Vec, + + /// Attach annotations to the template (key=value format, repeatable). + #[arg(long = "annotation", value_name = "KEY=VALUE")] + annotations: Vec, + + /// Set a non-secret environment variable in sandboxes created from this template. + /// Do not use this option for API keys, tokens, or other secrets; create + /// a provider and attach it when creating sandboxes instead. Repeatable. + #[arg(long = "env", value_name = "KEY=VALUE")] + envs: Vec, + + /// Suppress warnings when --env values look like credentials. + #[arg(long = "no-credential-warnings")] + no_credential_warnings: bool, + + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)] + output: OutputFormat, + }, + + /// Fetch a sandbox workload template by name. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Get { + /// Template name. + name: String, + + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)] + output: OutputFormat, + }, + + /// List sandbox workload templates. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + List { + /// Maximum number of templates to return. + #[arg(long, default_value_t = 100)] + limit: u32, + + /// Offset into the template list. + #[arg(long, default_value_t = 0)] + offset: u32, + + /// Filter templates by labels, e.g. env=prod,team=runtime. + #[arg(long)] + label_selector: Option, + + /// Print only template names (one per line). + #[arg(long, conflicts_with = "output")] + names: bool, + + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with = "names")] + output: OutputFormat, + + /// List templates across all workspaces (overrides --workspace). + #[arg(long)] + all_workspaces: bool, + }, + + /// Delete sandbox workload templates. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Delete { + /// Template names. + #[arg(required = true, num_args = 1.., value_name = "NAME")] + names: Vec, + }, +} + #[derive(Subcommand, Debug)] enum DraftCommands { /// Show network rules for a sandbox. @@ -3064,6 +3187,7 @@ async fn run_async() -> Result<()> { match command { SandboxCommands::Create { name, + template, from, upload, no_git_ignore, @@ -3157,6 +3281,7 @@ async fn run_async() -> Result<()> { &ctx.name, run::SandboxCreateConfig { name: name.as_deref(), + template: template.as_deref(), from: from.as_deref(), uploads: &upload_specs, keep, @@ -3393,6 +3518,89 @@ async fn run_async() -> Result<()> { .await?; } }, + SandboxCommands::Template(command) => match command { + SandboxTemplateCommands::Create { + name, + image, + cpu, + memory, + gpu, + driver_config_json, + ready_within, + max_burst, + labels, + annotations, + envs, + no_credential_warnings, + output, + } => { + let labels = run::parse_key_value_pairs(&labels, "--label")?; + let annotations = + run::parse_key_value_pairs(&annotations, "--annotation")?; + let environment = run::parse_env_pairs(&envs)?; + run::warn_credential_env_vars(&environment, no_credential_warnings); + let gpu_requirements: Option = + gpu.map(Into::into); + run::sandbox_template_create( + endpoint, + &name, + image.as_deref(), + cpu.as_deref(), + memory.as_deref(), + gpu_requirements, + driver_config_json.as_deref(), + ready_within.as_deref(), + max_burst, + labels, + annotations, + environment, + output.as_str(), + &cli.workspace, + &tls, + ) + .await?; + } + SandboxTemplateCommands::Get { name, output } => { + run::sandbox_template_get( + endpoint, + &name, + output.as_str(), + &cli.workspace, + &tls, + ) + .await?; + } + SandboxTemplateCommands::List { + limit, + offset, + label_selector, + names, + output, + all_workspaces, + } => { + run::sandbox_template_list( + endpoint, + limit, + offset, + label_selector.as_deref(), + names, + output.as_str(), + &cli.workspace, + all_workspaces, + &tls, + ) + .await?; + } + SandboxTemplateCommands::Delete { names } => { + run::sandbox_template_delete( + endpoint, + &names, + &cli.workspace, + &tls, + ) + .await?; + } + }, } } } @@ -5611,6 +5819,226 @@ mod tests { } } + #[test] + fn sandbox_create_template_flag_parses() { + let cli = Cli::try_parse_from([ + "openshell", + "sandbox", + "create", + "--template", + "gpu-kata", + "--provider", + "github", + ]) + .expect("sandbox create template flag should parse"); + + match cli.command { + Some(Commands::Sandbox { + command: + Some(SandboxCommands::Create { + template, + providers, + .. + }), + .. + }) => { + assert_eq!(template.as_deref(), Some("gpu-kata")); + assert_eq!(providers, vec!["github".to_string()]); + } + other => panic!("expected SandboxCommands::Create, got: {other:?}"), + } + } + + #[test] + fn sandbox_create_template_conflicts_with_inline_workload_flags() { + for (label, extra_args) in [ + ("--from", &["--from", "python:3.12"][..]), + ("--gpu", &["--gpu"][..]), + ("--cpu", &["--cpu", "1"][..]), + ("--memory", &["--memory", "2Gi"][..]), + ("--env", &["--env", "FOO=bar"][..]), + ( + "--driver-config-json", + &["--driver-config-json", r#"{"kubernetes":{}}"#][..], + ), + ] { + let args = ["openshell", "sandbox", "create", "--template", "base"] + .into_iter() + .chain(extra_args.iter().copied()); + let result = Cli::try_parse_from(args); + assert!(result.is_err(), "--template should conflict with {label}"); + } + } + + #[test] + fn sandbox_template_create_parses_workload_flags() { + let json = r#"{"kubernetes":{"pod":{"node_selector":{"pool":"gpu"}}}}"#; + let cli = Cli::try_parse_from([ + "openshell", + "sandbox", + "template", + "create", + "gpu-kata", + "--image", + "registry.example.com/agent:latest", + "--cpu", + "2", + "--memory", + "4Gi", + "--gpu", + "1", + "--driver-config-json", + json, + "--ready-within", + "5m", + "--max-burst", + "3", + "--label", + "team=runtime", + "--annotation", + "owner=platform", + "--env", + "FEATURE_FLAG=on", + "--no-credential-warnings", + "--output", + "json", + ]) + .expect("sandbox template create should parse"); + + match cli.command { + Some(Commands::Sandbox { + command: + Some(SandboxCommands::Template(SandboxTemplateCommands::Create { + name, + image, + cpu, + memory, + gpu, + driver_config_json, + ready_within, + max_burst, + labels, + annotations, + envs, + no_credential_warnings, + output, + })), + .. + }) => { + assert_eq!(name, "gpu-kata"); + assert_eq!(image.as_deref(), Some("registry.example.com/agent:latest")); + assert_eq!(cpu.as_deref(), Some("2")); + assert_eq!(memory.as_deref(), Some("4Gi")); + assert_eq!(gpu, Some(GpuCliRequest::Count(1))); + assert_eq!(driver_config_json.as_deref(), Some(json)); + assert_eq!(ready_within.as_deref(), Some("5m")); + assert_eq!(max_burst, Some(3)); + assert_eq!(labels, vec!["team=runtime".to_string()]); + assert_eq!(annotations, vec!["owner=platform".to_string()]); + assert_eq!(envs, vec!["FEATURE_FLAG=on".to_string()]); + assert!(no_credential_warnings); + assert!(matches!(output, OutputFormat::Json)); + } + other => panic!("expected SandboxTemplateCommands::Create, got: {other:?}"), + } + } + + #[test] + fn sandbox_template_list_parses_names_and_all_workspaces() { + let cli = Cli::try_parse_from([ + "openshell", + "sandbox", + "template", + "list", + "--names", + "--all-workspaces", + "--label-selector", + "team=runtime", + "--limit", + "25", + "--offset", + "5", + ]) + .expect("sandbox template list should parse"); + + match cli.command { + Some(Commands::Sandbox { + command: + Some(SandboxCommands::Template(SandboxTemplateCommands::List { + limit, + offset, + label_selector, + names, + all_workspaces, + .. + })), + .. + }) => { + assert_eq!(limit, 25); + assert_eq!(offset, 5); + assert_eq!(label_selector.as_deref(), Some("team=runtime")); + assert!(names); + assert!(all_workspaces); + } + other => panic!("expected SandboxTemplateCommands::List, got: {other:?}"), + } + } + + #[test] + fn sandbox_template_list_names_conflicts_with_output() { + let result = Cli::try_parse_from([ + "openshell", + "sandbox", + "template", + "list", + "--names", + "--output", + "json", + ]); + assert!(result.is_err()); + } + + #[test] + fn sandbox_template_create_image_is_optional() { + let cli = Cli::try_parse_from(["openshell", "sandbox", "template", "create", "base"]) + .expect("sandbox template create without --image should parse"); + + match cli.command { + Some(Commands::Sandbox { + command: + Some(SandboxCommands::Template(SandboxTemplateCommands::Create { image, .. })), + .. + }) => { + assert_eq!(image, None); + } + other => panic!("expected SandboxTemplateCommands::Create, got: {other:?}"), + } + } + + #[test] + fn sandbox_template_create_gpu_parses_driver_default() { + let cli = Cli::try_parse_from([ + "openshell", + "sandbox", + "template", + "create", + "gpu-kata", + "--gpu", + ]) + .expect("sandbox template create --gpu should parse"); + + match cli.command { + Some(Commands::Sandbox { + command: + Some(SandboxCommands::Template(SandboxTemplateCommands::Create { gpu, .. })), + .. + }) => { + assert_eq!(gpu, Some(GpuCliRequest::DriverDefault)); + } + other => panic!("expected SandboxTemplateCommands::Create, got: {other:?}"), + } + } + #[test] fn sandbox_create_gpu_parses_driver_default() { let cli = Cli::try_parse_from(["openshell", "sandbox", "create", "--gpu"]) diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 36cdaf01ba..619a8bbd22 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -37,26 +37,28 @@ use openshell_core::proto::ProviderProfileCategory; use openshell_core::proto::{ ApproveAllDraftChunksRequest, ApproveDraftChunkRequest, AttachSandboxProviderRequest, ClearDraftChunksRequest, ConfigureProviderRefreshRequest, CreateProviderRequest, - CreateSandboxRequest, CreateSshSessionRequest, DeleteInferenceRouteRequest, - DeleteProviderProfileRequest, DeleteProviderRefreshRequest, DeleteProviderRequest, - DeleteSandboxRequest, DeleteServiceRequest, DetachSandboxProviderRequest, ExecSandboxRequest, - ExposeServiceRequest, GetCurrentUserRequest, GetDraftHistoryRequest, GetDraftPolicyRequest, - GetGatewayConfigRequest, GetInferenceRouteRequest, GetProviderProfileRequest, - GetProviderRefreshStatusRequest, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxPolicyStatusRequest, - GetSandboxRequest, GetServiceRequest, GpuResourceRequirements, ImportProviderProfilesRequest, - LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest, - ListSandboxPoliciesRequest, ListSandboxProvidersRequest, ListSandboxesRequest, + CreateSandboxRequest, CreateSandboxTemplateRequest, CreateSshSessionRequest, + DeleteInferenceRouteRequest, DeleteProviderProfileRequest, DeleteProviderRefreshRequest, + DeleteProviderRequest, DeleteSandboxRequest, DeleteSandboxTemplateRequest, + DeleteServiceRequest, DetachSandboxProviderRequest, ExecSandboxRequest, ExposeServiceRequest, + GetCurrentUserRequest, GetDraftHistoryRequest, GetDraftPolicyRequest, GetGatewayConfigRequest, + GetInferenceRouteRequest, GetProviderProfileRequest, GetProviderRefreshStatusRequest, + GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, + GetSandboxPolicyStatusRequest, GetSandboxRequest, GetSandboxTemplateRequest, GetServiceRequest, + GpuResourceRequirements, ImportProviderProfilesRequest, LintProviderProfilesRequest, + ListProviderProfilesRequest, ListProvidersRequest, ListSandboxPoliciesRequest, + ListSandboxProvidersRequest, ListSandboxTemplatesRequest, ListSandboxesRequest, ListServicesRequest, PolicySource, PolicyStatus, Provider, ProviderCredentialRefreshRecoveryAction, ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest, ResourceRequirements, RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, - SandboxPhase, SandboxPolicy, SandboxSpec, SandboxTemplate, ServiceEndpointResponse, - SetInferenceRouteRequest, SettingScope, StartSandboxRequest, StopSandboxRequest, - TcpForwardFrame, TcpForwardInit, TcpRelayTarget, UpdateConfigRequest, - UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest, exec_sandbox_event, - tcp_forward_init, + SandboxPhase, SandboxPolicy, SandboxResources, SandboxServiceLevel, SandboxSpec, + SandboxStartup, SandboxTemplate, SandboxWorkloadConfig, SandboxWorkloadTemplate, + SandboxWorkloadTemplateSpec, ServiceEndpointResponse, SetInferenceRouteRequest, SettingScope, + StartSandboxRequest, StopSandboxRequest, TcpForwardFrame, TcpForwardInit, TcpRelayTarget, + UpdateConfigRequest, UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest, + exec_sandbox_event, tcp_forward_init, }; use openshell_core::settings; use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; @@ -400,6 +402,7 @@ async fn finalize_sandbox_create_session( #[derive(Debug)] pub struct SandboxCreateConfig<'a> { pub name: Option<&'a str>, + pub template: Option<&'a str>, pub from: Option<&'a str>, pub uploads: &'a [(String, Option, bool)], pub keep: bool, @@ -425,6 +428,7 @@ impl Default for SandboxCreateConfig<'_> { fn default() -> Self { Self { name: None, + template: None, from: None, uploads: &[], keep: false, @@ -458,6 +462,7 @@ pub async fn sandbox_create( ) -> Result { let SandboxCreateConfig { name, + template, from, uploads, keep, @@ -511,23 +516,42 @@ pub async fn sandbox_create( let effective_server = server.to_string(); let effective_tls = tls.clone(); + if template.is_some() + && (from.is_some() + || gpu_requirements.is_some() + || cpu.is_some() + || memory.is_some() + || driver_config_json.is_some() + || !environment.is_empty()) + { + return Err(miette::miette!( + "--template cannot be combined with inline workload flags" + )); + } + // Resolve the --from flag into a container image reference, building from - // a Dockerfile first if necessary. - let image: Option = match from { - Some(val) => { - let resolved = resolve_from(val)?; - match resolved { - ResolvedSource::Image(img) => Some(img), - ResolvedSource::Dockerfile { - dockerfile, - context, - } => { - let tag = build_from_dockerfile(&dockerfile, &context, gateway_name).await?; - Some(tag) + // a Dockerfile first if necessary. Template creates resolve workload shape + // on the gateway and skip local image handling. + let image: Option = if template.is_some() { + None + } else { + match from { + Some(val) => { + let resolved = resolve_from(val)?; + match resolved { + ResolvedSource::Image(img) => Some(img), + ResolvedSource::Dockerfile { + dockerfile, + context, + } => { + let tag = + build_from_dockerfile(&dockerfile, &context, gateway_name).await?; + Some(tag) + } } } + None => None, } - None => None, }; let inferred_types: Vec = inferred_provider_type(command).into_iter().collect(); let configured_providers = ensure_required_providers( @@ -540,12 +564,21 @@ pub async fn sandbox_create( .await?; let policy = load_sandbox_policy(policy)?; - let resource_limits = build_sandbox_resource_limits(cpu, memory)?; - let driver_config = driver_config_json - .map(parse_driver_config_json) - .transpose()?; + let resource_limits = if template.is_none() { + build_sandbox_resource_limits(cpu, memory)? + } else { + None + }; + let driver_config = if template.is_none() { + driver_config_json + .map(parse_driver_config_json) + .transpose()? + } else { + None + }; - let template = if image.is_some() || resource_limits.is_some() || driver_config.is_some() { + let inline_template = if image.is_some() || resource_limits.is_some() || driver_config.is_some() + { Some(SandboxTemplate { image: image.unwrap_or_default(), resources: resource_limits, @@ -582,10 +615,14 @@ pub async fn sandbox_create( let request = CreateSandboxRequest { spec: Some(SandboxSpec { resource_requirements, - environment, + environment: if template.is_none() { + environment + } else { + HashMap::new() + }, policy, providers: configured_providers, - template, + template: inline_template, command: main_command, tty: main_terminal, ..SandboxSpec::default() @@ -595,6 +632,7 @@ pub async fn sandbox_create( annotations, workspace: workspace.to_string(), await_main_process_attachment, + workload_template_name: template.unwrap_or_default().to_string(), }; let response = match client.create_sandbox(request).await { @@ -1441,6 +1479,15 @@ pub async fn sandbox_get( } } + if let Some(provenance) = &sandbox.created_from_workload_template { + println!( + " {} {}@{}", + "Workload template:".dimmed(), + provenance.name, + provenance.resource_version + ); + } + let policy_from_global = config.policy_source == PolicySource::Global as i32; println!( " {} {}", @@ -2226,6 +2273,16 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { || serde_json::json!({}), |m| serde_json::json!(m.annotations), ); + let created_from_workload_template = + sandbox + .created_from_workload_template + .as_ref() + .map(|provenance| { + serde_json::json!({ + "name": provenance.name, + "resource_version": provenance.resource_version, + }) + }); serde_json::json!({ "id": sandbox.object_id(), "name": sandbox.object_name(), @@ -2237,6 +2294,7 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { "phase": phase_name(sandbox.phase()), "current_policy_version": sandbox.current_policy_version(), "exit_code": sandbox.status.as_ref().and_then(|status| status.exit_code), + "created_from_workload_template": created_from_workload_template, }) } @@ -2496,6 +2554,583 @@ fn format_provider_attachment_table(providers: &[Provider], color: bool) -> Stri output } +#[allow(clippy::too_many_arguments, clippy::implicit_hasher)] +pub async fn sandbox_template_create( + server: &str, + name: &str, + image: Option<&str>, + cpu: Option<&str>, + memory: Option<&str>, + gpu_requirements: Option, + driver_config_json: Option<&str>, + ready_within: Option<&str>, + max_burst: Option, + labels: HashMap, + annotations: HashMap, + environment: HashMap, + output: &str, + workspace: &str, + tls: &TlsOptions, +) -> Result<()> { + let resources = if cpu.is_some() || memory.is_some() || gpu_requirements.is_some() { + Some(SandboxResources { + cpu: cpu + .map(validate_cpu_quantity) + .transpose()? + .unwrap_or_default(), + memory: memory + .map(validate_memory_quantity) + .transpose()? + .unwrap_or_default(), + gpu: gpu_requirements, + }) + } else { + None + }; + let driver_config = driver_config_json + .map(parse_driver_config_json) + .transpose()?; + let desired_service_level = build_template_service_level(ready_within, max_burst)?; + + let mut client = grpc_client(server, tls).await?; + let response = client + .create_sandbox_template(CreateSandboxTemplateRequest { + template: Some(SandboxWorkloadTemplate { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: String::new(), + name: name.to_string(), + created_at_ms: 0, + labels, + resource_version: 0, + annotations, + workspace: String::new(), + deletion_timestamp_ms: 0, + }), + spec: Some(SandboxWorkloadTemplateSpec { + workload: Some(SandboxWorkloadConfig { + image: image.unwrap_or_default().to_string(), + environment, + resources, + }), + driver_config, + desired_service_level, + }), + }), + workspace: workspace.to_string(), + }) + .await + .into_diagnostic()?; + + let template = response + .into_inner() + .template + .ok_or_else(|| miette!("sandbox template missing from response"))?; + if crate::output::print_output_single(output, &template, sandbox_template_to_json)? { + return Ok(()); + } + println!( + "{} Created sandbox template {}", + "✓".green().bold(), + template.object_name().bold() + ); + Ok(()) +} + +fn build_template_service_level( + ready_within: Option<&str>, + max_burst: Option, +) -> Result> { + if ready_within.is_none() && max_burst.is_none() { + return Ok(None); + } + let ready_within = ready_within + .map(parse_duration_to_ms) + .transpose()? + .map(|ms| { + if ms <= 0 { + Err(miette!("--ready-within must be greater than zero")) + } else { + Ok(duration_ms_to_proto(ms)) + } + }) + .transpose()?; + Ok(Some(SandboxServiceLevel { + startup: Some(SandboxStartup { + ready_within, + max_burst: max_burst.unwrap_or_default(), + }), + })) +} + +fn duration_ms_to_proto(ms: i64) -> prost_types::Duration { + prost_types::Duration { + seconds: ms / 1_000, + nanos: i32::try_from((ms % 1_000) * 1_000_000) + .expect("duration millisecond remainder fits in protobuf nanos"), + } +} + +pub async fn sandbox_template_get( + server: &str, + name: &str, + output: &str, + workspace: &str, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let response = client + .get_sandbox_template(GetSandboxTemplateRequest { + name: name.to_string(), + workspace: workspace.to_string(), + }) + .await + .into_diagnostic()?; + let template = response + .into_inner() + .template + .ok_or_else(|| miette!("sandbox template missing from response"))?; + + if crate::output::print_output_single(output, &template, sandbox_template_to_json)? { + return Ok(()); + } + + print_sandbox_template_detail(&template); + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub async fn sandbox_template_list( + server: &str, + limit: u32, + offset: u32, + label_selector: Option<&str>, + names_only: bool, + output: &str, + workspace: &str, + all_workspaces: bool, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let response = client + .list_sandbox_templates(ListSandboxTemplatesRequest { + limit, + offset, + workspace: if all_workspaces { + String::new() + } else { + workspace.to_string() + }, + all_workspaces, + label_selector: label_selector.unwrap_or_default().to_string(), + }) + .await + .into_diagnostic()?; + let templates = response.into_inner().templates; + + if crate::output::print_output_collection(output, &templates, sandbox_template_to_json)? { + return Ok(()); + } + + if templates.is_empty() { + if !names_only { + println!("No sandbox templates found."); + } + return Ok(()); + } + + if names_only { + for template in &templates { + if all_workspaces { + println!("{}/{}", template.object_workspace(), template.object_name()); + } else { + println!("{}", template.object_name()); + } + } + return Ok(()); + } + + print_sandbox_template_table(&templates, all_workspaces); + Ok(()) +} + +pub async fn sandbox_template_delete( + server: &str, + names: &[String], + workspace: &str, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + for name in names { + let response = client + .delete_sandbox_template(DeleteSandboxTemplateRequest { + name: name.clone(), + workspace: workspace.to_string(), + }) + .await + .into_diagnostic()?; + if response.into_inner().deleted { + println!("{} Deleted sandbox template {name}", "✓".green().bold()); + } else { + println!("Sandbox template {name} not found."); + } + } + Ok(()) +} + +fn sandbox_template_to_json(template: &SandboxWorkloadTemplate) -> serde_json::Value { + let mut obj = serde_json::Map::new(); + obj.insert("id".to_string(), serde_json::json!(template.object_id())); + obj.insert( + "name".to_string(), + serde_json::json!(template.object_name()), + ); + obj.insert( + "workspace".to_string(), + serde_json::json!(template.object_workspace()), + ); + + if let Some(metadata) = &template.metadata { + if metadata.resource_version != 0 { + obj.insert( + "resource_version".to_string(), + serde_json::json!(metadata.resource_version), + ); + } + if metadata.created_at_ms != 0 { + obj.insert( + "created_at".to_string(), + serde_json::json!(format_epoch_ms(metadata.created_at_ms)), + ); + } + if !metadata.labels.is_empty() { + obj.insert("labels".to_string(), serde_json::json!(metadata.labels)); + } + if !metadata.annotations.is_empty() { + obj.insert( + "annotations".to_string(), + serde_json::json!(metadata.annotations), + ); + } + } + + if let Some(spec) = &template.spec { + if let Some(workload) = &spec.workload { + obj.insert("image".to_string(), serde_json::json!(workload.image)); + if !workload.environment.is_empty() { + obj.insert( + "environment".to_string(), + serde_json::json!(workload.environment), + ); + } + if let Some(resources) = &workload.resources { + let mut resources_json = serde_json::Map::new(); + if !resources.cpu.is_empty() { + resources_json.insert("cpu".to_string(), serde_json::json!(resources.cpu)); + } + if !resources.memory.is_empty() { + resources_json + .insert("memory".to_string(), serde_json::json!(resources.memory)); + } + if let Some(gpu) = &resources.gpu { + let value = gpu + .count + .map_or_else(|| serde_json::json!("default"), serde_json::Value::from); + resources_json.insert("gpu".to_string(), value); + } + if !resources_json.is_empty() { + obj.insert( + "resources".to_string(), + serde_json::Value::Object(resources_json), + ); + } + } + } + if let Some(driver_config) = &spec.driver_config { + obj.insert( + "driver_config".to_string(), + openshell_core::proto_struct::struct_to_json_value(driver_config), + ); + } + if let Some(service_level) = &spec.desired_service_level + && let Some(startup) = &service_level.startup + { + let mut startup_json = serde_json::Map::new(); + if let Some(ready_within) = &startup.ready_within { + startup_json.insert( + "ready_within_ms".to_string(), + serde_json::json!(duration_to_ms(ready_within)), + ); + } + if startup.max_burst != 0 { + startup_json.insert( + "max_burst".to_string(), + serde_json::json!(startup.max_burst), + ); + } + if !startup_json.is_empty() { + obj.insert( + "startup".to_string(), + serde_json::Value::Object(startup_json), + ); + } + } + } + + serde_json::Value::Object(obj) +} + +fn print_sandbox_template_detail(template: &SandboxWorkloadTemplate) { + println!("{}", "Sandbox template:".cyan().bold()); + println!(); + println!(" {} {}", "Name:".dimmed(), template.object_name()); + println!( + " {} {}", + "Workspace:".dimmed(), + template.object_workspace() + ); + if let Some(metadata) = &template.metadata { + println!(" {} {}", "Id:".dimmed(), metadata.id); + println!( + " {} {}", + "Resource version:".dimmed(), + metadata.resource_version + ); + if metadata.created_at_ms != 0 { + println!( + " {} {}", + "Created:".dimmed(), + format_epoch_ms(metadata.created_at_ms) + ); + } + let labels = labels_display(&metadata.labels); + println!( + " {} {}", + "Labels:".dimmed(), + non_empty_or(&labels, "") + ); + } + if let Some(spec) = &template.spec + && let Some(workload) = &spec.workload + { + println!( + " {} {}", + "Image:".dimmed(), + non_empty_or(&workload.image, "") + ); + println!( + " {} {}", + "Environment:".dimmed(), + workload.environment.len() + ); + if let Some(resources) = &workload.resources { + println!( + " {} {}", + "CPU:".dimmed(), + non_empty_or(&resources.cpu, "") + ); + println!( + " {} {}", + "Memory:".dimmed(), + non_empty_or(&resources.memory, "") + ); + println!( + " {} {}", + "GPU:".dimmed(), + template_resources_gpu_display(resources).unwrap_or_else(|| "".to_string()) + ); + } + } + if let Some(startup) = template_startup(template) { + println!( + " {} {}", + "Ready within:".dimmed(), + startup + .ready_within + .as_ref() + .map_or_else(|| "".to_string(), duration_display) + ); + println!( + " {} {}", + "Max burst:".dimmed(), + if startup.max_burst == 0 { + "".to_string() + } else { + startup.max_burst.to_string() + } + ); + } +} + +fn print_sandbox_template_table(templates: &[SandboxWorkloadTemplate], show_workspace: bool) { + let name_width = templates + .iter() + .map(|template| template.object_name().len()) + .max() + .unwrap_or(4) + .max(4); + let workspace_width = if show_workspace { + templates + .iter() + .map(|template| template.object_workspace().len()) + .max() + .unwrap_or(9) + .max(9) + } else { + 0 + }; + let image_width = templates + .iter() + .map(|template| template_image(template).len()) + .max() + .unwrap_or(5) + .clamp(5, 48); + + if show_workspace { + println!( + "{: String { + template + .spec + .as_ref() + .and_then(|spec| spec.workload.as_ref()) + .map_or_else( + || "".to_string(), + |workload| non_empty_or(&workload.image, "").to_string(), + ) +} + +fn template_resources(template: &SandboxWorkloadTemplate) -> Option<&SandboxResources> { + template + .spec + .as_ref() + .and_then(|spec| spec.workload.as_ref()) + .and_then(|workload| workload.resources.as_ref()) +} + +fn template_resources_gpu_display(resources: &SandboxResources) -> Option { + if let Some(gpu) = &resources.gpu { + return Some( + gpu.count + .map_or_else(|| "default".to_string(), |count| count.to_string()), + ); + } + None +} + +fn template_startup(template: &SandboxWorkloadTemplate) -> Option<&SandboxStartup> { + template + .spec + .as_ref() + .and_then(|spec| spec.desired_service_level.as_ref()) + .and_then(|service_level| service_level.startup.as_ref()) +} + +fn duration_to_ms(duration: &prost_types::Duration) -> i64 { + duration.seconds.saturating_mul(1_000) + i64::from(duration.nanos / 1_000_000) +} + +fn duration_display(duration: &prost_types::Duration) -> String { + let total_ms = duration_to_ms(duration); + if total_ms % 3_600_000 == 0 { + format!("{}h", total_ms / 3_600_000) + } else if total_ms % 60_000 == 0 { + format!("{}m", total_ms / 60_000) + } else if total_ms % 1_000 == 0 { + format!("{}s", total_ms / 1_000) + } else { + format!("{total_ms}ms") + } +} + +fn labels_display(labels: &HashMap) -> String { + let mut pairs = labels + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>(); + pairs.sort(); + pairs.join(", ") +} + /// Delete a sandbox by name, or all sandboxes when `all` is true. pub async fn sandbox_delete( server: &str, @@ -7675,8 +8310,10 @@ mod tests { ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrant, ProviderProfile, ProviderProfileCredential, ResourceRequirements, Sandbox, SandboxCondition, SandboxPhase, SandboxPolicy, - SandboxPolicyRevision, SandboxStatus, ServiceEndpoint, ServiceEndpointResponse, - WorkspaceMember, WorkspaceRole, datamodel::v1::ObjectMeta, + SandboxPolicyRevision, SandboxResources, SandboxStatus, SandboxWorkloadConfig, + SandboxWorkloadTemplate, SandboxWorkloadTemplateProvenance, SandboxWorkloadTemplateSpec, + ServiceEndpoint, ServiceEndpointResponse, WorkspaceMember, WorkspaceRole, + datamodel::v1::ObjectMeta, }; #[test] @@ -9139,6 +9776,74 @@ mod tests { assert_eq!(json["labels"]["env"], "prod"); } + #[test] + fn sandbox_template_to_json_includes_metadata_labels_and_annotations() { + let template = SandboxWorkloadTemplate { + metadata: Some(ObjectMeta { + id: "template-123".to_string(), + name: "gpu-kata".to_string(), + labels: std::collections::HashMap::from([( + "team".to_string(), + "runtime".to_string(), + )]), + annotations: std::collections::HashMap::from([( + "owner".to_string(), + "platform".to_string(), + )]), + workspace: "default".to_string(), + ..Default::default() + }), + ..Default::default() + }; + + let json = super::sandbox_template_to_json(&template); + + assert_eq!(json["labels"]["team"], "runtime"); + assert_eq!(json["annotations"]["owner"], "platform"); + } + + #[test] + fn sandbox_template_to_json_formats_default_gpu_like_display_output() { + let template = SandboxWorkloadTemplate { + spec: Some(SandboxWorkloadTemplateSpec { + workload: Some(SandboxWorkloadConfig { + resources: Some(SandboxResources { + gpu: Some(GpuResourceRequirements { count: None }), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + + let json = super::sandbox_template_to_json(&template); + + assert_eq!(json["resources"]["gpu"], "default"); + } + + #[test] + fn sandbox_template_to_json_preserves_explicit_gpu_count_as_number() { + let template = SandboxWorkloadTemplate { + spec: Some(SandboxWorkloadTemplateSpec { + workload: Some(SandboxWorkloadConfig { + resources: Some(SandboxResources { + gpu: Some(GpuResourceRequirements { count: Some(2) }), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + + let json = super::sandbox_template_to_json(&template); + + assert_eq!(json["resources"]["gpu"], 2); + } + #[test] fn provider_to_json_omits_zero_metadata_fields() { let metadata = ObjectMeta { @@ -9237,6 +9942,10 @@ mod tests { created_at_ms: 1_609_459_200_000, ..Default::default() }), + created_from_workload_template: Some(SandboxWorkloadTemplateProvenance { + name: "gpu-kata".to_string(), + resource_version: "7".to_string(), + }), ..Default::default() }; sandbox.set_phase(SandboxPhase::Ready as i32); @@ -9256,6 +9965,11 @@ mod tests { assert_eq!(json["policy_source"], "global"); assert_eq!(json["revision"], 3); assert!(json["policy"].is_null()); + assert_eq!(json["created_from_workload_template"]["name"], "gpu-kata"); + assert_eq!( + json["created_from_workload_template"]["resource_version"], + "7" + ); } #[test] diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index 743cfec42c..2a4801b143 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -154,6 +154,8 @@ impl OpenShell for TestOpenShell { Ok(Response::new(ListSandboxesResponse::default())) } + unimplemented_sandbox_template_rpcs!(); + async fn list_sandbox_providers( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/helpers/mod.rs b/crates/openshell-cli/tests/helpers/mod.rs index a58e750b91..c4e9b4b75a 100644 --- a/crates/openshell-cli/tests/helpers/mod.rs +++ b/crates/openshell-cli/tests/helpers/mod.rs @@ -8,6 +8,95 @@ //! mod helpers; //! ``` +#[macro_export] +macro_rules! unimplemented_sandbox_template_rpcs { + () => { + fn create_sandbox_template<'life0, 'async_trait>( + &'life0 self, + _request: tonic::Request, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = Result< + tonic::Response, + tonic::Status, + >, + > + Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + Box::pin(async { Err(tonic::Status::unimplemented("unused")) }) + } + + fn get_sandbox_template<'life0, 'async_trait>( + &'life0 self, + _request: tonic::Request, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = Result< + tonic::Response, + tonic::Status, + >, + > + Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + Box::pin(async { Err(tonic::Status::unimplemented("unused")) }) + } + + fn list_sandbox_templates<'life0, 'async_trait>( + &'life0 self, + _request: tonic::Request, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = Result< + tonic::Response, + tonic::Status, + >, + > + Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + Box::pin(async { Err(tonic::Status::unimplemented("unused")) }) + } + + fn delete_sandbox_template<'life0, 'async_trait>( + &'life0 self, + _request: tonic::Request, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = Result< + tonic::Response, + tonic::Status, + >, + > + Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + Box::pin(async { Err(tonic::Status::unimplemented("unused")) }) + } + }; +} + use rcgen::{ BasicConstraints, Certificate, CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair, }; diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index e4735ac4c1..12c838baf1 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -113,6 +113,8 @@ impl OpenShell for TestOpenShell { )) } + unimplemented_sandbox_template_rpcs!(); + async fn list_sandbox_providers( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 46e3b903c0..39452b6de5 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -185,6 +185,7 @@ impl OpenShell for TestOpenShell { }), spec: None, status: None, + ..Sandbox::default() }), })) } @@ -196,6 +197,8 @@ impl OpenShell for TestOpenShell { Ok(Response::new(ListSandboxesResponse::default())) } + unimplemented_sandbox_template_rpcs!(); + async fn list_sandbox_providers( &self, request: tonic::Request, diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 51e2159a5c..7be771c442 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -14,20 +14,22 @@ use openshell_cli::tls::TlsOptions; use openshell_core::proto::open_shell_server::{OpenShell, OpenShellServer}; use openshell_core::proto::{ AttachSandboxProviderRequest, AttachSandboxProviderResponse, CreateProviderRequest, - CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderRequest, - DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - DetachSandboxProviderRequest, DetachSandboxProviderResponse, - ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, - ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, - GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, - GetSandboxProviderEnvironmentResponse, GetSandboxRequest, GpuResourceRequirements, - HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse, - ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxesRequest, - ListSandboxesResponse, PlatformEvent, ProviderResponse, RevokeSshSessionRequest, + CreateSandboxRequest, CreateSandboxTemplateRequest, CreateSshSessionRequest, + CreateSshSessionResponse, DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, + DeleteSandboxResponse, DeleteSandboxTemplateRequest, DetachSandboxProviderRequest, + DetachSandboxProviderResponse, ExchangeProviderSubjectTokenRequest, + ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, + GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, + GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, + GetSandboxProviderEnvironmentResponse, GetSandboxRequest, GetSandboxTemplateRequest, + GpuResourceRequirements, HealthRequest, HealthResponse, ListProvidersRequest, + ListProvidersResponse, ListSandboxProvidersRequest, ListSandboxProvidersResponse, + ListSandboxTemplatesRequest, ListSandboxTemplatesResponse, ListSandboxesRequest, + ListSandboxesResponse, PlatformEvent, Provider, ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, Sandbox, SandboxCondition, SandboxLogLine, SandboxPhase, - SandboxResponse, SandboxStatus, SandboxStreamEvent, ServiceStatus, SettingValue, - SupervisorMessage, UpdateProviderRequest, WatchSandboxRequest, sandbox_stream_event, + SandboxResponse, SandboxStatus, SandboxStreamEvent, SandboxTemplateResponse, + SandboxWorkloadTemplate, ServiceStatus, SettingValue, SupervisorMessage, UpdateProviderRequest, + WatchSandboxRequest, sandbox_stream_event, }; use std::collections::HashMap; use std::fs; @@ -56,6 +58,11 @@ struct SandboxState { ssh_session_requests: Arc, global_settings: Arc>>, gateway_config_requests: Arc, + providers: Arc>>, + template_create_requests: Arc>>, + template_get_requests: Arc>>, + template_list_requests: Arc>>, + template_delete_requests: Arc>>, } #[derive(Clone, Default)] @@ -180,6 +187,95 @@ impl OpenShell for TestOpenShell { Ok(Response::new(ListSandboxesResponse::default())) } + async fn create_sandbox_template( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + let mut template = request.template.clone().unwrap_or_default(); + let name = template + .metadata + .as_ref() + .map_or_else(|| "template".to_string(), |metadata| metadata.name.clone()); + template.metadata = Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: format!("template-{name}"), + name, + created_at_ms: 0, + labels: template + .metadata + .as_ref() + .map(|metadata| metadata.labels.clone()) + .unwrap_or_default(), + resource_version: 1, + annotations: HashMap::new(), + workspace: request.workspace.clone(), + deletion_timestamp_ms: 0, + }); + self.state + .template_create_requests + .lock() + .await + .push(request); + Ok(Response::new(SandboxTemplateResponse { + template: Some(template), + })) + } + + async fn get_sandbox_template( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + self.state + .template_get_requests + .lock() + .await + .push(request.clone()); + Ok(Response::new(SandboxTemplateResponse { + template: Some(SandboxWorkloadTemplate { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: format!("template-{}", request.name), + name: request.name, + created_at_ms: 0, + labels: HashMap::new(), + resource_version: 1, + annotations: HashMap::new(), + workspace: request.workspace, + deletion_timestamp_ms: 0, + }), + spec: None, + }), + })) + } + + async fn list_sandbox_templates( + &self, + request: tonic::Request, + ) -> Result, Status> { + self.state + .template_list_requests + .lock() + .await + .push(request.into_inner()); + Ok(Response::new(ListSandboxTemplatesResponse { + templates: Vec::new(), + })) + } + + async fn delete_sandbox_template( + &self, + request: tonic::Request, + ) -> Result, Status> { + self.state + .template_delete_requests + .lock() + .await + .push(request.into_inner()); + Ok(Response::new( + openshell_core::proto::DeleteSandboxTemplateResponse { deleted: true }, + )) + } + async fn list_sandbox_providers( &self, _request: tonic::Request, @@ -337,7 +433,9 @@ impl OpenShell for TestOpenShell { &self, _request: tonic::Request, ) -> Result, Status> { - Ok(Response::new(ListProvidersResponse::default())) + Ok(Response::new(ListProvidersResponse { + providers: self.state.providers.lock().await.clone(), + })) } async fn list_provider_profiles( @@ -1209,6 +1307,63 @@ async fn create_requests(server: &TestServer) -> Vec { server.openshell.state.create_requests.lock().await.clone() } +async fn template_create_requests(server: &TestServer) -> Vec { + server + .openshell + .state + .template_create_requests + .lock() + .await + .clone() +} + +async fn template_list_requests(server: &TestServer) -> Vec { + server + .openshell + .state + .template_list_requests + .lock() + .await + .clone() +} + +async fn template_delete_requests(server: &TestServer) -> Vec { + server + .openshell + .state + .template_delete_requests + .lock() + .await + .clone() +} + +async fn add_provider(server: &TestServer, name: &str, provider_type: &str) { + server + .openshell + .state + .providers + .lock() + .await + .push(Provider { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: format!("provider-{name}"), + name: name.to_string(), + created_at_ms: 0, + labels: HashMap::new(), + resource_version: 0, + annotations: HashMap::new(), + workspace: "default".to_string(), + deletion_timestamp_ms: 0, + }), + r#type: provider_type.to_string(), + credentials: HashMap::new(), + config: HashMap::new(), + credential_expires_at_ms: HashMap::new(), + profile_workspace: "default".to_string(), + credential_handles: HashMap::new(), + }); +} + fn test_tls(server: &TestServer) -> TlsOptions { server.tls.with_gateway_name("openshell") } @@ -1529,6 +1684,201 @@ async fn sandbox_create_sends_driver_config_json() { ); } +#[tokio::test] +async fn sandbox_create_with_template_sends_workload_template_name() { + let server = run_server().await; + add_provider(&server, "github", "github").await; + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + install_fake_ssh(&fake_ssh_dir); + + run::sandbox_create( + &server.endpoint, + "openshell", + run::SandboxCreateConfig { + name: Some("from-template"), + template: Some("gpu-kata"), + providers: &["github".to_string()], + command: &["echo".into(), "OK".into()], + ..test_config() + }, + "default", + &tls, + ) + .await + .expect("sandbox create should succeed"); + + let requests = create_requests(&server).await; + let request = requests.first().expect("create request should be recorded"); + assert_eq!(request.workload_template_name, "gpu-kata"); + let spec = request + .spec + .as_ref() + .expect("governance spec should be sent"); + assert_eq!(spec.providers, vec!["github".to_string()]); + assert!(spec.template.is_none()); + assert!(spec.environment.is_empty()); + assert!(spec.resource_requirements.is_none()); +} + +#[tokio::test] +async fn sandbox_template_create_sends_workload_template_resource() { + let server = run_server().await; + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + + run::sandbox_template_create( + &server.endpoint, + "gpu-kata", + Some("registry.example.com/agent:latest"), + Some("2"), + Some("4Gi"), + Some(GpuResourceRequirements { count: Some(1) }), + Some(r#"{"kubernetes":{"pod":{"node_selector":{"pool":"gpu"}}}}"#), + Some("5m"), + Some(3), + HashMap::from([("team".to_string(), "runtime".to_string())]), + HashMap::from([("owner".to_string(), "platform".to_string())]), + HashMap::from([("FEATURE_FLAG".to_string(), "on".to_string())]), + "table", + "default", + &tls, + ) + .await + .expect("template create should succeed"); + + let requests = template_create_requests(&server).await; + let request = requests + .first() + .expect("template create request should be recorded"); + assert_eq!(request.workspace, "default"); + let template = request.template.as_ref().expect("template should be sent"); + let metadata = template.metadata.as_ref().expect("metadata should be sent"); + assert_eq!(metadata.name, "gpu-kata"); + assert_eq!(metadata.labels.get("team"), Some(&"runtime".to_string())); + assert_eq!( + metadata.annotations.get("owner"), + Some(&"platform".to_string()) + ); + + let spec = template.spec.as_ref().expect("spec should be sent"); + let workload = spec.workload.as_ref().expect("workload should be sent"); + assert_eq!(workload.image, "registry.example.com/agent:latest"); + assert_eq!( + workload.environment.get("FEATURE_FLAG"), + Some(&"on".to_string()) + ); + let resources = workload + .resources + .as_ref() + .expect("resources should be sent"); + assert_eq!(resources.cpu, "2"); + assert_eq!(resources.memory, "4Gi"); + assert_eq!(resources.gpu.as_ref().and_then(|gpu| gpu.count), Some(1)); + assert!(spec.driver_config.is_some()); + let startup = spec + .desired_service_level + .as_ref() + .and_then(|service_level| service_level.startup.as_ref()) + .expect("startup service level should be sent"); + assert_eq!(startup.max_burst, 3); + assert_eq!( + startup + .ready_within + .as_ref() + .map(|duration| duration.seconds), + Some(300) + ); +} + +#[tokio::test] +async fn sandbox_template_list_and_delete_send_workspace_requests() { + let server = run_server().await; + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + + run::sandbox_template_list( + &server.endpoint, + 25, + 5, + Some("team=runtime"), + false, + "table", + "default", + false, + &tls, + ) + .await + .expect("template list should succeed"); + run::sandbox_template_delete(&server.endpoint, &["gpu-kata".to_string()], "default", &tls) + .await + .expect("template delete should succeed"); + + let list_requests = template_list_requests(&server).await; + let list_request = list_requests + .first() + .expect("template list request should be recorded"); + assert_eq!(list_request.limit, 25); + assert_eq!(list_request.offset, 5); + assert_eq!(list_request.label_selector, "team=runtime"); + assert_eq!(list_request.workspace, "default"); + assert!(!list_request.all_workspaces); + + let delete_requests = template_delete_requests(&server).await; + let delete_request = delete_requests + .first() + .expect("template delete request should be recorded"); + assert_eq!(delete_request.name, "gpu-kata"); + assert_eq!(delete_request.workspace, "default"); +} + +#[tokio::test] +async fn sandbox_template_create_allows_omitted_image() { + let server = run_server().await; + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + + run::sandbox_template_create( + &server.endpoint, + "base", + None, + None, + None, + None, + None, + None, + None, + HashMap::new(), + HashMap::new(), + HashMap::new(), + "table", + "default", + &tls, + ) + .await + .expect("template create without image should succeed"); + + let requests = template_create_requests(&server).await; + let request = requests + .first() + .expect("template create request should be recorded"); + let workload = request + .template + .as_ref() + .and_then(|template| template.spec.as_ref()) + .and_then(|spec| spec.workload.as_ref()) + .expect("workload should be sent"); + assert_eq!(workload.image, ""); +} + #[tokio::test] async fn sandbox_create_sends_gpu_default_request() { let server = run_server().await; @@ -2258,6 +2608,44 @@ async fn run_cli_sandbox_create( .unwrap() } +async fn run_cli_sandbox_template_create( + server: &TestServer, + name: &str, + extra_args: &[&str], +) -> std::process::Output { + let xdg_dir = tempfile::tempdir().unwrap(); + let tls_dir = xdg_dir.path().join("openshell/gateways/openshell/mtls"); + fs::create_dir_all(&tls_dir).unwrap(); + for filename in ["ca.crt", "tls.crt", "tls.key"] { + fs::copy(server.dir.path().join(filename), tls_dir.join(filename)).unwrap(); + } + + let mut cmd = tokio::process::Command::new(env!("CARGO_BIN_EXE_openshell")); + for (key, _) in std::env::vars().filter(|(k, _)| k.starts_with("OPENSHELL_")) { + cmd.env_remove(&key); + } + cmd.args([ + "--gateway", + "openshell", + "--gateway-endpoint", + &server.endpoint, + "sandbox", + "template", + "create", + name, + "--image", + "registry.example.com/agent:latest", + "--output=json", + ]) + .args(extra_args) + .env("XDG_CONFIG_HOME", xdg_dir.path()) + .env("HOME", xdg_dir.path()) + .env("OPENSHELL_PROVISION_TIMEOUT", "5") + .output() + .await + .unwrap() +} + #[tokio::test] async fn sandbox_create_json_stdout_is_parseable() { let server = run_server().await; @@ -2287,3 +2675,63 @@ async fn sandbox_create_yaml_stdout_is_parseable() { serde_yml::from_str::(&stdout) .unwrap_or_else(|err| panic!("stdout should contain only YAML: {err}\n{stdout}")); } + +#[tokio::test] +async fn sandbox_template_create_warns_for_credential_env_vars() { + let server = run_server().await; + + let result = run_cli_sandbox_template_create( + &server, + "credential-env", + &["--env", "OPENAI_API_KEY=plain-secret"], + ) + .await; + + assert!( + result.status.success(), + "sandbox template create failed:\n{}", + String::from_utf8_lossy(&result.stderr) + ); + let stderr = String::from_utf8_lossy(&result.stderr); + assert!( + stderr.contains("OPENAI_API_KEY looks like a credential"), + "template create should warn for credential-looking --env values: {stderr}" + ); + assert!( + stderr.contains("To hide it from the agent, use a provider instead"), + "warning should point users toward providers: {stderr}" + ); + + let requests = template_create_requests(&server).await; + assert_eq!(requests.len(), 1); +} + +#[tokio::test] +async fn sandbox_template_create_suppresses_credential_env_warnings() { + let server = run_server().await; + + let result = run_cli_sandbox_template_create( + &server, + "credential-env-suppressed", + &[ + "--env", + "OPENAI_API_KEY=plain-secret", + "--no-credential-warnings", + ], + ) + .await; + + assert!( + result.status.success(), + "sandbox template create failed:\n{}", + String::from_utf8_lossy(&result.stderr) + ); + let stderr = String::from_utf8_lossy(&result.stderr); + assert!( + !stderr.contains("OPENAI_API_KEY looks like a credential"), + "template create should suppress credential-looking --env warnings: {stderr}" + ); + + let requests = template_create_requests(&server).await; + assert_eq!(requests.len(), 1); +} diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index ee72728aa9..5b62c7c15c 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -138,6 +138,8 @@ impl OpenShell for TestOpenShell { Ok(Response::new(ListSandboxesResponse::default())) } + unimplemented_sandbox_template_rpcs!(); + async fn list_sandbox_providers( &self, _request: tonic::Request, diff --git a/crates/openshell-core/src/metadata.rs b/crates/openshell-core/src/metadata.rs index 8794c11d5d..f885812c4e 100644 --- a/crates/openshell-core/src/metadata.rs +++ b/crates/openshell-core/src/metadata.rs @@ -6,8 +6,9 @@ //! These traits provide uniform access to `ObjectMeta` fields across all resource types. use crate::proto::{ - InferenceRoute, ObjectForTest, Provider, Sandbox, SandboxStatus, ServiceEndpoint, SshSession, - StoredProviderCredentialRefreshState, StoredProviderProfile, Workspace, WorkspaceMember, + InferenceRoute, ObjectForTest, Provider, Sandbox, SandboxStatus, SandboxWorkloadTemplate, + ServiceEndpoint, SshSession, StoredProviderCredentialRefreshState, StoredProviderProfile, + Workspace, WorkspaceMember, }; use std::collections::HashMap; @@ -104,6 +105,49 @@ impl Sandbox { } } +// Implementations for SandboxWorkloadTemplate +impl ObjectId for SandboxWorkloadTemplate { + fn object_id(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.id.as_str()) + } +} + +impl ObjectName for SandboxWorkloadTemplate { + fn object_name(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.name.as_str()) + } +} + +impl ObjectLabels for SandboxWorkloadTemplate { + fn object_labels(&self) -> Option> { + self.metadata.as_ref().map(|m| m.labels.clone()) + } +} + +impl SetResourceVersion for SandboxWorkloadTemplate { + fn set_resource_version(&mut self, version: u64) { + if let Some(meta) = self.metadata.as_mut() { + meta.resource_version = version; + } + } +} + +impl GetResourceVersion for SandboxWorkloadTemplate { + fn get_resource_version(&self) -> u64 { + self.metadata.as_ref().map_or(0, |m| m.resource_version) + } +} + +impl ObjectWorkspace for SandboxWorkloadTemplate { + fn object_workspace(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.workspace.as_str()) + } + + fn requires_workspace() -> bool { + true + } +} + // Implementations for Workspace impl ObjectId for Workspace { fn object_id(&self) -> &str { diff --git a/crates/openshell-core/src/telemetry.rs b/crates/openshell-core/src/telemetry.rs index 780e5c7920..63aa5f9181 100644 --- a/crates/openshell-core/src/telemetry.rs +++ b/crates/openshell-core/src/telemetry.rs @@ -145,6 +145,7 @@ impl PolicyDecisionOperation { pub enum SandboxTemplateSource { Default, Image, + WorkloadTemplate, Undefined, } @@ -154,6 +155,7 @@ impl SandboxTemplateSource { match self { Self::Default => "default", Self::Image => "image", + Self::WorkloadTemplate => "workload_template", Self::Undefined => "undefined", } } diff --git a/crates/openshell-gateway-interceptors/src/proto_json.rs b/crates/openshell-gateway-interceptors/src/proto_json.rs index b0d6b0f53d..d7ea5a241c 100644 --- a/crates/openshell-gateway-interceptors/src/proto_json.rs +++ b/crates/openshell-gateway-interceptors/src/proto_json.rs @@ -317,6 +317,7 @@ mod tests { annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }; let bytes = request.encode_to_vec(); let json = codec diff --git a/crates/openshell-gateway-interceptors/src/runtime.rs b/crates/openshell-gateway-interceptors/src/runtime.rs index 02afddab75..cd0f59ea3c 100644 --- a/crates/openshell-gateway-interceptors/src/runtime.rs +++ b/crates/openshell-gateway-interceptors/src/runtime.rs @@ -1076,6 +1076,7 @@ mod tests { annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }; let bytes = request.encode_to_vec(); diff --git a/crates/openshell-sdk/README.md b/crates/openshell-sdk/README.md index 93afc5359e..cb42e12dc1 100644 --- a/crates/openshell-sdk/README.md +++ b/crates/openshell-sdk/README.md @@ -9,7 +9,8 @@ gateway-name resolution. ## Two layers - `OpenShellClient` — the curated, sandbox-focused surface: health, sandbox - CRUD, readiness/deletion waits, and non-streaming exec. + CRUD, reusable sandbox template CRUD, readiness/deletion waits, and + non-streaming exec. - `raw` — direct access to the generated tonic clients for RPCs the curated surface doesn't yet cover (inference, providers, policy, logs, settings, SSH, forwarding). @@ -44,10 +45,53 @@ mTLS (client certificates) is not supported. `OpenShellClient::connect(ClientConfig)` returns a connected client exposing `health`, `create_sandbox`, `get_sandbox`, `list_sandboxes`, `delete_sandbox`, +`create_sandbox_from_template`, `create_sandbox_template`, +`get_sandbox_template`, `list_sandbox_templates`, `delete_sandbox_template`, `wait_ready`, `wait_deleted`, and `exec`. Curated types (`SandboxSpec`, -`SandboxRef`, `Health`, `ListOptions`, `ExecOptions`, `SandboxPhase`) use -SDK-shaped enums rather than raw proto integers. Failures map to a typed -`SdkError` with a discriminable kind. +`SandboxRef`, `Health`, `ListOptions`, `SandboxTemplateListOptions`, +`ExecOptions`, `SandboxPhase`) use SDK-shaped enums rather than raw proto +integers where practical. Reusable template resources are exposed as +`SandboxWorkloadTemplate` proto aliases so callers can populate the full +portable workload shape and driver config. Failures map to a typed `SdkError` +with a discriminable kind. + +```rust +use openshell_sdk::{ + ClientConfig, OpenShellClient, SandboxTemplateCreateSpec, + SandboxWorkloadConfig, SandboxWorkloadTemplate, SandboxWorkloadTemplateSpec, +}; + +# async fn run() -> Result<(), openshell_sdk::SdkError> { +let client = OpenShellClient::connect(ClientConfig::new("http://127.0.0.1:8080")).await?; +client + .create_sandbox_template(SandboxWorkloadTemplate { + metadata: Some(openshell_sdk::raw::proto::datamodel::v1::ObjectMeta { + name: "python".to_string(), + ..Default::default() + }), + spec: Some(SandboxWorkloadTemplateSpec { + workload: Some(SandboxWorkloadConfig { + image: "ghcr.io/nvidia/openshell-community/sandboxes/python:latest".to_string(), + ..Default::default() + }), + ..Default::default() + }), + }) + .await?; + +let _sandbox = client + .create_sandbox_from_template(SandboxTemplateCreateSpec { + template_name: "python".to_string(), + policy: Some(openshell_sdk::raw::proto::SandboxPolicy { + version: 1, + ..Default::default() + }), + ..Default::default() + }) + .await?; +# Ok(()) +# } +``` ## Modules diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index a02b5735ae..b5486812f7 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -16,7 +16,7 @@ use crate::refresh::{RefreshedToken, TokenSource}; use crate::transport; use crate::types::{ ExecOptions, ExecResult, Health, ListOptions, SandboxPhase, SandboxRef, SandboxSpec, - WorkspaceRef, + SandboxTemplateCreateSpec, SandboxTemplateListOptions, SandboxWorkloadTemplate, WorkspaceRef, }; use futures::StreamExt; use openshell_core::proto; @@ -163,6 +163,86 @@ impl OpenShellClient { sandbox_from_response(response.sandbox) } + /// Create a new sandbox from a workspace-scoped workload template name. + pub async fn create_sandbox_from_template( + &self, + spec: SandboxTemplateCreateSpec, + ) -> Result { + let request = create_sandbox_from_template_request(spec); + let response = self + .unary(|mut grpc| { + let request = request.clone(); + async move { grpc.create_sandbox(request).await } + }) + .await?; + sandbox_from_response(response.sandbox) + } + + /// Create a reusable sandbox template in the default workspace. + pub async fn create_sandbox_template( + &self, + template: SandboxWorkloadTemplate, + ) -> Result { + let response = self + .unary(|mut grpc| { + let request = proto::CreateSandboxTemplateRequest { + template: Some(template.clone()), + workspace: String::new(), + }; + async move { grpc.create_sandbox_template(request).await } + }) + .await?; + sandbox_template_from_response(response.template) + } + + /// Fetch a reusable sandbox template by name from the default workspace. + pub async fn get_sandbox_template(&self, name: &str) -> Result { + let response = self + .unary(|mut grpc| { + let request = proto::GetSandboxTemplateRequest { + name: name.to_string(), + workspace: String::new(), + }; + async move { grpc.get_sandbox_template(request).await } + }) + .await?; + sandbox_template_from_response(response.template) + } + + /// List reusable sandbox templates in the default workspace or across all workspaces. + pub async fn list_sandbox_templates( + &self, + opts: SandboxTemplateListOptions, + ) -> Result> { + let response = self + .unary(|mut grpc| { + let request = proto::ListSandboxTemplatesRequest { + limit: opts.limit, + offset: opts.offset, + workspace: String::new(), + all_workspaces: opts.all_workspaces, + label_selector: opts.label_selector.clone(), + }; + async move { grpc.list_sandbox_templates(request).await } + }) + .await?; + Ok(response.templates) + } + + /// Delete a reusable sandbox template by name from the default workspace. + pub async fn delete_sandbox_template(&self, name: &str) -> Result { + let response = self + .unary(|mut grpc| { + let request = proto::DeleteSandboxTemplateRequest { + name: name.to_string(), + workspace: String::new(), + }; + async move { grpc.delete_sandbox_template(request).await } + }) + .await?; + Ok(response.deleted) + } + /// Fetch a sandbox by name. pub async fn get_sandbox(&self, name: &str) -> Result { let response = self @@ -567,6 +647,96 @@ impl WorkspaceScopedClient { sandbox_from_response(response.sandbox) } + /// Create a new sandbox from a template in this workspace. + pub async fn create_sandbox_from_template( + &self, + spec: SandboxTemplateCreateSpec, + ) -> Result { + let mut request = create_sandbox_from_template_request(spec); + request.workspace = self.workspace.clone(); + let response = self + .client + .unary(|mut grpc| { + let request = request.clone(); + async move { grpc.create_sandbox(request).await } + }) + .await?; + sandbox_from_response(response.sandbox) + } + + /// Create a reusable sandbox template in this workspace. + pub async fn create_sandbox_template( + &self, + template: SandboxWorkloadTemplate, + ) -> Result { + let response = self + .client + .unary(|mut grpc| { + let request = proto::CreateSandboxTemplateRequest { + template: Some(template.clone()), + workspace: self.workspace.clone(), + }; + async move { grpc.create_sandbox_template(request).await } + }) + .await?; + sandbox_template_from_response(response.template) + } + + /// Fetch a reusable sandbox template by name in this workspace. + pub async fn get_sandbox_template(&self, name: &str) -> Result { + let response = self + .client + .unary(|mut grpc| { + let request = proto::GetSandboxTemplateRequest { + name: name.to_string(), + workspace: self.workspace.clone(), + }; + async move { grpc.get_sandbox_template(request).await } + }) + .await?; + sandbox_template_from_response(response.template) + } + + /// List reusable sandbox templates in this workspace, or across all workspaces. + pub async fn list_sandbox_templates( + &self, + opts: SandboxTemplateListOptions, + ) -> Result> { + let response = self + .client + .unary(|mut grpc| { + let request = proto::ListSandboxTemplatesRequest { + limit: opts.limit, + offset: opts.offset, + workspace: if opts.all_workspaces { + String::new() + } else { + self.workspace.clone() + }, + all_workspaces: opts.all_workspaces, + label_selector: opts.label_selector.clone(), + }; + async move { grpc.list_sandbox_templates(request).await } + }) + .await?; + Ok(response.templates) + } + + /// Delete a reusable sandbox template by name in this workspace. + pub async fn delete_sandbox_template(&self, name: &str) -> Result { + let response = self + .client + .unary(|mut grpc| { + let request = proto::DeleteSandboxTemplateRequest { + name: name.to_string(), + workspace: self.workspace.clone(), + }; + async move { grpc.delete_sandbox_template(request).await } + }) + .await?; + Ok(response.deleted) + } + /// Fetch a sandbox by name in this workspace. pub async fn get_sandbox(&self, name: &str) -> Result { let response = self @@ -837,6 +1007,36 @@ fn create_sandbox_request(spec: SandboxSpec) -> proto::CreateSandboxRequest { annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), + } +} + +fn create_sandbox_from_template_request( + spec: SandboxTemplateCreateSpec, +) -> proto::CreateSandboxRequest { + let SandboxTemplateCreateSpec { + name, + template_name, + labels, + providers, + command, + tty, + policy, + } = spec; + proto::CreateSandboxRequest { + spec: Some(proto::SandboxSpec { + providers, + command, + tty, + policy, + ..proto::SandboxSpec::default() + }), + name: name.unwrap_or_default(), + labels, + annotations: HashMap::new(), + workspace: String::new(), + workload_template_name: template_name, + await_main_process_attachment: false, } } @@ -846,6 +1046,13 @@ fn sandbox_from_response(sandbox: Option) -> Result .ok_or_else(|| SdkError::invalid_config("sandbox missing from gateway response")) } +fn sandbox_template_from_response( + template: Option, +) -> Result { + template + .ok_or_else(|| SdkError::invalid_config("sandbox template missing from gateway response")) +} + fn map_status(status: tonic::Status) -> SdkError { let message = status.message().to_string(); match status.code() { diff --git a/crates/openshell-sdk/src/lib.rs b/crates/openshell-sdk/src/lib.rs index dbf2524a2a..985c7ecc05 100644 --- a/crates/openshell-sdk/src/lib.rs +++ b/crates/openshell-sdk/src/lib.rs @@ -6,7 +6,8 @@ //! Two layers: //! //! - [`OpenShellClient`] — the high-level sandbox-focused MVP surface: -//! health, sandbox CRUD, readiness/deletion waits, non-streaming exec. +//! health, sandbox CRUD, reusable sandbox templates, readiness/deletion +//! waits, and non-streaming exec. //! - [`raw`] — direct access to the generated tonic clients for RPCs the //! curated surface doesn't yet cover (inference, providers, policy, logs, //! settings, SSH, forwarding). @@ -46,6 +47,8 @@ pub use config::{AuthConfig, ClientConfig}; pub use error::SdkError; pub use refresh::{Refresh, RefreshError, RefreshedToken, TokenSource}; pub use types::{ - ExecOptions, ExecResult, Health, ListOptions, SandboxPhase, SandboxRef, SandboxSpec, - ServiceStatus, WorkspaceRef, + ExecOptions, ExecResult, Health, ListOptions, SandboxPhase, SandboxRef, SandboxResources, + SandboxServiceLevel, SandboxSpec, SandboxStartup, SandboxTemplateCreateSpec, + SandboxTemplateListOptions, SandboxWorkloadConfig, SandboxWorkloadTemplate, + SandboxWorkloadTemplateProvenance, SandboxWorkloadTemplateSpec, ServiceStatus, WorkspaceRef, }; diff --git a/crates/openshell-sdk/src/raw.rs b/crates/openshell-sdk/src/raw.rs index e974b19259..35d91f3325 100644 --- a/crates/openshell-sdk/src/raw.rs +++ b/crates/openshell-sdk/src/raw.rs @@ -22,11 +22,15 @@ pub use openshell_core::proto; pub use openshell_core::proto::inference_client::InferenceClient; pub use openshell_core::proto::open_shell_client::OpenShellClient as GrpcClient; pub use openshell_core::proto::{ - CreateSandboxRequest, CreateWorkspaceRequest, DeleteSandboxRequest, DeleteWorkspaceRequest, - ExecSandboxRequest, GetSandboxRequest, GetWorkspaceRequest, HealthRequest, - ListProvidersRequest, ListSandboxesRequest, ListWorkspacesRequest, Sandbox, - SandboxPhase as ProtoSandboxPhase, SandboxSpec as ProtoSandboxSpec, SandboxTemplate, - ServiceStatus as ProtoServiceStatus, StartSandboxRequest, StopSandboxRequest, Workspace, + CreateSandboxRequest, CreateSandboxTemplateRequest, CreateWorkspaceRequest, + DeleteSandboxRequest, DeleteSandboxTemplateRequest, DeleteWorkspaceRequest, ExecSandboxRequest, + GetSandboxRequest, GetSandboxTemplateRequest, GetWorkspaceRequest, HealthRequest, + ListProvidersRequest, ListSandboxTemplatesRequest, ListSandboxesRequest, ListWorkspacesRequest, + Sandbox, SandboxPhase as ProtoSandboxPhase, SandboxResources, SandboxServiceLevel, + SandboxSpec as ProtoSandboxSpec, SandboxStartup, SandboxTemplate, SandboxTemplateResponse, + SandboxWorkloadConfig, SandboxWorkloadTemplate, SandboxWorkloadTemplateProvenance, + SandboxWorkloadTemplateSpec, ServiceStatus as ProtoServiceStatus, StartSandboxRequest, + StopSandboxRequest, Workspace, }; /// Type alias for the gRPC client wrapped in the SDK's auth interceptor. diff --git a/crates/openshell-sdk/src/types.rs b/crates/openshell-sdk/src/types.rs index 9cb7171fd4..db2944474b 100644 --- a/crates/openshell-sdk/src/types.rs +++ b/crates/openshell-sdk/src/types.rs @@ -117,6 +117,60 @@ pub struct SandboxSpec { pub tty: bool, } +/// Caller intent for creating a sandbox from a named workload template. +#[derive(Clone, Debug, Default)] +pub struct SandboxTemplateCreateSpec { + /// Optional user-supplied sandbox name. When empty the server generates one. + pub name: Option, + /// Workspace-scoped template name to resolve at creation time. + pub template_name: String, + /// Labels attached to the sandbox. + pub labels: HashMap, + /// Provider names to attach. + pub providers: Vec, + /// Exact canonical command. Empty selects the gateway's scratch login shell. + pub command: Vec, + /// Allocate a retained pseudo-terminal for the canonical command. + pub tty: bool, + /// Create-time sandbox policy. The named workload template supplies runtime + /// workload fields; policy remains part of the sandbox's governance spec. + pub policy: Option, +} + +/// Reusable sandbox workload template resource. +/// +/// This is a raw proto alias because template specs intentionally expose the +/// full portable workload shape plus driver-owned config. +pub type SandboxWorkloadTemplate = proto::SandboxWorkloadTemplate; + +/// Desired reusable workload shape for a [`SandboxWorkloadTemplate`]. +pub type SandboxWorkloadTemplateSpec = proto::SandboxWorkloadTemplateSpec; + +/// Portable sandbox workload configuration for template-backed sandboxes. +pub type SandboxWorkloadConfig = proto::SandboxWorkloadConfig; + +/// Portable resource requirements for template-backed sandboxes. +pub type SandboxResources = proto::SandboxResources; + +/// Desired service level for sandboxes created from a template. +pub type SandboxServiceLevel = proto::SandboxServiceLevel; + +/// Startup service-level settings for template-backed sandboxes. +pub type SandboxStartup = proto::SandboxStartup; + +/// Options for listing reusable sandbox templates. +#[derive(Clone, Debug, Default)] +pub struct SandboxTemplateListOptions { + /// Maximum templates to return. `0` defers to the server default. + pub limit: u32, + /// Offset into the result list. + pub offset: u32, + /// Optional label selector in `key=value,key2=value2` form. + pub label_selector: String, + /// List templates across all workspaces. + pub all_workspaces: bool, +} + /// Reference to a sandbox owned by the gateway. #[derive(Clone, Debug)] #[non_exhaustive] @@ -128,12 +182,28 @@ pub struct SandboxRef { pub labels: HashMap, pub resource_version: u64, pub exit_code: Option, + pub created_from_workload_template: Option, +} + +/// Reusable workload template revision used to create a sandbox. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct SandboxWorkloadTemplateProvenance { + pub name: String, + pub resource_version: String, } impl SandboxRef { pub(crate) fn from_proto(sandbox: proto::Sandbox) -> Self { let phase = sandbox.phase().into(); let exit_code = sandbox.status.as_ref().and_then(|status| status.exit_code); + let created_from_workload_template = + sandbox + .created_from_workload_template + .map(|p| SandboxWorkloadTemplateProvenance { + name: p.name, + resource_version: p.resource_version, + }); let meta = sandbox.metadata.unwrap_or_default(); Self { id: meta.id, @@ -143,6 +213,7 @@ impl SandboxRef { labels: meta.labels, resource_version: meta.resource_version, exit_code, + created_from_workload_template, } } } diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 1cdac7da41..58633ceb17 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -12,7 +12,8 @@ use openshell_core::proto; use openshell_core::proto::open_shell_server::{OpenShell, OpenShellServer}; use openshell_sdk::{ AuthConfig, ClientConfig, ExecOptions, ListOptions, OpenShellClient, Refresh, RefreshError, - RefreshedToken, SandboxPhase, SandboxSpec, ServiceStatus as SdkServiceStatus, + RefreshedToken, SandboxPhase, SandboxSpec, SandboxTemplateCreateSpec, + SandboxTemplateListOptions, ServiceStatus as SdkServiceStatus, }; use std::collections::HashMap; use std::sync::Arc; @@ -29,6 +30,10 @@ struct MockState { last_get_name: Mutex>, last_get_workspace: Mutex>, last_create: Mutex>, + last_template_create: Mutex>, + last_template_get: Mutex>, + last_template_list: Mutex>, + last_template_delete: Mutex>, last_delete_name: Mutex>, last_delete_workspace: Mutex>, last_stop: Mutex>, @@ -61,6 +66,11 @@ fn sandbox_with_phase_ws( phase: proto::SandboxPhase, workspace: &str, ) -> proto::Sandbox { + let created_from_workload_template = + (name == "from-template").then(|| proto::SandboxWorkloadTemplateProvenance { + name: "python".to_string(), + resource_version: "7".to_string(), + }); proto::Sandbox { metadata: Some(proto::datamodel::v1::ObjectMeta { id: format!("id-{name}"), @@ -77,6 +87,7 @@ fn sandbox_with_phase_ws( phase: phase.into(), ..Default::default() }), + created_from_workload_template, } } @@ -98,6 +109,34 @@ fn workspace_proto(name: &str, phase: proto::datamodel::v1::WorkspacePhase) -> p } } +fn workload_template_proto(name: &str, workspace: &str) -> proto::SandboxWorkloadTemplate { + proto::SandboxWorkloadTemplate { + metadata: Some(proto::datamodel::v1::ObjectMeta { + id: format!("template-{workspace}-{name}"), + name: name.to_string(), + created_at_ms: 1_000_000, + labels: HashMap::new(), + annotations: HashMap::new(), + resource_version: 1, + deletion_timestamp_ms: 0, + workspace: workspace.to_string(), + }), + spec: Some(proto::SandboxWorkloadTemplateSpec { + workload: Some(proto::SandboxWorkloadConfig { + image: format!("ghcr.io/test/{name}:latest"), + environment: HashMap::new(), + resources: Some(proto::SandboxResources { + cpu: "1".to_string(), + memory: "512Mi".to_string(), + ..proto::SandboxResources::default() + }), + }), + driver_config: None, + desired_service_level: None, + }), + } +} + #[tonic::async_trait] impl OpenShell for TestOpenShell { async fn report_main_process_exit( @@ -177,6 +216,62 @@ impl OpenShell for TestOpenShell { })) } + async fn create_sandbox_template( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + let template = request + .template + .clone() + .ok_or_else(|| Status::invalid_argument("missing template"))?; + *self.state.last_template_create.lock().await = Some(request); + Ok(Response::new(proto::SandboxTemplateResponse { + template: Some(template), + })) + } + + async fn get_sandbox_template( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + let workspace = if request.workspace.is_empty() { + "default" + } else { + &request.workspace + }; + let template = workload_template_proto(&request.name, workspace); + *self.state.last_template_get.lock().await = Some(request); + Ok(Response::new(proto::SandboxTemplateResponse { + template: Some(template), + })) + } + + async fn list_sandbox_templates( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + *self.state.last_template_list.lock().await = Some(request); + Ok(Response::new(proto::ListSandboxTemplatesResponse { + templates: vec![ + workload_template_proto("python", "default"), + workload_template_proto("cuda", "gpu"), + ], + })) + } + + async fn delete_sandbox_template( + &self, + request: tonic::Request, + ) -> Result, Status> { + *self.state.last_template_delete.lock().await = Some(request.into_inner()); + Ok(Response::new(proto::DeleteSandboxTemplateResponse { + deleted: true, + })) + } + async fn stop_sandbox( &self, request: tonic::Request, @@ -817,6 +912,92 @@ async fn create_sandbox_passes_spec_through() { ); } +#[tokio::test] +async fn create_sandbox_from_template_passes_template_name() { + let state = Arc::new(MockState::default()); + let endpoint = start_mock(state.clone()).await; + let client = connect(&endpoint).await; + + let sandbox = client + .create_sandbox_from_template(SandboxTemplateCreateSpec { + name: Some("from-template".to_string()), + template_name: "python".to_string(), + providers: vec!["openai".to_string()], + command: vec!["python".to_string(), "-m".to_string(), "agent".to_string()], + tty: false, + policy: Some(proto::SandboxPolicy { + version: 1, + ..Default::default() + }), + ..Default::default() + }) + .await + .unwrap(); + assert_eq!(sandbox.name, "from-template"); + + let observed = state.last_create.lock().await.clone().unwrap(); + assert_eq!(observed.name, "from-template"); + assert_eq!(observed.workload_template_name, "python"); + let observed_spec = observed.spec.unwrap(); + assert_eq!(observed_spec.providers, vec!["openai".to_string()]); + assert_eq!(observed_spec.command, vec!["python", "-m", "agent"]); + assert!(!observed_spec.tty); + assert_eq!(observed_spec.policy.unwrap().version, 1); +} + +#[tokio::test] +async fn sandbox_template_crud_uses_default_workspace() { + let state = Arc::new(MockState::default()); + let endpoint = start_mock(state.clone()).await; + let client = connect(&endpoint).await; + + let created = client + .create_sandbox_template(workload_template_proto("python", "")) + .await + .unwrap(); + assert_eq!(created.metadata.as_ref().unwrap().name, "python"); + + let observed_create = state.last_template_create.lock().await.clone().unwrap(); + assert!(observed_create.workspace.is_empty()); + assert_eq!( + observed_create + .template + .as_ref() + .and_then(|template| template.metadata.as_ref()) + .unwrap() + .name, + "python" + ); + + let fetched = client.get_sandbox_template("python").await.unwrap(); + assert_eq!(fetched.metadata.as_ref().unwrap().name, "python"); + let observed_get = state.last_template_get.lock().await.clone().unwrap(); + assert_eq!(observed_get.name, "python"); + assert!(observed_get.workspace.is_empty()); + + let listed = client + .list_sandbox_templates(SandboxTemplateListOptions { + limit: 10, + offset: 2, + label_selector: String::new(), + all_workspaces: true, + }) + .await + .unwrap(); + assert_eq!(listed.len(), 2); + let observed_list = state.last_template_list.lock().await.clone().unwrap(); + assert_eq!(observed_list.limit, 10); + assert_eq!(observed_list.offset, 2); + assert!(observed_list.workspace.is_empty()); + assert!(observed_list.all_workspaces); + + let deleted = client.delete_sandbox_template("python").await.unwrap(); + assert!(deleted); + let observed_delete = state.last_template_delete.lock().await.clone().unwrap(); + assert_eq!(observed_delete.name, "python"); + assert!(observed_delete.workspace.is_empty()); +} + #[tokio::test] async fn get_sandbox_sends_name_and_maps_phase() { let state = Arc::new(MockState { @@ -835,6 +1016,21 @@ async fn get_sandbox_sends_name_and_maps_phase() { assert_eq!(observed.as_deref(), Some("my-box")); } +#[tokio::test] +async fn get_sandbox_preserves_workload_template_provenance() { + let state = Arc::new(MockState::default()); + let endpoint = start_mock(state.clone()).await; + let client = connect(&endpoint).await; + + let sandbox = client.get_sandbox("from-template").await.unwrap(); + + let provenance = sandbox + .created_from_workload_template + .expect("template provenance"); + assert_eq!(provenance.name, "python"); + assert_eq!(provenance.resource_version, "7"); +} + #[tokio::test] async fn list_sandboxes_propagates_filters() { let state = Arc::new(MockState::default()); @@ -1137,6 +1333,33 @@ async fn workspace_scoped_create_passes_workspace() { assert_eq!(observed.workspace, "staging"); } +#[tokio::test] +async fn workspace_scoped_create_from_template_passes_workspace() { + let state = Arc::new(MockState::default()); + let endpoint = start_mock(state.clone()).await; + let client = connect(&endpoint).await; + + let sandbox = client + .workspace("staging") + .create_sandbox_from_template(SandboxTemplateCreateSpec { + name: Some("from-template".to_string()), + template_name: "python".to_string(), + policy: Some(proto::SandboxPolicy { + version: 2, + ..Default::default() + }), + ..Default::default() + }) + .await + .unwrap(); + assert_eq!(sandbox.name, "from-template"); + + let observed = state.last_create.lock().await.clone().unwrap(); + assert_eq!(observed.workspace, "staging"); + assert_eq!(observed.workload_template_name, "python"); + assert_eq!(observed.spec.unwrap().policy.unwrap().version, 2); +} + #[tokio::test] async fn workspace_scoped_get_passes_workspace() { let state = Arc::new(MockState { @@ -1169,6 +1392,50 @@ async fn workspace_scoped_list_passes_workspace() { assert!(!observed.all_workspaces); } +#[tokio::test] +async fn workspace_scoped_sandbox_template_crud_passes_workspace() { + let state = Arc::new(MockState::default()); + let endpoint = start_mock(state.clone()).await; + let client = connect(&endpoint).await; + let ws = client.workspace("staging"); + + ws.create_sandbox_template(workload_template_proto("python", "staging")) + .await + .unwrap(); + let observed_create = state.last_template_create.lock().await.clone().unwrap(); + assert_eq!(observed_create.workspace, "staging"); + + ws.get_sandbox_template("python").await.unwrap(); + let observed_get = state.last_template_get.lock().await.clone().unwrap(); + assert_eq!(observed_get.name, "python"); + assert_eq!(observed_get.workspace, "staging"); + + let listed = ws + .list_sandbox_templates(SandboxTemplateListOptions::default()) + .await + .unwrap(); + assert_eq!(listed.len(), 2); + let observed_list = state.last_template_list.lock().await.clone().unwrap(); + assert_eq!(observed_list.workspace, "staging"); + assert!(!observed_list.all_workspaces); + + ws.list_sandbox_templates(SandboxTemplateListOptions { + all_workspaces: true, + ..Default::default() + }) + .await + .unwrap(); + let observed_all = state.last_template_list.lock().await.clone().unwrap(); + assert!(observed_all.workspace.is_empty()); + assert!(observed_all.all_workspaces); + + let deleted = ws.delete_sandbox_template("python").await.unwrap(); + assert!(deleted); + let observed_delete = state.last_template_delete.lock().await.clone().unwrap(); + assert_eq!(observed_delete.name, "python"); + assert_eq!(observed_delete.workspace, "staging"); +} + #[tokio::test] async fn workspace_scoped_delete_passes_workspace() { let state = Arc::new(MockState::default()); diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 053b08de9c..4fdae8c38e 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -35,7 +35,7 @@ use openshell_core::proto::compute::v1::{ }; use openshell_core::proto::{ PlatformEvent, Sandbox, SandboxCondition, SandboxPhase, SandboxSpec, SandboxStatus, - SandboxTemplate, ServiceEndpoint, SshSession, + SandboxTemplate, SandboxWorkloadTemplate, ServiceEndpoint, SshSession, }; use openshell_core::telemetry::TelemetryComputeDriver; use openshell_core::{ObjectLabels, ObjectWorkspace}; @@ -3974,6 +3974,12 @@ impl ObjectType for Sandbox { } } +impl ObjectType for SandboxWorkloadTemplate { + fn object_type() -> &'static str { + "sandbox_workload_template" + } +} + fn compute_error_from_status(status: Status) -> ComputeError { match status.code() { Code::AlreadyExists => ComputeError::AlreadyExists, diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 957a77cbac..8143e6058e 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -17,29 +17,31 @@ use openshell_core::proto::{ AttachSandboxProviderRequest, AttachSandboxProviderResponse, ClearDraftChunksRequest, ClearDraftChunksResponse, ComputeDriverCapabilities, ComputeDriverInfo, ConfigureProviderRefreshRequest, ConfigureProviderRefreshResponse, CreateProviderRequest, - CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, - CreateWorkspaceRequest, CreateWorkspaceResponse, DeleteProviderProfileRequest, - DeleteProviderProfileResponse, DeleteProviderRefreshRequest, DeleteProviderRefreshResponse, - DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - DeleteServiceRequest, DeleteServiceResponse, DeleteWorkspaceRequest, DeleteWorkspaceResponse, - DetachSandboxProviderRequest, DetachSandboxProviderResponse, EditDraftChunkRequest, - EditDraftChunkResponse, ExchangeProviderSubjectTokenRequest, - ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, - ExposeServiceRequest, FinalizeMainProcessExitRequest, FinalizeMainProcessExitResponse, - GatewayMessage, GetCurrentUserRequest, GetCurrentUserResponse, GetDraftHistoryRequest, - GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse, + CreateSandboxRequest, CreateSandboxTemplateRequest, CreateSshSessionRequest, + CreateSshSessionResponse, CreateWorkspaceRequest, CreateWorkspaceResponse, + DeleteProviderProfileRequest, DeleteProviderProfileResponse, DeleteProviderRefreshRequest, + DeleteProviderRefreshResponse, DeleteProviderRequest, DeleteProviderResponse, + DeleteSandboxRequest, DeleteSandboxResponse, DeleteSandboxTemplateRequest, + DeleteSandboxTemplateResponse, DeleteServiceRequest, DeleteServiceResponse, + DeleteWorkspaceRequest, DeleteWorkspaceResponse, DetachSandboxProviderRequest, + DetachSandboxProviderResponse, EditDraftChunkRequest, EditDraftChunkResponse, + ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, + ExecSandboxInput, ExecSandboxRequest, ExposeServiceRequest, FinalizeMainProcessExitRequest, + FinalizeMainProcessExitResponse, GatewayMessage, GetCurrentUserRequest, GetCurrentUserResponse, + GetDraftHistoryRequest, GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse, GetGatewayConfigRequest, GetGatewayConfigResponse, GetGatewayInfoRequest, GetGatewayInfoResponse, GetProviderProfileRequest, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxLogsResponse, GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest, - GetServiceRequest, GetWorkspaceRequest, GetWorkspaceResponse, HealthRequest, HealthResponse, - ImportProviderProfilesRequest, ImportProviderProfilesResponse, IssueSandboxTokenRequest, - IssueSandboxTokenResponse, LintProviderProfilesRequest, LintProviderProfilesResponse, - ListProviderProfilesRequest, ListProviderProfilesResponse, ListProvidersRequest, - ListProvidersResponse, ListSandboxPoliciesRequest, ListSandboxPoliciesResponse, - ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxesRequest, + GetSandboxTemplateRequest, GetServiceRequest, GetWorkspaceRequest, GetWorkspaceResponse, + HealthRequest, HealthResponse, ImportProviderProfilesRequest, ImportProviderProfilesResponse, + IssueSandboxTokenRequest, IssueSandboxTokenResponse, LintProviderProfilesRequest, + LintProviderProfilesResponse, ListProviderProfilesRequest, ListProviderProfilesResponse, + ListProvidersRequest, ListProvidersResponse, ListSandboxPoliciesRequest, + ListSandboxPoliciesResponse, ListSandboxProvidersRequest, ListSandboxProvidersResponse, + ListSandboxTemplatesRequest, ListSandboxTemplatesResponse, ListSandboxesRequest, ListSandboxesResponse, ListServicesRequest, ListServicesResponse, ListWorkspaceMembersRequest, ListWorkspaceMembersResponse, ListWorkspacesRequest, ListWorkspacesResponse, ProviderProfileResponse, ProviderResponse, PushSandboxLogsRequest, PushSandboxLogsResponse, @@ -48,12 +50,12 @@ use openshell_core::proto::{ RemoveWorkspaceMemberResponse, ReportMainProcessExitRequest, ReportMainProcessExitResponse, ReportPolicyStatusRequest, ReportPolicyStatusResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, RotateProviderCredentialRequest, RotateProviderCredentialResponse, - SandboxResponse, ServiceEndpointResponse, ServiceStatus, StartSandboxRequest, - StopSandboxRequest, SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, - SupervisorMessage, TcpForwardFrame, UndoDraftChunkRequest, UndoDraftChunkResponse, - UpdateConfigRequest, UpdateConfigResponse, UpdateProviderProfilesRequest, - UpdateProviderProfilesResponse, UpdateProviderRequest, WatchSandboxRequest, - open_shell_server::OpenShell, + SandboxResponse, SandboxTemplateResponse, ServiceEndpointResponse, ServiceStatus, + StartSandboxRequest, StopSandboxRequest, SubmitPolicyAnalysisRequest, + SubmitPolicyAnalysisResponse, SupervisorMessage, TcpForwardFrame, UndoDraftChunkRequest, + UndoDraftChunkResponse, UpdateConfigRequest, UpdateConfigResponse, + UpdateProviderProfilesRequest, UpdateProviderProfilesResponse, UpdateProviderRequest, + WatchSandboxRequest, open_shell_server::OpenShell, }; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -299,6 +301,34 @@ impl OpenShell for OpenShellService { sandbox::handle_list_sandboxes(&self.state, request).await } + async fn create_sandbox_template( + &self, + request: Request, + ) -> Result, Status> { + sandbox::handle_create_sandbox_template(&self.state, request).await + } + + async fn get_sandbox_template( + &self, + request: Request, + ) -> Result, Status> { + sandbox::handle_get_sandbox_template(&self.state, request).await + } + + async fn list_sandbox_templates( + &self, + request: Request, + ) -> Result, Status> { + sandbox::handle_list_sandbox_templates(&self.state, request).await + } + + async fn delete_sandbox_template( + &self, + request: Request, + ) -> Result, Status> { + sandbox::handle_delete_sandbox_template(&self.state, request).await + } + async fn list_sandbox_providers( &self, request: Request, diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 35765781c4..764c0bbfde 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -11365,6 +11365,7 @@ mod tests { ..SandboxSpec::default() }), status: None, + ..Sandbox::default() }; sandbox.set_phase(SandboxPhase::Ready as i32); store.put_message(&sandbox).await.unwrap(); @@ -11401,6 +11402,7 @@ mod tests { }), spec: Some(SandboxSpec::default()), status: None, + ..Sandbox::default() }; sandbox.set_phase(SandboxPhase::Ready as i32); store.put_message(&sandbox).await.unwrap(); diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 5364e0541e..d4d08da251 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -16,14 +16,19 @@ use crate::auth::workspace_authz::{ use crate::persistence::{ObjectLabels, ObjectType, WriteCondition, generate_name}; use futures::future; use openshell_core::net::set_tcp_nodelay_best_effort; +use openshell_core::proto::datamodel::v1::ObjectMeta; use openshell_core::proto::{ AttachSandboxProviderRequest, AttachSandboxProviderResponse, CreateSandboxRequest, - CreateSshSessionRequest, CreateSshSessionResponse, DeleteSandboxRequest, DeleteSandboxResponse, - DetachSandboxProviderRequest, DetachSandboxProviderResponse, ExecSandboxEvent, ExecSandboxExit, - ExecSandboxInput, ExecSandboxRequest, ExecSandboxStderr, ExecSandboxStdout, GetSandboxRequest, - ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxesRequest, - ListSandboxesResponse, Provider, RevokeSshSessionRequest, RevokeSshSessionResponse, - SandboxResponse, SandboxStreamEvent, SshRelayTarget, StartSandboxRequest, StopSandboxRequest, + CreateSandboxTemplateRequest, CreateSshSessionRequest, CreateSshSessionResponse, + DeleteSandboxRequest, DeleteSandboxResponse, DeleteSandboxTemplateRequest, + DeleteSandboxTemplateResponse, DetachSandboxProviderRequest, DetachSandboxProviderResponse, + ExecSandboxEvent, ExecSandboxExit, ExecSandboxInput, ExecSandboxRequest, ExecSandboxStderr, + ExecSandboxStdout, GetSandboxRequest, GetSandboxTemplateRequest, ListSandboxProvidersRequest, + ListSandboxProvidersResponse, ListSandboxTemplatesRequest, ListSandboxTemplatesResponse, + ListSandboxesRequest, ListSandboxesResponse, Provider, ResourceRequirements, + RevokeSshSessionRequest, RevokeSshSessionResponse, SandboxResources, SandboxResponse, + SandboxSpec, SandboxStreamEvent, SandboxTemplateResponse, SandboxWorkloadTemplate, + SandboxWorkloadTemplateProvenance, SshRelayTarget, StartSandboxRequest, StopSandboxRequest, TcpForwardFrame, TcpForwardInit, TcpRelayTarget, WatchSandboxRequest, relay_open, tcp_forward_init, }; @@ -31,8 +36,9 @@ use openshell_core::proto::{Sandbox, SandboxPhase, SandboxTemplate, SshSession}; use openshell_core::telemetry::{ LifecycleOperation, LifecycleResource, SandboxTemplateSource, TelemetryOutcome, }; -use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; +use openshell_core::{GetResourceVersion, ObjectId, ObjectName, ObjectWorkspace}; use prost::Message; +use prost_types::{Struct, Value, value::Kind}; use std::collections::HashMap; use std::net::IpAddr; use std::pin::Pin; @@ -52,14 +58,16 @@ use super::provider::{ get_provider_record, is_valid_env_key, validate_provider_environment_keys_unique_with_catalog, }; use super::validation::{ - level_matches, source_matches, validate_exec_request_fields, - validate_no_reserved_provider_policy_keys, validate_policy_safety, validate_sandbox_spec, + level_matches, source_matches, validate_dns1123_label, validate_exec_request_fields, + validate_no_reserved_provider_policy_keys, validate_policy_safety, + validate_sandbox_governance_spec, validate_sandbox_spec, }; use super::{MAX_PAGE_SIZE, MAX_PROVIDERS, MAX_ROUTABLE_NAME_LEN, clamp_limit}; use crate::persistence::current_time_ms; const TCP_FORWARD_CHUNK_SIZE: usize = 64 * 1024; const NO_LOGIN_SHELL_ENV: (&str, &str) = ("OPENSHELL_NO_LOGIN_SHELL", "1"); +const MAX_TEMPLATES_PER_WORKSPACE: u32 = 1000; #[derive(Debug)] pub struct WatchSandboxStream { @@ -158,30 +166,69 @@ pub(super) async fn handle_create_sandbox( ) -> Result, Status> { let create_request = request.get_ref().clone(); let result = handle_create_sandbox_inner(state, request).await; + let created_sandbox = result + .as_ref() + .ok() + .and_then(|response| response.get_ref().sandbox.as_ref()); emit_sandbox_create_telemetry( state, &create_request, + created_sandbox, TelemetryOutcome::from_success(result.is_ok()), ); result } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SandboxCreateTelemetryAttrs { + requested_gpu: bool, + provider_count: u64, + has_custom_policy: bool, + template_source: SandboxTemplateSource, +} + fn emit_sandbox_create_telemetry( state: &Arc, request: &CreateSandboxRequest, + created_sandbox: Option<&Sandbox>, outcome: TelemetryOutcome, ) { let compute_driver = state.compute.telemetry_compute_driver(); + let attrs = sandbox_create_telemetry_attrs(request, created_sandbox); + openshell_core::telemetry::emit_sandbox_create( + outcome, + attrs.requested_gpu, + attrs.provider_count, + attrs.has_custom_policy, + attrs.template_source, + compute_driver, + ); +} + +fn sandbox_create_telemetry_attrs( + request: &CreateSandboxRequest, + created_sandbox: Option<&Sandbox>, +) -> SandboxCreateTelemetryAttrs { + if !request.workload_template_name.trim().is_empty() { + let spec = created_sandbox + .and_then(|sandbox| sandbox.spec.as_ref()) + .or(request.spec.as_ref()); + return SandboxCreateTelemetryAttrs { + requested_gpu: spec.is_some_and(|spec| { + openshell_core::gpu::sandbox_gpu_requested(spec.resource_requirements.as_ref()) + }), + provider_count: spec.map_or(0, |spec| spec.providers.len() as u64), + has_custom_policy: spec.is_some_and(|spec| spec.policy.is_some()), + template_source: SandboxTemplateSource::WorkloadTemplate, + }; + } let Some(spec) = request.spec.as_ref() else { - openshell_core::telemetry::emit_sandbox_create( - outcome, - false, - 0, - false, - SandboxTemplateSource::Undefined, - compute_driver, - ); - return; + return SandboxCreateTelemetryAttrs { + requested_gpu: false, + provider_count: 0, + has_custom_policy: false, + template_source: SandboxTemplateSource::Undefined, + }; }; let template_source = if spec .template @@ -194,14 +241,12 @@ fn emit_sandbox_create_telemetry( }; let gpu_requested = openshell_core::gpu::sandbox_gpu_requested(spec.resource_requirements.as_ref()); - openshell_core::telemetry::emit_sandbox_create( - outcome, - gpu_requested, - spec.providers.len() as u64, - spec.policy.is_some(), + SandboxCreateTelemetryAttrs { + requested_gpu: gpu_requested, + provider_count: spec.providers.len() as u64, + has_custom_policy: spec.policy.is_some(), template_source, - compute_driver, - ); + } } async fn handle_create_sandbox_inner( @@ -211,27 +256,9 @@ async fn handle_create_sandbox_inner( let principal = super::extract_principal(&request)?; let request = request.into_inner(); let await_main_process_attachment = request.await_main_process_attachment; - let mut spec = request - .spec - .ok_or_else(|| Status::invalid_argument("spec is required"))?; - - // Every newly persisted sandbox has one explicit canonical process. This - // portable default also preserves compatibility with callers compiled - // before the main-process field was introduced. - if spec.command.is_empty() { - spec.command = vec!["/bin/bash".to_string(), "-l".to_string()]; - spec.tty = true; - } - - // Validate field sizes before any I/O (fail fast on oversized payloads). - validate_sandbox_spec(&request.name, &spec)?; + let workload_template_name = request.workload_template_name.trim().to_string(); - // Validate labels (keys and values must meet Kubernetes requirements). - for (key, value) in &request.labels { - crate::grpc::validation::validate_label_key(key)?; - crate::grpc::validation::validate_label_value(value)?; - } - crate::grpc::validation::validate_annotations(&request.annotations, "annotations")?; + validate_create_sandbox_request_pre_io(&request, &workload_template_name)?; let authz = authorize_workspace( &state.store, @@ -245,6 +272,42 @@ async fn handle_create_sandbox_inner( .await? .ensure_active()?; + let (mut spec, created_from_workload_template) = if workload_template_name.is_empty() { + let spec = request + .spec + .ok_or_else(|| Status::invalid_argument("spec is required"))?; + (spec, None) + } else { + let governance_spec = request.spec.unwrap_or_default(); + let template = state + .store + .get_message_by_name::(&workspace, &workload_template_name) + .await + .map_err(|e| Status::internal(format!("fetch sandbox template failed: {e}")))? + .ok_or_else(|| Status::not_found("sandbox template not found"))?; + let provenance = SandboxWorkloadTemplateProvenance { + name: template.object_name().to_string(), + resource_version: template.get_resource_version().to_string(), + }; + let mut resolved = sandbox_spec_from_stored_workload_template(&template)?; + resolved.policy = governance_spec.policy; + resolved.providers = governance_spec.providers; + resolved.command = governance_spec.command; + resolved.tty = governance_spec.tty; + (resolved, Some(provenance)) + }; + + // Every newly persisted sandbox has one explicit canonical process. This + // portable default also preserves compatibility with callers compiled + // before the main-process field was introduced. + if spec.command.is_empty() { + spec.command = vec!["/bin/bash".to_string(), "-l".to_string()]; + spec.tty = true; + } + + // Validate field sizes before any create-side effects. + validate_sandbox_spec(&request.name, &spec)?; + let _sandbox_sync_guard = if spec.providers.is_empty() { None } else { @@ -274,7 +337,7 @@ async fn handle_create_sandbox_inner( // Ensure the template always carries the resolved image. let template = spec.template.get_or_insert_with(SandboxTemplate::default); - if template.image.is_empty() { + if template.image.trim().is_empty() { template.image = state.compute.default_image().to_string(); } @@ -302,7 +365,7 @@ async fn handle_create_sandbox_inner( let now_ms = current_time_ms(); let mut sandbox = Sandbox { - metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + metadata: Some(ObjectMeta { id: id.clone(), name: name.clone(), created_at_ms: now_ms, @@ -314,6 +377,7 @@ async fn handle_create_sandbox_inner( }), spec: Some(spec), status: None, + created_from_workload_template, }; sandbox.set_phase(SandboxPhase::Provisioning as i32); @@ -372,6 +436,135 @@ async fn handle_create_sandbox_inner( })) } +fn validate_create_sandbox_request_pre_io( + request: &CreateSandboxRequest, + workload_template_name: &str, +) -> Result<(), Status> { + // Validate labels (keys and values must meet Kubernetes requirements). + for (key, value) in &request.labels { + crate::grpc::validation::validate_label_key(key)?; + crate::grpc::validation::validate_label_value(value)?; + } + crate::grpc::validation::validate_annotations(&request.annotations, "annotations")?; + + if workload_template_name.is_empty() { + let spec = request + .spec + .as_ref() + .ok_or_else(|| Status::invalid_argument("spec is required"))?; + return validate_sandbox_spec(&request.name, spec); + } + + validate_dns1123_label(workload_template_name, "workload_template_name")?; + if let Some(spec) = request.spec.as_ref() { + validate_template_create_governance_spec(spec)?; + validate_sandbox_governance_spec(&request.name, spec)?; + } else { + validate_sandbox_governance_spec(&request.name, &SandboxSpec::default())?; + } + Ok(()) +} + +fn validate_template_create_governance_spec(spec: &SandboxSpec) -> Result<(), Status> { + if !spec.log_level.is_empty() { + return Err(Status::invalid_argument( + "spec.log_level cannot be set when workload_template_name is set", + )); + } + if !spec.environment.is_empty() { + return Err(Status::invalid_argument( + "spec.environment cannot be set when workload_template_name is set", + )); + } + if spec.template.is_some() { + return Err(Status::invalid_argument( + "spec.template cannot be set when workload_template_name is set", + )); + } + if spec.resource_requirements.is_some() { + return Err(Status::invalid_argument( + "spec.resource_requirements cannot be set when workload_template_name is set", + )); + } + Ok(()) +} + +fn sandbox_spec_from_stored_workload_template( + template: &SandboxWorkloadTemplate, +) -> Result { + sandbox_spec_from_workload_template(template, tonic::Code::Internal) +} + +fn sandbox_spec_from_user_workload_template( + template: &SandboxWorkloadTemplate, +) -> Result { + sandbox_spec_from_workload_template(template, tonic::Code::InvalidArgument) +} + +fn sandbox_spec_from_workload_template( + template: &SandboxWorkloadTemplate, + missing_field_code: tonic::Code, +) -> Result { + let spec = template + .spec + .as_ref() + .ok_or_else(|| Status::new(missing_field_code, "sandbox template spec is required"))?; + let workload = spec + .workload + .as_ref() + .ok_or_else(|| Status::new(missing_field_code, "sandbox template workload is required"))?; + let resources = workload.resources.as_ref(); + Ok(SandboxSpec { + environment: workload.environment.clone(), + template: Some(SandboxTemplate { + image: workload.image.clone(), + resources: resources.and_then(template_resource_struct), + driver_config: spec.driver_config.clone(), + ..SandboxTemplate::default() + }), + resource_requirements: resources.and_then(template_gpu_requirements), + ..SandboxSpec::default() + }) +} + +fn template_gpu_requirements(resources: &SandboxResources) -> Option { + Some(ResourceRequirements { + gpu: Some(resources.gpu?), + }) +} + +fn template_resource_struct(resources: &SandboxResources) -> Option { + let mut limits = std::collections::BTreeMap::new(); + if !resources.cpu.is_empty() { + limits.insert( + "cpu".to_string(), + Value { + kind: Some(Kind::StringValue(resources.cpu.clone())), + }, + ); + } + if !resources.memory.is_empty() { + limits.insert( + "memory".to_string(), + Value { + kind: Some(Kind::StringValue(resources.memory.clone())), + }, + ); + } + if limits.is_empty() { + None + } else { + let mut fields = std::collections::BTreeMap::new(); + fields.insert( + "limits".to_string(), + Value { + kind: Some(Kind::StructValue(Struct { fields: limits })), + }, + ); + Some(Struct { fields }) + } +} + pub(super) async fn handle_get_sandbox( state: &Arc, request: Request, @@ -472,12 +665,106 @@ pub(super) async fn handle_list_sandboxes( Ok(Response::new(ListSandboxesResponse { sandboxes })) } -pub(super) async fn handle_list_sandbox_providers( +pub(super) async fn handle_create_sandbox_template( state: &Arc, - request: Request, -) -> Result, Status> { + request: Request, +) -> Result, Status> { + let principal = super::extract_principal(&request)?; + let req = request.into_inner(); + let template = req + .template + .ok_or_else(|| Status::invalid_argument("template is required"))?; + let metadata = template.metadata.clone().unwrap_or_default(); + let authz = authorize_workspace( + &state.store, + &state.admin_role, + &principal, + &req.workspace, + MinWorkspaceRole::Admin, + ) + .await?; + let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) + .await? + .ensure_active()?; + if !metadata.workspace.is_empty() && metadata.workspace != workspace { + return Err(Status::invalid_argument( + "template.metadata.workspace must match request workspace", + )); + } + if metadata.name.is_empty() { + return Err(Status::invalid_argument( + "template.metadata.name is required", + )); + } + + let mut resolved = template; + resolved.metadata = Some(ObjectMeta { + id: uuid::Uuid::new_v4().to_string(), + name: metadata.name, + created_at_ms: current_time_ms(), + labels: metadata.labels, + resource_version: 0, + annotations: metadata.annotations, + workspace: workspace.clone(), + deletion_timestamp_ms: 0, + }); + validate_sandbox_workload_template(&resolved)?; + + let labels_map = resolved.object_labels(); + let labels_json = if labels_map.as_ref().is_none_or(HashMap::is_empty) { + None + } else { + Some( + serde_json::to_string(&labels_map) + .map_err(|e| Status::internal(format!("failed to serialize labels: {e}")))?, + ) + }; + let write = state + .store + .create_if_workspace_count_below( + SandboxWorkloadTemplate::object_type(), + resolved.object_id(), + resolved.object_name(), + &workspace, + &resolved.encode_to_vec(), + labels_json.as_deref(), + u64::from(MAX_TEMPLATES_PER_WORKSPACE), + ) + .await; + let write = match write { + Ok(Some(write)) => write, + Ok(None) => { + return Err(Status::resource_exhausted(format!( + "workspace has reached the maximum of {MAX_TEMPLATES_PER_WORKSPACE} sandbox templates" + ))); + } + Err(crate::persistence::PersistenceError::UniqueViolation { .. }) => { + return Err(Status::already_exists("sandbox template already exists")); + } + Err(err) => { + return Err(Status::internal(format!( + "persist sandbox template failed: {err}" + ))); + } + }; + if let Some(metadata) = resolved.metadata.as_mut() { + metadata.resource_version = write.resource_version; + } + + Ok(Response::new(SandboxTemplateResponse { + template: Some(resolved), + })) +} + +pub(super) async fn handle_get_sandbox_template( + state: &Arc, + request: Request, +) -> Result, Status> { let principal = super::extract_principal(&request)?; let req = request.into_inner(); + if req.name.is_empty() { + return Err(Status::invalid_argument("name is required")); + } let authz = authorize_workspace( &state.store, &state.admin_role, @@ -489,35 +776,221 @@ pub(super) async fn handle_list_sandbox_providers( let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) .await? .name; - let sandbox = sandbox_by_name(state, &workspace, &req.sandbox_name).await?; - let providers = providers_for_sandbox(state, &sandbox, &workspace).await?; - Ok(Response::new(ListSandboxProvidersResponse { providers })) + let template = state + .store + .get_message_by_name::(&workspace, &req.name) + .await + .map_err(|e| Status::internal(format!("fetch sandbox template failed: {e}")))? + .ok_or_else(|| Status::not_found("sandbox template not found"))?; + Ok(Response::new(SandboxTemplateResponse { + template: Some(template), + })) } -pub(super) async fn handle_attach_sandbox_provider( +pub(super) async fn handle_list_sandbox_templates( state: &Arc, - request: Request, -) -> Result, Status> { + request: Request, +) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); + if request.all_workspaces && !request.workspace.is_empty() { + return Err(Status::invalid_argument( + "all_workspaces and workspace are mutually exclusive", + )); + } + let limit = clamp_limit(request.limit, 100, MAX_PAGE_SIZE); + let templates = if request.all_workspaces { + require_platform_admin(&state.admin_role, &principal)?; + if request.label_selector.is_empty() { + state + .store + .list_all_messages::(limit, request.offset) + .await + .map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))? + } else { + crate::grpc::validation::validate_label_selector(&request.label_selector)?; + state + .store + .list_all_messages_with_selector::( + &request.label_selector, + limit, + request.offset, + ) + .await + .map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))? + } + } else { + let authz = authorize_workspace( + &state.store, + &state.admin_role, + &principal, + &request.workspace, + MinWorkspaceRole::User, + ) + .await?; + let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) + .await? + .name; + if request.label_selector.is_empty() { + state + .store + .list_messages::(&workspace, limit, request.offset) + .await + .map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))? + } else { + crate::grpc::validation::validate_label_selector(&request.label_selector)?; + state + .store + .list_messages_with_selector::( + &workspace, + &request.label_selector, + limit, + request.offset, + ) + .await + .map_err(|e| { + Status::internal(format!("list sandbox templates with selector failed: {e}")) + })? + } + }; + Ok(Response::new(ListSandboxTemplatesResponse { templates })) +} + +pub(super) async fn handle_delete_sandbox_template( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = super::extract_principal(&request)?; + let req = request.into_inner(); + if req.name.is_empty() { + return Err(Status::invalid_argument("name is required")); + } let authz = authorize_workspace( &state.store, &state.admin_role, &principal, - &request.workspace, - MinWorkspaceRole::User, + &req.workspace, + MinWorkspaceRole::Admin, ) .await?; let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) .await? - .ensure_active()?; - if request.provider_name.is_empty() { - return Err(Status::invalid_argument("provider_name is required")); + .name; + let deleted = state + .store + .delete_by_name( + SandboxWorkloadTemplate::object_type(), + &workspace, + &req.name, + ) + .await + .map_err(|e| Status::internal(format!("delete sandbox template failed: {e}")))?; + Ok(Response::new(DeleteSandboxTemplateResponse { deleted })) +} + +fn validate_sandbox_workload_template(template: &SandboxWorkloadTemplate) -> Result<(), Status> { + super::validation::validate_object_metadata(template.metadata.as_ref(), "sandbox_template")?; + let name = template.object_name().to_string(); + validate_dns1123_label(&name, "template.metadata.name")?; + validate_sandbox_workload_template_service_level(template)?; + let spec = sandbox_spec_from_user_workload_template(template)?; + validate_sandbox_spec(&name, &spec)?; + Ok(()) +} + +fn validate_sandbox_workload_template_service_level( + template: &SandboxWorkloadTemplate, +) -> Result<(), Status> { + let Some(startup) = template + .spec + .as_ref() + .and_then(|spec| spec.desired_service_level.as_ref()) + .and_then(|service_level| service_level.startup.as_ref()) + else { + return Ok(()); + }; + if let Some(ready_within) = &startup.ready_within { + validate_positive_normalized_duration( + ready_within, + "template.spec.desired_service_level.startup.ready_within", + )?; } + Ok(()) +} - // Validate provider name would not violate sandbox spec constraints if added - // (pre-validation ensures CAS mutations preserve invariants) - if request.provider_name.len() > super::MAX_NAME_LEN { +fn validate_positive_normalized_duration( + duration: &prost_types::Duration, + field: &str, +) -> Result<(), Status> { + const MAX_DURATION_SECONDS: u64 = 315_576_000_000; + if duration.seconds.unsigned_abs() > MAX_DURATION_SECONDS + || duration.nanos.unsigned_abs() >= 1_000_000_000 + { + return Err(Status::invalid_argument(format!( + "{field} must be a valid protobuf Duration" + ))); + } + if (duration.seconds > 0 && duration.nanos < 0) || (duration.seconds < 0 && duration.nanos > 0) + { + return Err(Status::invalid_argument(format!( + "{field} must be a normalized protobuf Duration" + ))); + } + if duration.seconds < 0 || duration.nanos < 0 || (duration.seconds == 0 && duration.nanos == 0) + { + return Err(Status::invalid_argument(format!( + "{field} must be greater than zero" + ))); + } + Ok(()) +} + +pub(super) async fn handle_list_sandbox_providers( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = super::extract_principal(&request)?; + let req = request.into_inner(); + let authz = authorize_workspace( + &state.store, + &state.admin_role, + &principal, + &req.workspace, + MinWorkspaceRole::User, + ) + .await?; + let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) + .await? + .name; + let sandbox = sandbox_by_name(state, &workspace, &req.sandbox_name).await?; + let providers = providers_for_sandbox(state, &sandbox, &workspace).await?; + Ok(Response::new(ListSandboxProvidersResponse { providers })) +} + +pub(super) async fn handle_attach_sandbox_provider( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = super::extract_principal(&request)?; + let request = request.into_inner(); + let authz = authorize_workspace( + &state.store, + &state.admin_role, + &principal, + &request.workspace, + MinWorkspaceRole::User, + ) + .await?; + let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) + .await? + .ensure_active()?; + if request.provider_name.is_empty() { + return Err(Status::invalid_argument("provider_name is required")); + } + + // Validate provider name would not violate sandbox spec constraints if added + // (pre-validation ensures CAS mutations preserve invariants) + if request.provider_name.len() > super::MAX_NAME_LEN { return Err(Status::invalid_argument(format!( "provider_name exceeds maximum length ({} > {})", request.provider_name.len(), @@ -1753,7 +2226,7 @@ pub(super) async fn handle_create_ssh_session( 0 }; let session = SshSession { - metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + metadata: Some(ObjectMeta { id: token.clone(), name: generate_name(), created_at_ms: now_ms, @@ -2532,6 +3005,7 @@ mod tests { }; use crate::provider_profile_sources::ProviderProfileSources; use openshell_core::GatewayProviderProfileSourceConfig; + use openshell_core::proto::GpuResourceRequirements; use openshell_core::proto::datamodel::v1::ObjectMeta; async fn test_server_state_with_user_only_github_profile() -> Arc { @@ -2590,6 +3064,66 @@ mod tests { } } + #[test] + fn sandbox_create_telemetry_uses_resolved_template_gpu_request() { + let request = CreateSandboxRequest { + spec: Some(SandboxSpec { + providers: vec!["github".to_string()], + policy: Some(openshell_core::proto::SandboxPolicy::default()), + ..SandboxSpec::default() + }), + workload_template_name: "gpu-kata".to_string(), + ..CreateSandboxRequest::default() + }; + let created = Sandbox { + spec: Some(SandboxSpec { + providers: vec!["github".to_string()], + policy: Some(openshell_core::proto::SandboxPolicy::default()), + resource_requirements: Some(ResourceRequirements { + gpu: Some(GpuResourceRequirements { count: Some(1) }), + }), + ..SandboxSpec::default() + }), + created_from_workload_template: Some(SandboxWorkloadTemplateProvenance { + name: "gpu-kata".to_string(), + resource_version: "7".to_string(), + }), + ..Sandbox::default() + }; + + assert_eq!( + sandbox_create_telemetry_attrs(&request, Some(&created)), + SandboxCreateTelemetryAttrs { + requested_gpu: true, + provider_count: 1, + has_custom_policy: true, + template_source: SandboxTemplateSource::WorkloadTemplate, + } + ); + } + + #[test] + fn sandbox_create_telemetry_falls_back_to_request_for_unresolved_template() { + let request = CreateSandboxRequest { + spec: Some(SandboxSpec { + providers: vec!["github".to_string()], + ..SandboxSpec::default() + }), + workload_template_name: "missing-template".to_string(), + ..CreateSandboxRequest::default() + }; + + assert_eq!( + sandbox_create_telemetry_attrs(&request, None), + SandboxCreateTelemetryAttrs { + requested_gpu: false, + provider_count: 1, + has_custom_policy: false, + template_source: SandboxTemplateSource::WorkloadTemplate, + } + ); + } + #[test] fn shell_escape_safe_chars_pass_through() { assert_eq!(shell_escape("ls").unwrap(), "ls"); @@ -2899,7 +3433,7 @@ mod tests { workspace: "default".to_string(), deletion_timestamp_ms: 0, }), - spec: Some(openshell_core::proto::SandboxSpec { + spec: Some(SandboxSpec { log_level: "debug".to_string(), policy: Some(openshell_core::proto::SandboxPolicy::default()), providers, @@ -2912,6 +3446,41 @@ mod tests { sandbox } + fn test_workload_template(name: &str) -> SandboxWorkloadTemplate { + SandboxWorkloadTemplate { + metadata: Some(ObjectMeta { + id: String::new(), + name: name.to_string(), + created_at_ms: 0, + labels: HashMap::from([("team".to_string(), "runtime".to_string())]), + resource_version: 0, + annotations: HashMap::new(), + workspace: String::new(), + deletion_timestamp_ms: 0, + }), + spec: Some(openshell_core::proto::SandboxWorkloadTemplateSpec { + workload: Some(openshell_core::proto::SandboxWorkloadConfig { + image: "registry.example.com/agent:latest".to_string(), + environment: HashMap::from([("FEATURE_FLAG".to_string(), "on".to_string())]), + resources: Some(SandboxResources { + cpu: "2".to_string(), + memory: "4Gi".to_string(), + gpu: Some(GpuResourceRequirements { count: Some(1) }), + }), + }), + driver_config: None, + desired_service_level: None, + }), + } + } + + fn proto_string_value(value: &Value) -> Option<&str> { + match value.kind.as_ref() { + Some(Kind::StringValue(value)) => Some(value.as_str()), + _ => None, + } + } + #[tokio::test] #[ignore = "flaky under concurrent test execution"] async fn watch_producer_releases_request_span_when_client_disconnects() { @@ -3471,7 +4040,7 @@ mod tests { &state, authed_request(CreateSandboxRequest { name: "collision".to_string(), - spec: Some(openshell_core::proto::SandboxSpec { + spec: Some(SandboxSpec { providers: vec!["provider-a".to_string(), "provider-b".to_string()], ..Default::default() }), @@ -3479,6 +4048,7 @@ mod tests { annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }), ) .await @@ -3498,11 +4068,12 @@ mod tests { &state, authed_request(CreateSandboxRequest { name: "user-catalog".to_string(), - spec: Some(openshell_core::proto::SandboxSpec::default()), + spec: Some(SandboxSpec::default()), labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }), ) .await @@ -3531,7 +4102,7 @@ mod tests { &state, authed_request(CreateSandboxRequest { name: "reserved-policy-key".to_string(), - spec: Some(openshell_core::proto::SandboxSpec { + spec: Some(SandboxSpec { policy: Some(policy), ..Default::default() }), @@ -3539,6 +4110,7 @@ mod tests { annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }), ) .await @@ -3559,11 +4131,12 @@ mod tests { &state, authed_request(CreateSandboxRequest { name: "annotated".to_string(), - spec: Some(openshell_core::proto::SandboxSpec::default()), + spec: Some(SandboxSpec::default()), labels: HashMap::new(), annotations: HashMap::from([(annotation_key.clone(), annotation_value.clone())]), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }), ) .await @@ -3616,7 +4189,7 @@ mod tests { &state, authed_request(CreateSandboxRequest { name: "partial-id".to_string(), - spec: Some(openshell_core::proto::SandboxSpec { + spec: Some(SandboxSpec { policy: Some(policy), ..Default::default() }), @@ -3624,6 +4197,7 @@ mod tests { annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }), ) .await @@ -3680,7 +4254,7 @@ mod tests { &state, authed_request(CreateSandboxRequest { name: "kube-partial-id".to_string(), - spec: Some(openshell_core::proto::SandboxSpec { + spec: Some(SandboxSpec { policy: Some(policy), ..Default::default() }), @@ -3688,6 +4262,7 @@ mod tests { annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }), ) .await @@ -3714,11 +4289,12 @@ mod tests { &state, authed_request(CreateSandboxRequest { name: "bad-label".to_string(), - spec: Some(openshell_core::proto::SandboxSpec::default()), + spec: Some(SandboxSpec::default()), labels: HashMap::from([("team".to_string(), "x".repeat(512))]), annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }), ) .await @@ -3744,7 +4320,7 @@ mod tests { &task_state, authed_request(CreateSandboxRequest { name: "guarded-create".to_string(), - spec: Some(openshell_core::proto::SandboxSpec { + spec: Some(SandboxSpec { providers: vec!["work-github".to_string()], ..Default::default() }), @@ -3752,6 +4328,7 @@ mod tests { annotations: HashMap::new(), workspace: String::new(), await_main_process_attachment: false, + workload_template_name: String::new(), }), ) .await @@ -3776,6 +4353,815 @@ mod tests { ); } + #[tokio::test] + async fn sandbox_template_handlers_create_get_list_and_delete_workspace_resource() { + let state = test_server_state().await; + + let created = handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(test_workload_template("gpu-kata")), + workspace: "default".to_string(), + }), + ) + .await + .expect("template create should succeed") + .into_inner() + .template + .expect("template response"); + + let metadata = created.metadata.as_ref().expect("metadata"); + assert_eq!(metadata.name, "gpu-kata"); + assert_eq!(metadata.workspace, "default"); + assert!(!metadata.id.is_empty()); + assert_ne!(metadata.resource_version, 0); + + let fetched = handle_get_sandbox_template( + &state, + authed_request(GetSandboxTemplateRequest { + name: "gpu-kata".to_string(), + workspace: "default".to_string(), + }), + ) + .await + .expect("template get should succeed") + .into_inner() + .template + .expect("fetched template"); + assert_eq!(fetched.object_name(), "gpu-kata"); + assert_eq!(fetched.object_workspace(), "default"); + + let listed = handle_list_sandbox_templates( + &state, + authed_request(ListSandboxTemplatesRequest { + limit: 100, + offset: 0, + workspace: "default".to_string(), + all_workspaces: false, + label_selector: String::new(), + }), + ) + .await + .expect("template list should succeed") + .into_inner() + .templates; + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].object_name(), "gpu-kata"); + + let deleted = handle_delete_sandbox_template( + &state, + authed_request(DeleteSandboxTemplateRequest { + name: "gpu-kata".to_string(), + workspace: "default".to_string(), + }), + ) + .await + .expect("template delete should succeed") + .into_inner(); + assert!(deleted.deleted); + + let missing = handle_get_sandbox_template( + &state, + authed_request(GetSandboxTemplateRequest { + name: "gpu-kata".to_string(), + workspace: "default".to_string(), + }), + ) + .await + .expect_err("deleted template should not be fetchable"); + assert_eq!(missing.code(), tonic::Code::NotFound); + } + + #[tokio::test] + async fn sandbox_template_list_filters_by_label_selector() { + let state = test_server_state().await; + + let mut gpu = test_workload_template("gpu-kata"); + gpu.metadata + .as_mut() + .expect("metadata") + .labels + .insert("team".to_string(), "runtime".to_string()); + handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(gpu), + workspace: "default".to_string(), + }), + ) + .await + .expect("gpu template create should succeed"); + + let mut cpu = test_workload_template("cpu-base"); + cpu.metadata + .as_mut() + .expect("metadata") + .labels + .insert("team".to_string(), "batch".to_string()); + handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(cpu), + workspace: "default".to_string(), + }), + ) + .await + .expect("cpu template create should succeed"); + + let listed = handle_list_sandbox_templates( + &state, + authed_request(ListSandboxTemplatesRequest { + limit: 100, + offset: 0, + workspace: "default".to_string(), + all_workspaces: false, + label_selector: "team=runtime".to_string(), + }), + ) + .await + .expect("template list with label selector should succeed") + .into_inner() + .templates; + + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].object_name(), "gpu-kata"); + } + + #[tokio::test] + async fn sandbox_template_create_rejects_whitespace_name() { + let state = test_server_state().await; + + let err = handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(test_workload_template(" gpu-kata ")), + workspace: "default".to_string(), + }), + ) + .await + .expect_err("template names must be canonical DNS-1123 labels"); + + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("template.metadata.name")); + + let listed = handle_list_sandbox_templates( + &state, + authed_request(ListSandboxTemplatesRequest { + limit: 100, + offset: 0, + workspace: "default".to_string(), + all_workspaces: false, + label_selector: String::new(), + }), + ) + .await + .expect("template list should succeed") + .into_inner() + .templates; + assert!(listed.is_empty()); + } + + #[tokio::test] + async fn sandbox_template_create_empty_workspace_ignores_metadata_workspace() { + use openshell_core::proto::CreateWorkspaceRequest; + + let state = test_server_state().await; + crate::grpc::workspace::handle_create_workspace( + &state, + Request::new(CreateWorkspaceRequest { + name: "beta".to_string(), + labels: HashMap::new(), + }), + ) + .await + .expect("beta workspace should be created"); + + let mut template = test_workload_template("copied-template"); + template.metadata.as_mut().unwrap().workspace = "beta".to_string(); + + let err = handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(template), + workspace: String::new(), + }), + ) + .await + .expect_err("empty request workspace must default to default, not metadata workspace"); + + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("template.metadata.workspace")); + + let listed = handle_list_sandbox_templates( + &state, + authed_request(ListSandboxTemplatesRequest { + limit: 100, + offset: 0, + workspace: "beta".to_string(), + all_workspaces: false, + label_selector: String::new(), + }), + ) + .await + .expect("template list should succeed") + .into_inner() + .templates; + assert!(listed.is_empty()); + } + + #[tokio::test] + async fn sandbox_template_create_rejects_missing_spec_as_invalid_argument() { + let state = test_server_state().await; + let mut template = test_workload_template("missing-spec"); + template.spec = None; + + let err = handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(template), + workspace: "default".to_string(), + }), + ) + .await + .expect_err("template create should reject missing spec"); + + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("sandbox template spec")); + } + + #[test] + fn sandbox_template_validation_allows_positive_ready_within() { + let mut template = test_workload_template("gpu-kata"); + template.metadata.as_mut().unwrap().id = "template-gpu-kata".to_string(); + template.spec.as_mut().unwrap().desired_service_level = + Some(openshell_core::proto::SandboxServiceLevel { + startup: Some(openshell_core::proto::SandboxStartup { + ready_within: Some(prost_types::Duration { + seconds: 1, + nanos: 0, + }), + max_burst: 1, + }), + }); + + validate_sandbox_workload_template(&template).expect("positive ready_within should pass"); + } + + #[test] + fn sandbox_template_validation_rejects_non_positive_ready_within() { + for (duration, expected) in [ + ( + prost_types::Duration { + seconds: 0, + nanos: 0, + }, + "greater than zero", + ), + ( + prost_types::Duration { + seconds: -1, + nanos: 0, + }, + "greater than zero", + ), + ( + prost_types::Duration { + seconds: 0, + nanos: -1, + }, + "greater than zero", + ), + ] { + let mut template = test_workload_template("gpu-kata"); + template.metadata.as_mut().unwrap().id = "template-gpu-kata".to_string(); + template.spec.as_mut().unwrap().desired_service_level = + Some(openshell_core::proto::SandboxServiceLevel { + startup: Some(openshell_core::proto::SandboxStartup { + ready_within: Some(duration), + max_burst: 1, + }), + }); + + let err = validate_sandbox_workload_template(&template) + .expect_err("non-positive ready_within should be rejected"); + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains(expected), "{err:?}"); + assert!( + err.message() + .contains("template.spec.desired_service_level.startup.ready_within"), + "{err:?}" + ); + } + } + + #[test] + fn sandbox_template_validation_rejects_malformed_ready_within() { + for (duration, expected) in [ + ( + prost_types::Duration { + seconds: 1, + nanos: -1, + }, + "normalized", + ), + ( + prost_types::Duration { + seconds: 0, + nanos: 1_000_000_000, + }, + "valid protobuf Duration", + ), + ( + prost_types::Duration { + seconds: 315_576_000_001, + nanos: 0, + }, + "valid protobuf Duration", + ), + ] { + let mut template = test_workload_template("gpu-kata"); + template.metadata.as_mut().unwrap().id = "template-gpu-kata".to_string(); + template.spec.as_mut().unwrap().desired_service_level = + Some(openshell_core::proto::SandboxServiceLevel { + startup: Some(openshell_core::proto::SandboxStartup { + ready_within: Some(duration), + max_burst: 1, + }), + }); + + let err = validate_sandbox_workload_template(&template) + .expect_err("malformed ready_within should be rejected"); + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains(expected), "{err:?}"); + assert!( + err.message() + .contains("template.spec.desired_service_level.startup.ready_within"), + "{err:?}" + ); + } + } + + #[tokio::test] + async fn sandbox_template_create_rejects_workspace_quota() { + let state = test_server_state().await; + for index in 0..MAX_TEMPLATES_PER_WORKSPACE { + let mut template = test_workload_template(&format!("tmpl-{index}")); + let metadata = template.metadata.as_mut().expect("metadata"); + metadata.id = format!("template-{index}"); + metadata.workspace = "default".to_string(); + state.store.put_message(&template).await.unwrap(); + } + + let err = handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(test_workload_template("overflow")), + workspace: "default".to_string(), + }), + ) + .await + .expect_err("template create must reject a full workspace"); + + assert_eq!(err.code(), tonic::Code::ResourceExhausted); + assert!(err.message().contains("1000 sandbox templates")); + } + + #[tokio::test] + async fn sandbox_template_create_enforces_workspace_quota_concurrently() { + let state = test_server_state().await; + for index in 0..(MAX_TEMPLATES_PER_WORKSPACE - 1) { + let mut template = test_workload_template(&format!("tmpl-{index}")); + let metadata = template.metadata.as_mut().expect("metadata"); + metadata.id = format!("template-{index}"); + metadata.workspace = "default".to_string(); + state.store.put_message(&template).await.unwrap(); + } + + let mut handles = vec![]; + for index in 0..8 { + let state = Arc::clone(&state); + let handle = tokio::spawn(async move { + handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(test_workload_template(&format!("overflow-{index}"))), + workspace: "default".to_string(), + }), + ) + .await + }); + handles.push(handle); + } + + let results: Vec<_> = future::join_all(handles) + .await + .into_iter() + .map(|r| r.unwrap()) + .collect(); + + let successes = results.iter().filter(|r| r.is_ok()).count(); + let exhausted = results + .iter() + .filter(|r| { + r.as_ref() + .err() + .is_some_and(|e| e.code() == tonic::Code::ResourceExhausted) + }) + .count(); + + assert_eq!(successes, 1); + assert_eq!(exhausted, 7); + let count = state + .store + .count_in_workspace(SandboxWorkloadTemplate::object_type(), "default") + .await + .unwrap(); + assert_eq!(count, u64::from(MAX_TEMPLATES_PER_WORKSPACE)); + } + + #[test] + fn template_create_sandbox_spec_field_policy_is_exhaustive() { + assert_proto_fields_classified( + "openshell.v1.SandboxSpec", + &["policy", "providers", "command", "tty"], + &[ + "log_level", + "environment", + "template", + "resource_requirements", + ], + ); + } + + fn assert_proto_fields_classified( + message_name: &str, + copied_from_create_request: &[&str], + rejected_template_workload_overrides: &[&str], + ) { + let pool = prost_reflect::DescriptorPool::decode(openshell_core::FILE_DESCRIPTOR_SET) + .expect("decode descriptor set"); + let message = pool + .get_message_by_name(message_name) + .expect("message descriptor"); + let classified: std::collections::HashSet<&str> = copied_from_create_request + .iter() + .chain(rejected_template_workload_overrides.iter()) + .copied() + .collect(); + let actual: std::collections::HashSet = message + .fields() + .map(|field| field.name().to_string()) + .collect(); + + for field in &actual { + assert!( + classified.contains(field.as_str()), + "{message_name}.{field} is not classified for template-backed sandbox creates. \ + Add it to copied_from_create_request when callers own the create-time value, \ + or to rejected_template_workload_overrides when the workload template owns it." + ); + } + + for field in classified { + assert!( + actual.contains(field), + "{message_name}.{field} is classified for template-backed sandbox creates, \ + but the proto field no longer exists" + ); + } + } + + #[tokio::test] + async fn create_sandbox_from_workload_template_resolves_workload_and_preserves_governance() { + let state = test_server_state().await; + state + .store + .put_message(&test_provider("work-github", "github")) + .await + .unwrap(); + handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(test_workload_template("gpu-kata")), + workspace: "default".to_string(), + }), + ) + .await + .expect("template create should succeed"); + + let mut policy = openshell_core::proto::SandboxPolicy { + version: 1, + ..Default::default() + }; + policy.network_policies.insert( + "example".to_string(), + openshell_core::proto::NetworkPolicyRule { + name: "example".to_string(), + ..Default::default() + }, + ); + + let created = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "from-template".to_string(), + spec: Some(SandboxSpec { + providers: vec!["work-github".to_string()], + policy: Some(policy), + command: vec!["echo".to_string(), "template-create".to_string()], + tty: false, + ..Default::default() + }), + labels: HashMap::new(), + annotations: HashMap::new(), + workspace: "default".to_string(), + workload_template_name: "gpu-kata".to_string(), + await_main_process_attachment: false, + }), + ) + .await + .expect("sandbox create from template should succeed") + .into_inner() + .sandbox + .expect("created sandbox"); + + let provenance = created + .created_from_workload_template + .expect("template provenance"); + assert_eq!(provenance.name, "gpu-kata"); + assert!(!provenance.resource_version.is_empty()); + + let spec = created.spec.expect("resolved sandbox spec"); + assert_eq!(spec.providers, vec!["work-github".to_string()]); + assert!(spec.policy.is_some()); + assert_eq!( + spec.command, + vec!["echo".to_string(), "template-create".to_string()] + ); + assert!(!spec.tty); + assert_eq!( + spec.environment.get("FEATURE_FLAG"), + Some(&"on".to_string()) + ); + + let template = spec.template.expect("resolved inline template"); + assert_eq!(template.image, "registry.example.com/agent:latest"); + let limits = template + .resources + .as_ref() + .and_then(|resources| resources.fields.get("limits")) + .and_then(|limits| limits.kind.as_ref()) + .and_then(|kind| match kind { + Kind::StructValue(value) => Some(&value.fields), + _ => None, + }) + .expect("resource limits"); + assert_eq!(limits.get("cpu").and_then(proto_string_value), Some("2")); + assert_eq!( + limits.get("memory").and_then(proto_string_value), + Some("4Gi") + ); + assert_eq!( + spec.resource_requirements + .and_then(|requirements| requirements.gpu) + .and_then(|gpu| gpu.count), + Some(1) + ); + } + + #[tokio::test] + async fn create_sandbox_from_workload_template_defaults_whitespace_image() { + let state = test_server_state().await; + let mut template = test_workload_template("default-image"); + template + .spec + .as_mut() + .and_then(|spec| spec.workload.as_mut()) + .expect("test template workload") + .image = " ".to_string(); + handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(template), + workspace: "default".to_string(), + }), + ) + .await + .expect("template create should succeed"); + + let created = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "from-template".to_string(), + spec: Some(SandboxSpec::default()), + labels: HashMap::new(), + annotations: HashMap::new(), + workspace: "default".to_string(), + workload_template_name: "default-image".to_string(), + await_main_process_attachment: false, + }), + ) + .await + .expect("sandbox create from template should succeed") + .into_inner() + .sandbox + .expect("created sandbox"); + + let image = created + .spec + .and_then(|spec| spec.template) + .map(|template| template.image) + .expect("resolved template image"); + assert_eq!(image, state.compute.default_image()); + } + + #[tokio::test] + async fn create_sandbox_from_workload_template_preserves_default_gpu_request() { + let state = test_server_state().await; + let mut template = test_workload_template("default-gpu"); + template + .spec + .as_mut() + .and_then(|spec| spec.workload.as_mut()) + .and_then(|workload| workload.resources.as_mut()) + .expect("test template resources") + .gpu = Some(GpuResourceRequirements { count: None }); + handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(template), + workspace: "default".to_string(), + }), + ) + .await + .expect("template create should succeed"); + + let created = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "from-template".to_string(), + spec: Some(SandboxSpec::default()), + labels: HashMap::new(), + annotations: HashMap::new(), + workspace: "default".to_string(), + workload_template_name: "default-gpu".to_string(), + await_main_process_attachment: false, + }), + ) + .await + .expect("sandbox create from template should succeed") + .into_inner() + .sandbox + .expect("created sandbox"); + + let gpu = created + .spec + .as_ref() + .and_then(|spec| spec.resource_requirements.as_ref()) + .and_then(|requirements| requirements.gpu.as_ref()) + .expect("default GPU request should be preserved"); + assert_eq!(gpu.count, None); + } + + #[tokio::test] + async fn create_sandbox_from_corrupted_workload_template_returns_internal() { + let state = test_server_state().await; + let mut template = test_workload_template("corrupt-template"); + let metadata = template.metadata.as_mut().expect("metadata"); + metadata.id = "template-corrupt-template".to_string(); + metadata.workspace = "default".to_string(); + template.spec = None; + state.store.put_message(&template).await.unwrap(); + + let err = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "from-corrupt".to_string(), + spec: Some(SandboxSpec::default()), + labels: HashMap::new(), + annotations: HashMap::new(), + workspace: "default".to_string(), + workload_template_name: "corrupt-template".to_string(), + await_main_process_attachment: false, + }), + ) + .await + .expect_err("corrupted stored template should fail as server data corruption"); + + assert_eq!(err.code(), tonic::Code::Internal, "{}", err.message()); + assert!(err.message().contains("sandbox template spec")); + } + + #[tokio::test] + async fn create_sandbox_from_workload_template_rejects_inline_workload_overrides() { + let state = test_server_state().await; + handle_create_sandbox_template( + &state, + authed_request(CreateSandboxTemplateRequest { + template: Some(test_workload_template("gpu-kata")), + workspace: "default".to_string(), + }), + ) + .await + .expect("template create should succeed"); + + let err = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "bad-template-create".to_string(), + spec: Some(SandboxSpec { + environment: HashMap::from([("INLINE".to_string(), "blocked".to_string())]), + ..Default::default() + }), + labels: HashMap::new(), + annotations: HashMap::new(), + workspace: "default".to_string(), + workload_template_name: "gpu-kata".to_string(), + await_main_process_attachment: false, + }), + ) + .await + .expect_err("inline workload overrides should be rejected"); + + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("spec.environment")); + } + + #[tokio::test] + async fn create_sandbox_from_workload_template_rejects_malformed_template_name() { + let state = test_server_state().await; + + let err = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "bad-template-create".to_string(), + spec: Some(SandboxSpec::default()), + labels: HashMap::new(), + annotations: HashMap::new(), + workspace: "default".to_string(), + workload_template_name: "Invalid_Template_Name".to_string(), + await_main_process_attachment: false, + }), + ) + .await + .expect_err("malformed template name should be rejected before lookup"); + + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("workload_template_name")); + } + + #[tokio::test] + async fn create_sandbox_from_workload_template_rejects_oversized_governance_before_lookup() { + let state = test_server_state().await; + + let err = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "bad-template-create".to_string(), + spec: Some(SandboxSpec { + providers: (0..=MAX_PROVIDERS).map(|i| format!("p-{i}")).collect(), + ..Default::default() + }), + labels: HashMap::new(), + annotations: HashMap::new(), + workspace: "default".to_string(), + workload_template_name: "missing-template".to_string(), + await_main_process_attachment: false, + }), + ) + .await + .expect_err("oversized governance spec should be rejected before template lookup"); + + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("providers")); + } + + #[tokio::test] + async fn create_sandbox_rejects_oversized_direct_spec_before_workspace_lookup() { + let state = test_server_state().await; + + let err = handle_create_sandbox( + &state, + authed_request(CreateSandboxRequest { + name: "bad-direct-create".to_string(), + spec: Some(SandboxSpec { + providers: (0..=MAX_PROVIDERS).map(|i| format!("p-{i}")).collect(), + ..Default::default() + }), + labels: HashMap::new(), + annotations: HashMap::new(), + workspace: "missing-workspace".to_string(), + workload_template_name: String::new(), + await_main_process_attachment: false, + }), + ) + .await + .expect_err("oversized direct spec should be rejected before workspace lookup"); + + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("providers")); + } + #[tokio::test] async fn attach_sandbox_provider_rejects_credential_key_collisions() { let state = test_server_state().await; @@ -4678,7 +6064,7 @@ mod tests { &state, non_member_request(CreateSandboxRequest { workspace: "no-such-ws".into(), - spec: Some(openshell_core::proto::SandboxSpec::default()), + spec: Some(SandboxSpec::default()), ..Default::default() }), ) diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index eb53c80f29..c20dad7780 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -10,7 +10,7 @@ use openshell_core::proto::{ CredentialHandle, ExecSandboxRequest, Provider, SandboxPolicy as ProtoSandboxPolicy, - SandboxTemplate, + SandboxSpec, SandboxTemplate, }; use prost::Message; use tonic::Status; @@ -150,26 +150,12 @@ pub(super) fn validate_dns1123_label(name: &str, field: &str) -> Result<(), Stat /// Validate field sizes on a `CreateSandboxRequest` before persisting. /// /// Returns `INVALID_ARGUMENT` on the first field that exceeds its limit. -pub(super) fn validate_sandbox_spec( - name: &str, - spec: &openshell_core::proto::SandboxSpec, -) -> Result<(), Status> { +pub(super) fn validate_sandbox_spec(name: &str, spec: &SandboxSpec) -> Result<(), Status> { // --- request.name --- - if !name.is_empty() && name.len() > MAX_ROUTABLE_NAME_LEN { - return Err(Status::invalid_argument(format!( - "name exceeds maximum length ({} > {MAX_ROUTABLE_NAME_LEN})", - name.len() - ))); - } - validate_dns1123_label(name, "name")?; + validate_sandbox_name(name)?; // --- spec.providers --- - if spec.providers.len() > MAX_PROVIDERS { - return Err(Status::invalid_argument(format!( - "providers list exceeds maximum ({} > {MAX_PROVIDERS})", - spec.providers.len() - ))); - } + validate_sandbox_provider_count(spec)?; // --- spec.log_level --- if spec.log_level.len() > MAX_LOG_LEVEL_LEN { @@ -203,6 +189,45 @@ pub(super) fn validate_sandbox_spec( } // --- spec.policy serialized size --- + validate_sandbox_policy_size(spec)?; + + Ok(()) +} + +pub(super) fn validate_sandbox_governance_spec( + name: &str, + spec: &SandboxSpec, +) -> Result<(), Status> { + validate_sandbox_name(name)?; + validate_sandbox_provider_count(spec)?; + if !spec.command.is_empty() { + validate_main_process_command(&spec.command)?; + } + validate_sandbox_policy_size(spec)?; + Ok(()) +} + +fn validate_sandbox_name(name: &str) -> Result<(), Status> { + if !name.is_empty() && name.len() > MAX_ROUTABLE_NAME_LEN { + return Err(Status::invalid_argument(format!( + "name exceeds maximum length ({} > {MAX_ROUTABLE_NAME_LEN})", + name.len() + ))); + } + validate_dns1123_label(name, "name") +} + +fn validate_sandbox_provider_count(spec: &SandboxSpec) -> Result<(), Status> { + if spec.providers.len() > MAX_PROVIDERS { + return Err(Status::invalid_argument(format!( + "providers list exceeds maximum ({} > {MAX_PROVIDERS})", + spec.providers.len() + ))); + } + Ok(()) +} + +fn validate_sandbox_policy_size(spec: &SandboxSpec) -> Result<(), Status> { if let Some(ref policy) = spec.policy { let size = policy.encoded_len(); if size > MAX_POLICY_SIZE { @@ -244,7 +269,7 @@ fn validate_main_process_command(command: &[String]) -> Result<(), Status> { Ok(()) } -fn validate_gpu_request_fields(spec: &openshell_core::proto::SandboxSpec) -> Result<(), Status> { +fn validate_gpu_request_fields(spec: &SandboxSpec) -> Result<(), Status> { if openshell_core::gpu::sandbox_gpu_count(spec.resource_requirements.as_ref()) == Some(0) { return Err(Status::invalid_argument("gpu count must be greater than 0")); } diff --git a/crates/openshell-server/src/grpc/workspace.rs b/crates/openshell-server/src/grpc/workspace.rs index d83ffab0e8..7446938156 100644 --- a/crates/openshell-server/src/grpc/workspace.rs +++ b/crates/openshell-server/src/grpc/workspace.rs @@ -14,9 +14,9 @@ use openshell_core::proto::{ CreateWorkspaceResponse, DeleteWorkspaceRequest, DeleteWorkspaceResponse, GetWorkspaceRequest, GetWorkspaceResponse, InferenceRoute, ListWorkspaceMembersRequest, ListWorkspaceMembersResponse, ListWorkspacesRequest, ListWorkspacesResponse, Provider, - RemoveWorkspaceMemberRequest, RemoveWorkspaceMemberResponse, Sandbox, ServiceEndpoint, - SshSession, StoredProviderCredentialRefreshState, StoredProviderProfile, Workspace, - WorkspaceMember, WorkspaceRole, + RemoveWorkspaceMemberRequest, RemoveWorkspaceMemberResponse, Sandbox, SandboxWorkloadTemplate, + ServiceEndpoint, SshSession, StoredProviderCredentialRefreshState, StoredProviderProfile, + Workspace, WorkspaceMember, WorkspaceRole, }; use prost::Message; use tonic::{Request, Response, Status}; @@ -375,6 +375,7 @@ pub(super) async fn handle_delete_workspace( let mut blocking = Vec::new(); for (object_type, label) in [ (Sandbox::object_type(), "sandbox"), + (SandboxWorkloadTemplate::object_type(), "sandbox template"), (Provider::object_type(), "provider"), (StoredProviderProfile::object_type(), "provider profile"), (ServiceEndpoint::object_type(), "service"), @@ -819,6 +820,72 @@ mod tests { assert!(resp.deleted); } + #[tokio::test] + async fn delete_workspace_blocked_by_sandbox_template() { + let state = test_server_state().await; + + handle_create_workspace( + &state, + Request::new(CreateWorkspaceRequest { + name: "templated".to_string(), + labels: HashMap::new(), + }), + ) + .await + .unwrap(); + + let template = SandboxWorkloadTemplate { + metadata: Some(ObjectMeta { + id: "template-1".to_string(), + name: "gpu-kata".to_string(), + created_at_ms: 1_000_000, + labels: HashMap::new(), + annotations: HashMap::new(), + resource_version: 0, + workspace: "templated".to_string(), + deletion_timestamp_ms: 0, + }), + spec: None, + }; + state.store.put_message(&template).await.unwrap(); + + let err = handle_delete_workspace( + &state, + Request::new(DeleteWorkspaceRequest { + name: "templated".to_string(), + }), + ) + .await + .unwrap_err(); + assert_eq!(err.code(), Code::FailedPrecondition); + assert!( + err.message().contains("sandbox template"), + "error should name sandbox templates as blocking resources: {}", + err.message() + ); + + state + .store + .delete_by_name( + SandboxWorkloadTemplate::object_type(), + "templated", + "gpu-kata", + ) + .await + .unwrap(); + + let resp = handle_delete_workspace( + &state, + Request::new(DeleteWorkspaceRequest { + name: "templated".to_string(), + }), + ) + .await + .unwrap() + .into_inner(); + assert!(resp.deleted); + } + #[tokio::test] async fn delete_workspace_blocked_by_ssh_session() { let state = test_server_state().await; diff --git a/crates/openshell-server/src/persistence/mod.rs b/crates/openshell-server/src/persistence/mod.rs index 6934630110..716f26dad9 100644 --- a/crates/openshell-server/src/persistence/mod.rs +++ b/crates/openshell-server/src/persistence/mod.rs @@ -401,6 +401,39 @@ impl Store { )) } + /// Atomically insert a named object only if its workspace has fewer than + /// `max_count` objects of the same type. + /// + /// Returns `Ok(None)` when the quota is already full. A duplicate id or + /// `(object_type, workspace, name)` still returns + /// [`PersistenceError::UniqueViolation`]. + #[allow(clippy::too_many_arguments)] + #[tracing::instrument( + name = "store", + skip_all, + fields(otel.name = "store.create_if_workspace_count_below", otel.status_code = tracing::field::Empty, object_type = %object_type, object.id = %id, object.name = %name, workspace = %workspace, max_count = max_count) + )] + pub async fn create_if_workspace_count_below( + &self, + object_type: &str, + id: &str, + name: &str, + workspace: &str, + payload: &[u8], + labels: Option<&str>, + max_count: u64, + ) -> PersistenceResult> { + store_dispatch_traced!(self.create_if_workspace_count_below( + object_type, + id, + name, + workspace, + payload, + labels, + max_count + )) + } + /// Fetch an object by id. #[tracing::instrument( name = "store", diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index 89fe442695..19c50c6187 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -337,6 +337,73 @@ RETURNING resource_version, created_at_ms, updated_at_ms }) } + #[allow(clippy::too_many_arguments)] + pub async fn create_if_workspace_count_below( + &self, + object_type: &str, + id: &str, + name: &str, + workspace: &str, + payload: &[u8], + labels: Option<&str>, + max_count: u64, + ) -> PersistenceResult> { + let now_ms = current_time_ms(); + let labels_jsonb: Option = labels + .map(serde_json::from_str) + .transpose() + .map_err(|e| PersistenceError::Encode(format!("invalid labels JSON: {e}")))?; + let mut tx = self.pool.begin().await.map_err(|e| map_db_error(&e))?; + + sqlx::query("SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))") + .bind(object_type) + .bind(workspace) + .execute(&mut *tx) + .await + .map_err(|e| map_db_error(&e))?; + + let row: (i64,) = sqlx::query_as( + "SELECT COUNT(*) FROM objects WHERE object_type = $1 AND workspace = $2", + ) + .bind(object_type) + .bind(workspace) + .fetch_one(&mut *tx) + .await + .map_err(|e| map_db_error(&e))?; + let count = u64::try_from(row.0).unwrap_or(0); + if count >= max_count { + tx.commit().await.map_err(|e| map_db_error(&e))?; + return Ok(None); + } + + let row = sqlx::query( + r" +INSERT INTO objects (object_type, id, name, workspace, payload, created_at_ms, updated_at_ms, labels, resource_version) +VALUES ($1, $2, $3, $4, $5, $6, $6, COALESCE($7, '{}'::jsonb), 1) +RETURNING resource_version, created_at_ms, updated_at_ms +", + ) + .bind(object_type) + .bind(id) + .bind(name) + .bind(workspace) + .bind(payload) + .bind(now_ms) + .bind(labels_jsonb) + .fetch_one(&mut *tx) + .await + .map_err(|e| map_db_error(&e))?; + + tx.commit().await.map_err(|e| map_db_error(&e))?; + + let resource_version_i64: i64 = row.try_get("resource_version").unwrap_or(1); + Ok(Some(WriteResult { + resource_version: resource_version_i64.max(1).cast_unsigned(), + created_at_ms: row.get("created_at_ms"), + updated_at_ms: row.get("updated_at_ms"), + })) + } + pub async fn get( &self, object_type: &str, diff --git a/crates/openshell-server/src/persistence/sqlite.rs b/crates/openshell-server/src/persistence/sqlite.rs index c28318d261..c945c417f9 100644 --- a/crates/openshell-server/src/persistence/sqlite.rs +++ b/crates/openshell-server/src/persistence/sqlite.rs @@ -357,6 +357,67 @@ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7, ?8, 1) }) } + #[allow(clippy::too_many_arguments)] + pub async fn create_if_workspace_count_below( + &self, + object_type: &str, + id: &str, + name: &str, + workspace: &str, + payload: &[u8], + labels: Option<&str>, + max_count: u64, + ) -> PersistenceResult> { + let now_ms = current_time_ms(); + let mut tx = self + .pool + .begin_with("BEGIN IMMEDIATE") + .await + .map_err(|e| map_db_error(&e))?; + + let row: (i64,) = sqlx::query_as( + r#" +SELECT COUNT(*) FROM "objects" +WHERE "object_type" = ?1 AND "workspace" = ?2 +"#, + ) + .bind(object_type) + .bind(workspace) + .fetch_one(&mut *tx) + .await + .map_err(|e| map_db_error(&e))?; + let count = u64::try_from(row.0).unwrap_or(0); + if count >= max_count { + tx.commit().await.map_err(|e| map_db_error(&e))?; + return Ok(None); + } + + sqlx::query( + r#" +INSERT INTO "objects" ("object_type", "id", "name", "workspace", "payload", "created_at_ms", "updated_at_ms", "labels", "resource_version") +VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6, ?7, 1) +"#, + ) + .bind(object_type) + .bind(id) + .bind(name) + .bind(workspace) + .bind(payload) + .bind(now_ms) + .bind(labels.unwrap_or("{}")) + .execute(&mut *tx) + .await + .map_err(|e| map_db_error(&e))?; + + tx.commit().await.map_err(|e| map_db_error(&e))?; + + Ok(Some(WriteResult { + resource_version: 1, + created_at_ms: now_ms, + updated_at_ms: now_ms, + })) + } + pub async fn get( &self, object_type: &str, diff --git a/crates/openshell-server/src/persistence/tests.rs b/crates/openshell-server/src/persistence/tests.rs index 479f46e493..7882c9246c 100644 --- a/crates/openshell-server/src/persistence/tests.rs +++ b/crates/openshell-server/src/persistence/tests.rs @@ -1819,6 +1819,7 @@ async fn cas_update_message_cas_succeeds() { }), spec: None, status: None, + ..Sandbox::default() }; store.put_message(&sandbox).await.unwrap(); @@ -1861,6 +1862,7 @@ async fn cas_update_message_cas_conflicts_on_concurrent_updates() { }), spec: None, status: None, + ..Sandbox::default() }; store.put_message(&sandbox).await.unwrap(); @@ -1931,6 +1933,7 @@ async fn cas_update_message_cas_rejects_workspace_change() { }), spec: None, status: None, + ..Sandbox::default() }; store.put_message(&sandbox).await.unwrap(); @@ -1973,6 +1976,7 @@ async fn cas_update_message_cas_rejects_name_change() { }), spec: None, status: None, + ..Sandbox::default() }; store.put_message(&sandbox).await.unwrap(); diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index 9e3957abb6..a60fc8696b 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -98,6 +98,34 @@ impl OpenShell for TestOpenShell { Ok(Response::new(SandboxResponse::default())) } + async fn create_sandbox_template( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_sandbox_template( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn list_sandbox_templates( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn delete_sandbox_template( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn stop_sandbox( &self, _request: tonic::Request, diff --git a/crates/openshell-server/tests/supervisor_relay_integration.rs b/crates/openshell-server/tests/supervisor_relay_integration.rs index 6cfd3b009b..91ac50dcbd 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -82,6 +82,34 @@ impl OpenShell for RelayGateway { // ------ unused stubs ------ + async fn create_sandbox_template( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_sandbox_template( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn list_sandbox_templates( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn delete_sandbox_template( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + type ConnectSupervisorStream = ReceiverStream>; async fn connect_supervisor( &self, diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index f08f44a3ac..174f9910d0 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1405,6 +1405,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { annotations: HashMap::new(), workspace: workspace.clone(), await_main_process_attachment: false, + workload_template_name: String::new(), }; let sandbox_name = diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index a28d653ead..47853b189d 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -157,6 +157,59 @@ Local directories and Dockerfiles require a local gateway because the CLI builds through the local Docker daemon. Use a registry image reference for remote gateways. +## Reuse Workload Templates + +Sandbox workload templates let workspace admins define reusable runtime shapes for a workspace. A template stores the image, environment, resource requests, and driver-specific configuration that sandboxes should inherit. When you create a sandbox from a template, the create request can still attach providers, labels, and policy, but the workload comes from the named template. + +Create a template: + +```shell +openshell sandbox template create gpu-kata \ + --image registry.example.com/agent:latest \ + --cpu 2 \ + --memory 4Gi \ + --gpu 1 \ + --label team=runtime \ + --env FEATURE_FLAG=on +``` + +Use `--gpu` without a count when the template should request the active +driver's default GPU assignment. Use `--gpu COUNT` when the template needs a +specific number of GPUs. + +Add driver-specific settings when the active compute driver needs them: + +```shell +openshell sandbox template create gpu-kata \ + --image registry.example.com/agent:latest \ + --driver-config-json '{"kubernetes":{"pod":{"runtime_class_name":"kata-containers","node_selector":{"pool":"gpu"}}}}' +``` + +If you omit `--image`, the gateway applies its default sandbox image when a sandbox is created from the template. Use this when the template should only define resource, environment, or driver settings. + +Create a sandbox from a template: + +```shell +openshell sandbox create --template gpu-kata --provider github -- claude +``` + +The `--template` flag cannot be combined with inline workload flags such as `--from`, `--cpu`, `--memory`, `--gpu`, `--env`, or `--driver-config-json`. Put those values on the template instead. Create-time policy and provider attachments remain part of the sandbox request, so each sandbox can keep its own access boundary. + +Inspect and manage templates: + +```shell +openshell sandbox template list +openshell sandbox template list --label-selector team=runtime +openshell sandbox template get gpu-kata +openshell sandbox template delete gpu-kata +``` + +Use `--all-workspaces` with `sandbox template list` when you need an admin view across workspaces: + +```shell +openshell sandbox template list --all-workspaces +``` + ## Base Sandbox Container The `base` sandbox container is the default runtime image for standard OpenShell sandboxes unless the gateway overrides its default sandbox image. It is published as `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` and maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) repository. @@ -295,6 +348,24 @@ with SandboxClient.from_active_cluster() as client: assert sandbox.id in {s.id for s in matches} ``` +Create reusable sandbox templates through the Python SDK when several runs +should share the same workload shape: + +```python +from openshell import SandboxClient + +with SandboxClient.from_active_cluster() as client: + templates = client.sandbox_templates() + + templates.create( + workspace="default", + name="python", + image="ghcr.io/nvidia/openshell-community/sandboxes/python:latest", + ) + + sandbox = client.create_from_template(workspace="default", template_name="python") +``` + For non-interactive automation, pass a renewable client-credentials provider. Omitted issuer, client ID, audience, and scopes are read from the active gateway's metadata. The client requires TLS for non-loopback gateways: diff --git a/docs/sandboxes/manage-workspaces.mdx b/docs/sandboxes/manage-workspaces.mdx index 05378af5f5..86da25fbb9 100644 --- a/docs/sandboxes/manage-workspaces.mdx +++ b/docs/sandboxes/manage-workspaces.mdx @@ -9,8 +9,9 @@ position: 3 --- An OpenShell workspace is an access and resource isolation boundary. Sandboxes, -providers, services, policies, settings, and inference routes belong to a -workspace and are not visible to members of other workspaces. +sandbox workload templates, providers, services, policies, settings, and +inference routes belong to a workspace and are not visible to members of other +workspaces. The CLI targets the `default` workspace unless you set `--workspace` or `OPENSHELL_WORKSPACE`. The logical OpenShell workspace described here is @@ -49,6 +50,8 @@ The following table summarizes common operations. | Add Workspace Users or remove members | Any workspace | Assigned workspace | No | | Assign the Workspace Admin role | Any workspace | No | No | | Create, use, or delete sandboxes and services | Any workspace | Assigned workspace | Assigned workspace | +| Create or delete sandbox workload templates | Any workspace | Assigned workspace | No | +| Read or list sandbox workload templates | Any workspace | Assigned workspace | Assigned workspace | | Create, update, or delete providers | Any workspace | Assigned workspace | No | | Change workspace policy or settings | Any workspace | Assigned workspace | No | | Manage platform profiles or global configuration | Yes | No | No | @@ -187,11 +190,11 @@ be deleted. openshell workspace delete team-ml ``` -A custom workspace must not contain sandboxes, providers, provider profiles, -services, SSH sessions, settings, policies, draft policy chunks, or credential -refresh state. Remove those resources before retrying deletion. OpenShell -removes membership records and inference routes as part of successful -workspace deletion. +A custom workspace must not contain sandboxes, sandbox workload templates, +providers, provider profiles, services, SSH sessions, settings, policies, draft +policy chunks, or credential refresh state. Remove those resources before +retrying deletion. OpenShell removes membership records and inference routes as +part of successful workspace deletion. ## Next Steps diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 6dca586879..6dbf46a392 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -168,6 +168,11 @@ name = "workspace_lifecycle" path = "tests/workspace_lifecycle.rs" required-features = ["e2e"] +[[test]] +name = "sandbox_templates" +path = "tests/sandbox_templates.rs" +required-features = ["e2e"] + [[test]] name = "proxy_egress_pipeline" path = "tests/proxy_egress_pipeline.rs" diff --git a/e2e/rust/tests/sandbox_templates.rs b/e2e/rust/tests/sandbox_templates.rs new file mode 100644 index 0000000000..21c9233daf --- /dev/null +++ b/e2e/rust/tests/sandbox_templates.rs @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "e2e")] + +//! E2E coverage for reusable sandbox workload templates. + +use std::process::Stdio; +use std::time::{SystemTime, UNIX_EPOCH}; + +use openshell_e2e::harness::binary::{openshell_bin, openshell_cmd}; +use openshell_e2e::harness::output::strip_ansi; +use openshell_e2e::harness::sandbox::SandboxGuard; +use serde_json::Value; + +struct CliResult { + output: String, + success: bool, +} + +struct TemplateGuard { + name: String, +} + +impl TemplateGuard { + fn new(name: String) -> Self { + Self { name } + } + + async fn cleanup(mut self) { + delete_template(&self.name).await; + self.name.clear(); + } +} + +impl Drop for TemplateGuard { + fn drop(&mut self) { + if self.name.is_empty() { + return; + } + let bin = openshell_bin(); + let _ = std::process::Command::new(&bin) + .args(["sandbox", "template", "delete", &self.name]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +async fn run_cli(args: &[&str]) -> CliResult { + let mut cmd = openshell_cmd(); + cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped()); + + let output = cmd.output().await.expect("spawn openshell command"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let combined = format!("{stdout}{stderr}"); + + CliResult { + output: strip_ansi(&combined), + success: output.status.success(), + } +} + +async fn delete_template(name: &str) { + let mut cmd = openshell_cmd(); + cmd.args(["sandbox", "template", "delete", name]) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + let _ = cmd.status().await; +} + +fn unique_name(prefix: &str) -> String { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock before epoch") + .as_millis(); + let suffix = millis % 1_000_000; + format!("{prefix}-{suffix:06}") +} + +fn output_contains(output: &str, needle: &str) -> bool { + output.to_lowercase().contains(&needle.to_lowercase()) +} + +fn output_mentions_template_not_found(output: &str) -> bool { + output_contains(output, "sandbox template") && output_contains(output, "not found") +} + +#[tokio::test] +async fn sandbox_create_from_template_uses_reusable_workload() { + let template_name = unique_name("tmpl"); + let sandbox_name = unique_name("sb-tmpl"); + let template = TemplateGuard::new(template_name.clone()); + + let create_template = run_cli(&[ + "sandbox", + "template", + "create", + &template_name, + "--cpu", + "500m", + "--memory", + "512Mi", + "--label", + "e2e=sandbox-template", + "--env", + "FEATURE_FLAG=on", + ]) + .await; + assert!( + create_template.success, + "sandbox template create failed:\n{}", + create_template.output + ); + + let get_template = run_cli(&[ + "sandbox", + "template", + "get", + &template_name, + "--output", + "json", + ]) + .await; + assert!( + get_template.success, + "sandbox template get failed:\n{}", + get_template.output + ); + let template_json: Value = + serde_json::from_str(&get_template.output).expect("template JSON output"); + assert_eq!(template_json["name"].as_str(), Some(template_name.as_str())); + assert_eq!( + template_json["labels"]["e2e"].as_str(), + Some("sandbox-template") + ); + assert_eq!( + template_json["environment"]["FEATURE_FLAG"].as_str(), + Some("on") + ); + assert_eq!(template_json["resources"]["cpu"].as_str(), Some("500m")); + assert_eq!(template_json["resources"]["memory"].as_str(), Some("512Mi")); + + let list_templates = run_cli(&["sandbox", "template", "list", "--names"]).await; + assert!( + list_templates.success, + "sandbox template list failed:\n{}", + list_templates.output + ); + assert!( + list_templates + .output + .lines() + .any(|line| line.trim() == template_name), + "template list should include {template_name}:\n{}", + list_templates.output + ); + + let mut sandbox = SandboxGuard::create(&[ + "--name", + &sandbox_name, + "--template", + &template_name, + "--", + "sh", + "-lc", + "test \"$FEATURE_FLAG\" = on && echo template-env-ok", + ]) + .await + .expect("sandbox create from template should succeed"); + + assert!( + sandbox.create_output.contains("template-env-ok"), + "sandbox should inherit template environment:\n{}", + sandbox.create_output + ); + + sandbox.cleanup().await; + template.cleanup().await; +} + +#[tokio::test] +async fn sandbox_template_get_after_delete_returns_not_found() { + let template_name = unique_name("tmpl-del"); + let template = TemplateGuard::new(template_name.clone()); + + let create_template = run_cli(&["sandbox", "template", "create", &template_name]).await; + assert!( + create_template.success, + "sandbox template create failed:\n{}", + create_template.output + ); + + let delete_template = run_cli(&["sandbox", "template", "delete", &template_name]).await; + assert!( + delete_template.success, + "sandbox template delete failed:\n{}", + delete_template.output + ); + + let get_deleted = run_cli(&["sandbox", "template", "get", &template_name]).await; + assert!( + !get_deleted.success, + "deleted template should not be fetchable:\n{}", + get_deleted.output + ); + assert!( + output_mentions_template_not_found(&get_deleted.output), + "deleted template should return not-found:\n{}", + get_deleted.output + ); + + template.cleanup().await; +} + +#[tokio::test] +async fn sandbox_template_create_rejects_duplicate_name() { + let template_name = unique_name("tmpl-dupe"); + let template = TemplateGuard::new(template_name.clone()); + + let create_template = run_cli(&["sandbox", "template", "create", &template_name]).await; + assert!( + create_template.success, + "sandbox template create failed:\n{}", + create_template.output + ); + + let duplicate = run_cli(&["sandbox", "template", "create", &template_name]).await; + assert!( + !duplicate.success, + "duplicate sandbox template create should fail:\n{}", + duplicate.output + ); + assert!( + output_contains(&duplicate.output, "already exists"), + "duplicate sandbox template create should report already-exists:\n{}", + duplicate.output + ); + + template.cleanup().await; +} + +#[tokio::test] +async fn sandbox_create_from_missing_template_returns_not_found() { + let template_name = unique_name("tmpl-missing"); + let sandbox_name = unique_name("sb-missing"); + + let create_sandbox = run_cli(&[ + "sandbox", + "create", + "--name", + &sandbox_name, + "--template", + &template_name, + "--", + "true", + ]) + .await; + + if create_sandbox.success { + let _ = run_cli(&["sandbox", "delete", &sandbox_name]).await; + } + + assert!( + !create_sandbox.success, + "sandbox create from missing template should fail:\n{}", + create_sandbox.output + ); + assert!( + output_mentions_template_not_found(&create_sandbox.output), + "sandbox create from missing template should report not-found:\n{}", + create_sandbox.output + ); +} diff --git a/proto/openshell.proto b/proto/openshell.proto index a2d7a0ad02..138b973474 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -6,6 +6,7 @@ syntax = "proto3"; package openshell.v1; import "datamodel.proto"; +import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "options.proto"; import "sandbox.proto"; @@ -69,6 +70,46 @@ service OpenShell { }; } + // Create a reusable sandbox workload template. + rpc CreateSandboxTemplate(CreateSandboxTemplateRequest) + returns (SandboxTemplateResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "admin" + }; + } + + // Fetch a reusable sandbox workload template by name. + rpc GetSandboxTemplate(GetSandboxTemplateRequest) + returns (SandboxTemplateResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // List reusable sandbox workload templates. + rpc ListSandboxTemplates(ListSandboxTemplatesRequest) + returns (ListSandboxTemplatesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Delete a reusable sandbox workload template by name. + rpc DeleteSandboxTemplate(DeleteSandboxTemplateRequest) + returns (DeleteSandboxTemplateResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "admin" + }; + } + // List provider records attached to a sandbox. rpc ListSandboxProviders(ListSandboxProvidersRequest) returns (ListSandboxProvidersResponse) { @@ -813,6 +854,8 @@ message Sandbox { SandboxSpec spec = 2; // Latest user-facing observed status derived by the gateway. SandboxStatus status = 3; + // Read-only provenance for sandboxes created from a reusable workload template. + SandboxWorkloadTemplateProvenance created_from_workload_template = 20; reserved 4, 5; reserved "phase", "current_policy_version"; @@ -861,7 +904,12 @@ message GpuResourceRequirements { optional uint32 count = 1; } -// Public sandbox template mapped onto compute-driver template inputs. +// Historical inline compute template mapped onto compute-driver template inputs. +// +// Despite its name, this is not a reusable named sandbox template resource. It +// is an inline part of `SandboxSpec` kept for v1 compatibility. A future +// breaking API cleanup may rename this message to free `SandboxTemplate` for +// the reusable template resource now represented by `SandboxWorkloadTemplate`. message SandboxTemplate { // Fully-qualified OCI image reference used to boot the sandbox. string image = 1; @@ -892,6 +940,62 @@ message SandboxTemplate { google.protobuf.Struct driver_config = 11; } +// Reusable named sandbox workload template resource. +// +// This is the actual workspace-scoped template resource used to create +// sandboxes by reference. It uses the longer name in v1 to avoid colliding with +// the historical inline `SandboxTemplate` message. A future breaking API +// cleanup may rename this resource to `SandboxTemplate`. +message SandboxWorkloadTemplate { + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + openshell.datamodel.v1.ObjectMeta metadata = 1; + // Desired reusable workload shape and template-owned driver config. + SandboxWorkloadTemplateSpec spec = 2; +} + +message SandboxWorkloadTemplateSpec { + // Portable workload shape. + SandboxWorkloadConfig workload = 1; + // Driver-keyed opaque config envelope supplied by the template owner. + google.protobuf.Struct driver_config = 2; + // Desired service level associated with this template. + SandboxServiceLevel desired_service_level = 3; +} + +message SandboxWorkloadConfig { + // Fully-qualified OCI image reference used to boot the sandbox. + string image = 1; + // Environment variables injected into the sandbox runtime. + map environment = 2; + // Portable resource requirements for sandboxes created from this workload. + SandboxResources resources = 3; +} + +message SandboxResources { + // Portable CPU quantity, for example "500m" or "2". + string cpu = 1; + // Portable memory quantity, for example "512Mi" or "2Gi". + string memory = 2; + // GPU requirements for the sandbox workload. Presence indicates a GPU + // request. When count is omitted, the request uses the selected driver's + // default GPU assignment behavior. + GpuResourceRequirements gpu = 3; +} + +message SandboxServiceLevel { + SandboxStartup startup = 1; +} + +message SandboxStartup { + google.protobuf.Duration ready_within = 1; + uint32 max_burst = 2; +} + +message SandboxWorkloadTemplateProvenance { + string name = 1; + string resource_version = 2; +} + // User-facing sandbox status derived by the gateway from compute-driver observations. // // Public status does not embed driver-only flags such as `deleting`. @@ -982,6 +1086,49 @@ message CreateSandboxRequest { // the canonical main process. The supervisor keeps the terminal transport // alive until that attachment connects and closes naturally. bool await_main_process_attachment = 6; + // Workspace-scoped SandboxWorkloadTemplate name to resolve at creation time. + string workload_template_name = 7; +} + +message CreateSandboxTemplateRequest { + SandboxWorkloadTemplate template = 1; + // Workspace for the template. Empty defaults to "default". + string workspace = 2; +} + +message GetSandboxTemplateRequest { + string name = 1; + // Workspace scope. Empty defaults to "default". + string workspace = 2; +} + +message ListSandboxTemplatesRequest { + uint32 limit = 1; + uint32 offset = 2; + // Workspace scope. Empty defaults to "default". + string workspace = 3; + // List across all workspaces. Mutually exclusive with workspace. + bool all_workspaces = 4; + // Optional label selector in key=value comma-separated form. + string label_selector = 5; +} + +message DeleteSandboxTemplateRequest { + string name = 1; + // Workspace scope. Empty defaults to "default". + string workspace = 2; +} + +message SandboxTemplateResponse { + SandboxWorkloadTemplate template = 1; +} + +message ListSandboxTemplatesResponse { + repeated SandboxWorkloadTemplate templates = 1; +} + +message DeleteSandboxTemplateResponse { + bool deleted = 1; } // Get sandbox request. diff --git a/python/openshell/__init__.py b/python/openshell/__init__.py index 27f317578b..4001d2fb70 100644 --- a/python/openshell/__init__.py +++ b/python/openshell/__init__.py @@ -17,6 +17,8 @@ SandboxRef, SandboxSession, SandboxStatusRef, + SandboxTemplateClient, + SandboxWorkloadTemplateProvenanceRef, TlsConfig, WorkspaceClient, WorkspaceRef, @@ -41,6 +43,8 @@ "SandboxRef", "SandboxSession", "SandboxStatusRef", + "SandboxTemplateClient", + "SandboxWorkloadTemplateProvenanceRef", "TlsConfig", "WorkspaceClient", "WorkspaceRef", diff --git a/python/openshell/openshell_test.py b/python/openshell/openshell_test.py index 66daac6ee1..101b7b6833 100644 --- a/python/openshell/openshell_test.py +++ b/python/openshell/openshell_test.py @@ -9,3 +9,7 @@ def test_version() -> None: """Test that version is defined.""" assert openshell.__version__ + + +def test_sandbox_template_client_exported() -> None: + assert openshell.SandboxTemplateClient diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index 5653d8dc5e..07eeb6480e 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -394,6 +394,12 @@ def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]: return type(self), (dict(self),) +@dataclass(frozen=True) +class SandboxWorkloadTemplateProvenanceRef: + name: str + resource_version: str + + @dataclass(frozen=True) class SandboxRef: id: str @@ -403,6 +409,7 @@ class SandboxRef: # Excluded from equality/hash to preserve the original identity while the # immutable mapping remains safe for deepcopy, pickle, and asdict. labels: Mapping[str, str] = field(default_factory=_ImmutableLabels, compare=False) + created_from_workload_template: SandboxWorkloadTemplateProvenanceRef | None = None def __post_init__(self) -> None: object.__setattr__(self, "labels", _ImmutableLabels(self.labels)) @@ -726,6 +733,33 @@ def create( raise SandboxError("CreateSandbox returned empty sandbox id") return sandbox_ref + def create_from_template( + self, + *, + workspace: str, + template_name: str, + spec: openshell_pb2.SandboxSpec | None = None, + name: str | None = None, + labels: Mapping[str, str] | None = None, + ) -> SandboxRef: + if not template_name.strip(): + raise SandboxError("template_name is required") + request_spec = spec if spec is not None else openshell_pb2.SandboxSpec() + response = self._stub.CreateSandbox( + openshell_pb2.CreateSandboxRequest( + spec=request_spec, + name=name or "", + labels=dict(labels) if labels else {}, + workspace=workspace, + workload_template_name=template_name, + ), + timeout=self._timeout, + ) + sandbox_ref = _sandbox_ref(response.sandbox) + if sandbox_ref.id == "": + raise SandboxError("CreateSandbox returned empty sandbox id") + return sandbox_ref + def create_session( self, *, @@ -738,6 +772,29 @@ def create_session( self, self.create(workspace=workspace, spec=spec, name=name, labels=labels) ) + def create_session_from_template( + self, + *, + workspace: str, + template_name: str, + spec: openshell_pb2.SandboxSpec | None = None, + name: str | None = None, + labels: Mapping[str, str] | None = None, + ) -> SandboxSession: + return SandboxSession( + self, + self.create_from_template( + workspace=workspace, + template_name=template_name, + spec=spec, + name=name, + labels=labels, + ), + ) + + def sandbox_templates(self) -> SandboxTemplateClient: + return SandboxTemplateClient(self._channel, timeout=self._timeout) + def get(self, sandbox_name: str, *, workspace: str) -> SandboxRef: response = self._stub.GetSandbox( openshell_pb2.GetSandboxRequest(name=sandbox_name, workspace=workspace), @@ -1027,6 +1084,126 @@ def exec_python( ) +class SandboxTemplateClient: + """gRPC client for reusable sandbox template lifecycle operations.""" + + def __init__(self, channel: grpc.Channel, *, timeout: float = 30.0) -> None: + self._stub = openshell_pb2_grpc.OpenShellStub(channel) + self._timeout = timeout + + @classmethod + def from_sandbox_client(cls, client: SandboxClient) -> SandboxTemplateClient: + return client.sandbox_templates() + + def create( + self, + *, + workspace: str, + template: openshell_pb2.SandboxWorkloadTemplate | None = None, + name: str | None = None, + image: str | None = None, + labels: Mapping[str, str] | None = None, + annotations: Mapping[str, str] | None = None, + environment: Mapping[str, str] | None = None, + cpu: str | None = None, + memory: str | None = None, + gpu_count: int | None = None, + gpu: bool = False, + driver_config: Mapping[str, Any] | None = None, + ) -> openshell_pb2.SandboxWorkloadTemplate: + if template is None: + if name is None or not name.strip(): + raise SandboxError("name is required when template is omitted") + template = _sandbox_workload_template( + name=name, + image=image, + labels=labels, + annotations=annotations, + environment=environment, + cpu=cpu, + memory=memory, + gpu_count=gpu_count, + gpu=gpu, + driver_config=driver_config, + ) + elif ( + any(value is not None for value in (name, image, cpu, memory, gpu_count)) + or any( + bool(value) + for value in (labels, annotations, environment, driver_config) + ) + or gpu + ): + raise SandboxError( + "template cannot be combined with template builder fields" + ) + + response = self._stub.CreateSandboxTemplate( + openshell_pb2.CreateSandboxTemplateRequest( + workspace=workspace, + template=template, + ), + timeout=self._timeout, + ) + return response.template + + def get( + self, + name: str, + *, + workspace: str, + ) -> openshell_pb2.SandboxWorkloadTemplate: + response = self._stub.GetSandboxTemplate( + openshell_pb2.GetSandboxTemplateRequest(name=name, workspace=workspace), + timeout=self._timeout, + ) + return response.template + + def list( + self, + *, + workspace: str, + limit: int = 100, + offset: int = 0, + label_selector: str = "", + ) -> builtins.list[openshell_pb2.SandboxWorkloadTemplate]: + response = self._stub.ListSandboxTemplates( + openshell_pb2.ListSandboxTemplatesRequest( + workspace=workspace, + limit=limit, + offset=offset, + label_selector=label_selector, + ), + timeout=self._timeout, + ) + return list(response.templates) + + def list_for_all_workspaces( + self, + *, + limit: int = 100, + offset: int = 0, + label_selector: str = "", + ) -> builtins.list[openshell_pb2.SandboxWorkloadTemplate]: + response = self._stub.ListSandboxTemplates( + openshell_pb2.ListSandboxTemplatesRequest( + all_workspaces=True, + limit=limit, + offset=offset, + label_selector=label_selector, + ), + timeout=self._timeout, + ) + return list(response.templates) + + def delete(self, name: str, *, workspace: str) -> bool: + response = self._stub.DeleteSandboxTemplate( + openshell_pb2.DeleteSandboxTemplateRequest(name=name, workspace=workspace), + timeout=self._timeout, + ) + return bool(response.deleted) + + @dataclass(frozen=True) class InferenceRouteConfig: provider_name: str @@ -1182,6 +1359,7 @@ def __init__( spec: openshell_pb2.SandboxSpec | None = None, name: str | None = None, labels: Mapping[str, str] | None = None, + template_name: str | None = None, timeout: float = 30.0, ready_timeout_seconds: float = 120.0, auto_refresh: bool = True, @@ -1207,6 +1385,7 @@ def __init__( self._name = name # Copy so later caller mutation cannot change what gets sent on enter. self._labels = dict(labels) if labels is not None else None + self._template_name = template_name self._timeout = timeout self._ready_timeout_seconds = ready_timeout_seconds self._auto_refresh = auto_refresh @@ -1232,10 +1411,12 @@ def __enter__(self) -> Sandbox: # Creation metadata cannot be applied when attaching to an existing # sandbox; reject it before opening a connection. if self._sandbox_input is not None and ( - self._name is not None or self._labels is not None + self._name is not None + or self._labels is not None + or self._template_name is not None ): raise SandboxError( - "name and labels cannot be set when attaching to an existing sandbox" + "name, labels, and template_name cannot be set when attaching to an existing sandbox" ) client = SandboxClient.from_active_cluster( @@ -1248,7 +1429,15 @@ def __enter__(self) -> Sandbox: ) self._client = client - if self._sandbox_input is None: + if self._sandbox_input is None and self._template_name is not None: + self._session = client.create_session_from_template( + workspace=self._workspace, + template_name=self._template_name, + spec=self._spec, + name=self._name, + labels=self._labels, + ) + elif self._sandbox_input is None: self._session = client.create_session( workspace=self._workspace, spec=self._spec, @@ -1374,6 +1563,14 @@ def _serialize_python_callable( def _sandbox_ref(sandbox: openshell_pb2.Sandbox) -> SandboxRef: status = sandbox.status if sandbox.HasField("status") else None + provenance = ( + SandboxWorkloadTemplateProvenanceRef( + name=sandbox.created_from_workload_template.name, + resource_version=sandbox.created_from_workload_template.resource_version, + ) + if sandbox.HasField("created_from_workload_template") + else None + ) return SandboxRef( id=sandbox.metadata.id if sandbox.metadata else "", name=sandbox.metadata.name if sandbox.metadata else "", @@ -1386,6 +1583,7 @@ def _sandbox_ref(sandbox: openshell_pb2.Sandbox) -> SandboxRef: else None, ), labels=sandbox.metadata.labels if sandbox.metadata else {}, + created_from_workload_template=provenance, ) @@ -1398,6 +1596,50 @@ def _default_spec() -> openshell_pb2.SandboxSpec: return openshell_pb2.SandboxSpec() +def _sandbox_workload_template( + *, + name: str, + image: str | None = None, + labels: Mapping[str, str] | None = None, + annotations: Mapping[str, str] | None = None, + environment: Mapping[str, str] | None = None, + cpu: str | None = None, + memory: str | None = None, + gpu_count: int | None = None, + gpu: bool = False, + driver_config: Mapping[str, Any] | None = None, +) -> openshell_pb2.SandboxWorkloadTemplate: + if gpu_count is not None and gpu_count <= 0: + raise SandboxError("gpu_count must be greater than zero") + + template = openshell_pb2.SandboxWorkloadTemplate() + template.metadata.name = name + # The gateway permits an empty workload so it can apply the default image + # at sandbox create time, but the workload message itself is required. + template.spec.workload.SetInParent() + if labels: + template.metadata.labels.update(dict(labels)) + if annotations: + template.metadata.annotations.update(dict(annotations)) + + if image is not None: + template.spec.workload.image = image + if environment: + template.spec.workload.environment.update(dict(environment)) + if cpu is not None: + template.spec.workload.resources.cpu = cpu + if memory is not None: + template.spec.workload.resources.memory = memory + if gpu or gpu_count is not None: + template.spec.workload.resources.gpu.SetInParent() + if gpu_count is not None: + template.spec.workload.resources.gpu.count = gpu_count + if driver_config: + template.spec.driver_config.update(dict(driver_config)) + + return template + + def _xdg_config_home() -> pathlib.Path: configured = os.environ.get("XDG_CONFIG_HOME") if configured: diff --git a/python/openshell/sandbox_test.py b/python/openshell/sandbox_test.py index 23382d1093..5e12d0c8a3 100644 --- a/python/openshell/sandbox_test.py +++ b/python/openshell/sandbox_test.py @@ -29,6 +29,7 @@ SandboxError, SandboxRef, SandboxStatusRef, + SandboxTemplateClient, TlsConfig, _atomic_replace, _BearerAuthInterceptor, @@ -437,6 +438,13 @@ def _client_with_fake_stub(stub: object) -> SandboxClient: return client +def _template_client_with_fake_stub(stub: object) -> SandboxTemplateClient: + client = cast("SandboxTemplateClient", object.__new__(SandboxTemplateClient)) + client._timeout = 30.0 + client._stub = cast("Any", stub) + return client + + def test_exec_sends_stdin_payload() -> None: stub = _FakeStub() client = _client_with_fake_stub(stub) @@ -1991,6 +1999,20 @@ def _make_sandbox_proto( return sandbox +def _make_workload_template_proto( + name: str, + *, + workspace: str = "default", +) -> openshell_pb2.SandboxWorkloadTemplate: + template = openshell_pb2.SandboxWorkloadTemplate() + template.metadata.name = name + template.metadata.workspace = workspace + template.spec.workload.image = f"ghcr.io/test/{name}:latest" + template.spec.workload.resources.cpu = "1" + template.spec.workload.resources.memory = "512Mi" + return template + + class _FakeSandboxStub: def __init__(self, listed: list[openshell_pb2.Sandbox] | None = None) -> None: self.create_request: openshell_pb2.CreateSandboxRequest | None = None @@ -1999,7 +2021,18 @@ def __init__(self, listed: list[openshell_pb2.Sandbox] | None = None) -> None: self.delete_request: openshell_pb2.DeleteSandboxRequest | None = None self.stop_request: openshell_pb2.StopSandboxRequest | None = None self.start_request: openshell_pb2.StartSandboxRequest | None = None + self.create_template_request: ( + openshell_pb2.CreateSandboxTemplateRequest | None + ) = None + self.get_template_request: openshell_pb2.GetSandboxTemplateRequest | None = None + self.list_template_request: openshell_pb2.ListSandboxTemplatesRequest | None = ( + None + ) + self.delete_template_request: ( + openshell_pb2.DeleteSandboxTemplateRequest | None + ) = None self._listed = listed or [] + self._templates: list[openshell_pb2.SandboxWorkloadTemplate] = [] def GetSandbox( self, @@ -2080,12 +2113,55 @@ def ListSandboxes( _ = timeout return SimpleNamespace(sandboxes=list(self._listed)) + def CreateSandboxTemplate( + self, + request: openshell_pb2.CreateSandboxTemplateRequest, + timeout: float | None = None, + ) -> Any: + self.create_template_request = request + _ = timeout + self._templates.append(request.template) + return SimpleNamespace(template=request.template) + + def GetSandboxTemplate( + self, + request: openshell_pb2.GetSandboxTemplateRequest, + timeout: float | None = None, + ) -> Any: + self.get_template_request = request + _ = timeout + return SimpleNamespace( + template=_make_workload_template_proto( + request.name, + workspace=request.workspace or "default", + ) + ) + + def ListSandboxTemplates( + self, + request: openshell_pb2.ListSandboxTemplatesRequest, + timeout: float | None = None, + ) -> Any: + self.list_template_request = request + _ = timeout + return SimpleNamespace(templates=list(self._templates)) + + def DeleteSandboxTemplate( + self, + request: openshell_pb2.DeleteSandboxTemplateRequest, + timeout: float | None = None, + ) -> Any: + self.delete_template_request = request + _ = timeout + return SimpleNamespace(deleted=True) + class _RecordingHighLevelClient: """A stand-in for SandboxClient used to observe high-level forwarding.""" def __init__(self) -> None: self.create_kwargs: dict[str, Any] | None = None + self.create_template_kwargs: dict[str, Any] | None = None def create_session( self, @@ -2103,6 +2179,24 @@ def create_session( } return SimpleNamespace(sandbox=SimpleNamespace(name=name or "generated")) + def create_session_from_template( + self, + *, + workspace: str, + template_name: str, + spec: Any = None, + name: str | None = None, + labels: Any = None, + ) -> Any: + self.create_template_kwargs = { + "workspace": workspace, + "template_name": template_name, + "spec": spec, + "name": name, + "labels": labels, + } + return SimpleNamespace(sandbox=SimpleNamespace(name=name or "generated")) + def wait_ready( self, name: str, *, workspace: str, timeout_seconds: float = 300.0 ) -> SandboxRef: @@ -2129,6 +2223,255 @@ def test_create_forwards_name_and_labels() -> None: assert dict(ref.labels) == {"aiq": "deep-research"} +def test_create_from_template_forwards_workload_template_name() -> None: + stub = _FakeSandboxStub() + client = _client_with_fake_stub(stub) + spec = openshell_pb2.SandboxSpec( + providers=["github"], + command=["/opt/worker", "--serve"], + tty=True, + ) + + ref = client.create_from_template( + workspace="default", + template_name="gpu-kata", + spec=spec, + name="job-1", + labels={"team": "runtime"}, + ) + + assert stub.create_request is not None + assert stub.create_request.name == "job-1" + assert stub.create_request.workload_template_name == "gpu-kata" + assert dict(stub.create_request.labels) == {"team": "runtime"} + assert list(stub.create_request.spec.providers) == ["github"] + assert list(stub.create_request.spec.command) == ["/opt/worker", "--serve"] + assert stub.create_request.spec.tty is True + assert dict(ref.labels) == {"team": "runtime"} + + +def test_create_from_template_rejects_empty_template_name() -> None: + stub = _FakeSandboxStub() + client = _client_with_fake_stub(stub) + + with pytest.raises(SandboxError): + client.create_from_template(workspace="default", template_name=" ") + + assert stub.create_request is None + + +def test_sandbox_template_create_builds_template_from_public_fields() -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + + created = client.create( + workspace="default", + name="gpu-kata", + image="ghcr.io/test/gpu-kata:latest", + labels={"team": "runtime"}, + annotations={"owner": "platform"}, + environment={"FEATURE_FLAG": "on"}, + cpu="1", + memory="512Mi", + gpu_count=2, + driver_config={"kubernetes": {"runtime_class_name": "kata"}}, + ) + + assert created.metadata.name == "gpu-kata" + assert stub.create_template_request is not None + assert stub.create_template_request.workspace == "default" + template = stub.create_template_request.template + assert template.metadata.name == "gpu-kata" + assert dict(template.metadata.labels) == {"team": "runtime"} + assert dict(template.metadata.annotations) == {"owner": "platform"} + assert template.spec.workload.image == "ghcr.io/test/gpu-kata:latest" + assert dict(template.spec.workload.environment) == {"FEATURE_FLAG": "on"} + assert template.spec.workload.resources.cpu == "1" + assert template.spec.workload.resources.memory == "512Mi" + assert template.spec.workload.resources.gpu.count == 2 + assert template.spec.driver_config["kubernetes"]["runtime_class_name"] == "kata" + + +def test_sandbox_template_create_materializes_default_workload() -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + + client.create(workspace="default", name="base") + + assert stub.create_template_request is not None + template = stub.create_template_request.template + assert template.HasField("spec") + assert template.spec.HasField("workload") + assert template.spec.workload.image == "" + + +def test_sandbox_template_create_materializes_workload_with_driver_config_only() -> ( + None +): + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + + client.create( + workspace="default", + name="kata-default-image", + driver_config={"kubernetes": {"runtime_class_name": "kata"}}, + ) + + assert stub.create_template_request is not None + template = stub.create_template_request.template + assert template.HasField("spec") + assert template.spec.HasField("workload") + assert template.spec.workload.image == "" + assert template.spec.driver_config["kubernetes"]["runtime_class_name"] == "kata" + + +def test_sandbox_template_create_rejects_missing_public_name() -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + + with pytest.raises(SandboxError): + client.create(workspace="default", image="ghcr.io/test/python:latest") + + assert stub.create_template_request is None + + +def test_sandbox_template_create_rejects_template_and_builder_fields() -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + template = _make_workload_template_proto("gpu-kata") + + with pytest.raises(SandboxError): + client.create(workspace="default", template=template, image="override") + + assert stub.create_template_request is None + + +@pytest.mark.parametrize( + "builder_kwargs", + ( + {"labels": {}}, + {"annotations": {}}, + {"environment": {}}, + {"driver_config": {}}, + ), +) +def test_sandbox_template_create_allows_template_and_empty_builder_mappings( + builder_kwargs: dict[str, Any], +) -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + template = _make_workload_template_proto("gpu-kata") + + created = client.create( + workspace="default", + template=template, + **builder_kwargs, + ) + + assert created.metadata.name == "gpu-kata" + assert stub.create_template_request is not None + assert stub.create_template_request.template.metadata.name == template.metadata.name + assert ( + stub.create_template_request.template.spec.workload.image + == template.spec.workload.image + ) + + +@pytest.mark.parametrize( + "builder_kwargs", + ( + {"labels": {"team": "runtime"}}, + {"annotations": {"owner": "platform"}}, + {"environment": {"FEATURE_FLAG": "on"}}, + {"driver_config": {"kubernetes": {"runtime_class_name": "kata"}}}, + ), +) +def test_sandbox_template_create_rejects_template_and_non_empty_builder_mappings( + builder_kwargs: dict[str, Any], +) -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + template = _make_workload_template_proto("gpu-kata") + + with pytest.raises(SandboxError): + client.create( + workspace="default", + template=template, + **builder_kwargs, + ) + + assert stub.create_template_request is None + + +def test_sandbox_template_create_rejects_non_positive_gpu_count() -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + + with pytest.raises(SandboxError): + client.create(workspace="default", name="gpu-kata", gpu_count=0) + + assert stub.create_template_request is None + + +def test_sandbox_template_client_crud_forwards_requests() -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + template = _make_workload_template_proto("gpu-kata") + template.spec.driver_config.update({"kubernetes": {"runtime_class_name": "kata"}}) + + created = client.create(workspace="default", template=template) + + assert created.metadata.name == "gpu-kata" + assert stub.create_template_request is not None + assert stub.create_template_request.workspace == "default" + assert ( + stub.create_template_request.template.spec.workload.image + == "ghcr.io/test/gpu-kata:latest" + ) + assert ( + stub.create_template_request.template.spec.driver_config["kubernetes"][ + "runtime_class_name" + ] + == "kata" + ) + + got = client.get("gpu-kata", workspace="default") + assert got.metadata.name == "gpu-kata" + assert stub.get_template_request is not None + assert stub.get_template_request.name == "gpu-kata" + assert stub.get_template_request.workspace == "default" + + listed = client.list( + workspace="default", limit=50, offset=10, label_selector="team=runtime" + ) + assert len(listed) == 1 + assert stub.list_template_request is not None + assert stub.list_template_request.workspace == "default" + assert stub.list_template_request.limit == 50 + assert stub.list_template_request.offset == 10 + assert stub.list_template_request.label_selector == "team=runtime" + assert not stub.list_template_request.all_workspaces + + assert client.delete("gpu-kata", workspace="default") is True + assert stub.delete_template_request is not None + assert stub.delete_template_request.name == "gpu-kata" + assert stub.delete_template_request.workspace == "default" + + +def test_sandbox_template_list_for_all_workspaces_clears_workspace() -> None: + stub = _FakeSandboxStub() + client = _template_client_with_fake_stub(stub) + + client.list_for_all_workspaces(limit=100, offset=5, label_selector="team=runtime") + + assert stub.list_template_request is not None + assert stub.list_template_request.all_workspaces + assert stub.list_template_request.workspace == "" + assert stub.list_template_request.limit == 100 + assert stub.list_template_request.offset == 5 + assert stub.list_template_request.label_selector == "team=runtime" + + def test_stop_and_start_forward_workspace_and_return_phase() -> None: stub = _FakeSandboxStub() client = _client_with_fake_stub(stub) @@ -2365,6 +2708,44 @@ def test_high_level_creation_forwards_name_and_labels( } +def test_high_level_template_creation_forwards_template_name( + monkeypatch: pytest.MonkeyPatch, +) -> None: + recording = _RecordingHighLevelClient() + monkeypatch.setattr( + SandboxClient, + "from_active_cluster", + classmethod(lambda _cls, **_kwargs: recording), + ) + + spec = openshell_pb2.SandboxSpec( + providers=["github"], + command=["/opt/worker", "--serve"], + tty=True, + ) + sandbox = Sandbox( + workspace="staging", + template_name="gpu-kata", + spec=spec, + name="job-1", + labels={"team": "runtime"}, + delete_on_exit=False, + ) + sandbox.__enter__() + + assert recording.create_template_kwargs == { + "workspace": "staging", + "template_name": "gpu-kata", + "spec": spec, + "name": "job-1", + "labels": {"team": "runtime"}, + } + assert recording.create_template_kwargs is not None + forwarded_spec = recording.create_template_kwargs["spec"] + assert list(forwarded_spec.command) == ["/opt/worker", "--serve"] + assert forwarded_spec.tty is True + + def test_high_level_attach_rejects_name() -> None: sandbox = Sandbox(workspace="default", sandbox="existing-sandbox", name="job-1") @@ -2385,6 +2766,15 @@ def test_high_level_attach_rejects_labels() -> None: sandbox.__enter__() +def test_high_level_attach_rejects_template_name() -> None: + sandbox = Sandbox( + workspace="default", sandbox="existing-sandbox", template_name="gpu-kata" + ) + + with pytest.raises(SandboxError): + sandbox.__enter__() + + # --------------------------------------------------------------------------- # Workspace support # --------------------------------------------------------------------------- @@ -2453,6 +2843,18 @@ def test_sandbox_ref_includes_workspace_from_proto() -> None: assert ref.workspace == "production" +def test_sandbox_ref_includes_workload_template_provenance() -> None: + proto = _make_sandbox_proto("sandbox-1", "job-1") + proto.created_from_workload_template.name = "gpu-kata" + proto.created_from_workload_template.resource_version = "7" + + ref = _sandbox_ref(proto) + + assert ref.created_from_workload_template is not None + assert ref.created_from_workload_template.name == "gpu-kata" + assert ref.created_from_workload_template.resource_version == "7" + + def test_sandbox_session_delete_passes_workspace() -> None: from openshell.sandbox import SandboxSession diff --git a/sdk/go/docs/src/SUMMARY.md b/sdk/go/docs/src/SUMMARY.md index 7a0915bfeb..bf500f68eb 100644 --- a/sdk/go/docs/src/SUMMARY.md +++ b/sdk/go/docs/src/SUMMARY.md @@ -15,6 +15,7 @@ - [Overview](api/overview.md) - [Client](api/client.md) - [Sandboxes](api/sandboxes.md) +- [Sandbox Templates](api/sandbox-templates.md) - [Exec](api/exec.md) - [Providers](api/providers.md) - [Profiles](api/profiles.md) diff --git a/sdk/go/docs/src/api/client.md b/sdk/go/docs/src/api/client.md index 748de5327e..9fe8b4aed9 100644 --- a/sdk/go/docs/src/api/client.md +++ b/sdk/go/docs/src/api/client.md @@ -2,14 +2,17 @@ Constructor: `v1.NewClient(config)` -The `ClientInterface` is the root entry point for all SDK operations. It provides -typed accessors for each resource domain and manages the underlying gRPC connection. +`Client` is the root entry point for SDK operations. It provides typed accessors +for each resource domain and manages the underlying gRPC connection. The +`ClientInterface` covers the stable core accessors; additive helpers such as +`SandboxTemplates()` are available on the concrete client. ## Methods | Accessor | Returns | Description | |----------|---------|-------------| | `Sandboxes()` | `SandboxInterface` | Sandbox lifecycle management | +| `SandboxTemplates()` | `SandboxTemplateInterface` | Reusable sandbox template management | | `Providers()` | `ProviderInterface` | Provider CRUD and idempotent ensure | | `Services()` | `ServiceInterface` | Service exposure and management | | `Exec()` | `ExecInterface` | Command execution (run, stream, interactive) | diff --git a/sdk/go/docs/src/api/fake.md b/sdk/go/docs/src/api/fake.md index 084858c542..ae96fc9d39 100644 --- a/sdk/go/docs/src/api/fake.md +++ b/sdk/go/docs/src/api/fake.md @@ -61,6 +61,13 @@ client.AddSandbox("default", &types.Sandbox{ Status: types.SandboxStatus{Phase: types.SandboxReady}, }) +client.AddSandboxTemplate("default", &types.SandboxWorkloadTemplate{ + Name: "gpu-kata", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"}, + }, +}) + client.AddProvider("default", &types.Provider{ Name: "my-provider", Spec: types.ProviderSpec{Type: "docker"}, @@ -78,11 +85,13 @@ insertion does not affect the stored object. ## Sub-Client Coverage -The fake client implements every interface in `v1.ClientInterface`: +The fake client implements every stable accessor in `v1.ClientInterface` plus +additive concrete-client accessors such as `SandboxTemplates()`: | Accessor | Interface | Behavior | |----------|-----------|----------| | `Sandboxes()` | `SandboxInterface` | Full CRUD, Watch, WaitReady | +| `SandboxTemplates()` | `SandboxTemplateInterface` | Full CRUD | | `Providers()` | `ProviderInterface` | Full CRUD, Ensure | | `Workspaces()` | `WorkspaceInterface` | Full CRUD, Members | | `Health()` | `HealthInterface` | Configurable result | diff --git a/sdk/go/docs/src/api/overview.md b/sdk/go/docs/src/api/overview.md index d96bbd747a..0ff01c4ce4 100644 --- a/sdk/go/docs/src/api/overview.md +++ b/sdk/go/docs/src/api/overview.md @@ -1,6 +1,7 @@ # API Overview -The OpenShell Go SDK exposes 12 interfaces through the sub-client pattern. You access each interface through a typed accessor on the `Client`. +The OpenShell Go SDK exposes typed interfaces through the sub-client pattern. +You access each interface through a typed accessor on the concrete `Client`. ## Interface Summary @@ -9,6 +10,7 @@ The OpenShell Go SDK exposes 12 interfaces through the sub-client pattern. You a | Interface | Accessor | Description | |-----------|----------|-------------| | [SandboxInterface](sandboxes.md) | `client.Sandboxes()` | Create, manage, and watch sandbox lifecycle | +| [SandboxTemplateInterface](sandbox-templates.md) | `client.SandboxTemplates()` | Manage reusable sandbox templates | | [ExecInterface](exec.md) | `client.Exec()` | Run commands, stream output, interactive sessions | | [ProviderInterface](providers.md) | `client.Providers()` | Manage compute providers and their lifecycle | | [ServiceInterface](services.md) | `client.Services()` | Expose and manage HTTP services inside sandboxes | @@ -39,6 +41,7 @@ These are accessed through `client.Providers()`: Each interface has a reference page with method signatures and usage examples: - **[Sandboxes](sandboxes.md)**: Create sandboxes, wait for readiness, watch state changes, manage providers, retrieve logs. +- **[Sandbox Templates](sandbox-templates.md)**: Create reusable workload templates and launch sandboxes from them. - **[Exec](exec.md)**: Execute commands with one-shot, streaming, or interactive modes. - **[Providers](providers.md)**: Register and manage compute providers. Includes sub-clients for profiles and credential refresh. - **[Services](services.md)**: Expose and manage HTTP services inside sandboxes. diff --git a/sdk/go/docs/src/api/sandbox-templates.md b/sdk/go/docs/src/api/sandbox-templates.md new file mode 100644 index 0000000000..d1dcc86e47 --- /dev/null +++ b/sdk/go/docs/src/api/sandbox-templates.md @@ -0,0 +1,127 @@ +# Sandbox Templates + +Accessor: `client.SandboxTemplates()` + +Manage reusable, workspace-scoped sandbox workload templates. Templates own the +portable workload shape and optional driver-specific config. Sandbox creation +can reference a template by name while supplying only governance fields such as +providers or policy. + +The Go SDK names the reusable resource `SandboxWorkloadTemplate` because +`SandboxTemplate` already represents the legacy inline compute template inside +`SandboxSpec`. + +## Create + +Creates a reusable template in a workspace. + +```go +gpuCount := uint32(1) + +template, err := client.SandboxTemplates().Create(ctx, "default", &v1.SandboxWorkloadTemplate{ + Name: "gpu-kata", + Labels: map[string]string{ + "team": "platform", + }, + Spec: v1.SandboxWorkloadTemplateSpec{ + Workload: &v1.SandboxWorkloadConfig{ + Image: "nvcr.io/nvidia/openshell:latest", + Environment: map[string]string{ + "NVIDIA_VISIBLE_DEVICES": "all", + }, + Resources: &v1.SandboxResources{ + CPU: "2", + Memory: "8Gi", + GPU: &v1.SandboxGPURequirements{Count: &gpuCount}, + }, + }, + DriverConfig: map[string]any{ + "kubernetes": map[string]any{ + "pod": map[string]any{ + "runtime_class_name": "kata", + }, + }, + }, + DesiredServiceLevel: &v1.SandboxServiceLevel{ + Startup: &v1.SandboxStartup{ + ReadyWithin: 45 * time.Second, + MaxBurst: 2, + }, + }, + }, +}) +``` + +Set `GPU: &v1.SandboxGPURequirements{}` to request the active driver's default +GPU assignment without specifying a count. + +## Create a Sandbox From a Template + +Use `CreateSandboxFromTemplate` to create a sandbox from a named reusable +template without changing the legacy `Sandboxes()` interface. + +```go +sb, err := client.CreateSandboxFromTemplate( + ctx, + "default", + "training-run", + "gpu-kata", + &v1.SandboxSpec{ + Providers: []string{"openai"}, + Policy: policy, + }, + map[string]string{"job": "training"}, +) +``` + +When creating from a template, the spec should only include governance fields: +`Providers`, `Policy`, `Command`, and `TTY`. Workload fields such as image, +environment, CPU, memory, GPU, and driver config come from the template. + +## Get + +Retrieves a template by name. + +```go +template, err := client.SandboxTemplates().Get(ctx, "default", "gpu-kata") +fmt.Println(template.Spec.Workload.Image) +``` + +## List + +Lists templates in one workspace or across all workspaces. + +```go +templates, err := client.SandboxTemplates().List(ctx, "default", v1.ListOptions{ + Limit: 50, + Offset: 0, +}) + +allTemplates, err := client.SandboxTemplates().List(ctx, "", v1.ListOptions{ + AllWorkspaces: true, +}) +``` + +## Delete + +Deletes a template by name. Existing sandboxes created from the template are not +deleted. + +```go +deleted, err := client.SandboxTemplates().Delete(ctx, "default", "gpu-kata") +``` + +## Fake Client + +The fake client includes the same template sub-client and can be pre-populated +for tests. + +```go +client := fake.NewClient() +client.AddSandboxTemplate("default", &types.SandboxWorkloadTemplate{ + Name: "gpu-kata", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"}, + }, +}) +``` diff --git a/sdk/go/docs/src/api/sandboxes.md b/sdk/go/docs/src/api/sandboxes.md index a9db856786..698982fe13 100644 --- a/sdk/go/docs/src/api/sandboxes.md +++ b/sdk/go/docs/src/api/sandboxes.md @@ -20,6 +20,32 @@ sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSp }) ``` +Set `GPU: true` to request the active driver's default GPU assignment. Set +`GPUCount` when the sandbox needs a specific GPU count; a non-nil `GPUCount` +also implies `GPU`. + +## Create From Template + +Creates a new sandbox from a reusable sandbox workload template. The template +provides workload fields such as image, environment, resources, and driver +config. The create request supplies governance fields such as providers, +policy, command, and TTY. + +```go +sb, err := client.CreateSandboxFromTemplate(ctx, + "default", + "my-sandbox", + "gpu-kata", + &v1.SandboxSpec{ + Providers: []string{"openai"}, + Policy: policy, + }, + map[string]string{"team": "platform"}, +) +``` + +See [Sandbox Templates](sandbox-templates.md) for template CRUD. + ## Get Retrieves a sandbox by name. diff --git a/sdk/go/docs/src/introduction.md b/sdk/go/docs/src/introduction.md index 61f3a05ca2..fbe63dbb7f 100644 --- a/sdk/go/docs/src/introduction.md +++ b/sdk/go/docs/src/introduction.md @@ -4,7 +4,7 @@ The OpenShell Go SDK provides an idiomatic Go client for the OpenShell gateway A ## Key Features -- **Sub-client pattern**: A single `Client` provides typed accessors for each API domain (Sandboxes, Exec, Providers, Files, Health, SSH, TCP, Config, Policy, Services) +- **Sub-client pattern**: A single `Client` provides typed accessors for each API domain (Sandboxes, SandboxTemplates, Exec, Providers, Files, Health, SSH, TCP, Config, Policy, Services) - **Clean types**: SDK types are self-contained, so your code works with idiomatic Go types without extra dependencies - **Fake client for testing**: An in-memory implementation of the full `ClientInterface` for testing without a live gateway - **Watch and streaming**: First-class support for watching sandbox state changes and streaming command output diff --git a/sdk/go/openshell/v1/client.go b/sdk/go/openshell/v1/client.go index bc3ee43165..06d8183360 100644 --- a/sdk/go/openshell/v1/client.go +++ b/sdk/go/openshell/v1/client.go @@ -4,6 +4,7 @@ package v1 import ( + "context" "sync" "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" @@ -18,6 +19,8 @@ type Config = types.Config // ClientInterface defines the top-level SDK surface. type ClientInterface interface { Sandboxes() SandboxInterface + SandboxTemplates() SandboxTemplateInterface + CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) Providers() ProviderInterface Services() ServiceInterface Exec() ExecInterface @@ -49,18 +52,20 @@ type Client struct { closeOnce sync.Once closeErr error - sandboxes SandboxInterface - providers ProviderInterface - services ServiceInterface - exec ExecInterface - files FileInterface - health HealthInterface - ssh SSHInterface - tcp TCPInterface - cfg ConfigInterface - policy PolicyInterface - workspaces WorkspaceInterface - inference InferenceInterface + sandboxes SandboxInterface + templateCreate SandboxTemplateCreateInterface + templates SandboxTemplateInterface + providers ProviderInterface + services ServiceInterface + exec ExecInterface + files FileInterface + health HealthInterface + ssh SSHInterface + tcp TCPInterface + cfg ConfigInterface + policy PolicyInterface + workspaces WorkspaceInterface + inference InferenceInterface } // NewClient creates a new SDK client connected to the given gateway. @@ -93,7 +98,10 @@ func NewClient(cfg Config) (*Client, error) { config: cfg, } - c.sandboxes = newSandboxClient(conn) + sandboxes := newSandboxClient(conn) + c.sandboxes = sandboxes + c.templateCreate = sandboxes + c.templates = newSandboxTemplateClient(conn) c.providers = newProviderClient(conn) c.services = newServiceClient(conn) c.exec = newExecClient(conn, c.sandboxes) @@ -112,6 +120,15 @@ func NewClient(cfg Config) (*Client, error) { // Sandboxes returns the sandbox sub-client. func (c *Client) Sandboxes() SandboxInterface { return c.sandboxes } +// SandboxTemplates returns the reusable sandbox template sub-client. +func (c *Client) SandboxTemplates() SandboxTemplateInterface { return c.templates } + +// CreateSandboxFromTemplate creates a sandbox from a named workload template +// without changing the legacy Sandboxes() interface. +func (c *Client) CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) { + return c.templateCreate.CreateFromTemplate(ctx, workspace, name, templateName, spec, labels, opts...) +} + // Providers returns the provider sub-client. func (c *Client) Providers() ProviderInterface { return c.providers } diff --git a/sdk/go/openshell/v1/fake/fake.go b/sdk/go/openshell/v1/fake/fake.go index d2978bc08c..16a35fed8a 100644 --- a/sdk/go/openshell/v1/fake/fake.go +++ b/sdk/go/openshell/v1/fake/fake.go @@ -4,6 +4,7 @@ package fake import ( + "context" "sync" v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1" @@ -15,23 +16,26 @@ import ( // real gRPC connection. Create one with NewClient. type Client struct { sandboxStore *objectStore[*types.Sandbox] + templateStore *objectStore[*types.SandboxWorkloadTemplate] providerStore *objectStore[*types.Provider] workspaceStore *objectStore[*types.Workspace] memberStore *objectStore[*types.WorkspaceMember] sandboxBroadcaster *watchBroadcaster[*types.Sandbox] - sandboxes v1.SandboxInterface - providers v1.ProviderInterface - services v1.ServiceInterface - exec v1.ExecInterface - files v1.FileInterface - health v1.HealthInterface - ssh v1.SSHInterface - tcp v1.TCPInterface - cfg v1.ConfigInterface - policy v1.PolicyInterface - workspaces v1.WorkspaceInterface - inference v1.InferenceInterface + sandboxes v1.SandboxInterface + templateCreate v1.SandboxTemplateCreateInterface + templates v1.SandboxTemplateInterface + providers v1.ProviderInterface + services v1.ServiceInterface + exec v1.ExecInterface + files v1.FileInterface + health v1.HealthInterface + ssh v1.SSHInterface + tcp v1.TCPInterface + cfg v1.ConfigInterface + policy v1.PolicyInterface + workspaces v1.WorkspaceInterface + inference v1.InferenceInterface closeOnce sync.Once closed bool @@ -70,13 +74,17 @@ func WithCurrentUser(user *types.CurrentUser) ClientOption { func NewClient(opts ...ClientOption) *Client { fc := &Client{ sandboxStore: newobjectStore(sandboxName, copySandbox), + templateStore: newobjectStore(sandboxWorkloadTemplateName, copySandboxWorkloadTemplate), providerStore: newobjectStore(providerName, copyProvider), workspaceStore: newobjectStore(workspaceName, copyWorkspace), memberStore: newobjectStore(memberName, copyMember), sandboxBroadcaster: newWatchBroadcaster[*types.Sandbox](), } - fc.sandboxes = newFakeSandboxClient(fc.sandboxStore, fc.sandboxBroadcaster, fc.isClosed) + sandboxes := newFakeSandboxClient(fc.sandboxStore, fc.templateStore, fc.sandboxBroadcaster, fc.isClosed) + fc.sandboxes = sandboxes + fc.templateCreate = sandboxes + fc.templates = newFakeSandboxTemplateClient(fc.templateStore, fc.isClosed) fc.providers = newFakeProviderClient(fc.providerStore, fc.isClosed) fc.services = newFakeServiceClient(fc.isClosed) fc.exec = newFakeExecClient(fc.isClosed) @@ -107,6 +115,15 @@ func (fc *Client) isClosed() bool { // Sandboxes returns the sandbox sub-client. func (fc *Client) Sandboxes() v1.SandboxInterface { return fc.sandboxes } +// SandboxTemplates returns the reusable sandbox template sub-client. +func (fc *Client) SandboxTemplates() v1.SandboxTemplateInterface { return fc.templates } + +// CreateSandboxFromTemplate creates a sandbox from a named workload template +// without changing the legacy Sandboxes() interface. +func (fc *Client) CreateSandboxFromTemplate(ctx context.Context, workspace, name, templateName string, spec *types.SandboxSpec, labels map[string]string, opts ...types.CreateOptions) (*types.Sandbox, error) { + return fc.templateCreate.CreateFromTemplate(ctx, workspace, name, templateName, spec, labels, opts...) +} + // Providers returns the provider sub-client. func (fc *Client) Providers() v1.ProviderInterface { return fc.providers } @@ -164,6 +181,16 @@ func (fc *Client) AddSandbox(workspace string, sb *types.Sandbox) { fc.sandboxStore.Insert(workspace, sb) } +// AddSandboxTemplate inserts a sandbox workload template directly into the store. +// This is intended for pre-seeding test fixtures before the test begins. The +// template is deep-copied on insert. +func (fc *Client) AddSandboxTemplate(workspace string, template *types.SandboxWorkloadTemplate) { + if template == nil { + return + } + fc.templateStore.Insert(workspace, template) +} + // AddProvider inserts a provider directly into the store without triggering // any side effects. This is intended for pre-seeding test fixtures before // the test begins. The provider is deep-copied on insert. diff --git a/sdk/go/openshell/v1/fake/sandbox.go b/sdk/go/openshell/v1/fake/sandbox.go index 6fa0c316a9..5de3062d96 100644 --- a/sdk/go/openshell/v1/fake/sandbox.go +++ b/sdk/go/openshell/v1/fake/sandbox.go @@ -32,6 +32,10 @@ func copySandbox(sb *types.Sandbox) *types.Sandbox { t := *sb.DeletionTimestamp cp.DeletionTimestamp = &t } + if sb.CreatedFromWorkloadTemplate != nil { + provenance := *sb.CreatedFromWorkloadTemplate + cp.CreatedFromWorkloadTemplate = &provenance + } cp.Spec = copySandboxSpec(sb.Spec) cp.Status = copySandboxStatus(sb.Status) return &cp @@ -40,6 +44,7 @@ func copySandbox(sb *types.Sandbox) *types.Sandbox { func copySandboxSpec(s types.SandboxSpec) types.SandboxSpec { s.Environment = copyStringMap(s.Environment) s.Providers = copyStringSlice(s.Providers) + s.Command = copyStringSlice(s.Command) if s.Template != nil { t := copySandboxTemplate(*s.Template) s.Template = &t @@ -255,21 +260,27 @@ func copyStringSlice(s []string) []string { // fakeSandboxClient implements v1.SandboxInterface backed by an in-memory // objectStore and watchBroadcaster. type fakeSandboxClient struct { - store *objectStore[*types.Sandbox] - broadcaster *watchBroadcaster[*types.Sandbox] - closedFunc func() bool + store *objectStore[*types.Sandbox] + templateStore *objectStore[*types.SandboxWorkloadTemplate] + broadcaster *watchBroadcaster[*types.Sandbox] + closedFunc func() bool } +var _ v1.SandboxInterface = (*fakeSandboxClient)(nil) +var _ v1.SandboxTemplateCreateInterface = (*fakeSandboxClient)(nil) + // newFakeSandboxClient creates a new fakeSandboxClient. func newFakeSandboxClient( store *objectStore[*types.Sandbox], + templateStore *objectStore[*types.SandboxWorkloadTemplate], broadcaster *watchBroadcaster[*types.Sandbox], closedFunc func() bool, ) *fakeSandboxClient { return &fakeSandboxClient{ - store: store, - broadcaster: broadcaster, - closedFunc: closedFunc, + store: store, + templateStore: templateStore, + broadcaster: broadcaster, + closedFunc: closedFunc, } } @@ -315,6 +326,117 @@ func (c *fakeSandboxClient) Create(_ context.Context, workspace, name string, sp return result, nil } +// CreateFromTemplate creates a new sandbox from a named template with Provisioning phase. +func (c *fakeSandboxClient) CreateFromTemplate(_ context.Context, workspace, name, templateName string, spec *types.SandboxSpec, labels map[string]string, opts ...types.CreateOptions) (*types.Sandbox, error) { + if c.closedFunc() { + return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} + } + if templateName == "" { + return nil, &types.StatusError{Code: types.ErrorInvalidArgument, Message: "template name is required"} + } + if err := validateTemplateCreateSpec(spec); err != nil { + return nil, err + } + template, err := c.templateStore.Get(workspace, templateName) + if err != nil { + return nil, err + } + if spec == nil { + spec = &types.SandboxSpec{} + } + + var annotations map[string]string + if len(opts) > 0 { + annotations = copyStringMap(opts[0].Annotations) + } + + resolvedSpec := sandboxSpecFromWorkloadTemplate(template) + resolvedSpec.Providers = copyStringSlice(spec.Providers) + resolvedSpec.Policy = copySandboxPolicy(spec.Policy) + resolvedSpec.Command = copyStringSlice(spec.Command) + resolvedSpec.TTY = spec.TTY + + sb := &types.Sandbox{ + Name: name, + Workspace: workspace, + CreatedAt: time.Now(), + Labels: copyStringMap(labels), + Annotations: annotations, + ResourceVersion: 1, + Spec: resolvedSpec, + CreatedFromWorkloadTemplate: &types.SandboxWorkloadTemplateProvenance{ + Name: template.Name, + ResourceVersion: fmt.Sprint(template.ResourceVersion), + }, + Status: types.SandboxStatus{ + SandboxName: name, + Phase: types.SandboxProvisioning, + }, + } + + result, err := c.store.Create(workspace, sb) + if err != nil { + return nil, err + } + + c.broadcaster.Broadcast(types.Event[*types.Sandbox]{ + Type: types.EventAdded, + Object: copySandbox(result), + }, name) + + return result, nil +} + +func validateTemplateCreateSpec(spec *types.SandboxSpec) error { + if spec == nil { + return nil + } + if spec.LogLevel != "" || len(spec.Environment) > 0 || spec.Template != nil || spec.GPU || spec.GPUCount != nil { + return &types.StatusError{Code: types.ErrorInvalidArgument, Message: "template creates only allow policy, providers, command, and tty in spec"} + } + return nil +} + +func sandboxSpecFromWorkloadTemplate(template *types.SandboxWorkloadTemplate) types.SandboxSpec { + var spec types.SandboxSpec + if template == nil || template.Spec.Workload == nil { + return spec + } + + workload := template.Spec.Workload + spec.Environment = copyStringMap(workload.Environment) + spec.Template = &types.SandboxTemplate{ + Image: workload.Image, + Resources: sandboxTemplateResources(workload.Resources), + DriverConfig: copyAnyMap(template.Spec.DriverConfig), + } + if workload.Resources != nil && workload.Resources.GPU != nil { + spec.GPU = true + if workload.Resources.GPU.Count != nil { + count := *workload.Resources.GPU.Count + spec.GPUCount = &count + } + } + return spec +} + +func sandboxTemplateResources(resources *types.SandboxResources) map[string]any { + if resources == nil { + return nil + } + limits := make(map[string]any) + if resources.CPU != "" { + limits["cpu"] = resources.CPU + } + if resources.Memory != "" { + limits["memory"] = resources.Memory + } + if len(limits) == 0 { + return nil + } + return map[string]any{"limits": limits} +} + // Get retrieves a sandbox by name. func (c *fakeSandboxClient) Get(_ context.Context, workspace, name string) (*types.Sandbox, error) { if c.closedFunc() { diff --git a/sdk/go/openshell/v1/fake/sandbox_template.go b/sdk/go/openshell/v1/fake/sandbox_template.go new file mode 100644 index 0000000000..8dbf68cc12 --- /dev/null +++ b/sdk/go/openshell/v1/fake/sandbox_template.go @@ -0,0 +1,301 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package fake + +import ( + "context" + "slices" + "strings" + "time" + + v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1" + "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" +) + +func sandboxWorkloadTemplateName(template *types.SandboxWorkloadTemplate) string { + return template.Name +} + +func copySandboxWorkloadTemplate(template *types.SandboxWorkloadTemplate) *types.SandboxWorkloadTemplate { + if template == nil { + return nil + } + copied := *template + copied.Labels = copyStringMap(template.Labels) + copied.Annotations = copyStringMap(template.Annotations) + if template.DeletionTimestamp != nil { + t := *template.DeletionTimestamp + copied.DeletionTimestamp = &t + } + copied.Spec = copySandboxWorkloadTemplateSpec(template.Spec) + return &copied +} + +func copySandboxWorkloadTemplateSpec(spec types.SandboxWorkloadTemplateSpec) types.SandboxWorkloadTemplateSpec { + if spec.Workload != nil { + workload := *spec.Workload + workload.Environment = copyStringMap(spec.Workload.Environment) + if spec.Workload.Resources != nil { + resources := *spec.Workload.Resources + if spec.Workload.Resources.GPU != nil { + gpu := *spec.Workload.Resources.GPU + if spec.Workload.Resources.GPU.Count != nil { + count := *spec.Workload.Resources.GPU.Count + gpu.Count = &count + } + resources.GPU = &gpu + } + workload.Resources = &resources + } + spec.Workload = &workload + } + spec.DriverConfig = copyAnyMap(spec.DriverConfig) + if spec.DesiredServiceLevel != nil { + level := *spec.DesiredServiceLevel + if spec.DesiredServiceLevel.Startup != nil { + startup := *spec.DesiredServiceLevel.Startup + level.Startup = &startup + } + spec.DesiredServiceLevel = &level + } + return spec +} + +type fakeSandboxTemplateClient struct { + store *objectStore[*types.SandboxWorkloadTemplate] + closedFunc func() bool +} + +var _ v1.SandboxTemplateInterface = (*fakeSandboxTemplateClient)(nil) + +func newFakeSandboxTemplateClient( + store *objectStore[*types.SandboxWorkloadTemplate], + closedFunc func() bool, +) *fakeSandboxTemplateClient { + return &fakeSandboxTemplateClient{ + store: store, + closedFunc: closedFunc, + } +} + +func (c *fakeSandboxTemplateClient) Create(_ context.Context, workspace string, template *types.SandboxWorkloadTemplate) (*types.SandboxWorkloadTemplate, error) { + if c.closedFunc() { + return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} + } + if template == nil { + return nil, &types.StatusError{Code: types.ErrorInvalidArgument, Message: "template must not be nil"} + } + if err := validateSandboxWorkloadTemplate(template); err != nil { + return nil, err + } + + t := copySandboxWorkloadTemplate(template) + t.Workspace = workspace + t.CreatedAt = time.Now() + t.ResourceVersion = 1 + + return c.store.Create(workspace, t) +} + +func (c *fakeSandboxTemplateClient) Get(_ context.Context, workspace, name string) (*types.SandboxWorkloadTemplate, error) { + if c.closedFunc() { + return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} + } + return c.store.Get(workspace, name) +} + +func (c *fakeSandboxTemplateClient) List(_ context.Context, workspace string, opts ...v1.ListOptions) ([]*types.SandboxWorkloadTemplate, error) { + if c.closedFunc() { + return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} + } + var options v1.ListOptions + if len(opts) > 0 { + options = opts[0] + if options.Limit < 0 { + return nil, &types.StatusError{Code: types.ErrorInvalidArgument, Message: "limit must not be negative"} + } + if options.Offset < 0 { + return nil, &types.StatusError{Code: types.ErrorInvalidArgument, Message: "offset must not be negative"} + } + } + var templates []*types.SandboxWorkloadTemplate + if options.AllWorkspaces { + templates = c.store.ListAll() + } else { + templates = c.store.List(workspace) + } + slices.SortFunc(templates, compareSandboxWorkloadTemplatesForList) + templates, err := filterSandboxWorkloadTemplatesByLabelSelector(templates, options.LabelSelector) + if err != nil { + return nil, err + } + return paginateSandboxWorkloadTemplates(templates, options), nil +} + +func (c *fakeSandboxTemplateClient) Delete(_ context.Context, workspace, name string) (bool, error) { + if c.closedFunc() { + return false, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} + } + _, existed := c.store.DeleteAndGet(workspace, name) + return existed, nil +} + +func validateSandboxWorkloadTemplate(template *types.SandboxWorkloadTemplate) error { + if template.Name == "" { + return &types.StatusError{Code: types.ErrorInvalidArgument, Message: "sandbox_template.metadata.name is required"} + } + if !isDNS1123Label(template.Name) { + return &types.StatusError{Code: types.ErrorInvalidArgument, Message: "template.metadata.name must be a DNS-1123 label"} + } + if template.Spec.Workload == nil { + return &types.StatusError{Code: types.ErrorInvalidArgument, Message: "sandbox template workload is required"} + } + if err := validateSandboxWorkloadEnvironment(template.Spec.Workload.Environment); err != nil { + return err + } + if resources := template.Spec.Workload.Resources; resources != nil && resources.GPU != nil && resources.GPU.Count != nil && *resources.GPU.Count == 0 { + return &types.StatusError{Code: types.ErrorInvalidArgument, Message: "gpu count must be greater than 0"} + } + return nil +} + +func validateSandboxWorkloadEnvironment(environment map[string]string) error { + for key := range environment { + if !isValidEnvKey(key) { + return &types.StatusError{Code: types.ErrorInvalidArgument, Message: "spec.template.environment keys must match ^[A-Za-z_][A-Za-z0-9_]*$"} + } + if len(key) >= len("OPENSHELL_") && key[:len("OPENSHELL_")] == "OPENSHELL_" { + return &types.StatusError{Code: types.ErrorInvalidArgument, Message: "spec.template.environment keys starting with OPENSHELL_ are reserved"} + } + } + return nil +} + +func filterSandboxWorkloadTemplatesByLabelSelector( + templates []*types.SandboxWorkloadTemplate, + selector string, +) ([]*types.SandboxWorkloadTemplate, error) { + selector = strings.TrimSpace(selector) + if selector == "" { + return templates, nil + } + labels, err := parseSimpleLabelSelector(selector) + if err != nil { + return nil, err + } + filtered := make([]*types.SandboxWorkloadTemplate, 0, len(templates)) + for _, template := range templates { + if labelsMatchSelector(template.Labels, labels) { + filtered = append(filtered, template) + } + } + return filtered, nil +} + +func parseSimpleLabelSelector(selector string) (map[string]string, error) { + labels := make(map[string]string) + for _, part := range strings.Split(selector, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + key, value, ok := strings.Cut(part, "=") + if !ok || strings.TrimSpace(key) == "" { + return nil, &types.StatusError{Code: types.ErrorInvalidArgument, Message: "label selector must use key=value pairs"} + } + labels[strings.TrimSpace(key)] = strings.TrimSpace(value) + } + return labels, nil +} + +func labelsMatchSelector(labels map[string]string, selector map[string]string) bool { + for key, value := range selector { + if labels[key] != value { + return false + } + } + return true +} + +func compareSandboxWorkloadTemplatesForList(a, b *types.SandboxWorkloadTemplate) int { + if a.CreatedAt.Before(b.CreatedAt) { + return -1 + } + if a.CreatedAt.After(b.CreatedAt) { + return 1 + } + if a.Name < b.Name { + return -1 + } + if a.Name > b.Name { + return 1 + } + if a.Workspace < b.Workspace { + return -1 + } + if a.Workspace > b.Workspace { + return 1 + } + return 0 +} + +func paginateSandboxWorkloadTemplates( + templates []*types.SandboxWorkloadTemplate, + options v1.ListOptions, +) []*types.SandboxWorkloadTemplate { + if options.Offset >= len(templates) { + return templates[:0] + } + templates = templates[options.Offset:] + if options.Limit > 0 && options.Limit < len(templates) { + return templates[:options.Limit] + } + return templates +} + +func isDNS1123Label(name string) bool { + if len(name) == 0 || len(name) > 63 || name[0] == '-' || name[len(name)-1] == '-' { + return false + } + previousHyphen := false + for i := 0; i < len(name); i++ { + b := name[i] + if b == '-' { + if previousHyphen { + return false + } + previousHyphen = true + continue + } + previousHyphen = false + if !isASCIIDigit(b) && (b < 'a' || b > 'z') { + return false + } + } + return true +} + +func isValidEnvKey(key string) bool { + if key == "" { + return false + } + for i := 0; i < len(key); i++ { + b := key[i] + if i == 0 && b != '_' && !isASCIIAlpha(b) { + return false + } + if i > 0 && b != '_' && !isASCIIAlpha(b) && !isASCIIDigit(b) { + return false + } + } + return true +} + +func isASCIIAlpha(b byte) bool { + return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') +} + +func isASCIIDigit(b byte) bool { + return b >= '0' && b <= '9' +} diff --git a/sdk/go/openshell/v1/fake/sandbox_template_test.go b/sdk/go/openshell/v1/fake/sandbox_template_test.go new file mode 100644 index 0000000000..06a5187227 --- /dev/null +++ b/sdk/go/openshell/v1/fake/sandbox_template_test.go @@ -0,0 +1,481 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package fake + +import ( + "context" + "testing" + "time" + + "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestSandboxTemplateClient() *fakeSandboxTemplateClient { + store := newobjectStore(sandboxWorkloadTemplateName, copySandboxWorkloadTemplate) + return newFakeSandboxTemplateClient(store, func() bool { return false }) +} + +func testSandboxWorkloadTemplate(name string) *types.SandboxWorkloadTemplate { + return &types.SandboxWorkloadTemplate{ + Name: name, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "registry.example.com/agent:latest"}, + }, + } +} + +func TestIsDNS1123Label(t *testing.T) { + tests := map[string]bool{ + "": false, + "gpu-kata": true, + "Invalid_Template_Name": false, + "-gpu": false, + "gpu-": false, + "gpu--kata": false, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": false, + } + + for name, want := range tests { + assert.Equal(t, want, isDNS1123Label(name), "name %q", name) + } +} + +func TestSandboxTemplate_CreateGetListDelete(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + template := &types.SandboxWorkloadTemplate{ + Name: "gpu-kata", + Labels: map[string]string{"team": "platform"}, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"}, + DriverConfig: map[string]any{"kubernetes": map[string]any{"runtime_class_name": "kata"}}, + DesiredServiceLevel: &types.SandboxServiceLevel{ + Startup: &types.SandboxStartup{ReadyWithin: 30 * time.Second, MaxBurst: 4}, + }, + }, + } + + created, err := tc.Create(ctx, "default", template) + require.NoError(t, err) + assert.Equal(t, "gpu-kata", created.Name) + assert.Equal(t, "default", created.Workspace) + assert.Equal(t, uint64(1), created.ResourceVersion) + assert.NotZero(t, created.CreatedAt) + + got, err := tc.Get(ctx, "default", "gpu-kata") + require.NoError(t, err) + assert.Equal(t, "python:3.12", got.Spec.Workload.Image) + assert.Equal(t, "kata", got.Spec.DriverConfig["kubernetes"].(map[string]any)["runtime_class_name"]) + + listed, err := tc.List(ctx, "default") + require.NoError(t, err) + require.Len(t, listed, 1) + assert.Equal(t, "gpu-kata", listed[0].Name) + + deleted, err := tc.Delete(ctx, "default", "gpu-kata") + require.NoError(t, err) + assert.True(t, deleted) + _, err = tc.Get(ctx, "default", "gpu-kata") + require.Error(t, err) + assert.True(t, types.IsNotFound(err)) + + deleted, err = tc.Delete(ctx, "default", "gpu-kata") + require.NoError(t, err) + assert.False(t, deleted) +} + +func TestSandboxTemplate_CreateAlreadyExists(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + template := testSandboxWorkloadTemplate("gpu-kata") + _, err := tc.Create(ctx, "default", template) + require.NoError(t, err) + + _, err = tc.Create(ctx, "default", template) + require.Error(t, err) + assert.True(t, types.IsAlreadyExists(err)) +} + +func TestSandboxTemplate_ListAllWorkspaces(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + _, _ = tc.Create(ctx, "default", testSandboxWorkloadTemplate("default-template")) + _, _ = tc.Create(ctx, "team-a", testSandboxWorkloadTemplate("team-template")) + + listed, err := tc.List(ctx, "default", types.ListOptions{AllWorkspaces: true}) + require.NoError(t, err) + assert.Len(t, listed, 2) +} + +func TestSandboxTemplate_ListFiltersByLabelSelector(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + _, _ = tc.Create(ctx, "default", &types.SandboxWorkloadTemplate{ + Name: "runtime-template", + Labels: map[string]string{"team": "runtime"}, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"}, + }, + }) + _, _ = tc.Create(ctx, "default", &types.SandboxWorkloadTemplate{ + Name: "batch-template", + Labels: map[string]string{"team": "batch"}, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"}, + }, + }) + + listed, err := tc.List(ctx, "default", types.ListOptions{LabelSelector: "team=runtime"}) + + require.NoError(t, err) + require.Len(t, listed, 1) + assert.Equal(t, "runtime-template", listed[0].Name) +} + +func TestSandboxTemplate_ListRejectsNegativePagination(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + _, err := tc.List(ctx, "default", types.ListOptions{Limit: -1}) + require.Error(t, err) + assert.True(t, types.IsInvalidArgument(err)) + + _, err = tc.List(ctx, "default", types.ListOptions{Offset: -1}) + require.Error(t, err) + assert.True(t, types.IsInvalidArgument(err)) +} + +func TestSandboxTemplate_ListAppliesPaginationAfterFiltering(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + _, _ = tc.Create(ctx, "default", &types.SandboxWorkloadTemplate{ + Name: "runtime-a", + Labels: map[string]string{"team": "runtime"}, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"}, + }, + }) + _, _ = tc.Create(ctx, "default", &types.SandboxWorkloadTemplate{ + Name: "batch-a", + Labels: map[string]string{"team": "batch"}, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"}, + }, + }) + _, _ = tc.Create(ctx, "default", &types.SandboxWorkloadTemplate{ + Name: "runtime-b", + Labels: map[string]string{"team": "runtime"}, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"}, + }, + }) + + listed, err := tc.List(ctx, "default", types.ListOptions{ + LabelSelector: "team=runtime", + Offset: 1, + Limit: 1, + }) + + require.NoError(t, err) + require.Len(t, listed, 1) + assert.Equal(t, "runtime-b", listed[0].Name) + + listed, err = tc.List(ctx, "default", types.ListOptions{ + LabelSelector: "team=runtime", + Offset: 2, + }) + + require.NoError(t, err) + assert.Empty(t, listed) +} + +func TestSandboxTemplate_CreateSandboxFromTemplateRequiresExistingTemplate(t *testing.T) { + client := NewClient() + ctx := context.Background() + + _, err := client.CreateSandboxFromTemplate(ctx, "default", "job-1", "missing", nil, nil) + + require.Error(t, err) + assert.True(t, types.IsNotFound(err)) +} + +func TestSandboxTemplate_CreateSandboxFromTemplateResolvesWorkloadAndGovernance(t *testing.T) { + client := NewClient() + ctx := context.Background() + gpuCount := uint32(1) + policy := &types.SandboxPolicy{ + Version: 1, + NetworkPolicies: map[string]types.NetworkPolicyRule{ + "api": {Name: "api"}, + }, + } + client.AddSandboxTemplate("default", &types.SandboxWorkloadTemplate{ + Name: "gpu-kata", + ResourceVersion: 7, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{ + Image: "registry.example.com/agent:latest", + Environment: map[string]string{"FEATURE_FLAG": "on"}, + Resources: &types.SandboxResources{ + CPU: "2", + Memory: "4Gi", + GPU: &types.SandboxGPURequirements{Count: &gpuCount}, + }, + }, + DriverConfig: map[string]any{ + "kubernetes": map[string]any{"runtime_class_name": "kata-containers"}, + }, + }, + }) + + created, err := client.CreateSandboxFromTemplate( + ctx, + "default", + "job-1", + "gpu-kata", + &types.SandboxSpec{ + Providers: []string{"github"}, + Policy: policy, + }, + map[string]string{"team": "runtime"}, + ) + + require.NoError(t, err) + assert.Equal(t, "job-1", created.Name) + assert.Equal(t, map[string]string{"team": "runtime"}, created.Labels) + assert.Equal(t, map[string]string{"FEATURE_FLAG": "on"}, created.Spec.Environment) + require.NotNil(t, created.Spec.Template) + assert.Equal(t, "registry.example.com/agent:latest", created.Spec.Template.Image) + assert.Equal(t, map[string]any{"limits": map[string]any{"cpu": "2", "memory": "4Gi"}}, created.Spec.Template.Resources) + assert.Equal(t, "kata-containers", created.Spec.Template.DriverConfig["kubernetes"].(map[string]any)["runtime_class_name"]) + assert.True(t, created.Spec.GPU) + require.NotNil(t, created.Spec.GPUCount) + assert.Equal(t, uint32(1), *created.Spec.GPUCount) + assert.Equal(t, []string{"github"}, created.Spec.Providers) + require.NotNil(t, created.Spec.Policy) + assert.Equal(t, uint32(1), created.Spec.Policy.Version) + require.NotNil(t, created.CreatedFromWorkloadTemplate) + assert.Equal(t, "gpu-kata", created.CreatedFromWorkloadTemplate.Name) + assert.Equal(t, "7", created.CreatedFromWorkloadTemplate.ResourceVersion) +} + +func TestSandboxTemplate_CreateSandboxFromTemplateRejectsWorkloadOverrides(t *testing.T) { + client := NewClient() + ctx := context.Background() + gpuCount := uint32(1) + client.AddSandboxTemplate("default", &types.SandboxWorkloadTemplate{ + Name: "gpu-kata", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "registry.example.com/agent:latest"}, + }, + }) + + tests := map[string]*types.SandboxSpec{ + "log_level": { + LogLevel: "debug", + }, + "environment": { + Environment: map[string]string{"FEATURE_FLAG": "off"}, + }, + "template": { + Template: &types.SandboxTemplate{Image: "registry.example.com/override:latest"}, + }, + "gpu_count": { + GPUCount: &gpuCount, + }, + "gpu": { + GPU: true, + }, + } + + for name, spec := range tests { + t.Run(name, func(t *testing.T) { + _, err := client.CreateSandboxFromTemplate(ctx, "default", "job-"+name, "gpu-kata", spec, nil) + + require.Error(t, err) + assert.True(t, types.IsInvalidArgument(err)) + }) + } + + listed, err := client.Sandboxes().List(ctx, "default") + require.NoError(t, err) + assert.Empty(t, listed) +} + +func TestSandboxTemplate_DefaultGpuRequestRoundTripsTemplate(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + created, err := tc.Create(ctx, "default", &types.SandboxWorkloadTemplate{ + Name: "default-gpu", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{ + Resources: &types.SandboxResources{ + GPU: &types.SandboxGPURequirements{}, + }, + }, + }, + }) + + require.NoError(t, err) + require.NotNil(t, created.Spec.Workload.Resources.GPU) + assert.Nil(t, created.Spec.Workload.Resources.GPU.Count) + + got, err := tc.Get(ctx, "default", "default-gpu") + require.NoError(t, err) + require.NotNil(t, got.Spec.Workload.Resources.GPU) + assert.Nil(t, got.Spec.Workload.Resources.GPU.Count) +} + +func TestSandboxTemplate_CreateSandboxFromTemplatePreservesDefaultGPURequest(t *testing.T) { + client := NewClient() + ctx := context.Background() + + client.AddSandboxTemplate("default", &types.SandboxWorkloadTemplate{ + Name: "default-gpu", + ResourceVersion: 3, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{ + Image: "registry.example.com/agent:latest", + Resources: &types.SandboxResources{ + GPU: &types.SandboxGPURequirements{}, + }, + }, + }, + }) + + created, err := client.CreateSandboxFromTemplate( + ctx, + "default", + "job-default-gpu", + "default-gpu", + nil, + nil, + ) + + require.NoError(t, err) + assert.True(t, created.Spec.GPU) + assert.Nil(t, created.Spec.GPUCount) +} + +func TestSandboxTemplate_DeepCopy(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + template := &types.SandboxWorkloadTemplate{ + Name: "gpu-kata", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{ + Image: "python:3.12", + Environment: map[string]string{"KEY": "value"}, + }, + DriverConfig: map[string]any{"kubernetes": map[string]any{"runtime_class_name": "kata"}}, + }, + } + + created, err := tc.Create(ctx, "default", template) + require.NoError(t, err) + + template.Spec.Workload.Image = "mutated" + template.Spec.Workload.Environment["KEY"] = "mutated" + template.Spec.DriverConfig["kubernetes"].(map[string]any)["runtime_class_name"] = "mutated" + created.Spec.Workload.Image = "mutated-return" + + got, err := tc.Get(ctx, "default", "gpu-kata") + require.NoError(t, err) + assert.Equal(t, "python:3.12", got.Spec.Workload.Image) + assert.Equal(t, "value", got.Spec.Workload.Environment["KEY"]) + assert.Equal(t, "kata", got.Spec.DriverConfig["kubernetes"].(map[string]any)["runtime_class_name"]) +} + +func TestSandboxTemplate_CreateNil(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + + _, err := tc.Create(ctx, "default", nil) + require.Error(t, err) + assert.True(t, types.IsInvalidArgument(err)) +} + +func TestSandboxTemplate_CreateRejectsInvalidTemplate(t *testing.T) { + tc := newTestSandboxTemplateClient() + ctx := context.Background() + zeroGPUCount := uint32(0) + + tests := map[string]*types.SandboxWorkloadTemplate{ + "missing_name": { + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "registry.example.com/agent:latest"}, + }, + }, + "invalid_name": { + Name: "Invalid_Template_Name", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{Image: "registry.example.com/agent:latest"}, + }, + }, + "missing_workload": { + Name: "missing-workload", + }, + "invalid_environment": { + Name: "invalid-env", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{ + Image: "registry.example.com/agent:latest", + Environment: map[string]string{"BAD KEY": "value"}, + }, + }, + }, + "reserved_environment": { + Name: "reserved-env", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{ + Image: "registry.example.com/agent:latest", + Environment: map[string]string{"OPENSHELL_TOKEN": "value"}, + }, + }, + }, + "zero_gpu": { + Name: "zero-gpu", + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{ + Image: "registry.example.com/agent:latest", + Resources: &types.SandboxResources{ + GPU: &types.SandboxGPURequirements{Count: &zeroGPUCount}, + }, + }, + }, + }, + } + + for name, template := range tests { + t.Run(name, func(t *testing.T) { + _, err := tc.Create(ctx, "default", template) + + require.Error(t, err) + assert.True(t, types.IsInvalidArgument(err)) + }) + } + + listed, err := tc.List(ctx, "default") + require.NoError(t, err) + assert.Empty(t, listed) +} + +func TestSandboxTemplate_ClosedReturnsUnavailable(t *testing.T) { + store := newobjectStore(sandboxWorkloadTemplateName, copySandboxWorkloadTemplate) + tc := newFakeSandboxTemplateClient(store, func() bool { return true }) + ctx := context.Background() + + _, err := tc.Create(ctx, "default", &types.SandboxWorkloadTemplate{Name: "gpu-kata"}) + require.Error(t, err) + assert.True(t, types.IsUnavailable(err)) +} diff --git a/sdk/go/openshell/v1/fake/sandbox_test.go b/sdk/go/openshell/v1/fake/sandbox_test.go index b675621891..75ff32da2d 100644 --- a/sdk/go/openshell/v1/fake/sandbox_test.go +++ b/sdk/go/openshell/v1/fake/sandbox_test.go @@ -20,8 +20,9 @@ import ( // helper to build a minimal fake sandbox client for testing. func newTestSandboxClient() *fakeSandboxClient { store := newobjectStore(sandboxName, copySandbox) + templateStore := newobjectStore(sandboxWorkloadTemplateName, copySandboxWorkloadTemplate) broadcaster := newWatchBroadcaster[*types.Sandbox]() - return newFakeSandboxClient(store, broadcaster, func() bool { return false }) + return newFakeSandboxClient(store, templateStore, broadcaster, func() bool { return false }) } // --- T008: Sandbox CRUD tests --- @@ -913,6 +914,41 @@ func TestFakeSandboxCreateWithNilPolicy(t *testing.T) { assert.Nil(t, got.Spec.Policy) } +func TestFakeSandboxCreateFromTemplatePreservesCommandAndTTY(t *testing.T) { + sc := newTestSandboxClient() + ctx := context.Background() + sc.templateStore.Insert("default", &types.SandboxWorkloadTemplate{ + Name: "gpu-kata", + ResourceVersion: 7, + Spec: types.SandboxWorkloadTemplateSpec{ + Workload: &types.SandboxWorkloadConfig{ + Image: "registry.example.com/agent:latest", + }, + }, + }) + spec := &types.SandboxSpec{ + Providers: []string{"github"}, + Command: []string{"/opt/worker", "--serve"}, + TTY: true, + } + + created, err := sc.CreateFromTemplate(ctx, "default", "job-1", "gpu-kata", spec, map[string]string{"team": "runtime"}) + + require.NoError(t, err) + assert.Equal(t, []string{"/opt/worker", "--serve"}, created.Spec.Command) + assert.True(t, created.Spec.TTY) + assert.Equal(t, []string{"github"}, created.Spec.Providers) + require.NotNil(t, created.CreatedFromWorkloadTemplate) + assert.Equal(t, "gpu-kata", created.CreatedFromWorkloadTemplate.Name) + assert.Equal(t, "7", created.CreatedFromWorkloadTemplate.ResourceVersion) + + spec.Command[0] = "mutated" + got, err := sc.Get(ctx, "default", "job-1") + require.NoError(t, err) + assert.Equal(t, []string{"/opt/worker", "--serve"}, got.Spec.Command) + assert.True(t, got.Spec.TTY) +} + // --- T032: GetLogs stub tests --- func TestSandbox_GetLogs_ReturnsUnimplemented(t *testing.T) { @@ -924,8 +960,9 @@ func TestSandbox_GetLogs_ReturnsUnimplemented(t *testing.T) { func TestSandbox_GetLogs_ClosedReturnsUnavailable(t *testing.T) { store := newobjectStore(sandboxName, copySandbox) + templateStore := newobjectStore(sandboxWorkloadTemplateName, copySandboxWorkloadTemplate) broadcaster := newWatchBroadcaster[*types.Sandbox]() - sc := newFakeSandboxClient(store, broadcaster, func() bool { return true }) + sc := newFakeSandboxClient(store, templateStore, broadcaster, func() bool { return true }) _, err := sc.GetLogs(context.Background(), "default", "sb-1") require.Error(t, err) assert.True(t, types.IsUnavailable(err)) diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index ea9add6d21..ad20aca260 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -52,6 +52,62 @@ func TestConverterCoversAllProtoFields_SandboxTemplate(t *testing.T) { assertAllFieldsCovered(t, (&pb.SandboxTemplate{}).ProtoReflect().Descriptor(), handled, nil) } +func TestConverterCoversAllProtoFields_SandboxWorkloadTemplate(t *testing.T) { + handled := fieldSet{ + "metadata": true, + "spec": true, + } + + assertAllFieldsCovered(t, (&pb.SandboxWorkloadTemplate{}).ProtoReflect().Descriptor(), handled, nil) +} + +func TestConverterCoversAllProtoFields_SandboxWorkloadTemplateSpec(t *testing.T) { + handled := fieldSet{ + "workload": true, + "driver_config": true, + "desired_service_level": true, + } + + assertAllFieldsCovered(t, (&pb.SandboxWorkloadTemplateSpec{}).ProtoReflect().Descriptor(), handled, nil) +} + +func TestConverterCoversAllProtoFields_SandboxWorkloadConfig(t *testing.T) { + handled := fieldSet{ + "image": true, + "environment": true, + "resources": true, + } + + assertAllFieldsCovered(t, (&pb.SandboxWorkloadConfig{}).ProtoReflect().Descriptor(), handled, nil) +} + +func TestConverterCoversAllProtoFields_SandboxResources(t *testing.T) { + handled := fieldSet{ + "cpu": true, + "memory": true, + "gpu": true, + } + + assertAllFieldsCovered(t, (&pb.SandboxResources{}).ProtoReflect().Descriptor(), handled, nil) +} + +func TestConverterCoversAllProtoFields_SandboxServiceLevel(t *testing.T) { + handled := fieldSet{ + "startup": true, + } + + assertAllFieldsCovered(t, (&pb.SandboxServiceLevel{}).ProtoReflect().Descriptor(), handled, nil) +} + +func TestConverterCoversAllProtoFields_SandboxStartup(t *testing.T) { + handled := fieldSet{ + "ready_within": true, + "max_burst": true, + } + + assertAllFieldsCovered(t, (&pb.SandboxStartup{}).ProtoReflect().Descriptor(), handled, nil) +} + func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) { handled := fieldSet{ "sandbox_name": true, diff --git a/sdk/go/openshell/v1/internal/converter/sandbox.go b/sdk/go/openshell/v1/internal/converter/sandbox.go index 6b61053295..bf15f11059 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox.go @@ -5,10 +5,12 @@ package converter import ( "fmt" + "time" "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" ) @@ -31,6 +33,13 @@ func SandboxFromProto(s *pb.Sandbox) *types.Sandbox { result.DeletionTimestamp = TimeFromMillisPtr(m.GetDeletionTimestampMs()) } + if provenance := s.GetCreatedFromWorkloadTemplate(); provenance != nil { + result.CreatedFromWorkloadTemplate = &types.SandboxWorkloadTemplateProvenance{ + Name: provenance.GetName(), + ResourceVersion: provenance.GetResourceVersion(), + } + } + if spec := s.GetSpec(); spec != nil { result.Spec = sandboxSpecFromProto(spec) } @@ -72,8 +81,11 @@ func sandboxSpecFromProto(spec *pb.SandboxSpec) types.SandboxSpec { } if rr := spec.GetResourceRequirements(); rr != nil { - if gpu := rr.GetGpu(); gpu != nil && gpu.Count != nil { - result.GPUCount = gpu.Count + if gpu := rr.GetGpu(); gpu != nil { + result.GPU = true + if gpu.Count != nil { + result.GPUCount = gpu.Count + } } } result.Command = CopyStringSlice(spec.GetCommand()) @@ -219,7 +231,7 @@ func SandboxSpecToProto(spec *types.SandboxSpec) *pb.SandboxSpec { result.Template = tmpl } - if spec.GPUCount != nil { + if spec.GPU || spec.GPUCount != nil { result.ResourceRequirements = &pb.ResourceRequirements{ Gpu: &pb.GpuResourceRequirements{ Count: spec.GPUCount, @@ -264,3 +276,221 @@ func SandboxSpecToProtoChecked(spec *types.SandboxSpec) (*pb.SandboxSpec, error) } return result, nil } + +// SandboxWorkloadTemplateFromProto converts a reusable template proto to an SDK template. +func SandboxWorkloadTemplateFromProto(t *pb.SandboxWorkloadTemplate) *types.SandboxWorkloadTemplate { + if t == nil { + return nil + } + + result := &types.SandboxWorkloadTemplate{} + if m := t.GetMetadata(); m != nil { + result.ID = m.GetId() + result.Name = m.GetName() + result.CreatedAt = TimeFromMillis(m.GetCreatedAtMs()) + result.Labels = CopyStringMap(m.GetLabels()) + result.Annotations = CopyStringMap(m.GetAnnotations()) + result.ResourceVersion = m.GetResourceVersion() + result.Workspace = m.GetWorkspace() + result.DeletionTimestamp = TimeFromMillisPtr(m.GetDeletionTimestampMs()) + } + if spec := t.GetSpec(); spec != nil { + result.Spec = SandboxWorkloadTemplateSpecFromProto(spec) + } + return result +} + +// SandboxWorkloadTemplateSpecFromProto converts a reusable template spec proto. +func SandboxWorkloadTemplateSpecFromProto(spec *pb.SandboxWorkloadTemplateSpec) types.SandboxWorkloadTemplateSpec { + result := types.SandboxWorkloadTemplateSpec{} + if spec == nil { + return result + } + result.Workload = SandboxWorkloadConfigFromProto(spec.GetWorkload()) + result.DesiredServiceLevel = SandboxServiceLevelFromProto(spec.GetDesiredServiceLevel()) + if dc := spec.GetDriverConfig(); dc != nil { + result.DriverConfig = dc.AsMap() + } + return result +} + +// SandboxWorkloadConfigFromProto converts a portable workload proto. +func SandboxWorkloadConfigFromProto(workload *pb.SandboxWorkloadConfig) *types.SandboxWorkloadConfig { + if workload == nil { + return nil + } + return &types.SandboxWorkloadConfig{ + Image: workload.GetImage(), + Environment: CopyStringMap(workload.GetEnvironment()), + Resources: SandboxResourcesFromProto(workload.GetResources()), + } +} + +// SandboxResourcesFromProto converts portable resource requirements. +func SandboxResourcesFromProto(resources *pb.SandboxResources) *types.SandboxResources { + if resources == nil { + return nil + } + return &types.SandboxResources{ + CPU: resources.GetCpu(), + Memory: resources.GetMemory(), + GPU: sandboxResourceGpuFromProto(resources), + } +} + +// SandboxServiceLevelFromProto converts template service-level hints. +func SandboxServiceLevelFromProto(level *pb.SandboxServiceLevel) *types.SandboxServiceLevel { + if level == nil { + return nil + } + return &types.SandboxServiceLevel{ + Startup: SandboxStartupFromProto(level.GetStartup()), + } +} + +// SandboxStartupFromProto converts startup service-level hints. +func SandboxStartupFromProto(startup *pb.SandboxStartup) *types.SandboxStartup { + if startup == nil { + return nil + } + return &types.SandboxStartup{ + ReadyWithin: durationFromProto(startup.GetReadyWithin()), + MaxBurst: startup.GetMaxBurst(), + } +} + +// SandboxWorkloadTemplateToProto converts an SDK reusable template to proto. +func SandboxWorkloadTemplateToProto(t *types.SandboxWorkloadTemplate) *pb.SandboxWorkloadTemplate { + if t == nil { + return nil + } + return &pb.SandboxWorkloadTemplate{ + Metadata: &dm.ObjectMeta{ + Id: t.ID, + Name: t.Name, + CreatedAtMs: MillisFromTime(t.CreatedAt), + Labels: CopyStringMap(t.Labels), + Annotations: CopyStringMap(t.Annotations), + ResourceVersion: t.ResourceVersion, + Workspace: t.Workspace, + DeletionTimestampMs: MillisFromTimePtr(t.DeletionTimestamp), + }, + Spec: SandboxWorkloadTemplateSpecToProto(&t.Spec), + } +} + +// SandboxWorkloadTemplateSpecToProto converts an SDK reusable template spec to proto. +func SandboxWorkloadTemplateSpecToProto(spec *types.SandboxWorkloadTemplateSpec) *pb.SandboxWorkloadTemplateSpec { + if spec == nil { + return nil + } + result := &pb.SandboxWorkloadTemplateSpec{ + Workload: SandboxWorkloadConfigToProto(spec.Workload), + DesiredServiceLevel: SandboxServiceLevelToProto(spec.DesiredServiceLevel), + } + if spec.DriverConfig != nil { + if driverConfig, err := structpb.NewStruct(spec.DriverConfig); err == nil { + result.DriverConfig = driverConfig + } + } + return result +} + +// SandboxWorkloadConfigToProto converts an SDK portable workload to proto. +func SandboxWorkloadConfigToProto(workload *types.SandboxWorkloadConfig) *pb.SandboxWorkloadConfig { + if workload == nil { + return nil + } + return &pb.SandboxWorkloadConfig{ + Image: workload.Image, + Environment: CopyStringMap(workload.Environment), + Resources: SandboxResourcesToProto(workload.Resources), + } +} + +// SandboxResourcesToProto converts portable resource requirements. +func SandboxResourcesToProto(resources *types.SandboxResources) *pb.SandboxResources { + if resources == nil { + return nil + } + return &pb.SandboxResources{ + Cpu: resources.CPU, + Memory: resources.Memory, + Gpu: sandboxResourceGpuToProto(resources), + } +} + +func sandboxResourceGpuToProto(resources *types.SandboxResources) *pb.GpuResourceRequirements { + if resources == nil || resources.GPU == nil { + return nil + } + return &pb.GpuResourceRequirements{Count: CopyUint32Ptr(resources.GPU.Count)} +} + +func sandboxResourceGpuFromProto(resources *pb.SandboxResources) *types.SandboxGPURequirements { + if resources == nil || resources.GetGpu() == nil { + return nil + } + return &types.SandboxGPURequirements{Count: CopyUint32Ptr(resources.GetGpu().Count)} +} + +// SandboxServiceLevelToProto converts template service-level hints. +func SandboxServiceLevelToProto(level *types.SandboxServiceLevel) *pb.SandboxServiceLevel { + if level == nil { + return nil + } + return &pb.SandboxServiceLevel{ + Startup: SandboxStartupToProto(level.Startup), + } +} + +// SandboxStartupToProto converts startup service-level hints. +func SandboxStartupToProto(startup *types.SandboxStartup) *pb.SandboxStartup { + if startup == nil { + return nil + } + return &pb.SandboxStartup{ + ReadyWithin: durationToProto(startup.ReadyWithin), + MaxBurst: startup.MaxBurst, + } +} + +// SandboxWorkloadTemplateToProtoChecked converts an SDK reusable template and +// reports driver config values that protobuf Struct cannot represent. +func SandboxWorkloadTemplateToProtoChecked(t *types.SandboxWorkloadTemplate) (*pb.SandboxWorkloadTemplate, error) { + result := SandboxWorkloadTemplateToProto(t) + if t == nil { + return result, nil + } + if t.Spec.DriverConfig != nil { + driverConfig, err := structpb.NewStruct(t.Spec.DriverConfig) + if err != nil { + return nil, fmt.Errorf("driver config: %w", err) + } + result.Spec.DriverConfig = driverConfig + } + return result, nil +} + +// CopyUint32Ptr returns a copy of a *uint32 pointer. +func CopyUint32Ptr(p *uint32) *uint32 { + if p == nil { + return nil + } + v := *p + return &v +} + +func durationFromProto(d *durationpb.Duration) time.Duration { + if d == nil { + return 0 + } + return d.AsDuration() +} + +func durationToProto(d time.Duration) *durationpb.Duration { + if d == 0 { + return nil + } + return durationpb.New(d) +} diff --git a/sdk/go/openshell/v1/internal/converter/sandbox_test.go b/sdk/go/openshell/v1/internal/converter/sandbox_test.go index 9f68013845..e1687464b5 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox_test.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" ) @@ -60,6 +61,10 @@ func TestSandboxFromProto(t *testing.T) { Command: []string{"/opt/agent", "--serve"}, Tty: false, }, + CreatedFromWorkloadTemplate: &pb.SandboxWorkloadTemplateProvenance{ + Name: "gpu-kata", + ResourceVersion: "7", + }, Status: &pb.SandboxStatus{ SandboxName: "sb-compute-1", AgentPod: "agent-pod-xyz", @@ -93,11 +98,15 @@ func TestSandboxFromProto(t *testing.T) { assert.Equal(t, "prod", s.Workspace) require.NotNil(t, s.DeletionTimestamp) assert.Equal(t, time.UnixMilli(1700000060000).UTC(), *s.DeletionTimestamp) + require.NotNil(t, s.CreatedFromWorkloadTemplate) + assert.Equal(t, "gpu-kata", s.CreatedFromWorkloadTemplate.Name) + assert.Equal(t, "7", s.CreatedFromWorkloadTemplate.ResourceVersion) // Spec assert.Equal(t, "debug", s.Spec.LogLevel) assert.Equal(t, map[string]string{"FOO": "bar"}, s.Spec.Environment) assert.Equal(t, []string{"claude", "github"}, s.Spec.Providers) + assert.True(t, s.Spec.GPU) require.NotNil(t, s.Spec.GPUCount) assert.Equal(t, uint32(2), *s.Spec.GPUCount) assert.Equal(t, []string{"/opt/agent", "--serve"}, s.Spec.Command) @@ -173,10 +182,27 @@ func TestSandboxFromProto_NilFields(t *testing.T) { assert.Empty(t, s.Name) assert.True(t, s.CreatedAt.IsZero()) assert.Nil(t, s.Spec.Template) + assert.False(t, s.Spec.GPU) assert.Nil(t, s.Spec.GPUCount) assert.Equal(t, v1.SandboxUnknown, s.Status.Phase) } +func TestSandboxFromProto_DefaultGPURequest(t *testing.T) { + proto := &pb.Sandbox{ + Spec: &pb.SandboxSpec{ + ResourceRequirements: &pb.ResourceRequirements{ + Gpu: &pb.GpuResourceRequirements{}, + }, + }, + } + + s := SandboxFromProto(proto) + + require.NotNil(t, s) + assert.True(t, s.Spec.GPU) + assert.Nil(t, s.Spec.GPUCount) +} + func TestSandboxFromProto_Nil(t *testing.T) { s := SandboxFromProto(nil) assert.Nil(t, s) @@ -314,6 +340,137 @@ func TestSandboxToProto_NilTemplate(t *testing.T) { assert.Nil(t, p.Spec.ResourceRequirements) } +func TestSandboxToProto_DefaultGPURequest(t *testing.T) { + s := &v1.Sandbox{ + Spec: v1.SandboxSpec{ + GPU: true, + }, + } + + p := SandboxToProto(s) + + require.NotNil(t, p) + require.NotNil(t, p.Spec) + require.NotNil(t, p.Spec.ResourceRequirements) + require.NotNil(t, p.Spec.ResourceRequirements.Gpu) + assert.Nil(t, p.Spec.ResourceRequirements.Gpu.Count) +} + +func TestSandboxWorkloadTemplateRoundTrip(t *testing.T) { + gpuCount := uint32(2) + delTime := time.UnixMilli(1700000060000).UTC() + original := &v1.SandboxWorkloadTemplate{ + ID: "tmpl-1", + Name: "gpu-kata", + CreatedAt: time.UnixMilli(1700000000000).UTC(), + Labels: map[string]string{"team": "platform"}, + Annotations: map[string]string{"note": "fast-start"}, + ResourceVersion: 9, + Workspace: "prod", + DeletionTimestamp: &delTime, + Spec: v1.SandboxWorkloadTemplateSpec{ + Workload: &v1.SandboxWorkloadConfig{ + Image: "nvcr.io/nvidia/openshell:latest", + Environment: map[string]string{"CUDA_VISIBLE_DEVICES": "all"}, + Resources: &v1.SandboxResources{ + CPU: "2", + Memory: "8Gi", + GPU: &v1.SandboxGPURequirements{Count: &gpuCount}, + }, + }, + DriverConfig: map[string]any{ + "kubernetes": map[string]any{"runtimeClassName": "kata"}, + }, + DesiredServiceLevel: &v1.SandboxServiceLevel{ + Startup: &v1.SandboxStartup{ + ReadyWithin: 30 * time.Second, + MaxBurst: 3, + }, + }, + }, + } + + protoTemplate, err := SandboxWorkloadTemplateToProtoChecked(original) + require.NoError(t, err) + + require.NotNil(t, protoTemplate.Metadata) + assert.Equal(t, "gpu-kata", protoTemplate.Metadata.Name) + require.NotNil(t, protoTemplate.Spec) + require.NotNil(t, protoTemplate.Spec.Workload) + assert.Equal(t, "nvcr.io/nvidia/openshell:latest", protoTemplate.Spec.Workload.Image) + assert.Equal(t, map[string]string{"CUDA_VISIBLE_DEVICES": "all"}, protoTemplate.Spec.Workload.Environment) + require.NotNil(t, protoTemplate.Spec.Workload.Resources) + assert.Equal(t, "2", protoTemplate.Spec.Workload.Resources.Cpu) + assert.Equal(t, "8Gi", protoTemplate.Spec.Workload.Resources.Memory) + require.NotNil(t, protoTemplate.Spec.Workload.Resources.Gpu) + require.NotNil(t, protoTemplate.Spec.Workload.Resources.Gpu.Count) + assert.Equal(t, uint32(2), *protoTemplate.Spec.Workload.Resources.Gpu.Count) + require.NotNil(t, protoTemplate.Spec.DriverConfig) + require.NotNil(t, protoTemplate.Spec.DesiredServiceLevel) + require.NotNil(t, protoTemplate.Spec.DesiredServiceLevel.Startup) + assert.Equal(t, durationpb.New(30*time.Second), protoTemplate.Spec.DesiredServiceLevel.Startup.ReadyWithin) + assert.Equal(t, uint32(3), protoTemplate.Spec.DesiredServiceLevel.Startup.MaxBurst) + + back := SandboxWorkloadTemplateFromProto(protoTemplate) + require.NotNil(t, back) + assert.Equal(t, original.ID, back.ID) + assert.Equal(t, original.Name, back.Name) + assert.Equal(t, original.CreatedAt, back.CreatedAt) + assert.Equal(t, original.Labels, back.Labels) + assert.Equal(t, original.Annotations, back.Annotations) + assert.Equal(t, original.ResourceVersion, back.ResourceVersion) + assert.Equal(t, original.Workspace, back.Workspace) + require.NotNil(t, back.DeletionTimestamp) + assert.Equal(t, *original.DeletionTimestamp, *back.DeletionTimestamp) + require.NotNil(t, back.Spec.Workload) + assert.Equal(t, original.Spec.Workload.Image, back.Spec.Workload.Image) + assert.Equal(t, original.Spec.Workload.Environment, back.Spec.Workload.Environment) + require.NotNil(t, back.Spec.Workload.Resources) + assert.Equal(t, original.Spec.Workload.Resources.CPU, back.Spec.Workload.Resources.CPU) + assert.Equal(t, original.Spec.Workload.Resources.Memory, back.Spec.Workload.Resources.Memory) + require.NotNil(t, back.Spec.Workload.Resources.GPU) + require.NotNil(t, back.Spec.Workload.Resources.GPU.Count) + assert.Equal(t, *original.Spec.Workload.Resources.GPU.Count, *back.Spec.Workload.Resources.GPU.Count) + assert.Equal(t, "kata", back.Spec.DriverConfig["kubernetes"].(map[string]any)["runtimeClassName"]) + require.NotNil(t, back.Spec.DesiredServiceLevel) + require.NotNil(t, back.Spec.DesiredServiceLevel.Startup) + assert.Equal(t, 30*time.Second, back.Spec.DesiredServiceLevel.Startup.ReadyWithin) + assert.Equal(t, uint32(3), back.Spec.DesiredServiceLevel.Startup.MaxBurst) +} + +func TestSandboxWorkloadTemplateRoundTrip_DefaultGpuRequest(t *testing.T) { + original := &v1.SandboxWorkloadTemplate{ + Name: "default-gpu", + Spec: v1.SandboxWorkloadTemplateSpec{ + Workload: &v1.SandboxWorkloadConfig{ + Resources: &v1.SandboxResources{ + GPU: &v1.SandboxGPURequirements{}, + }, + }, + }, + } + + protoTemplate, err := SandboxWorkloadTemplateToProtoChecked(original) + require.NoError(t, err) + require.NotNil(t, protoTemplate.GetSpec().GetWorkload().GetResources().GetGpu()) + assert.Nil(t, protoTemplate.GetSpec().GetWorkload().GetResources().GetGpu().Count) + + back := SandboxWorkloadTemplateFromProto(protoTemplate) + require.NotNil(t, back.Spec.Workload.Resources.GPU) + assert.Nil(t, back.Spec.Workload.Resources.GPU.Count) +} + +func TestSandboxWorkloadTemplateToProtoChecked_RejectsUnrepresentableDriverConfig(t *testing.T) { + _, err := SandboxWorkloadTemplateToProtoChecked(&v1.SandboxWorkloadTemplate{ + Name: "bad", + Spec: v1.SandboxWorkloadTemplateSpec{ + DriverConfig: map[string]any{"invalid": make(chan int)}, + }, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "driver config") +} + func TestSandboxRoundTrip(t *testing.T) { userNS := false gpuCount := uint32(1) @@ -384,6 +541,7 @@ func TestSandboxRoundTrip(t *testing.T) { assert.Equal(t, original.Spec.LogLevel, back.Spec.LogLevel) assert.Equal(t, original.Spec.Environment, back.Spec.Environment) assert.Equal(t, original.Spec.Providers, back.Spec.Providers) + assert.True(t, back.Spec.GPU) require.NotNil(t, back.Spec.GPUCount) assert.Equal(t, *original.Spec.GPUCount, *back.Spec.GPUCount) require.NotNil(t, back.Spec.Template) diff --git a/sdk/go/openshell/v1/sandbox.go b/sdk/go/openshell/v1/sandbox.go index 871bbb1bf3..79174ac7f3 100644 --- a/sdk/go/openshell/v1/sandbox.go +++ b/sdk/go/openshell/v1/sandbox.go @@ -67,3 +67,9 @@ type SandboxInterface interface { Watch(ctx context.Context, workspace, name string, opts ...WatchOptions) (WatchInterface[*Sandbox], error) GetLogs(ctx context.Context, workspace, sandboxName string, opts ...LogOption) (*LogResult, error) } + +// SandboxTemplateCreateInterface defines additive sandbox creation from named +// workload templates without widening SandboxInterface. +type SandboxTemplateCreateInterface interface { + CreateFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) +} diff --git a/sdk/go/openshell/v1/sandbox_client.go b/sdk/go/openshell/v1/sandbox_client.go index f31cd40af9..75d8d4caa3 100644 --- a/sdk/go/openshell/v1/sandbox_client.go +++ b/sdk/go/openshell/v1/sandbox_client.go @@ -21,6 +21,9 @@ type sandboxClient struct { client pb.OpenShellClient } +var _ SandboxInterface = (*sandboxClient)(nil) +var _ SandboxTemplateCreateInterface = (*sandboxClient)(nil) + func newSandboxClient(conn grpc.ClientConnInterface) *sandboxClient { return &sandboxClient{client: pb.NewOpenShellClient(conn)} } @@ -46,6 +49,44 @@ func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec return converter.SandboxFromProto(resp.GetSandbox()), nil } +func (s *sandboxClient) CreateFromTemplate(ctx context.Context, workspace, name, templateName string, spec *SandboxSpec, labels map[string]string, opts ...CreateOptions) (*Sandbox, error) { + if templateName == "" { + return nil, &StatusError{Code: ErrorInvalidArgument, Message: "template name is required"} + } + if err := validateTemplateCreateSpec(spec); err != nil { + return nil, err + } + protoSpec, err := converter.SandboxSpecToProtoChecked(spec) + if err != nil { + return nil, &StatusError{Code: ErrorInvalidArgument, Message: err.Error()} + } + req := &pb.CreateSandboxRequest{ + Name: name, + Spec: protoSpec, + Labels: labels, + Workspace: workspace, + WorkloadTemplateName: templateName, + } + if len(opts) > 0 { + req.Annotations = converter.CopyStringMap(opts[0].Annotations) + } + resp, err := s.client.CreateSandbox(ctx, req) + if err != nil { + return nil, converter.FromGRPCError(err) + } + return converter.SandboxFromProto(resp.GetSandbox()), nil +} + +func validateTemplateCreateSpec(spec *SandboxSpec) error { + if spec == nil { + return nil + } + if spec.LogLevel != "" || len(spec.Environment) > 0 || spec.Template != nil || spec.GPU || spec.GPUCount != nil { + return &StatusError{Code: ErrorInvalidArgument, Message: "template creates only allow policy, providers, command, and tty in spec"} + } + return nil +} + func (s *sandboxClient) Get(ctx context.Context, workspace, name string) (*Sandbox, error) { resp, err := s.client.GetSandbox(ctx, &pb.GetSandboxRequest{ Name: name, diff --git a/sdk/go/openshell/v1/sandbox_client_test.go b/sdk/go/openshell/v1/sandbox_client_test.go index 91e80db148..7926b28a16 100644 --- a/sdk/go/openshell/v1/sandbox_client_test.go +++ b/sdk/go/openshell/v1/sandbox_client_test.go @@ -34,6 +34,7 @@ type mockSandboxServer struct { attachErr error detachErr error listProvErr error + createRequest *pb.CreateSandboxRequest watchEvents []*pb.SandboxStreamEvent watchErr error watchPostEventsErr error @@ -56,6 +57,7 @@ func newMockSandboxServer() *mockSandboxServer { func (s *mockSandboxServer) CreateSandbox(_ context.Context, req *pb.CreateSandboxRequest) (*pb.SandboxResponse, error) { s.mu.Lock() defer s.mu.Unlock() + s.createRequest = proto.Clone(req).(*pb.CreateSandboxRequest) if s.createErr != nil { return nil, s.createErr } @@ -265,6 +267,29 @@ func TestSandboxCreate(t *testing.T) { assert.Equal(t, SandboxProvisioning, result.Status.Phase) } +func TestSandboxCreate_DefaultGPURequest(t *testing.T) { + mock := newMockSandboxServer() + client, cleanup := setupSandboxTest(t, mock) + defer cleanup() + + result, err := client.Create(context.Background(), "default", "gpu-sandbox", &SandboxSpec{ + GPU: true, + }, nil) + + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.Spec.GPU) + assert.Nil(t, result.Spec.GPUCount) + + mock.mu.Lock() + defer mock.mu.Unlock() + require.NotNil(t, mock.createRequest) + require.NotNil(t, mock.createRequest.Spec) + require.NotNil(t, mock.createRequest.Spec.ResourceRequirements) + require.NotNil(t, mock.createRequest.Spec.ResourceRequirements.Gpu) + assert.Nil(t, mock.createRequest.Spec.ResourceRequirements.Gpu.Count) +} + func TestSandboxCreate_RejectsUnrepresentableResourcesBeforeRPC(t *testing.T) { mock := newMockSandboxServer() client, cleanup := setupSandboxTest(t, mock) @@ -280,6 +305,47 @@ func TestSandboxCreate_RejectsUnrepresentableResourcesBeforeRPC(t *testing.T) { assert.Empty(t, mock.sandboxes) } +func TestSandboxCreateFromTemplateRejectsGPUOverrideBeforeRPC(t *testing.T) { + mock := newMockSandboxServer() + client, cleanup := setupSandboxTest(t, mock) + defer cleanup() + + _, err := client.CreateFromTemplate(context.Background(), "default", "bad", "gpu-kata", &SandboxSpec{ + GPU: true, + }, nil) + + require.Error(t, err) + assert.True(t, IsInvalidArgument(err)) + mock.mu.Lock() + defer mock.mu.Unlock() + assert.Nil(t, mock.createRequest) +} + +func TestSandboxCreateFromTemplateSendsCommandAndTTY(t *testing.T) { + mock := newMockSandboxServer() + client, cleanup := setupSandboxTest(t, mock) + defer cleanup() + + result, err := client.CreateFromTemplate(context.Background(), "default", "job-1", "gpu-kata", &SandboxSpec{ + Providers: []string{"github"}, + Command: []string{"/opt/worker", "--serve"}, + TTY: true, + }, map[string]string{"team": "runtime"}) + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, []string{"/opt/worker", "--serve"}, result.Spec.Command) + assert.True(t, result.Spec.TTY) + + mock.mu.Lock() + defer mock.mu.Unlock() + require.NotNil(t, mock.createRequest) + assert.Equal(t, "gpu-kata", mock.createRequest.GetWorkloadTemplateName()) + require.NotNil(t, mock.createRequest.GetSpec()) + assert.Equal(t, []string{"/opt/worker", "--serve"}, mock.createRequest.GetSpec().GetCommand()) + assert.True(t, mock.createRequest.GetSpec().GetTty()) +} + func TestSandboxCreate_AlreadyExists(t *testing.T) { mock := newMockSandboxServer() mock.createErr = status.Error(codes.AlreadyExists, "sandbox already exists") diff --git a/sdk/go/openshell/v1/sandbox_template.go b/sdk/go/openshell/v1/sandbox_template.go new file mode 100644 index 0000000000..e2f2d71874 --- /dev/null +++ b/sdk/go/openshell/v1/sandbox_template.go @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "context" + + "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" +) + +// SandboxWorkloadTemplate is a reusable workspace-scoped sandbox template resource. +type SandboxWorkloadTemplate = types.SandboxWorkloadTemplate + +// SandboxWorkloadTemplateSpec holds reusable sandbox template settings. +type SandboxWorkloadTemplateSpec = types.SandboxWorkloadTemplateSpec + +// SandboxWorkloadConfig defines the portable workload for a reusable template. +type SandboxWorkloadConfig = types.SandboxWorkloadConfig + +// SandboxResources defines portable sandbox resource requirements. +type SandboxResources = types.SandboxResources + +// SandboxGPURequirements defines template GPU requirements. +type SandboxGPURequirements = types.SandboxGPURequirements + +// SandboxServiceLevel describes desired operational characteristics. +type SandboxServiceLevel = types.SandboxServiceLevel + +// SandboxStartup describes desired startup characteristics. +type SandboxStartup = types.SandboxStartup + +// SandboxWorkloadTemplateProvenance identifies the reusable template revision used to create a sandbox. +type SandboxWorkloadTemplateProvenance = types.SandboxWorkloadTemplateProvenance + +// SandboxTemplateInterface defines CRUD operations on reusable sandbox templates. +// +// The resource type is named SandboxWorkloadTemplate in the v1 Go SDK so it does +// not collide with the legacy inline SandboxTemplate field on SandboxSpec. +type SandboxTemplateInterface interface { + Create(ctx context.Context, workspace string, template *SandboxWorkloadTemplate) (*SandboxWorkloadTemplate, error) + Get(ctx context.Context, workspace, name string) (*SandboxWorkloadTemplate, error) + List(ctx context.Context, workspace string, opts ...ListOptions) ([]*SandboxWorkloadTemplate, error) + Delete(ctx context.Context, workspace, name string) (bool, error) +} diff --git a/sdk/go/openshell/v1/sandbox_template_client.go b/sdk/go/openshell/v1/sandbox_template_client.go new file mode 100644 index 0000000000..de6e19237c --- /dev/null +++ b/sdk/go/openshell/v1/sandbox_template_client.go @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "context" + + "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/converter" + pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" + "google.golang.org/grpc" +) + +type sandboxTemplateClient struct { + client pb.OpenShellClient +} + +var _ SandboxTemplateInterface = (*sandboxTemplateClient)(nil) + +func newSandboxTemplateClient(conn grpc.ClientConnInterface) *sandboxTemplateClient { + return &sandboxTemplateClient{client: pb.NewOpenShellClient(conn)} +} + +func (s *sandboxTemplateClient) Create(ctx context.Context, workspace string, template *SandboxWorkloadTemplate) (*SandboxWorkloadTemplate, error) { + if template == nil { + return nil, &StatusError{Code: ErrorInvalidArgument, Message: "template must not be nil"} + } + protoTemplate, err := converter.SandboxWorkloadTemplateToProtoChecked(template) + if err != nil { + return nil, &StatusError{Code: ErrorInvalidArgument, Message: err.Error()} + } + resp, err := s.client.CreateSandboxTemplate(ctx, &pb.CreateSandboxTemplateRequest{ + Template: protoTemplate, + Workspace: workspace, + }) + if err != nil { + return nil, converter.FromGRPCError(err) + } + return converter.SandboxWorkloadTemplateFromProto(resp.GetTemplate()), nil +} + +func (s *sandboxTemplateClient) Get(ctx context.Context, workspace, name string) (*SandboxWorkloadTemplate, error) { + resp, err := s.client.GetSandboxTemplate(ctx, &pb.GetSandboxTemplateRequest{ + Name: name, + Workspace: workspace, + }) + if err != nil { + return nil, converter.FromGRPCError(err) + } + return converter.SandboxWorkloadTemplateFromProto(resp.GetTemplate()), nil +} + +func (s *sandboxTemplateClient) List(ctx context.Context, workspace string, opts ...ListOptions) ([]*SandboxWorkloadTemplate, error) { + req := &pb.ListSandboxTemplatesRequest{ + Workspace: workspace, + } + if len(opts) > 0 { + if opts[0].Limit < 0 { + return nil, &StatusError{Code: ErrorInvalidArgument, Message: "limit must not be negative"} + } + if opts[0].Offset < 0 { + return nil, &StatusError{Code: ErrorInvalidArgument, Message: "offset must not be negative"} + } + req.Limit = uint32(opts[0].Limit) + req.Offset = uint32(opts[0].Offset) + req.LabelSelector = opts[0].LabelSelector + req.AllWorkspaces = opts[0].AllWorkspaces + if req.AllWorkspaces { + req.Workspace = "" + } + } + + resp, err := s.client.ListSandboxTemplates(ctx, req) + if err != nil { + return nil, converter.FromGRPCError(err) + } + + templates := make([]*SandboxWorkloadTemplate, 0, len(resp.GetTemplates())) + for _, protoTemplate := range resp.GetTemplates() { + templates = append(templates, converter.SandboxWorkloadTemplateFromProto(protoTemplate)) + } + return templates, nil +} + +func (s *sandboxTemplateClient) Delete(ctx context.Context, workspace, name string) (bool, error) { + resp, err := s.client.DeleteSandboxTemplate(ctx, &pb.DeleteSandboxTemplateRequest{ + Name: name, + Workspace: workspace, + }) + if err != nil { + return false, converter.FromGRPCError(err) + } + return resp.GetDeleted(), nil +} diff --git a/sdk/go/openshell/v1/sandbox_template_client_test.go b/sdk/go/openshell/v1/sandbox_template_client_test.go new file mode 100644 index 0000000000..4f9e3058c0 --- /dev/null +++ b/sdk/go/openshell/v1/sandbox_template_client_test.go @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "context" + "net" + "sync" + "testing" + "time" + + dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1" + pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" +) + +type mockSandboxTemplateServer struct { + pb.UnimplementedOpenShellServer + mu sync.Mutex + + templates map[string]*pb.SandboxWorkloadTemplate + + createRequest *pb.CreateSandboxTemplateRequest + getRequest *pb.GetSandboxTemplateRequest + listRequest *pb.ListSandboxTemplatesRequest + deleteRequest *pb.DeleteSandboxTemplateRequest + + createErr error + getErr error + listErr error + deleteErr error +} + +func newMockSandboxTemplateServer() *mockSandboxTemplateServer { + return &mockSandboxTemplateServer{ + templates: make(map[string]*pb.SandboxWorkloadTemplate), + } +} + +func (s *mockSandboxTemplateServer) CreateSandboxTemplate(_ context.Context, req *pb.CreateSandboxTemplateRequest) (*pb.SandboxTemplateResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.createRequest = proto.Clone(req).(*pb.CreateSandboxTemplateRequest) + if s.createErr != nil { + return nil, s.createErr + } + template := proto.Clone(req.GetTemplate()).(*pb.SandboxWorkloadTemplate) + if template.Metadata == nil { + template.Metadata = &dm.ObjectMeta{} + } + template.Metadata.Workspace = req.GetWorkspace() + template.Metadata.ResourceVersion = 1 + s.templates[template.Metadata.GetName()] = template + return &pb.SandboxTemplateResponse{Template: proto.Clone(template).(*pb.SandboxWorkloadTemplate)}, nil +} + +func (s *mockSandboxTemplateServer) GetSandboxTemplate(_ context.Context, req *pb.GetSandboxTemplateRequest) (*pb.SandboxTemplateResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.getRequest = proto.Clone(req).(*pb.GetSandboxTemplateRequest) + if s.getErr != nil { + return nil, s.getErr + } + template, ok := s.templates[req.GetName()] + if !ok { + return nil, status.Errorf(codes.NotFound, "template %q not found", req.GetName()) + } + return &pb.SandboxTemplateResponse{Template: proto.Clone(template).(*pb.SandboxWorkloadTemplate)}, nil +} + +func (s *mockSandboxTemplateServer) ListSandboxTemplates(_ context.Context, req *pb.ListSandboxTemplatesRequest) (*pb.ListSandboxTemplatesResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.listRequest = proto.Clone(req).(*pb.ListSandboxTemplatesRequest) + if s.listErr != nil { + return nil, s.listErr + } + templates := make([]*pb.SandboxWorkloadTemplate, 0, len(s.templates)) + for _, template := range s.templates { + templates = append(templates, proto.Clone(template).(*pb.SandboxWorkloadTemplate)) + } + return &pb.ListSandboxTemplatesResponse{Templates: templates}, nil +} + +func (s *mockSandboxTemplateServer) DeleteSandboxTemplate(_ context.Context, req *pb.DeleteSandboxTemplateRequest) (*pb.DeleteSandboxTemplateResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.deleteRequest = proto.Clone(req).(*pb.DeleteSandboxTemplateRequest) + if s.deleteErr != nil { + return nil, s.deleteErr + } + delete(s.templates, req.GetName()) + return &pb.DeleteSandboxTemplateResponse{Deleted: true}, nil +} + +func setupSandboxTemplateTest(t *testing.T, mock *mockSandboxTemplateServer) (*sandboxTemplateClient, func()) { + t.Helper() + lis := bufconn.Listen(bufSize) + srv := grpc.NewServer() + pb.RegisterOpenShellServer(srv, mock) + go func() { _ = srv.Serve(lis) }() + + conn, err := grpc.NewClient("passthrough:///bufconn", + grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) { + return lis.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + require.NoError(t, err) + + return newSandboxTemplateClient(conn), func() { + _ = conn.Close() + srv.Stop() + } +} + +func TestSandboxTemplateCreate(t *testing.T) { + mock := newMockSandboxTemplateServer() + client, cleanup := setupSandboxTemplateTest(t, mock) + defer cleanup() + gpuCount := uint32(1) + + template, err := client.Create(context.Background(), "default", &SandboxWorkloadTemplate{ + Name: "gpu-kata", + Labels: map[string]string{ + "team": "platform", + }, + Spec: SandboxWorkloadTemplateSpec{ + Workload: &SandboxWorkloadConfig{ + Image: "nvcr.io/nvidia/openshell:latest", + Environment: map[string]string{"NVIDIA_VISIBLE_DEVICES": "all"}, + Resources: &SandboxResources{ + CPU: "2", + Memory: "8Gi", + GPU: &SandboxGPURequirements{Count: &gpuCount}, + }, + }, + DriverConfig: map[string]any{ + "kubernetes": map[string]any{"runtimeClassName": "kata"}, + }, + DesiredServiceLevel: &SandboxServiceLevel{ + Startup: &SandboxStartup{ + ReadyWithin: 45 * time.Second, + MaxBurst: 2, + }, + }, + }, + }) + + require.NoError(t, err) + require.NotNil(t, template) + assert.Equal(t, "gpu-kata", template.Name) + assert.Equal(t, "default", template.Workspace) + assert.Equal(t, uint64(1), template.ResourceVersion) + mock.mu.Lock() + defer mock.mu.Unlock() + require.NotNil(t, mock.createRequest) + assert.Equal(t, "default", mock.createRequest.Workspace) + assert.Equal(t, "gpu-kata", mock.createRequest.Template.Metadata.Name) + assert.Equal(t, "2", mock.createRequest.Template.Spec.Workload.Resources.Cpu) + assert.Equal(t, "8Gi", mock.createRequest.Template.Spec.Workload.Resources.Memory) + require.NotNil(t, mock.createRequest.Template.Spec.Workload.Resources.Gpu) + require.NotNil(t, mock.createRequest.Template.Spec.Workload.Resources.Gpu.Count) + assert.Equal(t, uint32(1), *mock.createRequest.Template.Spec.Workload.Resources.Gpu.Count) + assert.Equal(t, durationpb.New(45*time.Second), mock.createRequest.Template.Spec.DesiredServiceLevel.Startup.ReadyWithin) +} + +func TestSandboxTemplateCreate_RejectsNilTemplate(t *testing.T) { + mock := newMockSandboxTemplateServer() + client, cleanup := setupSandboxTemplateTest(t, mock) + defer cleanup() + + _, err := client.Create(context.Background(), "default", nil) + + require.Error(t, err) + assert.True(t, IsInvalidArgument(err)) +} + +func TestSandboxTemplateCreate_RejectsUnrepresentableDriverConfigBeforeRPC(t *testing.T) { + mock := newMockSandboxTemplateServer() + client, cleanup := setupSandboxTemplateTest(t, mock) + defer cleanup() + + _, err := client.Create(context.Background(), "default", &SandboxWorkloadTemplate{ + Name: "bad", + Spec: SandboxWorkloadTemplateSpec{ + DriverConfig: map[string]any{"invalid": make(chan int)}, + }, + }) + + require.Error(t, err) + assert.True(t, IsInvalidArgument(err)) + mock.mu.Lock() + defer mock.mu.Unlock() + assert.Nil(t, mock.createRequest) +} + +func TestSandboxTemplateGetListDelete(t *testing.T) { + mock := newMockSandboxTemplateServer() + mock.templates["gpu-kata"] = &pb.SandboxWorkloadTemplate{ + Metadata: &dm.ObjectMeta{Name: "gpu-kata", Workspace: "default"}, + Spec: &pb.SandboxWorkloadTemplateSpec{ + Workload: &pb.SandboxWorkloadConfig{Image: "img:v1"}, + }, + } + client, cleanup := setupSandboxTemplateTest(t, mock) + defer cleanup() + + got, err := client.Get(context.Background(), "default", "gpu-kata") + require.NoError(t, err) + assert.Equal(t, "gpu-kata", got.Name) + assert.Equal(t, "img:v1", got.Spec.Workload.Image) + + list, err := client.List(context.Background(), "default", ListOptions{ + Limit: 10, + Offset: 2, + LabelSelector: "team=runtime", + AllWorkspaces: true, + }) + require.NoError(t, err) + require.Len(t, list, 1) + assert.Equal(t, "gpu-kata", list[0].Name) + + deleted, err := client.Delete(context.Background(), "default", "gpu-kata") + require.NoError(t, err) + assert.True(t, deleted) + + mock.mu.Lock() + defer mock.mu.Unlock() + require.NotNil(t, mock.getRequest) + assert.Equal(t, "default", mock.getRequest.Workspace) + assert.Equal(t, "gpu-kata", mock.getRequest.Name) + require.NotNil(t, mock.listRequest) + assert.Empty(t, mock.listRequest.Workspace) + assert.Equal(t, uint32(10), mock.listRequest.Limit) + assert.Equal(t, uint32(2), mock.listRequest.Offset) + assert.Equal(t, "team=runtime", mock.listRequest.LabelSelector) + assert.True(t, mock.listRequest.AllWorkspaces) + require.NotNil(t, mock.deleteRequest) + assert.Equal(t, "default", mock.deleteRequest.Workspace) + assert.Equal(t, "gpu-kata", mock.deleteRequest.Name) +} + +func TestSandboxTemplateList_RejectsNegativePagination(t *testing.T) { + mock := newMockSandboxTemplateServer() + client, cleanup := setupSandboxTemplateTest(t, mock) + defer cleanup() + + _, err := client.List(context.Background(), "default", ListOptions{Limit: -1}) + require.Error(t, err) + assert.True(t, IsInvalidArgument(err)) + + _, err = client.List(context.Background(), "default", ListOptions{Offset: -1}) + require.Error(t, err) + assert.True(t, IsInvalidArgument(err)) +} diff --git a/sdk/go/openshell/v1/types/sandbox.go b/sdk/go/openshell/v1/types/sandbox.go index 6ce51d84a1..8be13888ba 100644 --- a/sdk/go/openshell/v1/types/sandbox.go +++ b/sdk/go/openshell/v1/types/sandbox.go @@ -7,16 +7,17 @@ import "time" // Sandbox represents a sandbox instance. type Sandbox struct { - ID string - Name string - CreatedAt time.Time - Labels map[string]string - Annotations map[string]string - ResourceVersion uint64 - Workspace string - DeletionTimestamp *time.Time - Spec SandboxSpec - Status SandboxStatus + ID string + Name string + CreatedAt time.Time + Labels map[string]string + Annotations map[string]string + ResourceVersion uint64 + Workspace string + DeletionTimestamp *time.Time + CreatedFromWorkloadTemplate *SandboxWorkloadTemplateProvenance + Spec SandboxSpec + Status SandboxStatus } // SandboxSpec holds the desired state of a sandbox. @@ -25,7 +26,10 @@ type SandboxSpec struct { Environment map[string]string Template *SandboxTemplate Providers []string - GPUCount *uint32 + // GPU requests GPU resources using the active driver's default GPU assignment + // when GPUCount is nil. GPUCount implies GPU for backward compatibility. + GPU bool + GPUCount *uint32 // Policy is the security policy for the sandbox. Nil means no policy specified. Policy *SandboxPolicy Command []string @@ -45,6 +49,64 @@ type SandboxTemplate struct { DriverConfig map[string]any } +// SandboxWorkloadTemplate is a reusable workspace-scoped sandbox template resource. +type SandboxWorkloadTemplate struct { + ID string + Name string + CreatedAt time.Time + Labels map[string]string + Annotations map[string]string + ResourceVersion uint64 + Workspace string + DeletionTimestamp *time.Time + Spec SandboxWorkloadTemplateSpec +} + +// SandboxWorkloadTemplateSpec holds reusable sandbox template settings. +type SandboxWorkloadTemplateSpec struct { + Workload *SandboxWorkloadConfig + DriverConfig map[string]any + DesiredServiceLevel *SandboxServiceLevel +} + +// SandboxWorkloadConfig defines the portable workload for a reusable template. +type SandboxWorkloadConfig struct { + Image string + Environment map[string]string + Resources *SandboxResources +} + +// SandboxResources defines portable sandbox resource requirements. +type SandboxResources struct { + CPU string + Memory string + // GPU requests GPU resources for template-backed sandboxes. A non-nil GPU + // with nil Count requests the active driver's default GPU assignment. + GPU *SandboxGPURequirements +} + +// SandboxGPURequirements defines template GPU requirements. +type SandboxGPURequirements struct { + Count *uint32 +} + +// SandboxServiceLevel describes desired operational characteristics. +type SandboxServiceLevel struct { + Startup *SandboxStartup +} + +// SandboxStartup describes desired startup characteristics. +type SandboxStartup struct { + ReadyWithin time.Duration + MaxBurst uint32 +} + +// SandboxWorkloadTemplateProvenance identifies the reusable template revision used to create a sandbox. +type SandboxWorkloadTemplateProvenance struct { + Name string + ResourceVersion string +} + // SandboxStatus holds the observed state of a sandbox. type SandboxStatus struct { SandboxName string diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 0d2b86fce0..f9e7f39030 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -15,6 +15,7 @@ import ( sandboxv1 "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" @@ -1155,9 +1156,11 @@ type Sandbox struct { // Desired sandbox configuration submitted through the API. Spec *SandboxSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` // Latest user-facing observed status derived by the gateway. - Status *SandboxStatus `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Status *SandboxStatus `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + // Read-only provenance for sandboxes created from a reusable workload template. + CreatedFromWorkloadTemplate *SandboxWorkloadTemplateProvenance `protobuf:"bytes,20,opt,name=created_from_workload_template,json=createdFromWorkloadTemplate,proto3" json:"created_from_workload_template,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Sandbox) Reset() { @@ -1211,6 +1214,13 @@ func (x *Sandbox) GetStatus() *SandboxStatus { return nil } +func (x *Sandbox) GetCreatedFromWorkloadTemplate() *SandboxWorkloadTemplateProvenance { + if x != nil { + return x.CreatedFromWorkloadTemplate + } + return nil +} + // Desired sandbox configuration provided through the public API. type SandboxSpec struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1415,7 +1425,12 @@ func (x *GpuResourceRequirements) GetCount() uint32 { return 0 } -// Public sandbox template mapped onto compute-driver template inputs. +// Historical inline compute template mapped onto compute-driver template inputs. +// +// Despite its name, this is not a reusable named sandbox template resource. It +// is an inline part of `SandboxSpec` kept for v1 compatibility. A future +// breaking API cleanup may rename this message to free `SandboxTemplate` for +// the reusable template resource now represented by `SandboxWorkloadTemplate`. type SandboxTemplate struct { state protoimpl.MessageState `protogen:"open.v1"` // Fully-qualified OCI image reference used to boot the sandbox. @@ -1540,50 +1555,36 @@ func (x *SandboxTemplate) GetDriverConfig() *structpb.Struct { return nil } -// User-facing sandbox status derived by the gateway from compute-driver observations. +// Reusable named sandbox workload template resource. // -// Public status does not embed driver-only flags such as `deleting`. -type SandboxStatus struct { +// This is the actual workspace-scoped template resource used to create +// sandboxes by reference. It uses the longer name in v1 to avoid colliding with +// the historical inline `SandboxTemplate` message. A future breaking API +// cleanup may rename this resource to `SandboxTemplate`. +type SandboxWorkloadTemplate struct { state protoimpl.MessageState `protogen:"open.v1"` - // Compute-platform sandbox object name. - SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` - // Name of the agent pod or equivalent runtime instance. - AgentPod string `protobuf:"bytes,2,opt,name=agent_pod,json=agentPod,proto3" json:"agent_pod,omitempty"` - // File descriptor or endpoint for reaching the agent service, when available. - AgentFd string `protobuf:"bytes,3,opt,name=agent_fd,json=agentFd,proto3" json:"agent_fd,omitempty"` - // File descriptor or endpoint for reaching the sandbox service, when available. - SandboxFd string `protobuf:"bytes,4,opt,name=sandbox_fd,json=sandboxFd,proto3" json:"sandbox_fd,omitempty"` - // Latest user-facing readiness and lifecycle conditions. - Conditions []*SandboxCondition `protobuf:"bytes,5,rep,name=conditions,proto3" json:"conditions,omitempty"` - // Gateway-derived lifecycle summary. - Phase SandboxPhase `protobuf:"varint,6,opt,name=phase,proto3,enum=openshell.v1.SandboxPhase" json:"phase,omitempty"` - // Currently active policy version (updated when sandbox reports loaded). - CurrentPolicyVersion uint32 `protobuf:"varint,7,opt,name=current_policy_version,json=currentPolicyVersion,proto3" json:"current_policy_version,omitempty"` - // Supervisor instance currently associated with the canonical main process. - // The gateway uses this to reject stale exit reports after a restart. - MainProcessInstanceId string `protobuf:"bytes,8,opt,name=main_process_instance_id,json=mainProcessInstanceId,proto3" json:"main_process_instance_id,omitempty"` - // Normalized main process result. Signal exits use 128 + signal number. - // Presence indicates that the canonical main process exited. Exit code 0 - // produces Completed; nonzero and signal-normalized exits produce Error. - ExitCode *int32 `protobuf:"varint,9,opt,name=exit_code,json=exitCode,proto3,oneof" json:"exit_code,omitempty"` + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Desired reusable workload shape and template-owned driver config. + Spec *SandboxWorkloadTemplateSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SandboxStatus) Reset() { - *x = SandboxStatus{} +func (x *SandboxWorkloadTemplate) Reset() { + *x = SandboxWorkloadTemplate{} mi := &file_openshell_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxStatus) String() string { +func (x *SandboxWorkloadTemplate) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxStatus) ProtoMessage() {} +func (*SandboxWorkloadTemplate) ProtoMessage() {} -func (x *SandboxStatus) ProtoReflect() protoreflect.Message { +func (x *SandboxWorkloadTemplate) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1595,106 +1596,115 @@ func (x *SandboxStatus) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxStatus.ProtoReflect.Descriptor instead. -func (*SandboxStatus) Descriptor() ([]byte, []int) { +// Deprecated: Use SandboxWorkloadTemplate.ProtoReflect.Descriptor instead. +func (*SandboxWorkloadTemplate) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{17} } -func (x *SandboxStatus) GetSandboxName() string { +func (x *SandboxWorkloadTemplate) GetMetadata() *datamodelv1.ObjectMeta { if x != nil { - return x.SandboxName + return x.Metadata } - return "" + return nil } -func (x *SandboxStatus) GetAgentPod() string { +func (x *SandboxWorkloadTemplate) GetSpec() *SandboxWorkloadTemplateSpec { if x != nil { - return x.AgentPod + return x.Spec } - return "" + return nil } -func (x *SandboxStatus) GetAgentFd() string { - if x != nil { - return x.AgentFd - } - return "" +type SandboxWorkloadTemplateSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Portable workload shape. + Workload *SandboxWorkloadConfig `protobuf:"bytes,1,opt,name=workload,proto3" json:"workload,omitempty"` + // Driver-keyed opaque config envelope supplied by the template owner. + DriverConfig *structpb.Struct `protobuf:"bytes,2,opt,name=driver_config,json=driverConfig,proto3" json:"driver_config,omitempty"` + // Desired service level associated with this template. + DesiredServiceLevel *SandboxServiceLevel `protobuf:"bytes,3,opt,name=desired_service_level,json=desiredServiceLevel,proto3" json:"desired_service_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxWorkloadTemplateSpec) Reset() { + *x = SandboxWorkloadTemplateSpec{} + mi := &file_openshell_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *SandboxStatus) GetSandboxFd() string { - if x != nil { - return x.SandboxFd - } - return "" +func (x *SandboxWorkloadTemplateSpec) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *SandboxStatus) GetConditions() []*SandboxCondition { +func (*SandboxWorkloadTemplateSpec) ProtoMessage() {} + +func (x *SandboxWorkloadTemplateSpec) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[18] if x != nil { - return x.Conditions + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -func (x *SandboxStatus) GetPhase() SandboxPhase { - if x != nil { - return x.Phase - } - return SandboxPhase_SANDBOX_PHASE_UNSPECIFIED +// Deprecated: Use SandboxWorkloadTemplateSpec.ProtoReflect.Descriptor instead. +func (*SandboxWorkloadTemplateSpec) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{18} } -func (x *SandboxStatus) GetCurrentPolicyVersion() uint32 { +func (x *SandboxWorkloadTemplateSpec) GetWorkload() *SandboxWorkloadConfig { if x != nil { - return x.CurrentPolicyVersion + return x.Workload } - return 0 + return nil } -func (x *SandboxStatus) GetMainProcessInstanceId() string { +func (x *SandboxWorkloadTemplateSpec) GetDriverConfig() *structpb.Struct { if x != nil { - return x.MainProcessInstanceId + return x.DriverConfig } - return "" + return nil } -func (x *SandboxStatus) GetExitCode() int32 { - if x != nil && x.ExitCode != nil { - return *x.ExitCode +func (x *SandboxWorkloadTemplateSpec) GetDesiredServiceLevel() *SandboxServiceLevel { + if x != nil { + return x.DesiredServiceLevel } - return 0 + return nil } -// User-facing sandbox condition derived from driver-native conditions. -type SandboxCondition struct { +type SandboxWorkloadConfig struct { state protoimpl.MessageState `protogen:"open.v1"` - // Condition class, typically mirroring the underlying platform condition type. - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - // Condition status value such as `True`, `False`, or `Unknown`. - Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` - // Short machine-readable reason associated with the condition. - Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` - // Human-readable condition message. - Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` - // Timestamp reported by the underlying platform for the last transition. - LastTransitionTime string `protobuf:"bytes,5,opt,name=last_transition_time,json=lastTransitionTime,proto3" json:"last_transition_time,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Fully-qualified OCI image reference used to boot the sandbox. + Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` + // Environment variables injected into the sandbox runtime. + Environment map[string]string `protobuf:"bytes,2,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Portable resource requirements for sandboxes created from this workload. + Resources *SandboxResources `protobuf:"bytes,3,opt,name=resources,proto3" json:"resources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *SandboxCondition) Reset() { - *x = SandboxCondition{} - mi := &file_openshell_proto_msgTypes[18] +func (x *SandboxWorkloadConfig) Reset() { + *x = SandboxWorkloadConfig{} + mi := &file_openshell_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxCondition) String() string { +func (x *SandboxWorkloadConfig) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxCondition) ProtoMessage() {} +func (*SandboxWorkloadConfig) ProtoMessage() {} -func (x *SandboxCondition) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[18] +func (x *SandboxWorkloadConfig) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1705,80 +1715,61 @@ func (x *SandboxCondition) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxCondition.ProtoReflect.Descriptor instead. -func (*SandboxCondition) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{18} -} - -func (x *SandboxCondition) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *SandboxCondition) GetStatus() string { - if x != nil { - return x.Status - } - return "" +// Deprecated: Use SandboxWorkloadConfig.ProtoReflect.Descriptor instead. +func (*SandboxWorkloadConfig) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{19} } -func (x *SandboxCondition) GetReason() string { +func (x *SandboxWorkloadConfig) GetImage() string { if x != nil { - return x.Reason + return x.Image } return "" } -func (x *SandboxCondition) GetMessage() string { +func (x *SandboxWorkloadConfig) GetEnvironment() map[string]string { if x != nil { - return x.Message + return x.Environment } - return "" + return nil } -func (x *SandboxCondition) GetLastTransitionTime() string { +func (x *SandboxWorkloadConfig) GetResources() *SandboxResources { if x != nil { - return x.LastTransitionTime + return x.Resources } - return "" + return nil } -// Public platform event exposed on the sandbox watch stream. -type PlatformEvent struct { +type SandboxResources struct { state protoimpl.MessageState `protogen:"open.v1"` - // Event timestamp in milliseconds since epoch. - TimestampMs int64 `protobuf:"varint,1,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` - // Event source (e.g. "kubernetes", "docker", "process"). - Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` - // Event type/severity (e.g. "Normal", "Warning"). - Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` - // Short reason code (e.g. "Started", "Pulled", "Failed"). - Reason string `protobuf:"bytes,4,opt,name=reason,proto3" json:"reason,omitempty"` - // Human-readable event message. - Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` - // Optional metadata as key-value pairs. - Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Portable CPU quantity, for example "500m" or "2". + Cpu string `protobuf:"bytes,1,opt,name=cpu,proto3" json:"cpu,omitempty"` + // Portable memory quantity, for example "512Mi" or "2Gi". + Memory string `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` + // GPU requirements for the sandbox workload. Presence indicates a GPU + // request. When count is omitted, the request uses the selected driver's + // default GPU assignment behavior. + Gpu *GpuResourceRequirements `protobuf:"bytes,3,opt,name=gpu,proto3" json:"gpu,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *PlatformEvent) Reset() { - *x = PlatformEvent{} - mi := &file_openshell_proto_msgTypes[19] +func (x *SandboxResources) Reset() { + *x = SandboxResources{} + mi := &file_openshell_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *PlatformEvent) String() string { +func (x *SandboxResources) String() string { return protoimpl.X.MessageStringOf(x) } -func (*PlatformEvent) ProtoMessage() {} +func (*SandboxResources) ProtoMessage() {} -func (x *PlatformEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[19] +func (x *SandboxResources) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1789,88 +1780,54 @@ func (x *PlatformEvent) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use PlatformEvent.ProtoReflect.Descriptor instead. -func (*PlatformEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{19} -} - -func (x *PlatformEvent) GetTimestampMs() int64 { - if x != nil { - return x.TimestampMs - } - return 0 -} - -func (x *PlatformEvent) GetSource() string { - if x != nil { - return x.Source - } - return "" -} - -func (x *PlatformEvent) GetType() string { - if x != nil { - return x.Type - } - return "" +// Deprecated: Use SandboxResources.ProtoReflect.Descriptor instead. +func (*SandboxResources) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{20} } -func (x *PlatformEvent) GetReason() string { +func (x *SandboxResources) GetCpu() string { if x != nil { - return x.Reason + return x.Cpu } return "" } -func (x *PlatformEvent) GetMessage() string { +func (x *SandboxResources) GetMemory() string { if x != nil { - return x.Message + return x.Memory } return "" } -func (x *PlatformEvent) GetMetadata() map[string]string { +func (x *SandboxResources) GetGpu() *GpuResourceRequirements { if x != nil { - return x.Metadata + return x.Gpu } return nil } -// Create sandbox request. -type CreateSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Spec *SandboxSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` - // Optional user-supplied sandbox name. When empty the server generates one. - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - // Optional labels for the sandbox (key-value metadata). - Labels map[string]string `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Optional annotations for the sandbox (non-selector metadata). - Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Workspace for the sandbox. Empty defaults to "default". - Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` - // One-shot launch hint indicating that the creating client will attach to - // the canonical main process. The supervisor keeps the terminal transport - // alive until that attachment connects and closes naturally. - AwaitMainProcessAttachment bool `protobuf:"varint,6,opt,name=await_main_process_attachment,json=awaitMainProcessAttachment,proto3" json:"await_main_process_attachment,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type SandboxServiceLevel struct { + state protoimpl.MessageState `protogen:"open.v1"` + Startup *SandboxStartup `protobuf:"bytes,1,opt,name=startup,proto3" json:"startup,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *CreateSandboxRequest) Reset() { - *x = CreateSandboxRequest{} - mi := &file_openshell_proto_msgTypes[20] +func (x *SandboxServiceLevel) Reset() { + *x = SandboxServiceLevel{} + mi := &file_openshell_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CreateSandboxRequest) String() string { +func (x *SandboxServiceLevel) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CreateSandboxRequest) ProtoMessage() {} +func (*SandboxServiceLevel) ProtoMessage() {} -func (x *CreateSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[20] +func (x *SandboxServiceLevel) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1881,49 +1838,885 @@ func (x *CreateSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CreateSandboxRequest.ProtoReflect.Descriptor instead. -func (*CreateSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{20} +// Deprecated: Use SandboxServiceLevel.ProtoReflect.Descriptor instead. +func (*SandboxServiceLevel) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{21} } -func (x *CreateSandboxRequest) GetSpec() *SandboxSpec { +func (x *SandboxServiceLevel) GetStartup() *SandboxStartup { if x != nil { - return x.Spec + return x.Startup } return nil } -func (x *CreateSandboxRequest) GetName() string { - if x != nil { - return x.Name - } - return "" +type SandboxStartup struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReadyWithin *durationpb.Duration `protobuf:"bytes,1,opt,name=ready_within,json=readyWithin,proto3" json:"ready_within,omitempty"` + MaxBurst uint32 `protobuf:"varint,2,opt,name=max_burst,json=maxBurst,proto3" json:"max_burst,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *CreateSandboxRequest) GetLabels() map[string]string { - if x != nil { +func (x *SandboxStartup) Reset() { + *x = SandboxStartup{} + mi := &file_openshell_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStartup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStartup) ProtoMessage() {} + +func (x *SandboxStartup) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxStartup.ProtoReflect.Descriptor instead. +func (*SandboxStartup) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{22} +} + +func (x *SandboxStartup) GetReadyWithin() *durationpb.Duration { + if x != nil { + return x.ReadyWithin + } + return nil +} + +func (x *SandboxStartup) GetMaxBurst() uint32 { + if x != nil { + return x.MaxBurst + } + return 0 +} + +type SandboxWorkloadTemplateProvenance struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + ResourceVersion string `protobuf:"bytes,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxWorkloadTemplateProvenance) Reset() { + *x = SandboxWorkloadTemplateProvenance{} + mi := &file_openshell_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxWorkloadTemplateProvenance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxWorkloadTemplateProvenance) ProtoMessage() {} + +func (x *SandboxWorkloadTemplateProvenance) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxWorkloadTemplateProvenance.ProtoReflect.Descriptor instead. +func (*SandboxWorkloadTemplateProvenance) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{23} +} + +func (x *SandboxWorkloadTemplateProvenance) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SandboxWorkloadTemplateProvenance) GetResourceVersion() string { + if x != nil { + return x.ResourceVersion + } + return "" +} + +// User-facing sandbox status derived by the gateway from compute-driver observations. +// +// Public status does not embed driver-only flags such as `deleting`. +type SandboxStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Compute-platform sandbox object name. + SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + // Name of the agent pod or equivalent runtime instance. + AgentPod string `protobuf:"bytes,2,opt,name=agent_pod,json=agentPod,proto3" json:"agent_pod,omitempty"` + // File descriptor or endpoint for reaching the agent service, when available. + AgentFd string `protobuf:"bytes,3,opt,name=agent_fd,json=agentFd,proto3" json:"agent_fd,omitempty"` + // File descriptor or endpoint for reaching the sandbox service, when available. + SandboxFd string `protobuf:"bytes,4,opt,name=sandbox_fd,json=sandboxFd,proto3" json:"sandbox_fd,omitempty"` + // Latest user-facing readiness and lifecycle conditions. + Conditions []*SandboxCondition `protobuf:"bytes,5,rep,name=conditions,proto3" json:"conditions,omitempty"` + // Gateway-derived lifecycle summary. + Phase SandboxPhase `protobuf:"varint,6,opt,name=phase,proto3,enum=openshell.v1.SandboxPhase" json:"phase,omitempty"` + // Currently active policy version (updated when sandbox reports loaded). + CurrentPolicyVersion uint32 `protobuf:"varint,7,opt,name=current_policy_version,json=currentPolicyVersion,proto3" json:"current_policy_version,omitempty"` + // Supervisor instance currently associated with the canonical main process. + // The gateway uses this to reject stale exit reports after a restart. + MainProcessInstanceId string `protobuf:"bytes,8,opt,name=main_process_instance_id,json=mainProcessInstanceId,proto3" json:"main_process_instance_id,omitempty"` + // Normalized main process result. Signal exits use 128 + signal number. + // Presence indicates that the canonical main process exited. Exit code 0 + // produces Completed; nonzero and signal-normalized exits produce Error. + ExitCode *int32 `protobuf:"varint,9,opt,name=exit_code,json=exitCode,proto3,oneof" json:"exit_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStatus) Reset() { + *x = SandboxStatus{} + mi := &file_openshell_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStatus) ProtoMessage() {} + +func (x *SandboxStatus) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxStatus.ProtoReflect.Descriptor instead. +func (*SandboxStatus) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{24} +} + +func (x *SandboxStatus) GetSandboxName() string { + if x != nil { + return x.SandboxName + } + return "" +} + +func (x *SandboxStatus) GetAgentPod() string { + if x != nil { + return x.AgentPod + } + return "" +} + +func (x *SandboxStatus) GetAgentFd() string { + if x != nil { + return x.AgentFd + } + return "" +} + +func (x *SandboxStatus) GetSandboxFd() string { + if x != nil { + return x.SandboxFd + } + return "" +} + +func (x *SandboxStatus) GetConditions() []*SandboxCondition { + if x != nil { + return x.Conditions + } + return nil +} + +func (x *SandboxStatus) GetPhase() SandboxPhase { + if x != nil { + return x.Phase + } + return SandboxPhase_SANDBOX_PHASE_UNSPECIFIED +} + +func (x *SandboxStatus) GetCurrentPolicyVersion() uint32 { + if x != nil { + return x.CurrentPolicyVersion + } + return 0 +} + +func (x *SandboxStatus) GetMainProcessInstanceId() string { + if x != nil { + return x.MainProcessInstanceId + } + return "" +} + +func (x *SandboxStatus) GetExitCode() int32 { + if x != nil && x.ExitCode != nil { + return *x.ExitCode + } + return 0 +} + +// User-facing sandbox condition derived from driver-native conditions. +type SandboxCondition struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Condition class, typically mirroring the underlying platform condition type. + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // Condition status value such as `True`, `False`, or `Unknown`. + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + // Short machine-readable reason associated with the condition. + Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + // Human-readable condition message. + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + // Timestamp reported by the underlying platform for the last transition. + LastTransitionTime string `protobuf:"bytes,5,opt,name=last_transition_time,json=lastTransitionTime,proto3" json:"last_transition_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxCondition) Reset() { + *x = SandboxCondition{} + mi := &file_openshell_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxCondition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxCondition) ProtoMessage() {} + +func (x *SandboxCondition) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxCondition.ProtoReflect.Descriptor instead. +func (*SandboxCondition) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{25} +} + +func (x *SandboxCondition) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *SandboxCondition) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *SandboxCondition) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *SandboxCondition) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SandboxCondition) GetLastTransitionTime() string { + if x != nil { + return x.LastTransitionTime + } + return "" +} + +// Public platform event exposed on the sandbox watch stream. +type PlatformEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Event timestamp in milliseconds since epoch. + TimestampMs int64 `protobuf:"varint,1,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` + // Event source (e.g. "kubernetes", "docker", "process"). + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + // Event type/severity (e.g. "Normal", "Warning"). + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + // Short reason code (e.g. "Started", "Pulled", "Failed"). + Reason string `protobuf:"bytes,4,opt,name=reason,proto3" json:"reason,omitempty"` + // Human-readable event message. + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + // Optional metadata as key-value pairs. + Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlatformEvent) Reset() { + *x = PlatformEvent{} + mi := &file_openshell_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlatformEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlatformEvent) ProtoMessage() {} + +func (x *PlatformEvent) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlatformEvent.ProtoReflect.Descriptor instead. +func (*PlatformEvent) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{26} +} + +func (x *PlatformEvent) GetTimestampMs() int64 { + if x != nil { + return x.TimestampMs + } + return 0 +} + +func (x *PlatformEvent) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *PlatformEvent) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *PlatformEvent) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *PlatformEvent) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *PlatformEvent) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// Create sandbox request. +type CreateSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Spec *SandboxSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` + // Optional user-supplied sandbox name. When empty the server generates one. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Optional labels for the sandbox (key-value metadata). + Labels map[string]string `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional annotations for the sandbox (non-selector metadata). + Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Workspace for the sandbox. Empty defaults to "default". + Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` + // One-shot launch hint indicating that the creating client will attach to + // the canonical main process. The supervisor keeps the terminal transport + // alive until that attachment connects and closes naturally. + AwaitMainProcessAttachment bool `protobuf:"varint,6,opt,name=await_main_process_attachment,json=awaitMainProcessAttachment,proto3" json:"await_main_process_attachment,omitempty"` + // Workspace-scoped SandboxWorkloadTemplate name to resolve at creation time. + WorkloadTemplateName string `protobuf:"bytes,7,opt,name=workload_template_name,json=workloadTemplateName,proto3" json:"workload_template_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSandboxRequest) Reset() { + *x = CreateSandboxRequest{} + mi := &file_openshell_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSandboxRequest) ProtoMessage() {} + +func (x *CreateSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSandboxRequest.ProtoReflect.Descriptor instead. +func (*CreateSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{27} +} + +func (x *CreateSandboxRequest) GetSpec() *SandboxSpec { + if x != nil { + return x.Spec + } + return nil +} + +func (x *CreateSandboxRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateSandboxRequest) GetLabels() map[string]string { + if x != nil { return x.Labels } return nil } -func (x *CreateSandboxRequest) GetAnnotations() map[string]string { +func (x *CreateSandboxRequest) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + +func (x *CreateSandboxRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *CreateSandboxRequest) GetAwaitMainProcessAttachment() bool { + if x != nil { + return x.AwaitMainProcessAttachment + } + return false +} + +func (x *CreateSandboxRequest) GetWorkloadTemplateName() string { + if x != nil { + return x.WorkloadTemplateName + } + return "" +} + +type CreateSandboxTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Template *SandboxWorkloadTemplate `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` + // Workspace for the template. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSandboxTemplateRequest) Reset() { + *x = CreateSandboxTemplateRequest{} + mi := &file_openshell_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSandboxTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSandboxTemplateRequest) ProtoMessage() {} + +func (x *CreateSandboxTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSandboxTemplateRequest.ProtoReflect.Descriptor instead. +func (*CreateSandboxTemplateRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{28} +} + +func (x *CreateSandboxTemplateRequest) GetTemplate() *SandboxWorkloadTemplate { + if x != nil { + return x.Template + } + return nil +} + +func (x *CreateSandboxTemplateRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +type GetSandboxTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxTemplateRequest) Reset() { + *x = GetSandboxTemplateRequest{} + mi := &file_openshell_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxTemplateRequest) ProtoMessage() {} + +func (x *GetSandboxTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSandboxTemplateRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxTemplateRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{29} +} + +func (x *GetSandboxTemplateRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetSandboxTemplateRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +type ListSandboxTemplatesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + // List across all workspaces. Mutually exclusive with workspace. + AllWorkspaces bool `protobuf:"varint,4,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` + // Optional label selector in key=value comma-separated form. + LabelSelector string `protobuf:"bytes,5,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxTemplatesRequest) Reset() { + *x = ListSandboxTemplatesRequest{} + mi := &file_openshell_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxTemplatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxTemplatesRequest) ProtoMessage() {} + +func (x *ListSandboxTemplatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSandboxTemplatesRequest.ProtoReflect.Descriptor instead. +func (*ListSandboxTemplatesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{30} +} + +func (x *ListSandboxTemplatesRequest) GetLimit() uint32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListSandboxTemplatesRequest) GetOffset() uint32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *ListSandboxTemplatesRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *ListSandboxTemplatesRequest) GetAllWorkspaces() bool { + if x != nil { + return x.AllWorkspaces + } + return false +} + +func (x *ListSandboxTemplatesRequest) GetLabelSelector() string { + if x != nil { + return x.LabelSelector + } + return "" +} + +type DeleteSandboxTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSandboxTemplateRequest) Reset() { + *x = DeleteSandboxTemplateRequest{} + mi := &file_openshell_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSandboxTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSandboxTemplateRequest) ProtoMessage() {} + +func (x *DeleteSandboxTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteSandboxTemplateRequest.ProtoReflect.Descriptor instead. +func (*DeleteSandboxTemplateRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{31} +} + +func (x *DeleteSandboxTemplateRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteSandboxTemplateRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +type SandboxTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Template *SandboxWorkloadTemplate `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxTemplateResponse) Reset() { + *x = SandboxTemplateResponse{} + mi := &file_openshell_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxTemplateResponse) ProtoMessage() {} + +func (x *SandboxTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxTemplateResponse.ProtoReflect.Descriptor instead. +func (*SandboxTemplateResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{32} +} + +func (x *SandboxTemplateResponse) GetTemplate() *SandboxWorkloadTemplate { + if x != nil { + return x.Template + } + return nil +} + +type ListSandboxTemplatesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Templates []*SandboxWorkloadTemplate `protobuf:"bytes,1,rep,name=templates,proto3" json:"templates,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxTemplatesResponse) Reset() { + *x = ListSandboxTemplatesResponse{} + mi := &file_openshell_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxTemplatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxTemplatesResponse) ProtoMessage() {} + +func (x *ListSandboxTemplatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSandboxTemplatesResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxTemplatesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{33} +} + +func (x *ListSandboxTemplatesResponse) GetTemplates() []*SandboxWorkloadTemplate { + if x != nil { + return x.Templates + } + return nil +} + +type DeleteSandboxTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSandboxTemplateResponse) Reset() { + *x = DeleteSandboxTemplateResponse{} + mi := &file_openshell_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSandboxTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSandboxTemplateResponse) ProtoMessage() {} + +func (x *DeleteSandboxTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[34] if x != nil { - return x.Annotations + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -func (x *CreateSandboxRequest) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" +// Deprecated: Use DeleteSandboxTemplateResponse.ProtoReflect.Descriptor instead. +func (*DeleteSandboxTemplateResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{34} } -func (x *CreateSandboxRequest) GetAwaitMainProcessAttachment() bool { +func (x *DeleteSandboxTemplateResponse) GetDeleted() bool { if x != nil { - return x.AwaitMainProcessAttachment + return x.Deleted } return false } @@ -1941,7 +2734,7 @@ type GetSandboxRequest struct { func (x *GetSandboxRequest) Reset() { *x = GetSandboxRequest{} - mi := &file_openshell_proto_msgTypes[21] + mi := &file_openshell_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1953,7 +2746,7 @@ func (x *GetSandboxRequest) String() string { func (*GetSandboxRequest) ProtoMessage() {} func (x *GetSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[21] + mi := &file_openshell_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1966,7 +2759,7 @@ func (x *GetSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxRequest.ProtoReflect.Descriptor instead. func (*GetSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{21} + return file_openshell_proto_rawDescGZIP(), []int{35} } func (x *GetSandboxRequest) GetName() string { @@ -2000,7 +2793,7 @@ type ListSandboxesRequest struct { func (x *ListSandboxesRequest) Reset() { *x = ListSandboxesRequest{} - mi := &file_openshell_proto_msgTypes[22] + mi := &file_openshell_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2012,7 +2805,7 @@ func (x *ListSandboxesRequest) String() string { func (*ListSandboxesRequest) ProtoMessage() {} func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[22] + mi := &file_openshell_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2025,7 +2818,7 @@ func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{22} + return file_openshell_proto_rawDescGZIP(), []int{36} } func (x *ListSandboxesRequest) GetLimit() uint32 { @@ -2076,7 +2869,7 @@ type ListSandboxProvidersRequest struct { func (x *ListSandboxProvidersRequest) Reset() { *x = ListSandboxProvidersRequest{} - mi := &file_openshell_proto_msgTypes[23] + mi := &file_openshell_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2088,7 +2881,7 @@ func (x *ListSandboxProvidersRequest) String() string { func (*ListSandboxProvidersRequest) ProtoMessage() {} func (x *ListSandboxProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[23] + mi := &file_openshell_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2101,7 +2894,7 @@ func (x *ListSandboxProvidersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxProvidersRequest.ProtoReflect.Descriptor instead. func (*ListSandboxProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{23} + return file_openshell_proto_rawDescGZIP(), []int{37} } func (x *ListSandboxProvidersRequest) GetSandboxName() string { @@ -2138,7 +2931,7 @@ type AttachSandboxProviderRequest struct { func (x *AttachSandboxProviderRequest) Reset() { *x = AttachSandboxProviderRequest{} - mi := &file_openshell_proto_msgTypes[24] + mi := &file_openshell_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2150,7 +2943,7 @@ func (x *AttachSandboxProviderRequest) String() string { func (*AttachSandboxProviderRequest) ProtoMessage() {} func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[24] + mi := &file_openshell_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2163,7 +2956,7 @@ func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AttachSandboxProviderRequest.ProtoReflect.Descriptor instead. func (*AttachSandboxProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{24} + return file_openshell_proto_rawDescGZIP(), []int{38} } func (x *AttachSandboxProviderRequest) GetSandboxName() string { @@ -2214,7 +3007,7 @@ type DetachSandboxProviderRequest struct { func (x *DetachSandboxProviderRequest) Reset() { *x = DetachSandboxProviderRequest{} - mi := &file_openshell_proto_msgTypes[25] + mi := &file_openshell_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2226,7 +3019,7 @@ func (x *DetachSandboxProviderRequest) String() string { func (*DetachSandboxProviderRequest) ProtoMessage() {} func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[25] + mi := &file_openshell_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2239,7 +3032,7 @@ func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DetachSandboxProviderRequest.ProtoReflect.Descriptor instead. func (*DetachSandboxProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{25} + return file_openshell_proto_rawDescGZIP(), []int{39} } func (x *DetachSandboxProviderRequest) GetSandboxName() string { @@ -2283,7 +3076,7 @@ type DeleteSandboxRequest struct { func (x *DeleteSandboxRequest) Reset() { *x = DeleteSandboxRequest{} - mi := &file_openshell_proto_msgTypes[26] + mi := &file_openshell_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2295,7 +3088,7 @@ func (x *DeleteSandboxRequest) String() string { func (*DeleteSandboxRequest) ProtoMessage() {} func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[26] + mi := &file_openshell_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2308,7 +3101,7 @@ func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSandboxRequest.ProtoReflect.Descriptor instead. func (*DeleteSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{26} + return file_openshell_proto_rawDescGZIP(), []int{40} } func (x *DeleteSandboxRequest) GetName() string { @@ -2338,7 +3131,7 @@ type StopSandboxRequest struct { func (x *StopSandboxRequest) Reset() { *x = StopSandboxRequest{} - mi := &file_openshell_proto_msgTypes[27] + mi := &file_openshell_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2350,7 +3143,7 @@ func (x *StopSandboxRequest) String() string { func (*StopSandboxRequest) ProtoMessage() {} func (x *StopSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[27] + mi := &file_openshell_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2363,7 +3156,7 @@ func (x *StopSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StopSandboxRequest.ProtoReflect.Descriptor instead. func (*StopSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{27} + return file_openshell_proto_rawDescGZIP(), []int{41} } func (x *StopSandboxRequest) GetName() string { @@ -2393,7 +3186,7 @@ type StartSandboxRequest struct { func (x *StartSandboxRequest) Reset() { *x = StartSandboxRequest{} - mi := &file_openshell_proto_msgTypes[28] + mi := &file_openshell_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2405,7 +3198,7 @@ func (x *StartSandboxRequest) String() string { func (*StartSandboxRequest) ProtoMessage() {} func (x *StartSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[28] + mi := &file_openshell_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2418,7 +3211,7 @@ func (x *StartSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartSandboxRequest.ProtoReflect.Descriptor instead. func (*StartSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{28} + return file_openshell_proto_rawDescGZIP(), []int{42} } func (x *StartSandboxRequest) GetName() string { @@ -2445,7 +3238,7 @@ type SandboxResponse struct { func (x *SandboxResponse) Reset() { *x = SandboxResponse{} - mi := &file_openshell_proto_msgTypes[29] + mi := &file_openshell_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2457,7 +3250,7 @@ func (x *SandboxResponse) String() string { func (*SandboxResponse) ProtoMessage() {} func (x *SandboxResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[29] + mi := &file_openshell_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2470,7 +3263,7 @@ func (x *SandboxResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxResponse.ProtoReflect.Descriptor instead. func (*SandboxResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{29} + return file_openshell_proto_rawDescGZIP(), []int{43} } func (x *SandboxResponse) GetSandbox() *Sandbox { @@ -2490,7 +3283,7 @@ type ListSandboxesResponse struct { func (x *ListSandboxesResponse) Reset() { *x = ListSandboxesResponse{} - mi := &file_openshell_proto_msgTypes[30] + mi := &file_openshell_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2502,7 +3295,7 @@ func (x *ListSandboxesResponse) String() string { func (*ListSandboxesResponse) ProtoMessage() {} func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[30] + mi := &file_openshell_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2515,7 +3308,7 @@ func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{30} + return file_openshell_proto_rawDescGZIP(), []int{44} } func (x *ListSandboxesResponse) GetSandboxes() []*Sandbox { @@ -2535,7 +3328,7 @@ type ListSandboxProvidersResponse struct { func (x *ListSandboxProvidersResponse) Reset() { *x = ListSandboxProvidersResponse{} - mi := &file_openshell_proto_msgTypes[31] + mi := &file_openshell_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2547,7 +3340,7 @@ func (x *ListSandboxProvidersResponse) String() string { func (*ListSandboxProvidersResponse) ProtoMessage() {} func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[31] + mi := &file_openshell_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2560,7 +3353,7 @@ func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxProvidersResponse.ProtoReflect.Descriptor instead. func (*ListSandboxProvidersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{31} + return file_openshell_proto_rawDescGZIP(), []int{45} } func (x *ListSandboxProvidersResponse) GetProviders() []*datamodelv1.Provider { @@ -2582,7 +3375,7 @@ type AttachSandboxProviderResponse struct { func (x *AttachSandboxProviderResponse) Reset() { *x = AttachSandboxProviderResponse{} - mi := &file_openshell_proto_msgTypes[32] + mi := &file_openshell_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2594,7 +3387,7 @@ func (x *AttachSandboxProviderResponse) String() string { func (*AttachSandboxProviderResponse) ProtoMessage() {} func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[32] + mi := &file_openshell_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2607,7 +3400,7 @@ func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AttachSandboxProviderResponse.ProtoReflect.Descriptor instead. func (*AttachSandboxProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{32} + return file_openshell_proto_rawDescGZIP(), []int{46} } func (x *AttachSandboxProviderResponse) GetSandbox() *Sandbox { @@ -2636,7 +3429,7 @@ type DetachSandboxProviderResponse struct { func (x *DetachSandboxProviderResponse) Reset() { *x = DetachSandboxProviderResponse{} - mi := &file_openshell_proto_msgTypes[33] + mi := &file_openshell_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2648,7 +3441,7 @@ func (x *DetachSandboxProviderResponse) String() string { func (*DetachSandboxProviderResponse) ProtoMessage() {} func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[33] + mi := &file_openshell_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2661,7 +3454,7 @@ func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DetachSandboxProviderResponse.ProtoReflect.Descriptor instead. func (*DetachSandboxProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{33} + return file_openshell_proto_rawDescGZIP(), []int{47} } func (x *DetachSandboxProviderResponse) GetSandbox() *Sandbox { @@ -2688,7 +3481,7 @@ type DeleteSandboxResponse struct { func (x *DeleteSandboxResponse) Reset() { *x = DeleteSandboxResponse{} - mi := &file_openshell_proto_msgTypes[34] + mi := &file_openshell_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2700,7 +3493,7 @@ func (x *DeleteSandboxResponse) String() string { func (*DeleteSandboxResponse) ProtoMessage() {} func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[34] + mi := &file_openshell_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2713,7 +3506,7 @@ func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteSandboxResponse.ProtoReflect.Descriptor instead. func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{34} + return file_openshell_proto_rawDescGZIP(), []int{48} } func (x *DeleteSandboxResponse) GetDeleted() bool { @@ -2734,7 +3527,7 @@ type CreateSshSessionRequest struct { func (x *CreateSshSessionRequest) Reset() { *x = CreateSshSessionRequest{} - mi := &file_openshell_proto_msgTypes[35] + mi := &file_openshell_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2746,7 +3539,7 @@ func (x *CreateSshSessionRequest) String() string { func (*CreateSshSessionRequest) ProtoMessage() {} func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[35] + mi := &file_openshell_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2759,7 +3552,7 @@ func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSshSessionRequest.ProtoReflect.Descriptor instead. func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{35} + return file_openshell_proto_rawDescGZIP(), []int{49} } func (x *CreateSshSessionRequest) GetSandboxId() string { @@ -2802,7 +3595,7 @@ type CreateSshSessionResponse struct { func (x *CreateSshSessionResponse) Reset() { *x = CreateSshSessionResponse{} - mi := &file_openshell_proto_msgTypes[36] + mi := &file_openshell_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2814,7 +3607,7 @@ func (x *CreateSshSessionResponse) String() string { func (*CreateSshSessionResponse) ProtoMessage() {} func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[36] + mi := &file_openshell_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2827,7 +3620,7 @@ func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSshSessionResponse.ProtoReflect.Descriptor instead. func (*CreateSshSessionResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{36} + return file_openshell_proto_rawDescGZIP(), []int{50} } func (x *CreateSshSessionResponse) GetSandboxId() string { @@ -2898,7 +3691,7 @@ type ExposeServiceRequest struct { func (x *ExposeServiceRequest) Reset() { *x = ExposeServiceRequest{} - mi := &file_openshell_proto_msgTypes[37] + mi := &file_openshell_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2910,7 +3703,7 @@ func (x *ExposeServiceRequest) String() string { func (*ExposeServiceRequest) ProtoMessage() {} func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[37] + mi := &file_openshell_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2923,7 +3716,7 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{37} + return file_openshell_proto_rawDescGZIP(), []int{51} } func (x *ExposeServiceRequest) GetSandbox() string { @@ -2976,7 +3769,7 @@ type GetServiceRequest struct { func (x *GetServiceRequest) Reset() { *x = GetServiceRequest{} - mi := &file_openshell_proto_msgTypes[38] + mi := &file_openshell_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2988,7 +3781,7 @@ func (x *GetServiceRequest) String() string { func (*GetServiceRequest) ProtoMessage() {} func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[38] + mi := &file_openshell_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3001,7 +3794,7 @@ func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetServiceRequest.ProtoReflect.Descriptor instead. func (*GetServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{38} + return file_openshell_proto_rawDescGZIP(), []int{52} } func (x *GetServiceRequest) GetSandbox() string { @@ -3044,7 +3837,7 @@ type ListServicesRequest struct { func (x *ListServicesRequest) Reset() { *x = ListServicesRequest{} - mi := &file_openshell_proto_msgTypes[39] + mi := &file_openshell_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3056,7 +3849,7 @@ func (x *ListServicesRequest) String() string { func (*ListServicesRequest) ProtoMessage() {} func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[39] + mi := &file_openshell_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3069,7 +3862,7 @@ func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead. func (*ListServicesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{39} + return file_openshell_proto_rawDescGZIP(), []int{53} } func (x *ListServicesRequest) GetSandbox() string { @@ -3117,7 +3910,7 @@ type ListServicesResponse struct { func (x *ListServicesResponse) Reset() { *x = ListServicesResponse{} - mi := &file_openshell_proto_msgTypes[40] + mi := &file_openshell_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3129,7 +3922,7 @@ func (x *ListServicesResponse) String() string { func (*ListServicesResponse) ProtoMessage() {} func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[40] + mi := &file_openshell_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3142,7 +3935,7 @@ func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead. func (*ListServicesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{40} + return file_openshell_proto_rawDescGZIP(), []int{54} } func (x *ListServicesResponse) GetServices() []*ServiceEndpointResponse { @@ -3167,7 +3960,7 @@ type DeleteServiceRequest struct { func (x *DeleteServiceRequest) Reset() { *x = DeleteServiceRequest{} - mi := &file_openshell_proto_msgTypes[41] + mi := &file_openshell_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3179,7 +3972,7 @@ func (x *DeleteServiceRequest) String() string { func (*DeleteServiceRequest) ProtoMessage() {} func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[41] + mi := &file_openshell_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3192,7 +3985,7 @@ func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteServiceRequest.ProtoReflect.Descriptor instead. func (*DeleteServiceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{41} + return file_openshell_proto_rawDescGZIP(), []int{55} } func (x *DeleteServiceRequest) GetSandbox() string { @@ -3227,7 +4020,7 @@ type DeleteServiceResponse struct { func (x *DeleteServiceResponse) Reset() { *x = DeleteServiceResponse{} - mi := &file_openshell_proto_msgTypes[42] + mi := &file_openshell_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3239,7 +4032,7 @@ func (x *DeleteServiceResponse) String() string { func (*DeleteServiceResponse) ProtoMessage() {} func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[42] + mi := &file_openshell_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3252,7 +4045,7 @@ func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteServiceResponse.ProtoReflect.Descriptor instead. func (*DeleteServiceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{42} + return file_openshell_proto_rawDescGZIP(), []int{56} } func (x *DeleteServiceResponse) GetDeleted() bool { @@ -3283,7 +4076,7 @@ type ServiceEndpoint struct { func (x *ServiceEndpoint) Reset() { *x = ServiceEndpoint{} - mi := &file_openshell_proto_msgTypes[43] + mi := &file_openshell_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3295,7 +4088,7 @@ func (x *ServiceEndpoint) String() string { func (*ServiceEndpoint) ProtoMessage() {} func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[43] + mi := &file_openshell_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3308,7 +4101,7 @@ func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceEndpoint.ProtoReflect.Descriptor instead. func (*ServiceEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{43} + return file_openshell_proto_rawDescGZIP(), []int{57} } func (x *ServiceEndpoint) GetMetadata() *datamodelv1.ObjectMeta { @@ -3364,7 +4157,7 @@ type ServiceEndpointResponse struct { func (x *ServiceEndpointResponse) Reset() { *x = ServiceEndpointResponse{} - mi := &file_openshell_proto_msgTypes[44] + mi := &file_openshell_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3376,7 +4169,7 @@ func (x *ServiceEndpointResponse) String() string { func (*ServiceEndpointResponse) ProtoMessage() {} func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[44] + mi := &file_openshell_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3389,7 +4182,7 @@ func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceEndpointResponse.ProtoReflect.Descriptor instead. func (*ServiceEndpointResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{44} + return file_openshell_proto_rawDescGZIP(), []int{58} } func (x *ServiceEndpointResponse) GetEndpoint() *ServiceEndpoint { @@ -3417,7 +4210,7 @@ type RevokeSshSessionRequest struct { func (x *RevokeSshSessionRequest) Reset() { *x = RevokeSshSessionRequest{} - mi := &file_openshell_proto_msgTypes[45] + mi := &file_openshell_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3429,7 +4222,7 @@ func (x *RevokeSshSessionRequest) String() string { func (*RevokeSshSessionRequest) ProtoMessage() {} func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[45] + mi := &file_openshell_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3442,7 +4235,7 @@ func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeSshSessionRequest.ProtoReflect.Descriptor instead. func (*RevokeSshSessionRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{45} + return file_openshell_proto_rawDescGZIP(), []int{59} } func (x *RevokeSshSessionRequest) GetToken() string { @@ -3463,7 +4256,7 @@ type RevokeSshSessionResponse struct { func (x *RevokeSshSessionResponse) Reset() { *x = RevokeSshSessionResponse{} - mi := &file_openshell_proto_msgTypes[46] + mi := &file_openshell_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3475,7 +4268,7 @@ func (x *RevokeSshSessionResponse) String() string { func (*RevokeSshSessionResponse) ProtoMessage() {} func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[46] + mi := &file_openshell_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3488,7 +4281,7 @@ func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeSshSessionResponse.ProtoReflect.Descriptor instead. func (*RevokeSshSessionResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{46} + return file_openshell_proto_rawDescGZIP(), []int{60} } func (x *RevokeSshSessionResponse) GetRevoked() bool { @@ -3531,7 +4324,7 @@ type ExecSandboxRequest struct { func (x *ExecSandboxRequest) Reset() { *x = ExecSandboxRequest{} - mi := &file_openshell_proto_msgTypes[47] + mi := &file_openshell_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3543,7 +4336,7 @@ func (x *ExecSandboxRequest) String() string { func (*ExecSandboxRequest) ProtoMessage() {} func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[47] + mi := &file_openshell_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3556,7 +4349,7 @@ func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxRequest.ProtoReflect.Descriptor instead. func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{47} + return file_openshell_proto_rawDescGZIP(), []int{61} } func (x *ExecSandboxRequest) GetSandboxId() string { @@ -3639,7 +4432,7 @@ type ExecSandboxStdout struct { func (x *ExecSandboxStdout) Reset() { *x = ExecSandboxStdout{} - mi := &file_openshell_proto_msgTypes[48] + mi := &file_openshell_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3651,7 +4444,7 @@ func (x *ExecSandboxStdout) String() string { func (*ExecSandboxStdout) ProtoMessage() {} func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[48] + mi := &file_openshell_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3664,7 +4457,7 @@ func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxStdout.ProtoReflect.Descriptor instead. func (*ExecSandboxStdout) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{48} + return file_openshell_proto_rawDescGZIP(), []int{62} } func (x *ExecSandboxStdout) GetData() []byte { @@ -3684,7 +4477,7 @@ type ExecSandboxStderr struct { func (x *ExecSandboxStderr) Reset() { *x = ExecSandboxStderr{} - mi := &file_openshell_proto_msgTypes[49] + mi := &file_openshell_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3696,7 +4489,7 @@ func (x *ExecSandboxStderr) String() string { func (*ExecSandboxStderr) ProtoMessage() {} func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[49] + mi := &file_openshell_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3709,7 +4502,7 @@ func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxStderr.ProtoReflect.Descriptor instead. func (*ExecSandboxStderr) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{49} + return file_openshell_proto_rawDescGZIP(), []int{63} } func (x *ExecSandboxStderr) GetData() []byte { @@ -3729,7 +4522,7 @@ type ExecSandboxExit struct { func (x *ExecSandboxExit) Reset() { *x = ExecSandboxExit{} - mi := &file_openshell_proto_msgTypes[50] + mi := &file_openshell_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3741,7 +4534,7 @@ func (x *ExecSandboxExit) String() string { func (*ExecSandboxExit) ProtoMessage() {} func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[50] + mi := &file_openshell_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3754,7 +4547,7 @@ func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxExit.ProtoReflect.Descriptor instead. func (*ExecSandboxExit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{50} + return file_openshell_proto_rawDescGZIP(), []int{64} } func (x *ExecSandboxExit) GetExitCode() int32 { @@ -3779,7 +4572,7 @@ type ExecSandboxEvent struct { func (x *ExecSandboxEvent) Reset() { *x = ExecSandboxEvent{} - mi := &file_openshell_proto_msgTypes[51] + mi := &file_openshell_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3791,7 +4584,7 @@ func (x *ExecSandboxEvent) String() string { func (*ExecSandboxEvent) ProtoMessage() {} func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[51] + mi := &file_openshell_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3804,7 +4597,7 @@ func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxEvent.ProtoReflect.Descriptor instead. func (*ExecSandboxEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{51} + return file_openshell_proto_rawDescGZIP(), []int{65} } func (x *ExecSandboxEvent) GetPayload() isExecSandboxEvent_Payload { @@ -3886,7 +4679,7 @@ type TcpForwardInit struct { func (x *TcpForwardInit) Reset() { *x = TcpForwardInit{} - mi := &file_openshell_proto_msgTypes[52] + mi := &file_openshell_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3898,7 +4691,7 @@ func (x *TcpForwardInit) String() string { func (*TcpForwardInit) ProtoMessage() {} func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[52] + mi := &file_openshell_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3911,7 +4704,7 @@ func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpForwardInit.ProtoReflect.Descriptor instead. func (*TcpForwardInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{52} + return file_openshell_proto_rawDescGZIP(), []int{66} } func (x *TcpForwardInit) GetSandboxId() string { @@ -3990,7 +4783,7 @@ type TcpForwardFrame struct { func (x *TcpForwardFrame) Reset() { *x = TcpForwardFrame{} - mi := &file_openshell_proto_msgTypes[53] + mi := &file_openshell_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4002,7 +4795,7 @@ func (x *TcpForwardFrame) String() string { func (*TcpForwardFrame) ProtoMessage() {} func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[53] + mi := &file_openshell_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4015,7 +4808,7 @@ func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpForwardFrame.ProtoReflect.Descriptor instead. func (*TcpForwardFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{53} + return file_openshell_proto_rawDescGZIP(), []int{67} } func (x *TcpForwardFrame) GetPayload() isTcpForwardFrame_Payload { @@ -4074,7 +4867,7 @@ type ExecSandboxInput struct { func (x *ExecSandboxInput) Reset() { *x = ExecSandboxInput{} - mi := &file_openshell_proto_msgTypes[54] + mi := &file_openshell_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4086,7 +4879,7 @@ func (x *ExecSandboxInput) String() string { func (*ExecSandboxInput) ProtoMessage() {} func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[54] + mi := &file_openshell_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4099,7 +4892,7 @@ func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxInput.ProtoReflect.Descriptor instead. func (*ExecSandboxInput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{54} + return file_openshell_proto_rawDescGZIP(), []int{68} } func (x *ExecSandboxInput) GetPayload() isExecSandboxInput_Payload { @@ -4172,7 +4965,7 @@ type ExecSandboxWindowResize struct { func (x *ExecSandboxWindowResize) Reset() { *x = ExecSandboxWindowResize{} - mi := &file_openshell_proto_msgTypes[55] + mi := &file_openshell_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4184,7 +4977,7 @@ func (x *ExecSandboxWindowResize) String() string { func (*ExecSandboxWindowResize) ProtoMessage() {} func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[55] + mi := &file_openshell_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4197,7 +4990,7 @@ func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecSandboxWindowResize.ProtoReflect.Descriptor instead. func (*ExecSandboxWindowResize) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{55} + return file_openshell_proto_rawDescGZIP(), []int{69} } func (x *ExecSandboxWindowResize) GetCols() uint32 { @@ -4234,7 +5027,7 @@ type SshSession struct { func (x *SshSession) Reset() { *x = SshSession{} - mi := &file_openshell_proto_msgTypes[56] + mi := &file_openshell_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4246,7 +5039,7 @@ func (x *SshSession) String() string { func (*SshSession) ProtoMessage() {} func (x *SshSession) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[56] + mi := &file_openshell_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4259,7 +5052,7 @@ func (x *SshSession) ProtoReflect() protoreflect.Message { // Deprecated: Use SshSession.ProtoReflect.Descriptor instead. func (*SshSession) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{56} + return file_openshell_proto_rawDescGZIP(), []int{70} } func (x *SshSession) GetMetadata() *datamodelv1.ObjectMeta { @@ -4328,7 +5121,7 @@ type WatchSandboxRequest struct { func (x *WatchSandboxRequest) Reset() { *x = WatchSandboxRequest{} - mi := &file_openshell_proto_msgTypes[57] + mi := &file_openshell_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4340,7 +5133,7 @@ func (x *WatchSandboxRequest) String() string { func (*WatchSandboxRequest) ProtoMessage() {} func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[57] + mi := &file_openshell_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4353,7 +5146,7 @@ func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchSandboxRequest.ProtoReflect.Descriptor instead. func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{57} + return file_openshell_proto_rawDescGZIP(), []int{71} } func (x *WatchSandboxRequest) GetId() string { @@ -4443,7 +5236,7 @@ type SandboxStreamEvent struct { func (x *SandboxStreamEvent) Reset() { *x = SandboxStreamEvent{} - mi := &file_openshell_proto_msgTypes[58] + mi := &file_openshell_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4455,7 +5248,7 @@ func (x *SandboxStreamEvent) String() string { func (*SandboxStreamEvent) ProtoMessage() {} func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[58] + mi := &file_openshell_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4468,7 +5261,7 @@ func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStreamEvent.ProtoReflect.Descriptor instead. func (*SandboxStreamEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{58} + return file_openshell_proto_rawDescGZIP(), []int{72} } func (x *SandboxStreamEvent) GetPayload() isSandboxStreamEvent_Payload { @@ -4581,7 +5374,7 @@ type SandboxLogLine struct { func (x *SandboxLogLine) Reset() { *x = SandboxLogLine{} - mi := &file_openshell_proto_msgTypes[59] + mi := &file_openshell_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4593,7 +5386,7 @@ func (x *SandboxLogLine) String() string { func (*SandboxLogLine) ProtoMessage() {} func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[59] + mi := &file_openshell_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4606,7 +5399,7 @@ func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxLogLine.ProtoReflect.Descriptor instead. func (*SandboxLogLine) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{59} + return file_openshell_proto_rawDescGZIP(), []int{73} } func (x *SandboxLogLine) GetSandboxId() string { @@ -4667,7 +5460,7 @@ type SandboxStreamWarning struct { func (x *SandboxStreamWarning) Reset() { *x = SandboxStreamWarning{} - mi := &file_openshell_proto_msgTypes[60] + mi := &file_openshell_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4679,7 +5472,7 @@ func (x *SandboxStreamWarning) String() string { func (*SandboxStreamWarning) ProtoMessage() {} func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[60] + mi := &file_openshell_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4692,7 +5485,7 @@ func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStreamWarning.ProtoReflect.Descriptor instead. func (*SandboxStreamWarning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{60} + return file_openshell_proto_rawDescGZIP(), []int{74} } func (x *SandboxStreamWarning) GetMessage() string { @@ -4714,7 +5507,7 @@ type CreateProviderRequest struct { func (x *CreateProviderRequest) Reset() { *x = CreateProviderRequest{} - mi := &file_openshell_proto_msgTypes[61] + mi := &file_openshell_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4726,7 +5519,7 @@ func (x *CreateProviderRequest) String() string { func (*CreateProviderRequest) ProtoMessage() {} func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[61] + mi := &file_openshell_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4739,7 +5532,7 @@ func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateProviderRequest.ProtoReflect.Descriptor instead. func (*CreateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{61} + return file_openshell_proto_rawDescGZIP(), []int{75} } func (x *CreateProviderRequest) GetProvider() *datamodelv1.Provider { @@ -4768,7 +5561,7 @@ type GetProviderRequest struct { func (x *GetProviderRequest) Reset() { *x = GetProviderRequest{} - mi := &file_openshell_proto_msgTypes[62] + mi := &file_openshell_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4780,7 +5573,7 @@ func (x *GetProviderRequest) String() string { func (*GetProviderRequest) ProtoMessage() {} func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[62] + mi := &file_openshell_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4793,7 +5586,7 @@ func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRequest.ProtoReflect.Descriptor instead. func (*GetProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{62} + return file_openshell_proto_rawDescGZIP(), []int{76} } func (x *GetProviderRequest) GetName() string { @@ -4825,7 +5618,7 @@ type ListProvidersRequest struct { func (x *ListProvidersRequest) Reset() { *x = ListProvidersRequest{} - mi := &file_openshell_proto_msgTypes[63] + mi := &file_openshell_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4837,7 +5630,7 @@ func (x *ListProvidersRequest) String() string { func (*ListProvidersRequest) ProtoMessage() {} func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[63] + mi := &file_openshell_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4850,7 +5643,7 @@ func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProvidersRequest.ProtoReflect.Descriptor instead. func (*ListProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{63} + return file_openshell_proto_rawDescGZIP(), []int{77} } func (x *ListProvidersRequest) GetLimit() uint32 { @@ -4896,7 +5689,7 @@ type UpdateProviderRequest struct { func (x *UpdateProviderRequest) Reset() { *x = UpdateProviderRequest{} - mi := &file_openshell_proto_msgTypes[64] + mi := &file_openshell_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4908,7 +5701,7 @@ func (x *UpdateProviderRequest) String() string { func (*UpdateProviderRequest) ProtoMessage() {} func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[64] + mi := &file_openshell_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4921,7 +5714,7 @@ func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{64} + return file_openshell_proto_rawDescGZIP(), []int{78} } func (x *UpdateProviderRequest) GetProvider() *datamodelv1.Provider { @@ -4957,7 +5750,7 @@ type DeleteProviderRequest struct { func (x *DeleteProviderRequest) Reset() { *x = DeleteProviderRequest{} - mi := &file_openshell_proto_msgTypes[65] + mi := &file_openshell_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4969,7 +5762,7 @@ func (x *DeleteProviderRequest) String() string { func (*DeleteProviderRequest) ProtoMessage() {} func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[65] + mi := &file_openshell_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4982,7 +5775,7 @@ func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{65} + return file_openshell_proto_rawDescGZIP(), []int{79} } func (x *DeleteProviderRequest) GetName() string { @@ -5009,7 +5802,7 @@ type ProviderResponse struct { func (x *ProviderResponse) Reset() { *x = ProviderResponse{} - mi := &file_openshell_proto_msgTypes[66] + mi := &file_openshell_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5021,7 +5814,7 @@ func (x *ProviderResponse) String() string { func (*ProviderResponse) ProtoMessage() {} func (x *ProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[66] + mi := &file_openshell_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5034,7 +5827,7 @@ func (x *ProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderResponse.ProtoReflect.Descriptor instead. func (*ProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{66} + return file_openshell_proto_rawDescGZIP(), []int{80} } func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { @@ -5054,7 +5847,7 @@ type ListProvidersResponse struct { func (x *ListProvidersResponse) Reset() { *x = ListProvidersResponse{} - mi := &file_openshell_proto_msgTypes[67] + mi := &file_openshell_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5066,7 +5859,7 @@ func (x *ListProvidersResponse) String() string { func (*ListProvidersResponse) ProtoMessage() {} func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[67] + mi := &file_openshell_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5079,7 +5872,7 @@ func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProvidersResponse.ProtoReflect.Descriptor instead. func (*ListProvidersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{67} + return file_openshell_proto_rawDescGZIP(), []int{81} } func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { @@ -5103,7 +5896,7 @@ type ListProviderProfilesRequest struct { func (x *ListProviderProfilesRequest) Reset() { *x = ListProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[68] + mi := &file_openshell_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5115,7 +5908,7 @@ func (x *ListProviderProfilesRequest) String() string { func (*ListProviderProfilesRequest) ProtoMessage() {} func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[68] + mi := &file_openshell_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5128,7 +5921,7 @@ func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ListProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{68} + return file_openshell_proto_rawDescGZIP(), []int{82} } func (x *ListProviderProfilesRequest) GetLimit() uint32 { @@ -5166,7 +5959,7 @@ type GetProviderProfileRequest struct { func (x *GetProviderProfileRequest) Reset() { *x = GetProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[69] + mi := &file_openshell_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5178,7 +5971,7 @@ func (x *GetProviderProfileRequest) String() string { func (*GetProviderProfileRequest) ProtoMessage() {} func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[69] + mi := &file_openshell_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5191,7 +5984,7 @@ func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderProfileRequest.ProtoReflect.Descriptor instead. func (*GetProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{69} + return file_openshell_proto_rawDescGZIP(), []int{83} } func (x *GetProviderProfileRequest) GetId() string { @@ -5219,7 +6012,7 @@ type ProviderProfileImportItem struct { func (x *ProviderProfileImportItem) Reset() { *x = ProviderProfileImportItem{} - mi := &file_openshell_proto_msgTypes[70] + mi := &file_openshell_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5231,7 +6024,7 @@ func (x *ProviderProfileImportItem) String() string { func (*ProviderProfileImportItem) ProtoMessage() {} func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[70] + mi := &file_openshell_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5244,7 +6037,7 @@ func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileImportItem.ProtoReflect.Descriptor instead. func (*ProviderProfileImportItem) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{70} + return file_openshell_proto_rawDescGZIP(), []int{84} } func (x *ProviderProfileImportItem) GetProfile() *ProviderProfile { @@ -5275,7 +6068,7 @@ type ProviderProfileDiagnostic struct { func (x *ProviderProfileDiagnostic) Reset() { *x = ProviderProfileDiagnostic{} - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5287,7 +6080,7 @@ func (x *ProviderProfileDiagnostic) String() string { func (*ProviderProfileDiagnostic) ProtoMessage() {} func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5300,7 +6093,7 @@ func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiagnostic.ProtoReflect.Descriptor instead. func (*ProviderProfileDiagnostic) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{71} + return file_openshell_proto_rawDescGZIP(), []int{85} } func (x *ProviderProfileDiagnostic) GetSource() string { @@ -5357,7 +6150,7 @@ type ProviderCredentialTokenGrantAudienceOverride struct { func (x *ProviderCredentialTokenGrantAudienceOverride) Reset() { *x = ProviderCredentialTokenGrantAudienceOverride{} - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5369,7 +6162,7 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) String() string { func (*ProviderCredentialTokenGrantAudienceOverride) ProtoMessage() {} func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5382,7 +6175,7 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protorefle // Deprecated: Use ProviderCredentialTokenGrantAudienceOverride.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrantAudienceOverride) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{72} + return file_openshell_proto_rawDescGZIP(), []int{86} } func (x *ProviderCredentialTokenGrantAudienceOverride) GetHost() string { @@ -5436,7 +6229,7 @@ type ProviderCredentialTokenGrantSubjectToken struct { func (x *ProviderCredentialTokenGrantSubjectToken) Reset() { *x = ProviderCredentialTokenGrantSubjectToken{} - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5448,7 +6241,7 @@ func (x *ProviderCredentialTokenGrantSubjectToken) String() string { func (*ProviderCredentialTokenGrantSubjectToken) ProtoMessage() {} func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5461,7 +6254,7 @@ func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.M // Deprecated: Use ProviderCredentialTokenGrantSubjectToken.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrantSubjectToken) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{73} + return file_openshell_proto_rawDescGZIP(), []int{87} } func (x *ProviderCredentialTokenGrantSubjectToken) GetSource() string { @@ -5518,7 +6311,7 @@ type ProviderCredentialTokenGrant struct { func (x *ProviderCredentialTokenGrant) Reset() { *x = ProviderCredentialTokenGrant{} - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5530,7 +6323,7 @@ func (x *ProviderCredentialTokenGrant) String() string { func (*ProviderCredentialTokenGrant) ProtoMessage() {} func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5543,7 +6336,7 @@ func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialTokenGrant.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrant) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{74} + return file_openshell_proto_rawDescGZIP(), []int{88} } func (x *ProviderCredentialTokenGrant) GetTokenEndpoint() string { @@ -5635,7 +6428,7 @@ type ProviderProfileCredential struct { func (x *ProviderProfileCredential) Reset() { *x = ProviderProfileCredential{} - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5647,7 +6440,7 @@ func (x *ProviderProfileCredential) String() string { func (*ProviderProfileCredential) ProtoMessage() {} func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5660,7 +6453,7 @@ func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileCredential.ProtoReflect.Descriptor instead. func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{75} + return file_openshell_proto_rawDescGZIP(), []int{89} } func (x *ProviderProfileCredential) GetName() string { @@ -5745,7 +6538,7 @@ type ProviderCredentialRefreshMaterial struct { func (x *ProviderCredentialRefreshMaterial) Reset() { *x = ProviderCredentialRefreshMaterial{} - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5757,7 +6550,7 @@ func (x *ProviderCredentialRefreshMaterial) String() string { func (*ProviderCredentialRefreshMaterial) ProtoMessage() {} func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5770,7 +6563,7 @@ func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message // Deprecated: Use ProviderCredentialRefreshMaterial.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshMaterial) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{76} + return file_openshell_proto_rawDescGZIP(), []int{90} } func (x *ProviderCredentialRefreshMaterial) GetName() string { @@ -5815,7 +6608,7 @@ type ProviderCredentialRefreshOutput struct { func (x *ProviderCredentialRefreshOutput) Reset() { *x = ProviderCredentialRefreshOutput{} - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5827,7 +6620,7 @@ func (x *ProviderCredentialRefreshOutput) String() string { func (*ProviderCredentialRefreshOutput) ProtoMessage() {} func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5840,7 +6633,7 @@ func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshOutput.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshOutput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{77} + return file_openshell_proto_rawDescGZIP(), []int{91} } func (x *ProviderCredentialRefreshOutput) GetOutput() string { @@ -5872,7 +6665,7 @@ type ProviderCredentialRefresh struct { func (x *ProviderCredentialRefresh) Reset() { *x = ProviderCredentialRefresh{} - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5884,7 +6677,7 @@ func (x *ProviderCredentialRefresh) String() string { func (*ProviderCredentialRefresh) ProtoMessage() {} func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5897,7 +6690,7 @@ func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefresh.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefresh) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{78} + return file_openshell_proto_rawDescGZIP(), []int{92} } func (x *ProviderCredentialRefresh) GetStrategy() ProviderCredentialRefreshStrategy { @@ -5980,7 +6773,7 @@ type ProviderCredentialRefreshStatus struct { func (x *ProviderCredentialRefreshStatus) Reset() { *x = ProviderCredentialRefreshStatus{} - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5992,7 +6785,7 @@ func (x *ProviderCredentialRefreshStatus) String() string { func (*ProviderCredentialRefreshStatus) ProtoMessage() {} func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6005,7 +6798,7 @@ func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshStatus.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{79} + return file_openshell_proto_rawDescGZIP(), []int{93} } func (x *ProviderCredentialRefreshStatus) GetProviderName() string { @@ -6110,7 +6903,7 @@ type ProviderProfileDiscovery struct { func (x *ProviderProfileDiscovery) Reset() { *x = ProviderProfileDiscovery{} - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6122,7 +6915,7 @@ func (x *ProviderProfileDiscovery) String() string { func (*ProviderProfileDiscovery) ProtoMessage() {} func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6135,7 +6928,7 @@ func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiscovery.ProtoReflect.Descriptor instead. func (*ProviderProfileDiscovery) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{80} + return file_openshell_proto_rawDescGZIP(), []int{94} } func (x *ProviderProfileDiscovery) GetCredentials() []string { @@ -6199,7 +6992,7 @@ type StoredProviderCredentialRefreshState struct { func (x *StoredProviderCredentialRefreshState) Reset() { *x = StoredProviderCredentialRefreshState{} - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6211,7 +7004,7 @@ func (x *StoredProviderCredentialRefreshState) String() string { func (*StoredProviderCredentialRefreshState) ProtoMessage() {} func (x *StoredProviderCredentialRefreshState) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6224,7 +7017,7 @@ func (x *StoredProviderCredentialRefreshState) ProtoReflect() protoreflect.Messa // Deprecated: Use StoredProviderCredentialRefreshState.ProtoReflect.Descriptor instead. func (*StoredProviderCredentialRefreshState) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{81} + return file_openshell_proto_rawDescGZIP(), []int{95} } func (x *StoredProviderCredentialRefreshState) GetMetadata() *datamodelv1.ObjectMeta { @@ -6407,7 +7200,7 @@ type StoredRefreshMaterialDeletion struct { func (x *StoredRefreshMaterialDeletion) Reset() { *x = StoredRefreshMaterialDeletion{} - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6419,7 +7212,7 @@ func (x *StoredRefreshMaterialDeletion) String() string { func (*StoredRefreshMaterialDeletion) ProtoMessage() {} func (x *StoredRefreshMaterialDeletion) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6432,7 +7225,7 @@ func (x *StoredRefreshMaterialDeletion) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredRefreshMaterialDeletion.ProtoReflect.Descriptor instead. func (*StoredRefreshMaterialDeletion) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{82} + return file_openshell_proto_rawDescGZIP(), []int{96} } func (x *StoredRefreshMaterialDeletion) GetMaterialKey() string { @@ -6461,7 +7254,7 @@ type GetProviderRefreshStatusRequest struct { func (x *GetProviderRefreshStatusRequest) Reset() { *x = GetProviderRefreshStatusRequest{} - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6473,7 +7266,7 @@ func (x *GetProviderRefreshStatusRequest) String() string { func (*GetProviderRefreshStatusRequest) ProtoMessage() {} func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6486,7 +7279,7 @@ func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusRequest.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{83} + return file_openshell_proto_rawDescGZIP(), []int{97} } func (x *GetProviderRefreshStatusRequest) GetProvider() string { @@ -6519,7 +7312,7 @@ type GetProviderRefreshStatusResponse struct { func (x *GetProviderRefreshStatusResponse) Reset() { *x = GetProviderRefreshStatusResponse{} - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6531,7 +7324,7 @@ func (x *GetProviderRefreshStatusResponse) String() string { func (*GetProviderRefreshStatusResponse) ProtoMessage() {} func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6544,7 +7337,7 @@ func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusResponse.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{84} + return file_openshell_proto_rawDescGZIP(), []int{98} } func (x *GetProviderRefreshStatusResponse) GetCredentials() []*ProviderCredentialRefreshStatus { @@ -6573,7 +7366,7 @@ type ConfigureProviderRefreshRequest struct { func (x *ConfigureProviderRefreshRequest) Reset() { *x = ConfigureProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6585,7 +7378,7 @@ func (x *ConfigureProviderRefreshRequest) String() string { func (*ConfigureProviderRefreshRequest) ProtoMessage() {} func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6598,7 +7391,7 @@ func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{85} + return file_openshell_proto_rawDescGZIP(), []int{99} } func (x *ConfigureProviderRefreshRequest) GetProvider() string { @@ -6659,7 +7452,7 @@ type ConfigureProviderRefreshResponse struct { func (x *ConfigureProviderRefreshResponse) Reset() { *x = ConfigureProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6671,7 +7464,7 @@ func (x *ConfigureProviderRefreshResponse) String() string { func (*ConfigureProviderRefreshResponse) ProtoMessage() {} func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6684,7 +7477,7 @@ func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{86} + return file_openshell_proto_rawDescGZIP(), []int{100} } func (x *ConfigureProviderRefreshResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -6706,7 +7499,7 @@ type RotateProviderCredentialRequest struct { func (x *RotateProviderCredentialRequest) Reset() { *x = RotateProviderCredentialRequest{} - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6718,7 +7511,7 @@ func (x *RotateProviderCredentialRequest) String() string { func (*RotateProviderCredentialRequest) ProtoMessage() {} func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6731,7 +7524,7 @@ func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialRequest.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{87} + return file_openshell_proto_rawDescGZIP(), []int{101} } func (x *RotateProviderCredentialRequest) GetProvider() string { @@ -6764,7 +7557,7 @@ type RotateProviderCredentialResponse struct { func (x *RotateProviderCredentialResponse) Reset() { *x = RotateProviderCredentialResponse{} - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6776,7 +7569,7 @@ func (x *RotateProviderCredentialResponse) String() string { func (*RotateProviderCredentialResponse) ProtoMessage() {} func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6789,7 +7582,7 @@ func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialResponse.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{88} + return file_openshell_proto_rawDescGZIP(), []int{102} } func (x *RotateProviderCredentialResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -6811,7 +7604,7 @@ type DeleteProviderRefreshRequest struct { func (x *DeleteProviderRefreshRequest) Reset() { *x = DeleteProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6823,7 +7616,7 @@ func (x *DeleteProviderRefreshRequest) String() string { func (*DeleteProviderRefreshRequest) ProtoMessage() {} func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6836,7 +7629,7 @@ func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{89} + return file_openshell_proto_rawDescGZIP(), []int{103} } func (x *DeleteProviderRefreshRequest) GetProvider() string { @@ -6869,7 +7662,7 @@ type DeleteProviderRefreshResponse struct { func (x *DeleteProviderRefreshResponse) Reset() { *x = DeleteProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6881,7 +7674,7 @@ func (x *DeleteProviderRefreshResponse) String() string { func (*DeleteProviderRefreshResponse) ProtoMessage() {} func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6894,7 +7687,7 @@ func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{90} + return file_openshell_proto_rawDescGZIP(), []int{104} } func (x *DeleteProviderRefreshResponse) GetDeleted() bool { @@ -6934,7 +7727,7 @@ type ProviderProfile struct { func (x *ProviderProfile) Reset() { *x = ProviderProfile{} - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6946,7 +7739,7 @@ func (x *ProviderProfile) String() string { func (*ProviderProfile) ProtoMessage() {} func (x *ProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6959,7 +7752,7 @@ func (x *ProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfile.ProtoReflect.Descriptor instead. func (*ProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{91} + return file_openshell_proto_rawDescGZIP(), []int{105} } func (x *ProviderProfile) GetId() string { @@ -7064,7 +7857,7 @@ type StoredProviderProfile struct { func (x *StoredProviderProfile) Reset() { *x = StoredProviderProfile{} - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7076,7 +7869,7 @@ func (x *StoredProviderProfile) String() string { func (*StoredProviderProfile) ProtoMessage() {} func (x *StoredProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7089,7 +7882,7 @@ func (x *StoredProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredProviderProfile.ProtoReflect.Descriptor instead. func (*StoredProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{92} + return file_openshell_proto_rawDescGZIP(), []int{106} } func (x *StoredProviderProfile) GetMetadata() *datamodelv1.ObjectMeta { @@ -7116,7 +7909,7 @@ type ProviderProfileResponse struct { func (x *ProviderProfileResponse) Reset() { *x = ProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7128,7 +7921,7 @@ func (x *ProviderProfileResponse) String() string { func (*ProviderProfileResponse) ProtoMessage() {} func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7141,7 +7934,7 @@ func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileResponse.ProtoReflect.Descriptor instead. func (*ProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{93} + return file_openshell_proto_rawDescGZIP(), []int{107} } func (x *ProviderProfileResponse) GetProfile() *ProviderProfile { @@ -7161,7 +7954,7 @@ type ListProviderProfilesResponse struct { func (x *ListProviderProfilesResponse) Reset() { *x = ListProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7173,7 +7966,7 @@ func (x *ListProviderProfilesResponse) String() string { func (*ListProviderProfilesResponse) ProtoMessage() {} func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7186,7 +7979,7 @@ func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{94} + return file_openshell_proto_rawDescGZIP(), []int{108} } func (x *ListProviderProfilesResponse) GetProfiles() []*ProviderProfile { @@ -7209,7 +8002,7 @@ type ImportProviderProfilesRequest struct { func (x *ImportProviderProfilesRequest) Reset() { *x = ImportProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7221,7 +8014,7 @@ func (x *ImportProviderProfilesRequest) String() string { func (*ImportProviderProfilesRequest) ProtoMessage() {} func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7234,7 +8027,7 @@ func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{95} + return file_openshell_proto_rawDescGZIP(), []int{109} } func (x *ImportProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { @@ -7263,7 +8056,7 @@ type ImportProviderProfilesResponse struct { func (x *ImportProviderProfilesResponse) Reset() { *x = ImportProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7275,7 +8068,7 @@ func (x *ImportProviderProfilesResponse) String() string { func (*ImportProviderProfilesResponse) ProtoMessage() {} func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7288,7 +8081,7 @@ func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{96} + return file_openshell_proto_rawDescGZIP(), []int{110} } func (x *ImportProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -7332,7 +8125,7 @@ type UpdateProviderProfilesRequest struct { func (x *UpdateProviderProfilesRequest) Reset() { *x = UpdateProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7344,7 +8137,7 @@ func (x *UpdateProviderProfilesRequest) String() string { func (*UpdateProviderProfilesRequest) ProtoMessage() {} func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7357,7 +8150,7 @@ func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{97} + return file_openshell_proto_rawDescGZIP(), []int{111} } func (x *UpdateProviderProfilesRequest) GetProfile() *ProviderProfileImportItem { @@ -7400,7 +8193,7 @@ type UpdateProviderProfilesResponse struct { func (x *UpdateProviderProfilesResponse) Reset() { *x = UpdateProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7412,7 +8205,7 @@ func (x *UpdateProviderProfilesResponse) String() string { func (*UpdateProviderProfilesResponse) ProtoMessage() {} func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7425,7 +8218,7 @@ func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{98} + return file_openshell_proto_rawDescGZIP(), []int{112} } func (x *UpdateProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -7462,7 +8255,7 @@ type LintProviderProfilesRequest struct { func (x *LintProviderProfilesRequest) Reset() { *x = LintProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7474,7 +8267,7 @@ func (x *LintProviderProfilesRequest) String() string { func (*LintProviderProfilesRequest) ProtoMessage() {} func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7487,7 +8280,7 @@ func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*LintProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{99} + return file_openshell_proto_rawDescGZIP(), []int{113} } func (x *LintProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { @@ -7515,7 +8308,7 @@ type LintProviderProfilesResponse struct { func (x *LintProviderProfilesResponse) Reset() { *x = LintProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7527,7 +8320,7 @@ func (x *LintProviderProfilesResponse) String() string { func (*LintProviderProfilesResponse) ProtoMessage() {} func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7540,7 +8333,7 @@ func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*LintProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{100} + return file_openshell_proto_rawDescGZIP(), []int{114} } func (x *LintProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -7567,7 +8360,7 @@ type DeleteProviderResponse struct { func (x *DeleteProviderResponse) Reset() { *x = DeleteProviderResponse{} - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7579,7 +8372,7 @@ func (x *DeleteProviderResponse) String() string { func (*DeleteProviderResponse) ProtoMessage() {} func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7592,7 +8385,7 @@ func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{101} + return file_openshell_proto_rawDescGZIP(), []int{115} } func (x *DeleteProviderResponse) GetDeleted() bool { @@ -7615,7 +8408,7 @@ type DeleteProviderProfileRequest struct { func (x *DeleteProviderProfileRequest) Reset() { *x = DeleteProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7627,7 +8420,7 @@ func (x *DeleteProviderProfileRequest) String() string { func (*DeleteProviderProfileRequest) ProtoMessage() {} func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7640,7 +8433,7 @@ func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{102} + return file_openshell_proto_rawDescGZIP(), []int{116} } func (x *DeleteProviderProfileRequest) GetId() string { @@ -7667,7 +8460,7 @@ type DeleteProviderProfileResponse struct { func (x *DeleteProviderProfileResponse) Reset() { *x = DeleteProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7679,7 +8472,7 @@ func (x *DeleteProviderProfileResponse) String() string { func (*DeleteProviderProfileResponse) ProtoMessage() {} func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7692,7 +8485,7 @@ func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{103} + return file_openshell_proto_rawDescGZIP(), []int{117} } func (x *DeleteProviderProfileResponse) GetDeleted() bool { @@ -7717,7 +8510,7 @@ type GetSandboxProviderEnvironmentRequest struct { func (x *GetSandboxProviderEnvironmentRequest) Reset() { *x = GetSandboxProviderEnvironmentRequest{} - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7729,7 +8522,7 @@ func (x *GetSandboxProviderEnvironmentRequest) String() string { func (*GetSandboxProviderEnvironmentRequest) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7742,7 +8535,7 @@ func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use GetSandboxProviderEnvironmentRequest.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{104} + return file_openshell_proto_rawDescGZIP(), []int{118} } func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { @@ -7771,7 +8564,7 @@ type StaticCredentialEndpointBinding struct { func (x *StaticCredentialEndpointBinding) Reset() { *x = StaticCredentialEndpointBinding{} - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7783,7 +8576,7 @@ func (x *StaticCredentialEndpointBinding) String() string { func (*StaticCredentialEndpointBinding) ProtoMessage() {} func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7796,7 +8589,7 @@ func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialEndpointBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialEndpointBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{105} + return file_openshell_proto_rawDescGZIP(), []int{119} } func (x *StaticCredentialEndpointBinding) GetHost() string { @@ -7840,7 +8633,7 @@ type StaticCredentialBinding struct { func (x *StaticCredentialBinding) Reset() { *x = StaticCredentialBinding{} - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7852,7 +8645,7 @@ func (x *StaticCredentialBinding) String() string { func (*StaticCredentialBinding) ProtoMessage() {} func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7865,7 +8658,7 @@ func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{106} + return file_openshell_proto_rawDescGZIP(), []int{120} } func (x *StaticCredentialBinding) GetEndpoints() []*StaticCredentialEndpointBinding { @@ -7916,7 +8709,7 @@ type GetSandboxProviderEnvironmentResponse struct { func (x *GetSandboxProviderEnvironmentResponse) Reset() { *x = GetSandboxProviderEnvironmentResponse{} - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7928,7 +8721,7 @@ func (x *GetSandboxProviderEnvironmentResponse) String() string { func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7941,7 +8734,7 @@ func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{107} + return file_openshell_proto_rawDescGZIP(), []int{121} } func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { @@ -8003,7 +8796,7 @@ type ExchangeProviderSubjectTokenRequest struct { func (x *ExchangeProviderSubjectTokenRequest) Reset() { *x = ExchangeProviderSubjectTokenRequest{} - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8015,7 +8808,7 @@ func (x *ExchangeProviderSubjectTokenRequest) String() string { func (*ExchangeProviderSubjectTokenRequest) ProtoMessage() {} func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8028,7 +8821,7 @@ func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use ExchangeProviderSubjectTokenRequest.ProtoReflect.Descriptor instead. func (*ExchangeProviderSubjectTokenRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{108} + return file_openshell_proto_rawDescGZIP(), []int{122} } func (x *ExchangeProviderSubjectTokenRequest) GetSandboxId() string { @@ -8070,7 +8863,7 @@ type ExchangeProviderSubjectTokenResponse struct { func (x *ExchangeProviderSubjectTokenResponse) Reset() { *x = ExchangeProviderSubjectTokenResponse{} - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8082,7 +8875,7 @@ func (x *ExchangeProviderSubjectTokenResponse) String() string { func (*ExchangeProviderSubjectTokenResponse) ProtoMessage() {} func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8095,7 +8888,7 @@ func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use ExchangeProviderSubjectTokenResponse.ProtoReflect.Descriptor instead. func (*ExchangeProviderSubjectTokenResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{109} + return file_openshell_proto_rawDescGZIP(), []int{123} } func (x *ExchangeProviderSubjectTokenResponse) GetAccessToken() string { @@ -8166,7 +8959,7 @@ type UpdateConfigRequest struct { func (x *UpdateConfigRequest) Reset() { *x = UpdateConfigRequest{} - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8178,7 +8971,7 @@ func (x *UpdateConfigRequest) String() string { func (*UpdateConfigRequest) ProtoMessage() {} func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8191,7 +8984,7 @@ func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{110} + return file_openshell_proto_rawDescGZIP(), []int{124} } func (x *UpdateConfigRequest) GetName() string { @@ -8281,7 +9074,7 @@ type PolicyMergeOperation struct { func (x *PolicyMergeOperation) Reset() { *x = PolicyMergeOperation{} - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8293,7 +9086,7 @@ func (x *PolicyMergeOperation) String() string { func (*PolicyMergeOperation) ProtoMessage() {} func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8306,7 +9099,7 @@ func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyMergeOperation.ProtoReflect.Descriptor instead. func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{111} + return file_openshell_proto_rawDescGZIP(), []int{125} } func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { @@ -8420,7 +9213,7 @@ type AddNetworkRule struct { func (x *AddNetworkRule) Reset() { *x = AddNetworkRule{} - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8432,7 +9225,7 @@ func (x *AddNetworkRule) String() string { func (*AddNetworkRule) ProtoMessage() {} func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8445,7 +9238,7 @@ func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use AddNetworkRule.ProtoReflect.Descriptor instead. func (*AddNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{112} + return file_openshell_proto_rawDescGZIP(), []int{126} } func (x *AddNetworkRule) GetRuleName() string { @@ -8473,7 +9266,7 @@ type RemoveNetworkEndpoint struct { func (x *RemoveNetworkEndpoint) Reset() { *x = RemoveNetworkEndpoint{} - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8485,7 +9278,7 @@ func (x *RemoveNetworkEndpoint) String() string { func (*RemoveNetworkEndpoint) ProtoMessage() {} func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8498,7 +9291,7 @@ func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{113} + return file_openshell_proto_rawDescGZIP(), []int{127} } func (x *RemoveNetworkEndpoint) GetRuleName() string { @@ -8531,7 +9324,7 @@ type RemoveNetworkRule struct { func (x *RemoveNetworkRule) Reset() { *x = RemoveNetworkRule{} - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8543,7 +9336,7 @@ func (x *RemoveNetworkRule) String() string { func (*RemoveNetworkRule) ProtoMessage() {} func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8556,7 +9349,7 @@ func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkRule.ProtoReflect.Descriptor instead. func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{114} + return file_openshell_proto_rawDescGZIP(), []int{128} } func (x *RemoveNetworkRule) GetRuleName() string { @@ -8577,7 +9370,7 @@ type AddDenyRules struct { func (x *AddDenyRules) Reset() { *x = AddDenyRules{} - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8589,7 +9382,7 @@ func (x *AddDenyRules) String() string { func (*AddDenyRules) ProtoMessage() {} func (x *AddDenyRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8602,7 +9395,7 @@ func (x *AddDenyRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDenyRules.ProtoReflect.Descriptor instead. func (*AddDenyRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{115} + return file_openshell_proto_rawDescGZIP(), []int{129} } func (x *AddDenyRules) GetHost() string { @@ -8637,7 +9430,7 @@ type AddAllowRules struct { func (x *AddAllowRules) Reset() { *x = AddAllowRules{} - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8649,7 +9442,7 @@ func (x *AddAllowRules) String() string { func (*AddAllowRules) ProtoMessage() {} func (x *AddAllowRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8662,7 +9455,7 @@ func (x *AddAllowRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAllowRules.ProtoReflect.Descriptor instead. func (*AddAllowRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{116} + return file_openshell_proto_rawDescGZIP(), []int{130} } func (x *AddAllowRules) GetHost() string { @@ -8696,7 +9489,7 @@ type RemoveNetworkBinary struct { func (x *RemoveNetworkBinary) Reset() { *x = RemoveNetworkBinary{} - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8708,7 +9501,7 @@ func (x *RemoveNetworkBinary) String() string { func (*RemoveNetworkBinary) ProtoMessage() {} func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8721,7 +9514,7 @@ func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkBinary.ProtoReflect.Descriptor instead. func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{117} + return file_openshell_proto_rawDescGZIP(), []int{131} } func (x *RemoveNetworkBinary) GetRuleName() string { @@ -8757,7 +9550,7 @@ type UpdateConfigResponse struct { func (x *UpdateConfigResponse) Reset() { *x = UpdateConfigResponse{} - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8769,7 +9562,7 @@ func (x *UpdateConfigResponse) String() string { func (*UpdateConfigResponse) ProtoMessage() {} func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8782,7 +9575,7 @@ func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{118} + return file_openshell_proto_rawDescGZIP(), []int{132} } func (x *UpdateConfigResponse) GetVersion() uint32 { @@ -8837,7 +9630,7 @@ type GetSandboxPolicyStatusRequest struct { func (x *GetSandboxPolicyStatusRequest) Reset() { *x = GetSandboxPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8849,7 +9642,7 @@ func (x *GetSandboxPolicyStatusRequest) String() string { func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8862,7 +9655,7 @@ func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{119} + return file_openshell_proto_rawDescGZIP(), []int{133} } func (x *GetSandboxPolicyStatusRequest) GetName() string { @@ -8906,7 +9699,7 @@ type GetSandboxPolicyStatusResponse struct { func (x *GetSandboxPolicyStatusResponse) Reset() { *x = GetSandboxPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8918,7 +9711,7 @@ func (x *GetSandboxPolicyStatusResponse) String() string { func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8931,7 +9724,7 @@ func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{120} + return file_openshell_proto_rawDescGZIP(), []int{134} } func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { @@ -8965,7 +9758,7 @@ type ListSandboxPoliciesRequest struct { func (x *ListSandboxPoliciesRequest) Reset() { *x = ListSandboxPoliciesRequest{} - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8977,7 +9770,7 @@ func (x *ListSandboxPoliciesRequest) String() string { func (*ListSandboxPoliciesRequest) ProtoMessage() {} func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8990,7 +9783,7 @@ func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{121} + return file_openshell_proto_rawDescGZIP(), []int{135} } func (x *ListSandboxPoliciesRequest) GetName() string { @@ -9038,7 +9831,7 @@ type ListSandboxPoliciesResponse struct { func (x *ListSandboxPoliciesResponse) Reset() { *x = ListSandboxPoliciesResponse{} - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9050,7 +9843,7 @@ func (x *ListSandboxPoliciesResponse) String() string { func (*ListSandboxPoliciesResponse) ProtoMessage() {} func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9063,7 +9856,7 @@ func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{122} + return file_openshell_proto_rawDescGZIP(), []int{136} } func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { @@ -9090,7 +9883,7 @@ type ReportPolicyStatusRequest struct { func (x *ReportPolicyStatusRequest) Reset() { *x = ReportPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9102,7 +9895,7 @@ func (x *ReportPolicyStatusRequest) String() string { func (*ReportPolicyStatusRequest) ProtoMessage() {} func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9115,7 +9908,7 @@ func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{123} + return file_openshell_proto_rawDescGZIP(), []int{137} } func (x *ReportPolicyStatusRequest) GetSandboxId() string { @@ -9155,7 +9948,7 @@ type ReportPolicyStatusResponse struct { func (x *ReportPolicyStatusResponse) Reset() { *x = ReportPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9167,7 +9960,7 @@ func (x *ReportPolicyStatusResponse) String() string { func (*ReportPolicyStatusResponse) ProtoMessage() {} func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9180,7 +9973,7 @@ func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{124} + return file_openshell_proto_rawDescGZIP(), []int{138} } // A versioned policy revision with metadata. @@ -9208,7 +10001,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9220,7 +10013,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9233,7 +10026,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{125} + return file_openshell_proto_rawDescGZIP(), []int{139} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -9313,7 +10106,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9325,7 +10118,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9338,7 +10131,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{126} + return file_openshell_proto_rawDescGZIP(), []int{140} } func (x *GetSandboxLogsRequest) GetSandboxId() string { @@ -9396,7 +10189,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9408,7 +10201,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9421,7 +10214,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{127} + return file_openshell_proto_rawDescGZIP(), []int{141} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -9447,7 +10240,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9459,7 +10252,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9472,7 +10265,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{128} + return file_openshell_proto_rawDescGZIP(), []int{142} } // Get sandbox logs response. @@ -9488,7 +10281,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9500,7 +10293,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9513,7 +10306,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{129} + return file_openshell_proto_rawDescGZIP(), []int{143} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -9546,7 +10339,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9558,7 +10351,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9571,7 +10364,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{130} + return file_openshell_proto_rawDescGZIP(), []int{144} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -9662,7 +10455,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9674,7 +10467,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9687,7 +10480,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{131} + return file_openshell_proto_rawDescGZIP(), []int{145} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -9789,7 +10582,7 @@ type SupervisorHello struct { func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9801,7 +10594,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9814,7 +10607,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{132} + return file_openshell_proto_rawDescGZIP(), []int{146} } func (x *SupervisorHello) GetSandboxId() string { @@ -9844,7 +10637,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9856,7 +10649,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9869,7 +10662,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{133} + return file_openshell_proto_rawDescGZIP(), []int{147} } func (x *SessionAccepted) GetSessionId() string { @@ -9897,7 +10690,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9909,7 +10702,7 @@ func (x *SessionRejected) String() string { func (*SessionRejected) ProtoMessage() {} func (x *SessionRejected) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9922,7 +10715,7 @@ func (x *SessionRejected) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. func (*SessionRejected) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{134} + return file_openshell_proto_rawDescGZIP(), []int{148} } func (x *SessionRejected) GetReason() string { @@ -9941,7 +10734,7 @@ type SupervisorHeartbeat struct { func (x *SupervisorHeartbeat) Reset() { *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9953,7 +10746,7 @@ func (x *SupervisorHeartbeat) String() string { func (*SupervisorHeartbeat) ProtoMessage() {} func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9966,7 +10759,7 @@ func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{135} + return file_openshell_proto_rawDescGZIP(), []int{149} } // Gateway heartbeat. @@ -9978,7 +10771,7 @@ type GatewayHeartbeat struct { func (x *GatewayHeartbeat) Reset() { *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9990,7 +10783,7 @@ func (x *GatewayHeartbeat) String() string { func (*GatewayHeartbeat) ProtoMessage() {} func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10003,7 +10796,7 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{136} + return file_openshell_proto_rawDescGZIP(), []int{150} } // Terminal result reported before the supervisor shuts down. A successful RPC @@ -10020,7 +10813,7 @@ type ReportMainProcessExitRequest struct { func (x *ReportMainProcessExitRequest) Reset() { *x = ReportMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10032,7 +10825,7 @@ func (x *ReportMainProcessExitRequest) String() string { func (*ReportMainProcessExitRequest) ProtoMessage() {} func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10045,7 +10838,7 @@ func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{137} + return file_openshell_proto_rawDescGZIP(), []int{151} } func (x *ReportMainProcessExitRequest) GetSandboxId() string { @@ -10077,7 +10870,7 @@ type ReportMainProcessExitResponse struct { func (x *ReportMainProcessExitResponse) Reset() { *x = ReportMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10089,7 +10882,7 @@ func (x *ReportMainProcessExitResponse) String() string { func (*ReportMainProcessExitResponse) ProtoMessage() {} func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10102,7 +10895,7 @@ func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{138} + return file_openshell_proto_rawDescGZIP(), []int{152} } // Terminal-delivery completion reported after all expected foreground SSH @@ -10117,7 +10910,7 @@ type FinalizeMainProcessExitRequest struct { func (x *FinalizeMainProcessExitRequest) Reset() { *x = FinalizeMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10129,7 +10922,7 @@ func (x *FinalizeMainProcessExitRequest) String() string { func (*FinalizeMainProcessExitRequest) ProtoMessage() {} func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10142,7 +10935,7 @@ func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{139} + return file_openshell_proto_rawDescGZIP(), []int{153} } func (x *FinalizeMainProcessExitRequest) GetSandboxId() string { @@ -10167,7 +10960,7 @@ type FinalizeMainProcessExitResponse struct { func (x *FinalizeMainProcessExitResponse) Reset() { *x = FinalizeMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10179,7 +10972,7 @@ func (x *FinalizeMainProcessExitResponse) String() string { func (*FinalizeMainProcessExitResponse) ProtoMessage() {} func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10192,7 +10985,7 @@ func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{140} + return file_openshell_proto_rawDescGZIP(), []int{154} } // Gateway requests the supervisor to open a relay channel. @@ -10221,7 +11014,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10233,7 +11026,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10246,7 +11039,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{141} + return file_openshell_proto_rawDescGZIP(), []int{155} } func (x *RelayOpen) GetChannelId() string { @@ -10313,7 +11106,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10325,7 +11118,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10338,7 +11131,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{142} + return file_openshell_proto_rawDescGZIP(), []int{156} } // TCP target dialed by the supervisor from inside the sandbox. @@ -10354,7 +11147,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10366,7 +11159,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10379,7 +11172,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{143} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *TcpRelayTarget) GetHost() string { @@ -10407,7 +11200,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10419,7 +11212,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10432,7 +11225,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{144} + return file_openshell_proto_rawDescGZIP(), []int{158} } func (x *RelayInit) GetChannelId() string { @@ -10459,7 +11252,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10471,7 +11264,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10484,7 +11277,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{145} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -10543,7 +11336,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10555,7 +11348,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10568,7 +11361,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{146} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *RelayOpenResult) GetChannelId() string { @@ -10605,7 +11398,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10617,7 +11410,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10630,7 +11423,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{147} + return file_openshell_proto_rawDescGZIP(), []int{161} } func (x *RelayClose) GetChannelId() string { @@ -10664,7 +11457,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10676,7 +11469,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10689,7 +11482,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{148} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *L7RequestSample) GetMethod() string { @@ -10763,7 +11556,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10775,7 +11568,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10788,7 +11581,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{149} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *DenialSummary) GetSandboxId() string { @@ -10923,7 +11716,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10935,7 +11728,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10948,7 +11741,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{150} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -10981,7 +11774,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10993,7 +11786,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11006,7 +11799,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{151} + return file_openshell_proto_rawDescGZIP(), []int{165} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -11094,7 +11887,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11106,7 +11899,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11119,7 +11912,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{152} + return file_openshell_proto_rawDescGZIP(), []int{166} } func (x *PolicyChunk) GetId() string { @@ -11307,7 +12100,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11319,7 +12112,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11332,7 +12125,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{153} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -11390,7 +12183,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11402,7 +12195,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11415,7 +12208,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{168} } func (x *SubmitPolicyAnalysisRequest) GetSummaries() []*DenialSummary { @@ -11478,7 +12271,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11490,7 +12283,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11503,7 +12296,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -11549,7 +12342,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11561,7 +12354,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11574,7 +12367,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{170} } func (x *GetDraftPolicyRequest) GetName() string { @@ -11614,7 +12407,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11626,7 +12419,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11639,7 +12432,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -11688,7 +12481,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11700,7 +12493,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11713,7 +12506,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{172} } func (x *ApproveDraftChunkRequest) GetName() string { @@ -11756,7 +12549,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11768,7 +12561,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11781,7 +12574,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -11815,7 +12608,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11827,7 +12620,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11840,7 +12633,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{174} } func (x *RejectDraftChunkRequest) GetName() string { @@ -11879,7 +12672,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11891,7 +12684,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11904,7 +12697,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{175} } // Approve all pending chunks. @@ -11918,7 +12711,7 @@ type DraftChunkApproval struct { func (x *DraftChunkApproval) Reset() { *x = DraftChunkApproval{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11930,7 +12723,7 @@ func (x *DraftChunkApproval) String() string { func (*DraftChunkApproval) ProtoMessage() {} func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11943,7 +12736,7 @@ func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. func (*DraftChunkApproval) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *DraftChunkApproval) GetChunkId() string { @@ -11977,7 +12770,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11989,7 +12782,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12002,7 +12795,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *ApproveAllDraftChunksRequest) GetName() string { @@ -12050,7 +12843,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12062,7 +12855,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12075,7 +12868,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -12123,7 +12916,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12135,7 +12928,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12148,7 +12941,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{179} } func (x *EditDraftChunkRequest) GetName() string { @@ -12187,7 +12980,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12199,7 +12992,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12212,7 +13005,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{180} } // Reverse an approval (remove merged rule from active policy). @@ -12230,7 +13023,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12242,7 +13035,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12255,7 +13048,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *UndoDraftChunkRequest) GetName() string { @@ -12291,7 +13084,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12303,7 +13096,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12316,7 +13109,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -12346,7 +13139,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12358,7 +13151,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12371,7 +13164,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *ClearDraftChunksRequest) GetName() string { @@ -12398,7 +13191,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12410,7 +13203,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12423,7 +13216,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -12446,7 +13239,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12458,7 +13251,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12471,7 +13264,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{185} } func (x *GetDraftHistoryRequest) GetName() string { @@ -12505,7 +13298,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12517,7 +13310,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12530,7 +13323,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *DraftHistoryEntry) GetTimestampMs() int64 { @@ -12571,7 +13364,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12583,7 +13376,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12596,7 +13389,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{187} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -12625,7 +13418,7 @@ type PolicyRevisionPayload struct { func (x *PolicyRevisionPayload) Reset() { *x = PolicyRevisionPayload{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12637,7 +13430,7 @@ func (x *PolicyRevisionPayload) String() string { func (*PolicyRevisionPayload) ProtoMessage() {} func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12650,7 +13443,7 @@ func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyRevisionPayload.ProtoReflect.Descriptor instead. func (*PolicyRevisionPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *PolicyRevisionPayload) GetPolicy() *sandboxv1.SandboxPolicy { @@ -12729,7 +13522,7 @@ type DraftChunkPayload struct { func (x *DraftChunkPayload) Reset() { *x = DraftChunkPayload{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12741,7 +13534,7 @@ func (x *DraftChunkPayload) String() string { func (*DraftChunkPayload) ProtoMessage() {} func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12754,7 +13547,7 @@ func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkPayload.ProtoReflect.Descriptor instead. func (*DraftChunkPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{189} } func (x *DraftChunkPayload) GetRuleName() string { @@ -12902,7 +13695,7 @@ type StoredPolicyRevision struct { func (x *StoredPolicyRevision) Reset() { *x = StoredPolicyRevision{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12914,7 +13707,7 @@ func (x *StoredPolicyRevision) String() string { func (*StoredPolicyRevision) ProtoMessage() {} func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12927,7 +13720,7 @@ func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredPolicyRevision.ProtoReflect.Descriptor instead. func (*StoredPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{190} } func (x *StoredPolicyRevision) GetId() string { @@ -13036,7 +13829,7 @@ type StoredDraftChunk struct { func (x *StoredDraftChunk) Reset() { *x = StoredDraftChunk{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13048,7 +13841,7 @@ func (x *StoredDraftChunk) String() string { func (*StoredDraftChunk) ProtoMessage() {} func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13061,7 +13854,7 @@ func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredDraftChunk.ProtoReflect.Descriptor instead. func (*StoredDraftChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{191} } func (x *StoredDraftChunk) GetId() string { @@ -13252,7 +14045,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13264,7 +14057,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13277,7 +14070,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *CreateWorkspaceRequest) GetName() string { @@ -13304,7 +14097,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13316,7 +14109,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13329,7 +14122,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{193} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -13350,7 +14143,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13362,7 +14155,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13375,7 +14168,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{194} } func (x *GetWorkspaceRequest) GetName() string { @@ -13395,7 +14188,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13407,7 +14200,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13420,7 +14213,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{195} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -13443,7 +14236,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13455,7 +14248,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13468,7 +14261,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{196} } func (x *ListWorkspacesRequest) GetLimit() uint32 { @@ -13502,7 +14295,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13514,7 +14307,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13527,7 +14320,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{197} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -13548,7 +14341,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13560,7 +14353,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13573,7 +14366,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{198} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -13593,7 +14386,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13605,7 +14398,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13618,7 +14411,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{185} + return file_openshell_proto_rawDescGZIP(), []int{199} } func (x *DeleteWorkspaceResponse) GetDeleted() bool { @@ -13642,7 +14435,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13654,7 +14447,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[200] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13667,7 +14460,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{186} + return file_openshell_proto_rawDescGZIP(), []int{200} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -13706,7 +14499,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13718,7 +14511,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[201] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13731,7 +14524,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{187} + return file_openshell_proto_rawDescGZIP(), []int{201} } func (x *AddWorkspaceMemberRequest) GetWorkspace() string { @@ -13765,7 +14558,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13777,7 +14570,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[202] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13790,7 +14583,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{188} + return file_openshell_proto_rawDescGZIP(), []int{202} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -13813,7 +14606,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13825,7 +14618,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[203] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13838,7 +14631,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{189} + return file_openshell_proto_rawDescGZIP(), []int{203} } func (x *RemoveWorkspaceMemberRequest) GetWorkspace() string { @@ -13865,7 +14658,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13877,7 +14670,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[204] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13890,7 +14683,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{190} + return file_openshell_proto_rawDescGZIP(), []int{204} } func (x *RemoveWorkspaceMemberResponse) GetRemoved() bool { @@ -13913,7 +14706,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13925,7 +14718,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[205] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13938,7 +14731,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{191} + return file_openshell_proto_rawDescGZIP(), []int{205} } func (x *ListWorkspaceMembersRequest) GetWorkspace() string { @@ -13972,7 +14765,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13984,7 +14777,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[206] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13997,7 +14790,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{192} + return file_openshell_proto_rawDescGZIP(), []int{206} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -14025,7 +14818,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[207] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14037,7 +14830,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[207] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14050,7 +14843,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{193} + return file_openshell_proto_rawDescGZIP(), []int{207} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -14078,7 +14871,7 @@ var File_openshell_proto protoreflect.FileDescriptor const file_openshell_proto_rawDesc = "" + "\n" + - "\x0fopenshell.proto\x12\fopenshell.v1\x1a\x0fdatamodel.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\roptions.proto\x1a\rsandbox.proto\"\x1a\n" + + "\x0fopenshell.proto\x12\fopenshell.v1\x1a\x0fdatamodel.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\roptions.proto\x1a\rsandbox.proto\"\x1a\n" + "\x18IssueSandboxTokenRequest\"[\n" + "\x19IssueSandboxTokenResponse\x12\x1a\n" + "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12\"\n" + @@ -14111,11 +14904,12 @@ const file_openshell_proto_rawDesc = "" + "\x19ComputeDriverCapabilities\x12\x1f\n" + "\vdriver_name\x18\x01 \x01(\tR\n" + "driverName\x12%\n" + - "\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\"\xd8\x01\n" + + "\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\"\xce\x02\n" + "\aSandbox\x12>\n" + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12-\n" + "\x04spec\x18\x02 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x123\n" + - "\x06status\x18\x03 \x01(\v2\x1b.openshell.v1.SandboxStatusR\x06statusJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\x05phaseR\x16current_policy_version\"\x83\x04\n" + + "\x06status\x18\x03 \x01(\v2\x1b.openshell.v1.SandboxStatusR\x06status\x12t\n" + + "\x1ecreated_from_workload_template\x18\x14 \x01(\v2/.openshell.v1.SandboxWorkloadTemplateProvenanceR\x1bcreatedFromWorkloadTemplateJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\x05phaseR\x16current_policy_version\"\x83\x04\n" + "\vSandboxSpec\x12\x1b\n" + "\tlog_level\x18\x01 \x01(\tR\blogLevel\x12L\n" + "\venvironment\x18\x05 \x03(\v2*.openshell.v1.SandboxSpec.EnvironmentEntryR\venvironment\x129\n" + @@ -14156,7 +14950,33 @@ const file_openshell_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x12\n" + "\x10_user_namespacesJ\x04\b\t\x10\n" + - "R\x16volume_claim_templates\"\x9a\x03\n" + + "R\x16volume_claim_templates\"\x98\x01\n" + + "\x17SandboxWorkloadTemplate\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12=\n" + + "\x04spec\x18\x02 \x01(\v2).openshell.v1.SandboxWorkloadTemplateSpecR\x04spec\"\xf3\x01\n" + + "\x1bSandboxWorkloadTemplateSpec\x12?\n" + + "\bworkload\x18\x01 \x01(\v2#.openshell.v1.SandboxWorkloadConfigR\bworkload\x12<\n" + + "\rdriver_config\x18\x02 \x01(\v2\x17.google.protobuf.StructR\fdriverConfig\x12U\n" + + "\x15desired_service_level\x18\x03 \x01(\v2!.openshell.v1.SandboxServiceLevelR\x13desiredServiceLevel\"\x83\x02\n" + + "\x15SandboxWorkloadConfig\x12\x14\n" + + "\x05image\x18\x01 \x01(\tR\x05image\x12V\n" + + "\venvironment\x18\x02 \x03(\v24.openshell.v1.SandboxWorkloadConfig.EnvironmentEntryR\venvironment\x12<\n" + + "\tresources\x18\x03 \x01(\v2\x1e.openshell.v1.SandboxResourcesR\tresources\x1a>\n" + + "\x10EnvironmentEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"u\n" + + "\x10SandboxResources\x12\x10\n" + + "\x03cpu\x18\x01 \x01(\tR\x03cpu\x12\x16\n" + + "\x06memory\x18\x02 \x01(\tR\x06memory\x127\n" + + "\x03gpu\x18\x03 \x01(\v2%.openshell.v1.GpuResourceRequirementsR\x03gpu\"M\n" + + "\x13SandboxServiceLevel\x126\n" + + "\astartup\x18\x01 \x01(\v2\x1c.openshell.v1.SandboxStartupR\astartup\"k\n" + + "\x0eSandboxStartup\x12<\n" + + "\fready_within\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\vreadyWithin\x12\x1b\n" + + "\tmax_burst\x18\x02 \x01(\rR\bmaxBurst\"b\n" + + "!SandboxWorkloadTemplateProvenance\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12)\n" + + "\x10resource_version\x18\x02 \x01(\tR\x0fresourceVersion\"\x9a\x03\n" + "\rSandboxStatus\x12!\n" + "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12\x1b\n" + "\tagent_pod\x18\x02 \x01(\tR\bagentPod\x12\x19\n" + @@ -14187,20 +15007,42 @@ const file_openshell_proto_rawDesc = "" + "\bmetadata\x18\x06 \x03(\v2).openshell.v1.PlatformEvent.MetadataEntryR\bmetadata\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd4\x03\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x8a\x04\n" + "\x14CreateSandboxRequest\x12-\n" + "\x04spec\x18\x01 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12F\n" + "\x06labels\x18\x03 \x03(\v2..openshell.v1.CreateSandboxRequest.LabelsEntryR\x06labels\x12U\n" + "\vannotations\x18\x04 \x03(\v23.openshell.v1.CreateSandboxRequest.AnnotationsEntryR\vannotations\x12\x1c\n" + "\tworkspace\x18\x05 \x01(\tR\tworkspace\x12A\n" + - "\x1dawait_main_process_attachment\x18\x06 \x01(\bR\x1aawaitMainProcessAttachment\x1a9\n" + + "\x1dawait_main_process_attachment\x18\x06 \x01(\bR\x1aawaitMainProcessAttachment\x124\n" + + "\x16workload_template_name\x18\a \x01(\tR\x14workloadTemplateName\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"E\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x7f\n" + + "\x1cCreateSandboxTemplateRequest\x12A\n" + + "\btemplate\x18\x01 \x01(\v2%.openshell.v1.SandboxWorkloadTemplateR\btemplate\x12\x1c\n" + + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"M\n" + + "\x19GetSandboxTemplateRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\xb7\x01\n" + + "\x1bListSandboxTemplatesRequest\x12\x14\n" + + "\x05limit\x18\x01 \x01(\rR\x05limit\x12\x16\n" + + "\x06offset\x18\x02 \x01(\rR\x06offset\x12\x1c\n" + + "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12%\n" + + "\x0eall_workspaces\x18\x04 \x01(\bR\rallWorkspaces\x12%\n" + + "\x0elabel_selector\x18\x05 \x01(\tR\rlabelSelector\"P\n" + + "\x1cDeleteSandboxTemplateRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\\\n" + + "\x17SandboxTemplateResponse\x12A\n" + + "\btemplate\x18\x01 \x01(\v2%.openshell.v1.SandboxWorkloadTemplateR\btemplate\"c\n" + + "\x1cListSandboxTemplatesResponse\x12C\n" + + "\ttemplates\x18\x01 \x03(\v2%.openshell.v1.SandboxWorkloadTemplateR\ttemplates\"9\n" + + "\x1dDeleteSandboxTemplateResponse\x12\x18\n" + + "\adeleted\x18\x01 \x01(\bR\adeleted\"E\n" + "\x11GetSandboxRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\xb0\x01\n" + @@ -15211,7 +16053,7 @@ const file_openshell_proto_rawDesc = "" + "1PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY\x10\x01\x12;\n" + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE\x10\x02\x12A\n" + "=PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION\x10\x03\x12;\n" + - "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x042\xb4G\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x042\xf7K\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -15225,7 +16067,15 @@ const file_openshell_proto_rawDesc = "" + "GetSandbox\x12\x1f.openshell.v1.GetSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\" \x82\xb5\x18\x1c\n" + "\x06bearer\x12\x04user\"\fsandbox:read\x12z\n" + "\rListSandboxes\x12\".openshell.v1.ListSandboxesRequest\x1a#.openshell.v1.ListSandboxesResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x8e\x01\n" + + "\x15CreateSandboxTemplate\x12*.openshell.v1.CreateSandboxTemplateRequest\x1a%.openshell.v1.SandboxTemplateResponse\"\"\x82\xb5\x18\x1e\n" + + "\x06bearer\x12\x05admin\"\rsandbox:write\x12\x86\x01\n" + + "\x12GetSandboxTemplate\x12'.openshell.v1.GetSandboxTemplateRequest\x1a%.openshell.v1.SandboxTemplateResponse\" \x82\xb5\x18\x1c\n" + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x8f\x01\n" + + "\x14ListSandboxTemplates\x12).openshell.v1.ListSandboxTemplatesRequest\x1a*.openshell.v1.ListSandboxTemplatesResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x94\x01\n" + + "\x15DeleteSandboxTemplate\x12*.openshell.v1.DeleteSandboxTemplateRequest\x1a+.openshell.v1.DeleteSandboxTemplateResponse\"\"\x82\xb5\x18\x1e\n" + + "\x06bearer\x12\x05admin\"\rsandbox:write\x12\x8f\x01\n" + "\x14ListSandboxProviders\x12).openshell.v1.ListSandboxProvidersRequest\x1a*.openshell.v1.ListSandboxProvidersResponse\" \x82\xb5\x18\x1c\n" + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x93\x01\n" + "\x15AttachSandboxProvider\x12*.openshell.v1.AttachSandboxProviderRequest\x1a+.openshell.v1.AttachSandboxProviderResponse\"!\x82\xb5\x18\x1d\n" + @@ -15368,7 +16218,7 @@ func file_openshell_proto_rawDescGZIP() []byte { } var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 8) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 219) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 234) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase (ProviderCredentialTokenGrantType)(0), // 1: openshell.v1.ProviderCredentialTokenGrantType @@ -15395,535 +16245,573 @@ var file_openshell_proto_goTypes = []any{ (*ResourceRequirements)(nil), // 22: openshell.v1.ResourceRequirements (*GpuResourceRequirements)(nil), // 23: openshell.v1.GpuResourceRequirements (*SandboxTemplate)(nil), // 24: openshell.v1.SandboxTemplate - (*SandboxStatus)(nil), // 25: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 26: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 27: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 28: openshell.v1.CreateSandboxRequest - (*GetSandboxRequest)(nil), // 29: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 30: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 31: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 32: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 33: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 34: openshell.v1.DeleteSandboxRequest - (*StopSandboxRequest)(nil), // 35: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 36: openshell.v1.StartSandboxRequest - (*SandboxResponse)(nil), // 37: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 38: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 39: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 40: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 41: openshell.v1.DetachSandboxProviderResponse - (*DeleteSandboxResponse)(nil), // 42: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 43: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 44: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 45: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 46: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 47: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 48: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 49: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 50: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 51: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 52: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 53: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 54: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 55: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 56: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 57: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 58: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 59: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 60: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 61: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 62: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 63: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 64: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 65: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 66: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 67: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 68: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 69: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 70: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 71: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 72: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 73: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 74: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 75: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 76: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 77: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 78: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 79: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 80: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 81: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 82: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 83: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 84: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 85: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 86: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 87: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 88: openshell.v1.ProviderProfileDiscovery - (*StoredProviderCredentialRefreshState)(nil), // 89: openshell.v1.StoredProviderCredentialRefreshState - (*StoredRefreshMaterialDeletion)(nil), // 90: openshell.v1.StoredRefreshMaterialDeletion - (*GetProviderRefreshStatusRequest)(nil), // 91: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 92: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 93: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 94: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 95: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 96: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 97: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 98: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 99: openshell.v1.ProviderProfile - (*StoredProviderProfile)(nil), // 100: openshell.v1.StoredProviderProfile - (*ProviderProfileResponse)(nil), // 101: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 102: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 103: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 104: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 105: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 106: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 107: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 108: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 109: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 110: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 111: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 112: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 113: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 114: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 115: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ExchangeProviderSubjectTokenRequest)(nil), // 116: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 117: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 118: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 119: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 120: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 121: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 122: openshell.v1.RemoveNetworkRule - (*AddDenyRules)(nil), // 123: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 124: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 125: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 126: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 127: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 128: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 129: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 130: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 131: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 132: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 133: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 134: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 135: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 136: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 137: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 138: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 139: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 140: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 141: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 142: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 143: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 144: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 145: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 146: openshell.v1.ReportMainProcessExitResponse - (*FinalizeMainProcessExitRequest)(nil), // 147: openshell.v1.FinalizeMainProcessExitRequest - (*FinalizeMainProcessExitResponse)(nil), // 148: openshell.v1.FinalizeMainProcessExitResponse - (*RelayOpen)(nil), // 149: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 150: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 151: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 152: openshell.v1.RelayInit - (*RelayFrame)(nil), // 153: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 154: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 155: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 156: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 157: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 158: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 159: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 160: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 161: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 162: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 163: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 164: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 165: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 166: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 167: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 168: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 169: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 170: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 171: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 172: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 173: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 174: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 175: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 176: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 177: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 178: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 179: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 180: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 181: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 182: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 183: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 184: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 185: openshell.v1.StoredDraftChunk - (*CreateWorkspaceRequest)(nil), // 186: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 187: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 188: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 189: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 190: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 191: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 192: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 193: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 194: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 195: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 196: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 197: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 198: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 199: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 200: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 201: openshell.v1.ExtensionServiceCredential - nil, // 202: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 203: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 204: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 205: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 206: openshell.v1.PlatformEvent.MetadataEntry - nil, // 207: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 208: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 209: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 210: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 211: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 212: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - nil, // 213: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - nil, // 214: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - nil, // 215: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 216: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 219: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 220: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 221: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 222: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 223: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 224: openshell.v1.PolicyRevisionPayload.ProvenanceEntry - nil, // 225: openshell.v1.StoredPolicyRevision.ProvenanceEntry - nil, // 226: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 227: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 228: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 229: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 230: openshell.datamodel.v1.Provider - (*datamodelv1.CredentialHandle)(nil), // 231: openshell.datamodel.v1.CredentialHandle - (*sandboxv1.NetworkEndpoint)(nil), // 232: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 233: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 234: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 235: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 236: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 237: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 238: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 239: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 240: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 241: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 242: openshell.sandbox.v1.GetGatewayConfigResponse + (*SandboxWorkloadTemplate)(nil), // 25: openshell.v1.SandboxWorkloadTemplate + (*SandboxWorkloadTemplateSpec)(nil), // 26: openshell.v1.SandboxWorkloadTemplateSpec + (*SandboxWorkloadConfig)(nil), // 27: openshell.v1.SandboxWorkloadConfig + (*SandboxResources)(nil), // 28: openshell.v1.SandboxResources + (*SandboxServiceLevel)(nil), // 29: openshell.v1.SandboxServiceLevel + (*SandboxStartup)(nil), // 30: openshell.v1.SandboxStartup + (*SandboxWorkloadTemplateProvenance)(nil), // 31: openshell.v1.SandboxWorkloadTemplateProvenance + (*SandboxStatus)(nil), // 32: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 33: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 34: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 35: openshell.v1.CreateSandboxRequest + (*CreateSandboxTemplateRequest)(nil), // 36: openshell.v1.CreateSandboxTemplateRequest + (*GetSandboxTemplateRequest)(nil), // 37: openshell.v1.GetSandboxTemplateRequest + (*ListSandboxTemplatesRequest)(nil), // 38: openshell.v1.ListSandboxTemplatesRequest + (*DeleteSandboxTemplateRequest)(nil), // 39: openshell.v1.DeleteSandboxTemplateRequest + (*SandboxTemplateResponse)(nil), // 40: openshell.v1.SandboxTemplateResponse + (*ListSandboxTemplatesResponse)(nil), // 41: openshell.v1.ListSandboxTemplatesResponse + (*DeleteSandboxTemplateResponse)(nil), // 42: openshell.v1.DeleteSandboxTemplateResponse + (*GetSandboxRequest)(nil), // 43: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 44: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 45: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 46: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 47: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 48: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 49: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 50: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 51: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 52: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 53: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 54: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 55: openshell.v1.DetachSandboxProviderResponse + (*DeleteSandboxResponse)(nil), // 56: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 57: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 58: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 59: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 60: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 61: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 62: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 63: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 64: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 65: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 66: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 67: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 68: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 69: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 70: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 71: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 72: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 73: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 74: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 75: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 76: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 77: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 78: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 79: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 80: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 81: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 82: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 83: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 84: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 85: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 86: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 87: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 88: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 89: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 90: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 91: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 92: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 93: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 94: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 95: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 96: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 97: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 98: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 99: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 100: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 101: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 102: openshell.v1.ProviderProfileDiscovery + (*StoredProviderCredentialRefreshState)(nil), // 103: openshell.v1.StoredProviderCredentialRefreshState + (*StoredRefreshMaterialDeletion)(nil), // 104: openshell.v1.StoredRefreshMaterialDeletion + (*GetProviderRefreshStatusRequest)(nil), // 105: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 106: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 107: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 108: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 109: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 110: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 111: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 112: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 113: openshell.v1.ProviderProfile + (*StoredProviderProfile)(nil), // 114: openshell.v1.StoredProviderProfile + (*ProviderProfileResponse)(nil), // 115: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 116: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 117: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 118: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 119: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 120: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 121: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 122: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 123: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 124: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 125: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 126: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 127: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 128: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 129: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 130: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 131: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 132: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 133: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 134: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 135: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 136: openshell.v1.RemoveNetworkRule + (*AddDenyRules)(nil), // 137: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 138: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 139: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 140: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 141: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 142: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 143: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 144: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 145: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 146: openshell.v1.ReportPolicyStatusResponse + (*SandboxPolicyRevision)(nil), // 147: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 148: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 149: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 150: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 151: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 152: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 153: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 154: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 155: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 156: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 157: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 158: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 159: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 160: openshell.v1.ReportMainProcessExitResponse + (*FinalizeMainProcessExitRequest)(nil), // 161: openshell.v1.FinalizeMainProcessExitRequest + (*FinalizeMainProcessExitResponse)(nil), // 162: openshell.v1.FinalizeMainProcessExitResponse + (*RelayOpen)(nil), // 163: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 164: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 165: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 166: openshell.v1.RelayInit + (*RelayFrame)(nil), // 167: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 168: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 169: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 170: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 171: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 172: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 173: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 174: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 175: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 176: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 177: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 178: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 179: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 180: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 181: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 182: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 183: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 184: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 185: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 186: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 187: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 188: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 189: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 190: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 191: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 192: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 193: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 194: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 195: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 196: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 197: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 198: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 199: openshell.v1.StoredDraftChunk + (*CreateWorkspaceRequest)(nil), // 200: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 201: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 202: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 203: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 204: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 205: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 206: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 207: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 208: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 209: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 210: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 211: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 212: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 213: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 214: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 215: openshell.v1.ExtensionServiceCredential + nil, // 216: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 217: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 218: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 219: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 220: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 221: openshell.v1.PlatformEvent.MetadataEntry + nil, // 222: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 223: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 224: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 225: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 226: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 227: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + nil, // 228: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + nil, // 229: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + nil, // 230: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 231: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 232: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 233: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 234: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 235: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 236: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 237: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 238: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 239: openshell.v1.PolicyRevisionPayload.ProvenanceEntry + nil, // 240: openshell.v1.StoredPolicyRevision.ProvenanceEntry + nil, // 241: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 242: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 243: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 244: google.protobuf.Struct + (*durationpb.Duration)(nil), // 245: google.protobuf.Duration + (*datamodelv1.Provider)(nil), // 246: openshell.datamodel.v1.Provider + (*datamodelv1.CredentialHandle)(nil), // 247: openshell.datamodel.v1.CredentialHandle + (*sandboxv1.NetworkEndpoint)(nil), // 248: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 249: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 250: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 251: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 252: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 253: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 254: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 255: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 256: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 257: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 258: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 201, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 215, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential 5, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus 5, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus 18, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo 19, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 227, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 242, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 21, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 25, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 202, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 24, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 228, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 22, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 23, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 203, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 204, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 205, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 229, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 229, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 26, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition - 0, // 19: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 206, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 21, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 207, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 208, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 20, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 20, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 230, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 20, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 20, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 52, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 227, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 51, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 209, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 56, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 57, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 58, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 150, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 151, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 60, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 55, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 63, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 227, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 20, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 67, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 27, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 68, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 161, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 210, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 230, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 230, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 211, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 230, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 230, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 99, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 80, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 1, // 55: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 81, // 56: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 86, // 57: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 82, // 58: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 2, // 59: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 84, // 60: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 85, // 61: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 2, // 62: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 7, // 63: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 227, // 64: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 2, // 65: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 212, // 66: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 213, // 67: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 214, // 68: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - 90, // 69: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion - 7, // 70: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 231, // 71: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle - 87, // 72: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 73: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 215, // 74: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 87, // 75: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 87, // 76: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 3, // 77: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 83, // 78: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 232, // 79: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 233, // 80: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 88, // 81: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 216, // 82: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 227, // 83: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 99, // 84: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile - 99, // 85: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 99, // 86: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 78, // 87: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 88: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 99, // 89: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 78, // 90: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 91: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 99, // 92: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 78, // 93: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 79, // 94: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 113, // 95: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 217, // 96: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 218, // 97: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 219, // 98: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 220, // 99: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 228, // 100: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 234, // 101: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 119, // 102: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 221, // 103: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 120, // 104: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 121, // 105: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 122, // 106: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 123, // 107: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 124, // 108: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 125, // 109: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 235, // 110: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 236, // 111: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 237, // 112: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 222, // 113: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 133, // 114: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 133, // 115: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 4, // 116: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 4, // 117: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 228, // 118: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 223, // 119: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 67, // 120: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 67, // 121: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 140, // 122: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 143, // 123: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 154, // 124: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 155, // 125: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 141, // 126: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 142, // 127: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 144, // 128: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 149, // 129: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 155, // 130: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 150, // 131: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 151, // 132: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 152, // 133: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 156, // 134: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 158, // 135: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 235, // 136: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 228, // 137: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 228, // 138: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 157, // 139: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 160, // 140: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 159, // 141: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 160, // 142: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 170, // 143: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 235, // 144: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 180, // 145: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 228, // 146: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 224, // 147: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 235, // 148: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 228, // 149: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 228, // 150: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 225, // 151: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 228, // 152: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 228, // 153: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 154: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 238, // 155: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 238, // 156: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 238, // 157: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 227, // 158: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 159: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 6, // 160: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 194, // 161: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 194, // 162: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 231, // 163: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle - 83, // 164: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 114, // 165: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 12, // 166: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 14, // 167: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 16, // 168: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 28, // 169: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 29, // 170: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 30, // 171: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 31, // 172: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 32, // 173: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 33, // 174: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 34, // 175: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 35, // 176: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 36, // 177: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 43, // 178: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 45, // 179: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 46, // 180: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 47, // 181: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 49, // 182: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 53, // 183: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 55, // 184: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 61, // 185: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 62, // 186: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 69, // 187: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 70, // 188: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 71, // 189: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 76, // 190: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 77, // 191: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 103, // 192: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 105, // 193: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 107, // 194: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 72, // 195: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 91, // 196: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 93, // 197: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 95, // 198: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 97, // 199: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 73, // 200: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 110, // 201: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 239, // 202: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 240, // 203: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 118, // 204: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 127, // 205: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 129, // 206: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 131, // 207: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 112, // 208: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 116, // 209: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 134, // 210: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 135, // 211: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 138, // 212: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 145, // 213: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 147, // 214: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 153, // 215: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 65, // 216: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 162, // 217: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 164, // 218: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 166, // 219: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 168, // 220: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 171, // 221: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 173, // 222: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 175, // 223: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 177, // 224: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 179, // 225: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 8, // 226: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 10, // 227: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 186, // 228: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 188, // 229: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 190, // 230: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 192, // 231: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 195, // 232: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 197, // 233: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 199, // 234: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 13, // 235: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 15, // 236: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 17, // 237: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 37, // 238: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 239: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 38, // 240: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 39, // 241: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 40, // 242: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 41, // 243: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 42, // 244: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 37, // 245: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 246: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 44, // 247: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 52, // 248: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 52, // 249: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 48, // 250: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 50, // 251: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 54, // 252: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 59, // 253: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 61, // 254: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 59, // 255: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 74, // 256: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 74, // 257: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 75, // 258: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 102, // 259: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 101, // 260: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 104, // 261: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 106, // 262: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 108, // 263: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 74, // 264: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 92, // 265: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 94, // 266: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 96, // 267: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 98, // 268: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 109, // 269: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 111, // 270: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 241, // 271: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 242, // 272: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 126, // 273: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 128, // 274: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 130, // 275: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 132, // 276: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 115, // 277: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 117, // 278: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 137, // 279: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 136, // 280: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 139, // 281: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 146, // 282: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 148, // 283: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 153, // 284: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 66, // 285: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 163, // 286: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 165, // 287: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 167, // 288: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 169, // 289: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 172, // 290: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 174, // 291: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 176, // 292: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 178, // 293: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 181, // 294: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 9, // 295: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 11, // 296: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 187, // 297: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 189, // 298: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 191, // 299: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 193, // 300: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 196, // 301: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 198, // 302: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 200, // 303: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 235, // [235:304] is the sub-list for method output_type - 166, // [166:235] is the sub-list for method input_type - 166, // [166:166] is the sub-list for extension type_name - 166, // [166:166] is the sub-list for extension extendee - 0, // [0:166] is the sub-list for field type_name + 32, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 31, // 8: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance + 216, // 9: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 24, // 10: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 243, // 11: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 22, // 12: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 23, // 13: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 217, // 14: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 218, // 15: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 219, // 16: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 244, // 17: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 244, // 18: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 242, // 19: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 26, // 20: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec + 27, // 21: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig + 244, // 22: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 29, // 23: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel + 220, // 24: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 28, // 25: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources + 23, // 26: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements + 30, // 27: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup + 245, // 28: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 33, // 29: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 0, // 30: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase + 221, // 31: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 21, // 32: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 222, // 33: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 223, // 34: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 25, // 35: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 25, // 36: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate + 25, // 37: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate + 20, // 38: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 20, // 39: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 246, // 40: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 20, // 41: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 20, // 42: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 66, // 43: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 242, // 44: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 65, // 45: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 224, // 46: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 70, // 47: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 71, // 48: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 72, // 49: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 164, // 50: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 165, // 51: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 74, // 52: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 69, // 53: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 77, // 54: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 242, // 55: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 20, // 56: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 81, // 57: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 34, // 58: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 82, // 59: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 175, // 60: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 225, // 61: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 246, // 62: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 246, // 63: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 226, // 64: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 246, // 65: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 246, // 66: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 113, // 67: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 94, // 68: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 1, // 69: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 95, // 70: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 100, // 71: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 96, // 72: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 2, // 73: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 98, // 74: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 99, // 75: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 2, // 76: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 7, // 77: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 242, // 78: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 2, // 79: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 227, // 80: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 228, // 81: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 229, // 82: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + 104, // 83: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion + 7, // 84: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 247, // 85: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle + 101, // 86: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 2, // 87: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 230, // 88: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 101, // 89: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 101, // 90: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 3, // 91: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 97, // 92: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 248, // 93: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 249, // 94: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 102, // 95: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 231, // 96: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 242, // 97: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 113, // 98: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile + 113, // 99: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 113, // 100: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 92, // 101: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 93, // 102: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 113, // 103: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 92, // 104: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 93, // 105: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 113, // 106: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 92, // 107: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 93, // 108: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 127, // 109: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 232, // 110: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 233, // 111: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 234, // 112: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 235, // 113: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 243, // 114: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 250, // 115: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 133, // 116: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 236, // 117: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 134, // 118: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 135, // 119: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 136, // 120: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 137, // 121: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 138, // 122: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 139, // 123: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 251, // 124: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 252, // 125: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 253, // 126: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 237, // 127: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 147, // 128: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 147, // 129: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 4, // 130: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 4, // 131: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 243, // 132: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 238, // 133: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 81, // 134: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 81, // 135: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 154, // 136: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 157, // 137: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 168, // 138: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 169, // 139: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 155, // 140: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 156, // 141: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 158, // 142: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 163, // 143: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 169, // 144: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 164, // 145: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 165, // 146: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 166, // 147: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 170, // 148: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 172, // 149: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 251, // 150: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 243, // 151: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 243, // 152: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 171, // 153: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 174, // 154: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 173, // 155: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 174, // 156: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 184, // 157: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 251, // 158: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 194, // 159: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 243, // 160: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 239, // 161: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 251, // 162: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 243, // 163: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 243, // 164: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 240, // 165: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 243, // 166: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 243, // 167: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 241, // 168: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 254, // 169: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 254, // 170: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 254, // 171: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 242, // 172: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 173: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 6, // 174: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 208, // 175: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 208, // 176: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 247, // 177: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 97, // 178: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 128, // 179: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 12, // 180: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 14, // 181: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 16, // 182: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 35, // 183: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 43, // 184: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 44, // 185: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 36, // 186: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 37, // 187: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 38, // 188: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 39, // 189: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 45, // 190: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 46, // 191: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 47, // 192: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 48, // 193: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 49, // 194: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 50, // 195: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 57, // 196: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 59, // 197: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 60, // 198: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 61, // 199: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 63, // 200: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 67, // 201: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 69, // 202: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 75, // 203: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 76, // 204: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 83, // 205: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 84, // 206: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 85, // 207: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 90, // 208: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 91, // 209: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 117, // 210: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 119, // 211: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 121, // 212: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 86, // 213: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 105, // 214: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 107, // 215: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 109, // 216: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 111, // 217: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 87, // 218: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 124, // 219: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 255, // 220: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 256, // 221: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 132, // 222: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 141, // 223: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 143, // 224: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 145, // 225: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 126, // 226: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 130, // 227: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 148, // 228: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 149, // 229: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 152, // 230: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 159, // 231: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 161, // 232: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 167, // 233: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 79, // 234: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 176, // 235: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 178, // 236: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 180, // 237: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 182, // 238: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 185, // 239: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 187, // 240: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 189, // 241: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 191, // 242: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 193, // 243: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 8, // 244: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 10, // 245: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 200, // 246: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 202, // 247: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 204, // 248: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 206, // 249: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 209, // 250: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 211, // 251: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 213, // 252: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 13, // 253: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 15, // 254: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 17, // 255: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 51, // 256: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 51, // 257: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 52, // 258: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 40, // 259: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 40, // 260: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 41, // 261: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 42, // 262: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 53, // 263: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 54, // 264: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 55, // 265: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 56, // 266: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 51, // 267: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 51, // 268: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 58, // 269: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 66, // 270: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 66, // 271: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 62, // 272: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 64, // 273: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 68, // 274: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 73, // 275: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 75, // 276: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 73, // 277: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 88, // 278: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 88, // 279: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 89, // 280: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 116, // 281: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 115, // 282: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 118, // 283: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 120, // 284: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 122, // 285: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 88, // 286: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 106, // 287: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 108, // 288: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 110, // 289: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 112, // 290: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 123, // 291: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 125, // 292: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 257, // 293: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 258, // 294: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 140, // 295: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 142, // 296: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 144, // 297: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 146, // 298: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 129, // 299: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 131, // 300: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 151, // 301: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 150, // 302: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 153, // 303: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 160, // 304: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 162, // 305: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 167, // 306: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 80, // 307: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 177, // 308: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 179, // 309: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 181, // 310: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 183, // 311: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 186, // 312: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 188, // 313: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 190, // 314: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 192, // 315: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 195, // 316: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 9, // 317: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 11, // 318: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 201, // 319: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 203, // 320: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 205, // 321: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 207, // 322: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 210, // 323: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 212, // 324: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 214, // 325: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 253, // [253:326] is the sub-list for method output_type + 180, // [180:253] is the sub-list for method input_type + 180, // [180:180] is the sub-list for extension type_name + 180, // [180:180] is the sub-list for extension extendee + 0, // [0:180] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -15933,34 +16821,34 @@ func file_openshell_proto_init() { } file_openshell_proto_msgTypes[15].OneofWrappers = []any{} file_openshell_proto_msgTypes[16].OneofWrappers = []any{} - file_openshell_proto_msgTypes[17].OneofWrappers = []any{} - file_openshell_proto_msgTypes[51].OneofWrappers = []any{ + file_openshell_proto_msgTypes[24].OneofWrappers = []any{} + file_openshell_proto_msgTypes[65].OneofWrappers = []any{ (*ExecSandboxEvent_Stdout)(nil), (*ExecSandboxEvent_Stderr)(nil), (*ExecSandboxEvent_Exit)(nil), } - file_openshell_proto_msgTypes[52].OneofWrappers = []any{ + file_openshell_proto_msgTypes[66].OneofWrappers = []any{ (*TcpForwardInit_Ssh)(nil), (*TcpForwardInit_Tcp)(nil), } - file_openshell_proto_msgTypes[53].OneofWrappers = []any{ + file_openshell_proto_msgTypes[67].OneofWrappers = []any{ (*TcpForwardFrame_Init)(nil), (*TcpForwardFrame_Data)(nil), } - file_openshell_proto_msgTypes[54].OneofWrappers = []any{ + file_openshell_proto_msgTypes[68].OneofWrappers = []any{ (*ExecSandboxInput_Start)(nil), (*ExecSandboxInput_Stdin)(nil), (*ExecSandboxInput_Resize)(nil), } - file_openshell_proto_msgTypes[58].OneofWrappers = []any{ + file_openshell_proto_msgTypes[72].OneofWrappers = []any{ (*SandboxStreamEvent_Sandbox)(nil), (*SandboxStreamEvent_Log)(nil), (*SandboxStreamEvent_Event)(nil), (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[85].OneofWrappers = []any{} - file_openshell_proto_msgTypes[111].OneofWrappers = []any{ + file_openshell_proto_msgTypes[99].OneofWrappers = []any{} + file_openshell_proto_msgTypes[125].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), (*PolicyMergeOperation_RemoveRule)(nil), @@ -15968,36 +16856,36 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_AddAllowRules)(nil), (*PolicyMergeOperation_RemoveBinary)(nil), } - file_openshell_proto_msgTypes[130].OneofWrappers = []any{ + file_openshell_proto_msgTypes[144].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), (*SupervisorMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[131].OneofWrappers = []any{ + file_openshell_proto_msgTypes[145].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), (*GatewayMessage_RelayOpen)(nil), (*GatewayMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[141].OneofWrappers = []any{ + file_openshell_proto_msgTypes[155].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[145].OneofWrappers = []any{ + file_openshell_proto_msgTypes[159].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[176].OneofWrappers = []any{} - file_openshell_proto_msgTypes[177].OneofWrappers = []any{} + file_openshell_proto_msgTypes[190].OneofWrappers = []any{} + file_openshell_proto_msgTypes[191].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), NumEnums: 8, - NumMessages: 219, + NumMessages: 234, NumExtensions: 0, NumServices: 1, }, diff --git a/sdk/go/proto/openshellv1/openshell_grpc.pb.go b/sdk/go/proto/openshellv1/openshell_grpc.pb.go index 9220f22999..61bc081437 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -29,6 +29,10 @@ const ( OpenShell_CreateSandbox_FullMethodName = "/openshell.v1.OpenShell/CreateSandbox" OpenShell_GetSandbox_FullMethodName = "/openshell.v1.OpenShell/GetSandbox" OpenShell_ListSandboxes_FullMethodName = "/openshell.v1.OpenShell/ListSandboxes" + OpenShell_CreateSandboxTemplate_FullMethodName = "/openshell.v1.OpenShell/CreateSandboxTemplate" + OpenShell_GetSandboxTemplate_FullMethodName = "/openshell.v1.OpenShell/GetSandboxTemplate" + OpenShell_ListSandboxTemplates_FullMethodName = "/openshell.v1.OpenShell/ListSandboxTemplates" + OpenShell_DeleteSandboxTemplate_FullMethodName = "/openshell.v1.OpenShell/DeleteSandboxTemplate" OpenShell_ListSandboxProviders_FullMethodName = "/openshell.v1.OpenShell/ListSandboxProviders" OpenShell_AttachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/AttachSandboxProvider" OpenShell_DetachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/DetachSandboxProvider" @@ -119,6 +123,14 @@ type OpenShellClient interface { GetSandbox(ctx context.Context, in *GetSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) // List sandboxes. ListSandboxes(ctx context.Context, in *ListSandboxesRequest, opts ...grpc.CallOption) (*ListSandboxesResponse, error) + // Create a reusable sandbox workload template. + CreateSandboxTemplate(ctx context.Context, in *CreateSandboxTemplateRequest, opts ...grpc.CallOption) (*SandboxTemplateResponse, error) + // Fetch a reusable sandbox workload template by name. + GetSandboxTemplate(ctx context.Context, in *GetSandboxTemplateRequest, opts ...grpc.CallOption) (*SandboxTemplateResponse, error) + // List reusable sandbox workload templates. + ListSandboxTemplates(ctx context.Context, in *ListSandboxTemplatesRequest, opts ...grpc.CallOption) (*ListSandboxTemplatesResponse, error) + // Delete a reusable sandbox workload template by name. + DeleteSandboxTemplate(ctx context.Context, in *DeleteSandboxTemplateRequest, opts ...grpc.CallOption) (*DeleteSandboxTemplateResponse, error) // List provider records attached to a sandbox. ListSandboxProviders(ctx context.Context, in *ListSandboxProvidersRequest, opts ...grpc.CallOption) (*ListSandboxProvidersResponse, error) // Attach a provider record to an existing sandbox. @@ -354,6 +366,46 @@ func (c *openShellClient) ListSandboxes(ctx context.Context, in *ListSandboxesRe return out, nil } +func (c *openShellClient) CreateSandboxTemplate(ctx context.Context, in *CreateSandboxTemplateRequest, opts ...grpc.CallOption) (*SandboxTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxTemplateResponse) + err := c.cc.Invoke(ctx, OpenShell_CreateSandboxTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetSandboxTemplate(ctx context.Context, in *GetSandboxTemplateRequest, opts ...grpc.CallOption) (*SandboxTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxTemplateResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListSandboxTemplates(ctx context.Context, in *ListSandboxTemplatesRequest, opts ...grpc.CallOption) (*ListSandboxTemplatesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSandboxTemplatesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListSandboxTemplates_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteSandboxTemplate(ctx context.Context, in *DeleteSandboxTemplateRequest, opts ...grpc.CallOption) (*DeleteSandboxTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteSandboxTemplateResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteSandboxTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) ListSandboxProviders(ctx context.Context, in *ListSandboxProvidersRequest, opts ...grpc.CallOption) (*ListSandboxProvidersResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListSandboxProvidersResponse) @@ -1042,6 +1094,14 @@ type OpenShellServer interface { GetSandbox(context.Context, *GetSandboxRequest) (*SandboxResponse, error) // List sandboxes. ListSandboxes(context.Context, *ListSandboxesRequest) (*ListSandboxesResponse, error) + // Create a reusable sandbox workload template. + CreateSandboxTemplate(context.Context, *CreateSandboxTemplateRequest) (*SandboxTemplateResponse, error) + // Fetch a reusable sandbox workload template by name. + GetSandboxTemplate(context.Context, *GetSandboxTemplateRequest) (*SandboxTemplateResponse, error) + // List reusable sandbox workload templates. + ListSandboxTemplates(context.Context, *ListSandboxTemplatesRequest) (*ListSandboxTemplatesResponse, error) + // Delete a reusable sandbox workload template by name. + DeleteSandboxTemplate(context.Context, *DeleteSandboxTemplateRequest) (*DeleteSandboxTemplateResponse, error) // List provider records attached to a sandbox. ListSandboxProviders(context.Context, *ListSandboxProvidersRequest) (*ListSandboxProvidersResponse, error) // Attach a provider record to an existing sandbox. @@ -1235,6 +1295,18 @@ func (UnimplementedOpenShellServer) GetSandbox(context.Context, *GetSandboxReque func (UnimplementedOpenShellServer) ListSandboxes(context.Context, *ListSandboxesRequest) (*ListSandboxesResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListSandboxes not implemented") } +func (UnimplementedOpenShellServer) CreateSandboxTemplate(context.Context, *CreateSandboxTemplateRequest) (*SandboxTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateSandboxTemplate not implemented") +} +func (UnimplementedOpenShellServer) GetSandboxTemplate(context.Context, *GetSandboxTemplateRequest) (*SandboxTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxTemplate not implemented") +} +func (UnimplementedOpenShellServer) ListSandboxTemplates(context.Context, *ListSandboxTemplatesRequest) (*ListSandboxTemplatesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSandboxTemplates not implemented") +} +func (UnimplementedOpenShellServer) DeleteSandboxTemplate(context.Context, *DeleteSandboxTemplateRequest) (*DeleteSandboxTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteSandboxTemplate not implemented") +} func (UnimplementedOpenShellServer) ListSandboxProviders(context.Context, *ListSandboxProvidersRequest) (*ListSandboxProvidersResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListSandboxProviders not implemented") } @@ -1553,6 +1625,78 @@ func _OpenShell_ListSandboxes_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _OpenShell_CreateSandboxTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateSandboxTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).CreateSandboxTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_CreateSandboxTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).CreateSandboxTemplate(ctx, req.(*CreateSandboxTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetSandboxTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxTemplate(ctx, req.(*GetSandboxTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListSandboxTemplates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSandboxTemplatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListSandboxTemplates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListSandboxTemplates_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListSandboxTemplates(ctx, req.(*ListSandboxTemplatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteSandboxTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteSandboxTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteSandboxTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteSandboxTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteSandboxTemplate(ctx, req.(*DeleteSandboxTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_ListSandboxProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListSandboxProvidersRequest) if err := dec(in); err != nil { @@ -2649,6 +2793,22 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListSandboxes", Handler: _OpenShell_ListSandboxes_Handler, }, + { + MethodName: "CreateSandboxTemplate", + Handler: _OpenShell_CreateSandboxTemplate_Handler, + }, + { + MethodName: "GetSandboxTemplate", + Handler: _OpenShell_GetSandboxTemplate_Handler, + }, + { + MethodName: "ListSandboxTemplates", + Handler: _OpenShell_ListSandboxTemplates_Handler, + }, + { + MethodName: "DeleteSandboxTemplate", + Handler: _OpenShell_DeleteSandboxTemplate_Handler, + }, { MethodName: "ListSandboxProviders", Handler: _OpenShell_ListSandboxProviders_Handler, diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index b0e7ed696c..322b1a092c 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -152,11 +152,54 @@ await client.sandbox.setSetting(name, 'feature.enabled', { value: { case: 'boolV Sandbox-scoped `setPolicy` may only change `networkPolicies`; static fields (`filesystem`, `landlock`, `process`) must match the create-time policy. Sandbox-scoped setting deletes are rejected by the gateway, so only upsert (`setSetting`) is exposed here. +## Sandbox templates + +Sandbox workload templates are reusable, workspace-scoped runtime shapes. They +own image, environment, resource, and driver-specific settings; sandbox creation +from a template can still attach labels, providers, and create-time policy. + +```ts +import { OpenShellClient, type SandboxWorkloadTemplate } from '@nvidia/openshell-sdk' + +const client = await OpenShellClient.connect({ gateway, oidcToken }) + +const template: SandboxWorkloadTemplate = await client.sandboxTemplates.create( + { + metadata: { name: 'python', labels: { team: 'runtime' } }, + spec: { + workload: { + image: 'ghcr.io/nvidia/openshell-community/sandboxes/python:latest', + environment: { FEATURE_FLAG: 'on' }, + resources: { cpu: '1', memory: '512Mi' }, + }, + driverConfig: { kubernetes: { pod: { runtime_class_name: 'kata-containers' } } }, + }, + }, + { workspace: 'default' }, +) + +const sandbox = await client.sandbox.createFromTemplate({ + templateName: template.metadata!.name, + workspace: 'default', + providers: ['github'], + policy: { version: 1, networkPolicies: {} }, +}) + +await client.sandboxTemplates.get('python', { workspace: 'default' }) +await client.sandboxTemplates.list({ workspace: 'default', limit: 100 }) +await client.sandboxTemplates.delete('python', { workspace: 'default' }) +``` + +Use `allWorkspaces: true` on `list()` for a platform-admin view. The SDK clears +the workspace field in that request because the gateway treats `workspace` and +`allWorkspaces` as mutually exclusive. + ## Surface and roadmap The SDK's goal is agent parity: anything the OpenShell gateway can do should be reachable from typed code, not only the CLI. The API is organized as scoped sub-clients over one shared connection, mirroring the CLI's verbs. - `client.sandbox` (`SandboxClient`) is available today: sandbox lifecycle, exec, forward, SSH, sandbox-scoped providers, config, and policy. +- `client.sandboxTemplates` (`SandboxTemplateClient`) is available today: reusable sandbox workload template CRUD. - `client.gateway` (`GatewayClient`) is planned: gateway-scoped config and settings, health, and cluster status. - `client.providers` (`ProviderClient`) is planned: gateway-scoped provider CRUD and profiles. diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index 29a7438c69..5b12a48e23 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -17,6 +17,7 @@ import { POLICY_SOURCE_NAMES, Pushable, SandboxClient, + SandboxTemplateClient, SCOPE_NAMES, STATUS_NAMES, } from './client.js'; @@ -30,15 +31,25 @@ function client(impl: Partial>): SandboxClient { return new SandboxClient(transport); } +function templateClient(impl: Partial>): SandboxTemplateClient { + const transport: Transport = createRouterTransport((router) => { + router.service(OpenShell, impl); + }); + return new SandboxTemplateClient(transport); +} + function readySandbox( name: string, id: string, resourceVersion = 7n, + createdFromWorkloadTemplate?: { name: string; resourceVersion: string }, + workspace = 'default', ): MessageInitShape { return { sandbox: { - metadata: { id, name, labels: { team: 'aire' }, resourceVersion }, + metadata: { id, name, workspace, labels: { team: 'aire' }, resourceVersion }, status: { phase: SandboxPhase.READY }, + createdFromWorkloadTemplate, }, }; } @@ -261,6 +272,207 @@ describe('create', () => { expect(created.spec?.providers).toEqual(['claude']); }); + it('createFromTemplate sends the workload template name with governance fields only', async () => { + let created: { + workloadTemplateName?: string; + name?: string; + workspace?: string; + labels?: Record; + spec?: { + policy?: { version?: number }; + providers?: string[]; + command?: string[]; + tty?: boolean; + template?: { image?: string }; + }; + } = {}; + const sandbox = client({ + createSandbox: (req) => { + created = req; + return readySandbox('job-1', 'sb-id', 7n, undefined, req.workspace || 'default'); + }, + }); + const ref = await sandbox.createFromTemplate({ + name: 'job-1', + workspace: 'staging', + templateName: 'gpu-kata', + labels: { team: 'runtime' }, + providers: ['github'], + command: ['/opt/worker', '--serve'], + tty: true, + policy: { version: 1, networkPolicies: {} }, + }); + + expect(created.workloadTemplateName).toBe('gpu-kata'); + expect(created.name).toBe('job-1'); + expect(created.workspace).toBe('staging'); + expect(created.labels).toEqual({ team: 'runtime' }); + expect(created.spec?.providers).toEqual(['github']); + expect(created.spec?.command).toEqual(['/opt/worker', '--serve']); + expect(created.spec?.tty).toBe(true); + expect(created.spec?.policy?.version).toBe(1); + expect(created.spec?.template).toBeUndefined(); + expect(ref.workspace).toBe('staging'); + }); + + it('propagates workspace through sandbox lifecycle calls', async () => { + const observed: { + create?: { workspace?: string }; + get?: { workspace?: string }; + list?: { workspace?: string; allWorkspaces?: boolean }; + delete?: { workspace?: string }; + attach?: { workspace?: string }; + detach?: { workspace?: string }; + listProviders?: { workspace?: string }; + updatePolicy?: { workspace?: string }; + updateSetting?: { workspace?: string }; + configGets: string[]; + execGet?: string; + interactiveGet?: string; + sshGet?: string; + forwardGet?: string; + } = { configGets: [] }; + const sandbox = client({ + createSandbox: (req) => { + observed.create = req; + return readySandbox(req.name || 'sb', 'sb-created', 7n, undefined, req.workspace || 'default'); + }, + getSandbox: (req) => { + if (req.name === 'exec') observed.execGet = req.workspace; + else if (req.name === 'interactive') observed.interactiveGet = req.workspace; + else if (req.name === 'ssh') observed.sshGet = req.workspace; + else if (req.name === 'forward') observed.forwardGet = req.workspace; + else if (req.name === 'config') observed.configGets.push(req.workspace); + else observed.get = req; + return readySandbox(req.name, `${req.name}-id`, 7n, undefined, req.workspace || 'default'); + }, + listSandboxes: (req) => { + observed.list = req; + return { + sandboxes: [ + { + metadata: { + id: 'listed-id', + name: 'listed', + workspace: req.workspace || 'default', + labels: { team: 'aire' }, + resourceVersion: 7n, + }, + status: { phase: SandboxPhase.READY }, + }, + ], + }; + }, + deleteSandbox: (req) => { + observed.delete = req; + return { deleted: true }; + }, + attachSandboxProvider: (req) => { + observed.attach = req; + return { + sandbox: readySandbox(req.sandboxName, 'attach-id', 7n, undefined, req.workspace || 'default').sandbox, + attached: true, + }; + }, + detachSandboxProvider: (req) => { + observed.detach = req; + return { + sandbox: readySandbox(req.sandboxName, 'detach-id', 7n, undefined, req.workspace || 'default').sandbox, + detached: true, + }; + }, + listSandboxProviders: (req) => { + observed.listProviders = req; + return { providers: [] }; + }, + updateConfig: (req) => { + if (req.settingKey) observed.updateSetting = req; + else observed.updatePolicy = req; + return { version: 5, policyHash: 'hash', settingsRevision: 10n, deleted: false }; + }, + getSandboxConfig: () => ({ + policy: { version: 1, networkPolicies: {} }, + version: 5, + policyHash: 'hash', + settings: {}, + configRevision: 1n, + policySource: PolicySource.SANDBOX, + globalPolicyVersion: 0, + providerEnvRevision: 0n, + }), + // eslint-disable-next-line require-yield + execSandbox: async function* () { + yield { payload: { case: 'exit', value: { exitCode: 0 } } }; + }, + // eslint-disable-next-line require-yield + execSandboxInteractive: async function* () { + yield { payload: { case: 'exit', value: { exitCode: 0 } } }; + }, + createSshSession: (req) => ({ + sandboxId: req.sandboxId, + token: 'tok', + gatewayHost: 'gw', + gatewayPort: 443, + gatewayScheme: 'https', + hostKeyFingerprint: '', + expiresAtMs: 0n, + }), + revokeSshSession: () => ({ revoked: true }), + }); + + const created = await sandbox.create({ name: 'direct', workspace: 'staging', image: 'img' }); + const got = await sandbox.get('lookup', { workspace: 'staging' }); + const listed = await sandbox.list({ workspace: 'staging', limit: 10 }); + const deleted = await sandbox.delete('lookup', { workspace: 'staging' }); + await expect(sandbox.waitReady('lookup', 1, { workspace: 'staging' })).resolves.toMatchObject({ + workspace: 'staging', + }); + await expect(sandbox.exec('exec', ['true'], { workspace: 'staging' })).resolves.toMatchObject({ exitCode: 0 }); + const interactive = await sandbox.execInteractive('interactive', ['true'], { workspace: 'staging' }); + for await (const _event of interactive.output) { + // drain + } + await sandbox.createSshSession('ssh', { workspace: 'staging' }); + const attached = await sandbox.attachProvider('lookup', 'github', { workspace: 'staging' }); + const detached = await sandbox.detachProvider('lookup', 'github', { workspace: 'staging' }); + await sandbox.listProviders('lookup', { workspace: 'staging' }); + await sandbox.getConfig('config', { workspace: 'staging' }); + await sandbox.setPolicy('lookup', { version: 1, networkPolicies: {} }, { workspace: 'staging' }); + await sandbox.setSetting( + 'lookup', + 'feature.enabled', + { value: { case: 'boolValue', value: true } }, + { workspace: 'staging' }, + ); + + expect(created.workspace).toBe('staging'); + expect(got.workspace).toBe('staging'); + expect(listed[0]?.workspace).toBe('staging'); + expect(deleted).toBe(true); + expect(attached.sandbox.workspace).toBe('staging'); + expect(detached.sandbox.workspace).toBe('staging'); + expect(observed.create?.workspace).toBe('staging'); + expect(observed.get?.workspace).toBe('staging'); + expect(observed.list).toMatchObject({ workspace: 'staging', allWorkspaces: false }); + expect(observed.delete?.workspace).toBe('staging'); + expect(observed.execGet).toBe('staging'); + expect(observed.interactiveGet).toBe('staging'); + expect(observed.sshGet).toBe('staging'); + expect(observed.attach?.workspace).toBe('staging'); + expect(observed.detach?.workspace).toBe('staging'); + expect(observed.listProviders?.workspace).toBe('staging'); + expect(observed.configGets).toContain('staging'); + expect(observed.updatePolicy?.workspace).toBe('staging'); + expect(observed.updateSetting?.workspace).toBe('staging'); + }); + + it('createFromTemplate rejects an empty template name locally', async () => { + const sandbox = client({}); + await expect(sandbox.createFromTemplate({ templateName: ' ' })).rejects.toMatchObject({ + code: 'invalid_config', + }); + }); + it('rejects gateway sandboxes missing required metadata', async () => { const sandbox = client({ getSandbox: () => ({ sandbox: { status: { phase: SandboxPhase.READY } } }), @@ -288,6 +500,158 @@ describe('create', () => { exitCode: 9, }); }); + + it('maps workload template provenance onto SandboxRef', async () => { + const sandbox = client({ + getSandbox: () => + readySandbox('from-template', 'sb-id', 7n, { + name: 'gpu-kata', + resourceVersion: '42', + }), + }); + + const ref = await sandbox.get('from-template'); + + expect(ref.createdFromWorkloadTemplate).toEqual({ + name: 'gpu-kata', + resourceVersion: '42', + }); + }); +}); + +describe('sandbox templates', () => { + it('create sends the template resource and workspace', async () => { + let observed: { + workspace?: string; + template?: { + metadata?: { name?: string; labels?: Record }; + spec?: { + workload?: { + image?: string; + environment?: Record; + resources?: { cpu?: string; memory?: string; gpu?: { count?: number } }; + }; + driverConfig?: Record; + }; + }; + } = {}; + const templates = templateClient({ + createSandboxTemplate: (req) => { + observed = req; + return { + template: { + metadata: { + id: 'template-python', + name: req.template?.metadata?.name ?? '', + labels: req.template?.metadata?.labels ?? {}, + workspace: req.workspace, + resourceVersion: 1n, + }, + spec: req.template?.spec, + }, + }; + }, + }); + + const created = await templates.create( + { + metadata: { name: 'python', labels: { team: 'runtime' } }, + spec: { + workload: { + image: 'ghcr.io/nvidia/openshell-community/sandboxes/python:latest', + environment: { FEATURE_FLAG: 'on' }, + resources: { cpu: '1', memory: '512Mi', gpu: { count: 1 } }, + }, + driverConfig: { kubernetes: { runtime_class_name: 'kata-containers' } }, + }, + }, + { workspace: 'default' }, + ); + + expect(observed.workspace).toBe('default'); + expect(observed.template?.metadata?.name).toBe('python'); + expect(observed.template?.metadata?.labels).toEqual({ team: 'runtime' }); + expect(observed.template?.spec?.workload?.environment).toEqual({ FEATURE_FLAG: 'on' }); + expect(observed.template?.spec?.workload?.resources?.gpu?.count).toBe(1); + expect(created.metadata?.workspace).toBe('default'); + expect(created.metadata?.resourceVersion).toBe(1n); + }); + + it('get list and delete forward workspace and pagination', async () => { + const observed: { + get?: { name?: string; workspace?: string }; + list?: { limit?: number; offset?: number; workspace?: string; allWorkspaces?: boolean }; + delete?: { name?: string; workspace?: string }; + } = {}; + const templates = templateClient({ + getSandboxTemplate: (req) => { + observed.get = req; + return { + template: { + metadata: { id: 'template-gpu-kata', name: req.name, workspace: req.workspace }, + spec: { workload: { image: 'img:v1' } }, + }, + }; + }, + listSandboxTemplates: (req) => { + observed.list = req; + return { + templates: [ + { + metadata: { id: 'template-python', name: 'python', workspace: req.workspace || 'default' }, + spec: { workload: { image: 'img:v1' } }, + }, + ], + }; + }, + deleteSandboxTemplate: (req) => { + observed.delete = req; + return { deleted: true }; + }, + }); + + const got = await templates.get('gpu-kata', { workspace: 'staging' }); + const listed = await templates.list({ workspace: 'staging', limit: 10, offset: 2, labelSelector: 'team=runtime' }); + const deleted = await templates.delete('gpu-kata', { workspace: 'staging' }); + + expect(got.metadata?.name).toBe('gpu-kata'); + expect(listed).toHaveLength(1); + expect(deleted).toBe(true); + expect(observed.get).toMatchObject({ name: 'gpu-kata', workspace: 'staging' }); + expect(observed.list).toMatchObject({ + limit: 10, + offset: 2, + workspace: 'staging', + allWorkspaces: false, + labelSelector: 'team=runtime', + }); + expect(observed.delete).toMatchObject({ name: 'gpu-kata', workspace: 'staging' }); + }); + + it('list clears workspace when allWorkspaces is set', async () => { + let observed: { workspace?: string; allWorkspaces?: boolean } = {}; + const templates = templateClient({ + listSandboxTemplates: (req) => { + observed = req; + return { templates: [] }; + }, + }); + + await templates.list({ workspace: 'staging', allWorkspaces: true }); + + expect(observed.workspace).toBe(''); + expect(observed.allWorkspaces).toBe(true); + }); + + it('rejects empty names and missing template responses locally', async () => { + const templates = templateClient({ + getSandboxTemplate: () => ({}), + }); + + await expect(templates.get(' ')).rejects.toMatchObject({ code: 'invalid_config' }); + await expect(templates.delete(' ')).rejects.toMatchObject({ code: 'invalid_config' }); + await expect(templates.get('missing-response')).rejects.toMatchObject({ code: 'invalid_config' }); + }); }); describe('waits', () => { diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index d2bf520800..25b6f952f8 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -17,12 +17,13 @@ import type { MessageInitShape } from '@bufbuild/protobuf'; import { type CallOptions, type Client, createClient, type Transport } from '@connectrpc/connect'; import { errorCode, fromConnect, SdkError } from './errors.js'; import type { Provider } from './gen/datamodel_pb.js'; -import type { Sandbox, UpdateConfigResponse } from './gen/openshell_pb.js'; +import type { Sandbox, SandboxWorkloadTemplate, UpdateConfigResponse } from './gen/openshell_pb.js'; import { type ExecSandboxInputSchema, OpenShell, SandboxPhase, type SandboxSpecSchema, + type SandboxWorkloadTemplateSchema, ServiceStatus, type TcpForwardFrameSchema, } from './gen/openshell_pb.js'; @@ -31,9 +32,16 @@ import { PolicySource, type SandboxPolicySchema, SettingScope, type SettingValue import { validateSshResponse } from './ssh-validate.js'; import { buildTransport, type ConnectOptions } from './transport.js'; -// The policy and setting value shapes are the generated protobuf messages; -// re-export them rather than re-curating a parallel surface. Callers round-trip -// `getConfig().policy` back into `setPolicy`, and build `SettingValue`s inline. +// Generated protobuf message shapes that callers need to populate or round-trip +// directly. Re-export these rather than re-curating parallel surfaces. +export type { + SandboxResources, + SandboxServiceLevel, + SandboxStartup, + SandboxWorkloadConfig, + SandboxWorkloadTemplate, + SandboxWorkloadTemplateSpec, +} from './gen/openshell_pb.js'; export type { SandboxPolicy, SettingValue } from './gen/sandbox_pb.js'; export type { ConnectOptions }; export { errorCode }; @@ -78,6 +86,8 @@ export interface Health { export interface SandboxSpec { name?: string; + /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + workspace?: string; image?: string; labels?: Record; environment?: Record; @@ -103,24 +113,74 @@ export interface SandboxSpec { rawSpec?: MessageInitShape; } +export interface SandboxFromTemplateSpec { + name?: string; + /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + workspace?: string; + templateName: string; + labels?: Record; + providers?: string[]; + /** Exact canonical command. Empty selects the gateway scratch shell. */ + command?: string[]; + /** Allocate a retained pseudo-terminal for the canonical command. */ + tty?: boolean; + /** + * Create-time sandbox policy (the safety boundary). The named workload + * template supplies runtime workload fields. + */ + policy?: MessageInitShape; +} + export interface SandboxRef { id: string; name: string; + workspace: string; phase: SandboxPhaseName; labels: Record; /** u64 rendered as a string — JS numbers can't hold it safely. */ resourceVersion: string; mainProcessInstanceId?: string; exitCode?: number; + createdFromWorkloadTemplate?: SandboxWorkloadTemplateProvenance; +} + +export interface SandboxWorkloadTemplateProvenance { + name: string; + resourceVersion: string; } export interface ListOptions { limit?: number; offset?: number; labelSelector?: string; + /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + workspace?: string; + /** List across all workspaces. Requires platform admin permission. */ + allWorkspaces?: boolean; +} + +export interface SandboxWorkspaceOptions { + /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + workspace?: string; +} + +export type SandboxCallOptions = CallOptions & SandboxWorkspaceOptions; + +export interface SandboxTemplateWorkspaceOptions { + /** Workspace scope. Omit or use an empty string for the gateway default workspace. */ + workspace?: string; +} + +export interface SandboxTemplateListOptions extends SandboxTemplateWorkspaceOptions { + limit?: number; + offset?: number; + /** Optional label selector in key=value comma-separated form. */ + labelSelector?: string; + /** List templates across all workspaces. Requires platform admin permission. */ + allWorkspaces?: boolean; } -export interface ExecOptions { +export interface ExecOptions extends SandboxWorkspaceOptions { workdir?: string; environment?: Record; timeoutSecs?: number; @@ -158,7 +218,7 @@ export interface ExecExitEvent { /** An exec stream item: a stdout/stderr chunk or the terminal exit event. */ export type ExecStreamEvent = ExecStreamChunk | ExecExitEvent; -export interface ExecInteractiveOptions { +export interface ExecInteractiveOptions extends SandboxWorkspaceOptions { workdir?: string; environment?: Record; timeoutSecs?: number; @@ -190,12 +250,12 @@ export interface ExecInteractiveSession { } /** Cancellation for the poll-based wait helpers. */ -export interface WaitOptions { +export interface WaitOptions extends SandboxWorkspaceOptions { /** Abort the wait (and the in-flight poll RPC) early. */ signal?: AbortSignal; } -export interface ForwardOptions { +export interface ForwardOptions extends SandboxWorkspaceOptions { /** Loopback TCP port inside the sandbox to dial. */ targetPort: number; /** Target host inside the sandbox (loopback only). Default 127.0.0.1. */ @@ -248,7 +308,7 @@ export interface ProviderChange { changed: boolean; } -export interface ProviderChangeOptions { +export interface ProviderChangeOptions extends SandboxWorkspaceOptions { /** Pin the sandbox resource version for optimistic concurrency (u64 as string). */ expectedResourceVersion?: string; } @@ -274,7 +334,7 @@ export interface SandboxConfig { providerEnvRevision: string; } -export interface SetPolicyOptions { +export interface SetPolicyOptions extends SandboxWorkspaceOptions { /** Pin the sandbox resource version for optimistic concurrency (u64 as string). */ expectedResourceVersion?: string; /** Poll getConfig until the applied policy hash is observed. */ @@ -346,14 +406,26 @@ function sandboxRef(sandbox: Sandbox | undefined): SandboxRef { return { id: meta.id, name: meta.name, + workspace: meta.workspace, phase: phaseName(sandbox.status?.phase ?? SandboxPhase.UNSPECIFIED), labels: meta?.labels ?? {}, resourceVersion: (meta?.resourceVersion ?? 0n).toString(), mainProcessInstanceId: sandbox.status?.mainProcessInstanceId || undefined, exitCode: sandbox.status?.exitCode, + createdFromWorkloadTemplate: sandbox.createdFromWorkloadTemplate + ? { + name: sandbox.createdFromWorkloadTemplate.name, + resourceVersion: sandbox.createdFromWorkloadTemplate.resourceVersion, + } + : undefined, }; } +function sandboxTemplate(template: SandboxWorkloadTemplate | undefined): SandboxWorkloadTemplate { + if (!template) throw new SdkError('invalid_config', 'sandbox template missing from gateway response'); + return template; +} + function providerRef(provider: Provider): ProviderRef { const meta = provider.metadata; return { @@ -419,6 +491,16 @@ function versionPin(value: string | undefined): bigint { const FORWARD_CHUNK = 64 * 1024; +function workspaceOption(options?: SandboxWorkspaceOptions | null): string { + return options?.workspace ?? ''; +} + +function requestCallOptions(options?: SandboxCallOptions | null): CallOptions | undefined { + if (!options) return undefined; + const { workspace: _workspace, ...callOptions } = options; + return callOptions; +} + // Build CallOptions that bound one poll RPC by the remaining wall-clock budget // and honor caller cancellation, so a stalled RPC cannot outlive the deadline. function deadlineOptions(remainingMs: number, signal?: AbortSignal): CallOptions { @@ -536,6 +618,85 @@ export class Pushable implements AsyncIterable { } } +// ---- sandbox template client ---------------------------------------------- + +// Reusable sandbox workload template lifecycle. Templates intentionally return +// generated proto messages because the resource owns portable workload fields +// plus driver-specific config that should not be lossy in the curated layer. +export class SandboxTemplateClient { + private readonly grpc: Client; + + readonly raw: Client; + readonly transport: Transport; + + constructor(transport: Transport, grpc = createClient(OpenShell, transport)) { + this.transport = transport; + this.grpc = grpc; + this.raw = this.grpc; + } + + static async connect(options: ConnectOptions): Promise { + return new SandboxTemplateClient(buildTransport(options)); + } + + async create( + template: MessageInitShape, + options?: SandboxTemplateWorkspaceOptions | null, + ): Promise { + try { + const resp = await this.grpc.createSandboxTemplate({ + template, + workspace: options?.workspace ?? '', + }); + return sandboxTemplate(resp.template); + } catch (e) { + throw e instanceof SdkError ? e : fromConnect(e); + } + } + + async get(name: string, options?: SandboxTemplateWorkspaceOptions | null): Promise { + if (name.trim() === '') throw new SdkError('invalid_config', 'template name is required'); + try { + const resp = await this.grpc.getSandboxTemplate({ + name, + workspace: options?.workspace ?? '', + }); + return sandboxTemplate(resp.template); + } catch (e) { + throw e instanceof SdkError ? e : fromConnect(e); + } + } + + async list(options?: SandboxTemplateListOptions | null): Promise { + try { + const allWorkspaces = options?.allWorkspaces ?? false; + const resp = await this.grpc.listSandboxTemplates({ + limit: options?.limit ?? 0, + offset: options?.offset ?? 0, + workspace: allWorkspaces ? '' : (options?.workspace ?? ''), + allWorkspaces, + labelSelector: options?.labelSelector ?? '', + }); + return resp.templates; + } catch (e) { + throw fromConnect(e); + } + } + + async delete(name: string, options?: SandboxTemplateWorkspaceOptions | null): Promise { + if (name.trim() === '') throw new SdkError('invalid_config', 'template name is required'); + try { + const resp = await this.grpc.deleteSandboxTemplate({ + name, + workspace: options?.workspace ?? '', + }); + return resp.deleted; + } catch (e) { + throw fromConnect(e); + } + } +} + // ---- sandbox client -------------------------------------------------------- // Sandbox lifecycle + exec. Usable standalone via `SandboxClient.connect()`, @@ -590,6 +751,7 @@ export class SandboxClient { const resp = await this.grpc.createSandbox({ name: spec.name ?? '', labels: spec.labels ?? {}, + workspace: spec.workspace ?? '', spec: specInit, }); return sandboxRef(resp.sandbox); @@ -598,9 +760,33 @@ export class SandboxClient { } } - async get(name: string, callOptions?: CallOptions): Promise { + async createFromTemplate(spec: SandboxFromTemplateSpec): Promise { + if (spec.templateName.trim() === '') throw new SdkError('invalid_config', 'templateName is required'); try { - const resp = await this.grpc.getSandbox({ name }, callOptions); + const resp = await this.grpc.createSandbox({ + name: spec.name ?? '', + labels: spec.labels ?? {}, + workspace: spec.workspace ?? '', + spec: { + providers: spec.providers ?? [], + command: spec.command ?? [], + tty: spec.tty ?? false, + policy: spec.policy, + }, + workloadTemplateName: spec.templateName, + }); + return sandboxRef(resp.sandbox); + } catch (e) { + throw fromConnect(e); + } + } + + async get(name: string, options?: SandboxCallOptions | null): Promise { + try { + const resp = await this.grpc.getSandbox( + { name, workspace: workspaceOption(options) }, + requestCallOptions(options), + ); return sandboxRef(resp.sandbox); } catch (e) { throw fromConnect(e); @@ -609,10 +795,13 @@ export class SandboxClient { async list(options?: ListOptions | null): Promise { try { + const allWorkspaces = options?.allWorkspaces ?? false; const resp = await this.grpc.listSandboxes({ limit: options?.limit ?? 0, offset: options?.offset ?? 0, labelSelector: options?.labelSelector ?? '', + workspace: allWorkspaces ? '' : (options?.workspace ?? ''), + allWorkspaces, }); return resp.sandboxes.map((s) => sandboxRef(s)); } catch (e) { @@ -620,9 +809,9 @@ export class SandboxClient { } } - async delete(name: string): Promise { + async delete(name: string, options?: SandboxWorkspaceOptions | null): Promise { try { - const resp = await this.grpc.deleteSandbox({ name }); + const resp = await this.grpc.deleteSandbox({ name, workspace: workspaceOption(options) }); return resp.deleted; } catch (e) { throw fromConnect(e); @@ -641,7 +830,10 @@ export class SandboxClient { if (Date.now() >= deadline) throw new SdkError('connect', `timed out waiting for sandbox '${name}'`); let ref: SandboxRef; try { - ref = await this.get(name, deadlineOptions(deadline - Date.now(), signal)); + ref = await this.get(name, { + ...deadlineOptions(deadline - Date.now(), signal), + workspace: options?.workspace, + }); } catch (e) { throw mapWaitError(e, name, deadline, signal); } @@ -664,7 +856,7 @@ export class SandboxClient { if (signal?.aborted) throw new SdkError('connect', `wait for sandbox '${name}' aborted`); if (Date.now() >= deadline) throw new SdkError('connect', `timed out waiting for sandbox '${name}' to delete`); try { - await this.get(name, deadlineOptions(deadline - Date.now(), signal)); + await this.get(name, { ...deadlineOptions(deadline - Date.now(), signal), workspace: options?.workspace }); } catch (e) { if (e instanceof SdkError && e.code === 'not_found') return; throw mapWaitError(e, name, deadline, signal); @@ -687,7 +879,10 @@ export class SandboxClient { ): AsyncGenerator { try { // Resolve the sandbox id first, exactly like the gateway client. - const sandbox = await this.get(name, options?.signal ? { signal: options.signal } : undefined); + const sandbox = await this.get(name, { + workspace: options?.workspace, + ...(options?.signal ? { signal: options.signal } : {}), + }); const stream = this.grpc.execSandbox( { sandboxId: sandbox.id, @@ -761,7 +956,9 @@ export class SandboxClient { ): Promise { let sandboxId: string; try { - sandboxId = (await this.get(name, options?.signal ? { signal: options.signal } : undefined)).id; + sandboxId = ( + await this.get(name, { workspace: options?.workspace, ...(options?.signal ? { signal: options.signal } : {}) }) + ).id; } catch (e) { throw e instanceof SdkError ? e : fromConnect(e); } @@ -880,7 +1077,7 @@ export class SandboxClient { let sandboxId: string; try { - const ref = await this.get(name, opts.signal ? { signal: opts.signal } : undefined); + const ref = await this.get(name, { workspace: opts.workspace, ...(opts.signal ? { signal: opts.signal } : {}) }); if (ref.phase !== 'ready') { throw new SdkError('connect', `sandbox '${name}' is not ready (phase: ${ref.phase})`); } @@ -1061,9 +1258,9 @@ export class SandboxClient { // Mint a short-lived SSH session token for the sandbox — the input side of // ssh-config / ProxyCommand and forwardTcp authorization. - async createSshSession(name: string): Promise { + async createSshSession(name: string, options?: SandboxWorkspaceOptions | null): Promise { try { - const sandbox = await this.get(name); + const sandbox = await this.get(name, options); const resp = await this.grpc.createSshSession({ sandboxId: sandbox.id }); // Reject any response outside the proto trust-boundary contract before // handing these values to the caller (they feed OpenSSH ProxyCommand). @@ -1101,6 +1298,7 @@ export class SandboxClient { sandboxName: name, providerName: provider, expectedResourceVersion: versionPin(options?.expectedResourceVersion), + workspace: workspaceOption(options), }); return { sandbox: sandboxRef(resp.sandbox), changed: resp.attached }; } catch (e) { @@ -1118,6 +1316,7 @@ export class SandboxClient { sandboxName: name, providerName: provider, expectedResourceVersion: versionPin(options?.expectedResourceVersion), + workspace: workspaceOption(options), }); return { sandbox: sandboxRef(resp.sandbox), changed: resp.detached }; } catch (e) { @@ -1125,19 +1324,19 @@ export class SandboxClient { } } - async listProviders(name: string): Promise { + async listProviders(name: string, options?: SandboxWorkspaceOptions | null): Promise { try { - const resp = await this.grpc.listSandboxProviders({ sandboxName: name }); + const resp = await this.grpc.listSandboxProviders({ sandboxName: name, workspace: workspaceOption(options) }); return resp.providers.map((p) => providerRef(p)); } catch (e) { throw fromConnect(e); } } - async getConfig(name: string, callOptions?: CallOptions): Promise { + async getConfig(name: string, options?: SandboxCallOptions | null): Promise { try { - const sandbox = await this.get(name, callOptions); - const resp = await this.grpc.getSandboxConfig({ sandboxId: sandbox.id }, callOptions); + const sandbox = await this.get(name, options); + const resp = await this.grpc.getSandboxConfig({ sandboxId: sandbox.id }, requestCallOptions(options)); return sandboxConfig(resp); } catch (e) { throw e instanceof SdkError ? e : fromConnect(e); @@ -1159,9 +1358,11 @@ export class SandboxClient { policy, global: false, expectedResourceVersion: versionPin(options?.expectedResourceVersion), + workspace: workspaceOption(options), }); const result = updateConfigResult(resp); - if (options?.wait) await this.waitForPolicyHash(name, result.policyHash, options.waitTimeoutSecs); + if (options?.wait) + await this.waitForPolicyHash(name, result.policyHash, options.waitTimeoutSecs, options.workspace); return result; } catch (e) { throw e instanceof SdkError ? e : fromConnect(e); @@ -1174,6 +1375,7 @@ export class SandboxClient { name: string, key: string, value: MessageInitShape, + options?: SandboxWorkspaceOptions | null, ): Promise { try { const resp = await this.grpc.updateConfig({ @@ -1181,6 +1383,7 @@ export class SandboxClient { settingKey: key, settingValue: value, global: false, + workspace: workspaceOption(options), }); return updateConfigResult(resp); } catch (e) { @@ -1191,13 +1394,18 @@ export class SandboxClient { // Poll getConfig until the applied policy hash is observed. Each poll RPC is // bounded by the remaining deadline (deadlineOptions), so a stalled getConfig // cannot make the returned promise outlive timeoutSecs. - private async waitForPolicyHash(name: string, policyHash: string, timeoutSecs = 60): Promise { + private async waitForPolicyHash( + name: string, + policyHash: string, + timeoutSecs = 60, + workspace?: string, + ): Promise { const deadline = Date.now() + timeoutSecs * 1000; let delay = 100; for (;;) { let config: SandboxConfig; try { - config = await this.getConfig(name, deadlineOptions(deadline - Date.now())); + config = await this.getConfig(name, { ...deadlineOptions(deadline - Date.now()), workspace }); } catch (e) { if (Date.now() >= deadline) { throw new SdkError('connect', `timed out waiting for policy '${policyHash}' on sandbox '${name}'`); @@ -1219,6 +1427,8 @@ export class SandboxClient { export class OpenShellClient { /** Sandbox lifecycle + exec: create/get/list/delete, waitReady/waitDeleted, exec. */ readonly sandbox: SandboxClient; + /** Reusable sandbox workload template lifecycle. */ + readonly sandboxTemplates: SandboxTemplateClient; /** * Advanced escape hatch: a generated client for every gateway RPC, including @@ -1238,6 +1448,7 @@ export class OpenShellClient { this.grpc = createClient(OpenShell, transport); this.raw = this.grpc; this.sandbox = new SandboxClient(transport, this.grpc); + this.sandboxTemplates = new SandboxTemplateClient(transport, this.grpc); } /** diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 61cdce3871..31571865be 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -23,10 +23,20 @@ export type { ProviderChangeOptions, ProviderRef, SandboxConfig, + SandboxFromTemplateSpec, SandboxPhaseName, SandboxPolicy, SandboxRef, + SandboxResources, + SandboxServiceLevel, SandboxSpec, + SandboxStartup, + SandboxTemplateListOptions, + SandboxTemplateWorkspaceOptions, + SandboxWorkloadConfig, + SandboxWorkloadTemplate, + SandboxWorkloadTemplateProvenance, + SandboxWorkloadTemplateSpec, SetPolicyOptions, SettingScopeName, SettingValue, @@ -34,7 +44,7 @@ export type { UpdateConfigResult, WaitOptions, } from './client.js'; -export { errorCode, OpenShellClient, SandboxClient } from './client.js'; +export { errorCode, OpenShellClient, SandboxClient, SandboxTemplateClient } from './client.js'; export type { SdkErrorCode } from './errors.js'; export { SdkError } from './errors.js'; export type { ClientCredentialsOptions, OidcTokenProvider } from './oidc.js'; From 07f2db7e2323fa954c8bd90d7c491f225282f0d1 Mon Sep 17 00:00:00 2001 From: Simon Scatton <44714756+SDAChess@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:45:59 +0000 Subject: [PATCH 07/16] fix(release): publish prerelease helm charts (#3126) Signed-off-by: Simon Scatton --- .github/workflows/release-tag.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index d47a6c7cb5..b3c45c97fb 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -732,7 +732,6 @@ jobs: release-helm: name: Release Helm Chart (OCI) needs: [compute-versions, release, tag-ghcr-release] - if: needs.compute-versions.outputs.is_prerelease != 'true' runs-on: ubuntu-latest timeout-minutes: 10 permissions: From 35f0556d31363f56aac457d6ea1fbab1abda014b Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 2 Sep 2026 09:51:53 +0000 Subject: [PATCH 08/16] test(guest): consolidate rootless Podman provisioning (#3125) * fix(test-guest): support Nix stat on macOS Signed-off-by: Evan Lezar * test(guest): version Ubuntu test guest profiles Signed-off-by: Evan Lezar * test(guest): consolidate rootless Podman provisioning Signed-off-by: Evan Lezar --------- Signed-off-by: Evan Lezar --- nix/test-guest/README.md | 63 +++++++++++-------- nix/test-guest/cache.sh | 6 +- .../configuration/podman-rootless.yml | 36 +++++++++++ nix/test-guest/configuration/podman.yml | 42 ------------- .../configuration/tasks/podman-common.yml | 13 ++++ .../tasks/podman-rootless/fedora.yml | 12 ++++ .../tasks/podman-rootless/shared.yml | 56 +++++++++++++++++ .../tasks/podman-rootless/ubuntu.yml | 29 +++++++++ nix/test-guest/default.nix | 18 +++++- .../distros/{ubuntu.nix => ubuntu-24-04.nix} | 0 nix/test-guest/distros/ubuntu-26-04.nix | 25 ++++++++ nix/test-guest/run.sh | 24 +++---- 12 files changed, 239 insertions(+), 85 deletions(-) create mode 100644 nix/test-guest/configuration/podman-rootless.yml delete mode 100644 nix/test-guest/configuration/podman.yml create mode 100644 nix/test-guest/configuration/tasks/podman-common.yml create mode 100644 nix/test-guest/configuration/tasks/podman-rootless/fedora.yml create mode 100644 nix/test-guest/configuration/tasks/podman-rootless/shared.yml create mode 100644 nix/test-guest/configuration/tasks/podman-rootless/ubuntu.yml rename nix/test-guest/distros/{ubuntu.nix => ubuntu-24-04.nix} (100%) create mode 100644 nix/test-guest/distros/ubuntu-26-04.nix diff --git a/nix/test-guest/README.md b/nix/test-guest/README.md index b608cb19fe..3bf1687205 100644 --- a/nix/test-guest/README.md +++ b/nix/test-guest/README.md @@ -33,13 +33,20 @@ nix/test-guest/ ├── cache-lib.sh ├── cache-seal.sh ├── distros/ -│ ├── ubuntu.nix +│ ├── ubuntu-24-04.nix +│ ├── ubuntu-26-04.nix │ ├── centos.nix │ ├── fedora.nix │ └── rocky.nix └── configuration/ ├── docker.yml - ├── podman.yml + ├── podman-rootless.yml + ├── tasks/ + │ ├── podman-common.yml + │ └── podman-rootless/ + │ ├── fedora.yml + │ ├── shared.yml + │ └── ubuntu.yml └── selinux.yml ``` @@ -56,22 +63,26 @@ The root [`flake.nix`](../../flake.nix) exposes this directory as the `test-gues ## Supported configurations -| Distro | Docker | Podman | SELinux | Package format | +| Distro | Docker | Rootless Podman | SELinux | Package format | | --- | --- | --- | --- | --- | -| Ubuntu 24.04 | Yes | Yes | No | `.deb` | -| CentOS Stream 10 | No | Yes | Yes | `.rpm` | +| Ubuntu 24.04 | Yes | No | No | `.deb` | +| Ubuntu 26.04 | Yes | Yes | No | `.deb` | +| CentOS Stream 10 | No | No | Yes | `.rpm` | | Fedora 44 | No | Yes | Yes | `.rpm` | -| Rocky Linux 9 | Yes | Yes | Yes | `.rpm` | +| Rocky Linux 9 | Yes | No | Yes | `.rpm` | The `snapd` configuration is available for Ubuntu and prepares snapd for local Snap lifecycle experiments. It does not install Docker, because the Snap gateway reproduction uses the Docker **Snap** and its `docker:docker-daemon` interface rather than the host-package Docker configuration. -The Ubuntu 24.04 Podman configuration is available for runtime and packaging -checks, but its Podman 4 release does not provide the `pasta` rootless network -helper required by OpenShell sandbox callbacks. OpenShell Podman E2E runs use -the Fedora guest, which provides Podman 5 and `pasta`. +`podman-rootless` configures the explicit rootless Podman guest setup used by +OpenShell tests. It supports Fedora and Ubuntu 26.04 or later. Ubuntu adds the +AppArmor rule that permits `pasta` to receive Podman stop signals. Fedora +installs the subordinate-ID utilities and rootless storage and network helpers. +Both configurations verify rootless mode and the `pasta` network helper +required by OpenShell sandbox callbacks. Ubuntu 24.04 ships Podman 4, which +does not provide that helper. List the available distros and configurations: @@ -84,30 +95,30 @@ nix run .#test-guest -- --list Boot a base Ubuntu VM: ```shell -nix run .#test-guest -- --distro ubuntu +nix run .#test-guest -- --distro ubuntu-24-04 ``` Apply the Docker configuration before opening the SSH session: ```shell -nix run .#test-guest -- --distro ubuntu --with docker +nix run .#test-guest -- --distro ubuntu-24-04 --with docker ``` Other combinations use the same interface: ```shell nix run .#test-guest -- --distro rocky --with docker -nix run .#test-guest -- --distro centos --with podman -nix run .#test-guest -- --distro fedora --with podman +nix run .#test-guest -- --distro ubuntu-26-04 --with podman-rootless +nix run .#test-guest -- --distro fedora --with podman-rootless ``` Configurations are repeatable: ```shell nix run .#test-guest -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker \ - --with podman + --with podman-rootless ``` Ensure SELinux is enforcing on CentOS, Fedora, or Rocky: @@ -138,7 +149,7 @@ The `test-guest-cache` app ensures a prepared disk exists for one exact distro, ```shell nix run .#test-guest-cache -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker ``` @@ -147,7 +158,7 @@ backing cache: ```shell nix run .#test-guest-cache -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker \ --repository ghcr.io/nvidia/openshell/test-guest-cache \ --digest sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef @@ -157,7 +168,7 @@ The command never publishes implicitly. Add `--push` after authenticating ORAS t ```shell nix run .#test-guest-cache -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker \ --repository ghcr.io/nvidia/openshell/test-guest-cache \ --push @@ -184,8 +195,8 @@ The default cache directory is `${XDG_CACHE_HOME:-$HOME/.cache}/openshell/test-g Cache command options: ```text ---distro NAME Base distro: ubuntu, centos, fedora, or rocky ---with NAME Apply docker, podman, or selinux; repeatable +--distro NAME Base distro: ubuntu-24-04, ubuntu-26-04, centos, fedora, or rocky +--with NAME Apply docker, podman-rootless, selinux, or snapd; repeatable --repository REF OCI repository without a tag --digest DIGEST Trusted OCI manifest digest required for pulls --cache-dir PATH Override the local prepared-disk cache directory @@ -209,7 +220,7 @@ Install the package in an Ubuntu VM and run a command: ```shell nix run .#test-guest -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with docker \ --install artifacts/openshell_0.0.0-local_arm64.deb \ -- openshell --version @@ -226,7 +237,7 @@ guest file preserves the source's ordinary permission bits: ```shell nix run .#test-guest -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --copy ./openshell:/usr/local/bin/openshell \ -- openshell --version ``` @@ -241,7 +252,7 @@ gateway. On each failure it prints snapd and gateway journals. ```shell nix run .#test-guest -- \ - --distro ubuntu \ + --distro ubuntu-24-04 \ --with snapd \ --keep \ --copy ./openshell_*.snap:/tmp/openshell.snap \ @@ -259,8 +270,8 @@ The destination must be an absolute guest path. Copied files are installed with ## Runner options ```text ---distro NAME Base distro: ubuntu, centos, fedora, or rocky ---with NAME Apply docker, podman, or selinux; repeatable +--distro NAME Base distro: ubuntu-24-04, ubuntu-26-04, centos, fedora, or rocky +--with NAME Apply docker, podman-rootless, selinux, or snapd; repeatable --install PATH Install a .deb or .rpm package; repeatable --copy SRC:DEST Copy a regular file into the guest, preserving its host mode; repeatable diff --git a/nix/test-guest/cache.sh b/nix/test-guest/cache.sh index 1103b3c289..f29a9b3492 100644 --- a/nix/test-guest/cache.sh +++ b/nix/test-guest/cache.sh @@ -12,8 +12,8 @@ Usage: nix run .#test-guest-cache -- --distro DISTRO [OPTIONS] Options: - --distro NAME Base distro: ubuntu, centos, fedora, or rocky - --with NAME Apply a configuration; repeatable (docker, podman, selinux) + --distro NAME Base distro: ubuntu-24-04, ubuntu-26-04, centos, fedora, or rocky + --with NAME Apply a configuration; repeatable (docker, podman-rootless, selinux, snapd) --repository REF OCI repository without a tag --digest DIGEST Trusted OCI manifest digest required for pulls --cache-dir PATH Override the local prepared-disk cache directory @@ -380,7 +380,7 @@ build_local() { for configuration in "${configurations[@]}"; do case "${configuration}" in docker) validation+='; docker info >/dev/null' ;; - podman) validation+='; podman info >/dev/null' ;; + podman-rootless) validation+='; podman info >/dev/null' ;; selinux) validation+='; test "$(getenforce)" = Enforcing' ;; esac done diff --git a/nix/test-guest/configuration/podman-rootless.yml b/nix/test-guest/configuration/podman-rootless.yml new file mode 100644 index 0000000000..23fef05e5e --- /dev/null +++ b/nix/test-guest/configuration/podman-rootless.yml @@ -0,0 +1,36 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# PROTOTYPE: Configure rootless Podman in a disposable test guest. + +- name: Configure rootless Podman + hosts: test_vm + become: true + gather_facts: true + + tasks: + - name: Validate rootless Podman support + ansible.builtin.assert: + that: + - >- + ansible_facts.distribution == "Fedora" or + (ansible_facts.distribution == "Ubuntu" and + ansible_facts.distribution_version is version("26.04", ">=")) + fail_msg: >- + Rootless Podman requires Fedora or Ubuntu 26.04 or newer, not + {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}. + + - name: Install common Podman prerequisites + ansible.builtin.import_tasks: tasks/podman-common.yml + + - name: Configure rootless Podman on Ubuntu + ansible.builtin.import_tasks: tasks/podman-rootless/ubuntu.yml + when: ansible_facts.distribution == "Ubuntu" + + - name: Configure rootless Podman on Fedora + ansible.builtin.import_tasks: tasks/podman-rootless/fedora.yml + when: ansible_facts.distribution == "Fedora" + + - name: Configure shared rootless Podman settings + ansible.builtin.import_tasks: tasks/podman-rootless/shared.yml diff --git a/nix/test-guest/configuration/podman.yml b/nix/test-guest/configuration/podman.yml deleted file mode 100644 index 1c79199074..0000000000 --- a/nix/test-guest/configuration/podman.yml +++ /dev/null @@ -1,42 +0,0 @@ ---- -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# PROTOTYPE: Configure Podman in a disposable test guest. - -- name: Configure Podman - hosts: test_vm - become: true - gather_facts: true - - tasks: - - name: Validate Podman support - ansible.builtin.assert: - that: - - ansible_facts.distribution in ["Ubuntu", "CentOS", "Fedora", "Rocky"] - fail_msg: >- - Podman is unsupported on {{ ansible_facts.distribution }}. - - - name: Refresh Ubuntu package metadata - ansible.builtin.apt: - update_cache: true - when: ansible_facts.distribution == "Ubuntu" - - - name: Install Podman dependencies - ansible.builtin.package: - name: podman - state: present - - - name: Enable the rootless Podman API socket - ansible.builtin.systemd_service: - name: podman.socket - scope: user - enabled: true - state: started - become: false - - - name: Verify rootless Podman - ansible.builtin.command: - cmd: podman info - become: false - changed_when: false diff --git a/nix/test-guest/configuration/tasks/podman-common.yml b/nix/test-guest/configuration/tasks/podman-common.yml new file mode 100644 index 0000000000..18a0f1304a --- /dev/null +++ b/nix/test-guest/configuration/tasks/podman-common.yml @@ -0,0 +1,13 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +- name: Refresh Ubuntu package metadata + ansible.builtin.apt: + update_cache: true + when: ansible_facts.distribution == "Ubuntu" + +- name: Install Podman + ansible.builtin.package: + name: podman + state: present diff --git a/nix/test-guest/configuration/tasks/podman-rootless/fedora.yml b/nix/test-guest/configuration/tasks/podman-rootless/fedora.yml new file mode 100644 index 0000000000..7d7d08a261 --- /dev/null +++ b/nix/test-guest/configuration/tasks/podman-rootless/fedora.yml @@ -0,0 +1,12 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +- name: Install Fedora rootless Podman prerequisites + ansible.builtin.dnf: + name: + - fuse-overlayfs + - passt + - shadow-utils + - shadow-utils-subid + state: present diff --git a/nix/test-guest/configuration/tasks/podman-rootless/shared.yml b/nix/test-guest/configuration/tasks/podman-rootless/shared.yml new file mode 100644 index 0000000000..9f4eba6ae9 --- /dev/null +++ b/nix/test-guest/configuration/tasks/podman-rootless/shared.yml @@ -0,0 +1,56 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +- name: Verify test user subordinate ID mappings + ansible.builtin.command: + argv: + - awk + - "-F:" + - >- + $1 == "openshell" && $2 ~ /^[0-9]+$/ && + $3 ~ /^[1-9][0-9]*$/ && $3 >= 65536 { found = 1 } + END { exit !found } + - "{{ item }}" + loop: + - /etc/subuid + - /etc/subgid + changed_when: false + +- name: Enable the rootless Podman API socket + ansible.builtin.systemd_service: + name: podman.socket + scope: user + enabled: true + state: started + become: false + +- name: Verify rootless Podman + ansible.builtin.command: + cmd: podman info + become: false + changed_when: false + +- name: Verify rootless Podman mode + ansible.builtin.command: + argv: + - podman + - info + - --format + - "{% raw %}{{.Host.Security.Rootless}}{% endraw %}" + become: false + changed_when: false + register: podman_rootless + failed_when: podman_rootless.stdout != "true" + +- name: Verify rootless Podman uses pasta + ansible.builtin.command: + argv: + - podman + - info + - --format + - "{% raw %}{{.Host.RootlessNetworkCmd}}{% endraw %}" + become: false + changed_when: false + register: podman_rootless_network + failed_when: podman_rootless_network.stdout != "pasta" diff --git a/nix/test-guest/configuration/tasks/podman-rootless/ubuntu.yml b/nix/test-guest/configuration/tasks/podman-rootless/ubuntu.yml new file mode 100644 index 0000000000..25d474e415 --- /dev/null +++ b/nix/test-guest/configuration/tasks/podman-rootless/ubuntu.yml @@ -0,0 +1,29 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +- name: Install Ubuntu rootless Podman prerequisites + ansible.builtin.apt: + name: + - apparmor + - fuse-overlayfs + - passt + - uidmap + state: present + +- name: Allow pasta to receive Podman stop signals + ansible.builtin.lineinfile: + path: /etc/apparmor.d/usr.bin.pasta + insertafter: "^ include $" + line: " signal (receive) peer=podman," + state: present + register: pasta_apparmor_profile + +- name: Reload pasta AppArmor profile + ansible.builtin.command: + argv: + - apparmor_parser + - --replace + - /etc/apparmor.d/usr.bin.pasta + when: pasta_apparmor_profile.changed + changed_when: pasta_apparmor_profile.changed diff --git a/nix/test-guest/default.nix b/nix/test-guest/default.nix index 753f266ab4..3b03c1fa95 100644 --- a/nix/test-guest/default.nix +++ b/nix/test-guest/default.nix @@ -18,7 +18,8 @@ let if isAarch64 then "${qemu}/bin/qemu-system-aarch64" else "${qemu}/bin/qemu-system-x86_64"; distros = { - ubuntu = import ./distros/ubuntu.nix { inherit pkgs architecture; }; + ubuntu-24-04 = import ./distros/ubuntu-24-04.nix { inherit pkgs architecture; }; + ubuntu-26-04 = import ./distros/ubuntu-26-04.nix { inherit pkgs architecture; }; centos = import ./distros/centos.nix { inherit pkgs architecture; }; fedora = import ./distros/fedora.nix { inherit pkgs architecture; }; rocky = import ./distros/rocky.nix { inherit pkgs architecture; }; @@ -26,11 +27,18 @@ let configurations = { docker = ./configuration/docker.yml; - podman = ./configuration/podman.yml; + podman-rootless = ./configuration/podman-rootless.yml; selinux = ./configuration/selinux.yml; snapd = ./configuration/snapd.yml; }; + configurationTasks = [ + "podman-common.yml" + "podman-rootless/fedora.yml" + "podman-rootless/shared.yml" + "podman-rootless/ubuntu.yml" + ]; + mkDistroProfile = name: distro: pkgs.writeText "openshell-test-guest-${name}" '' @@ -52,7 +60,11 @@ let ); configurationCatalog = pkgs.linkFarm "openshell-test-guest-configurations" ( - pkgs.lib.mapAttrsToList (name: path: { inherit name path; }) configurations + (pkgs.lib.mapAttrsToList (name: path: { inherit name path; }) configurations) + ++ (map (name: { + name = "tasks/${name}"; + path = ./configuration/tasks/${name}; + }) configurationTasks) ); runtimeInputs = [ diff --git a/nix/test-guest/distros/ubuntu.nix b/nix/test-guest/distros/ubuntu-24-04.nix similarity index 100% rename from nix/test-guest/distros/ubuntu.nix rename to nix/test-guest/distros/ubuntu-24-04.nix diff --git a/nix/test-guest/distros/ubuntu-26-04.nix b/nix/test-guest/distros/ubuntu-26-04.nix new file mode 100644 index 0000000000..7f37fec138 --- /dev/null +++ b/nix/test-guest/distros/ubuntu-26-04.nix @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{ pkgs, architecture }: + +let + imageArchitecture = if architecture == "aarch64" then "arm64" else "amd64"; + imageUrl = "https://cloud-images.ubuntu.com/releases/releases/26.04/release/ubuntu-26.04-server-cloudimg-${imageArchitecture}.img"; + imageHash = + if architecture == "aarch64" then + "sha256-PhE/3UHznhNyk3UXO7KueT+H3G20KU5SUf8kdpcXiLo=" + else + "sha256-gZa+nXlYBZy1bGx1yA/fbO6KiIW8FJ6nkdfbHH75MDU="; +in +{ + osId = "ubuntu"; + osVersion = "26.04"; + packageFamily = "deb"; + inherit imageUrl imageHash; + image = pkgs.fetchurl { + name = "ubuntu-26.04-server-cloudimg-${imageArchitecture}.img"; + url = imageUrl; + hash = imageHash; + }; +} diff --git a/nix/test-guest/run.sh b/nix/test-guest/run.sh index 6be58eb235..8704633828 100644 --- a/nix/test-guest/run.sh +++ b/nix/test-guest/run.sh @@ -12,8 +12,8 @@ Usage: nix run .#test-guest -- --distro DISTRO [OPTIONS] [-- COMMAND...] Options: - --distro NAME Base distro: ubuntu, centos, fedora, or rocky - --with NAME Apply a configuration; repeatable (docker, podman, selinux, snapd) + --distro NAME Base distro: ubuntu-24-04, ubuntu-26-04, centos, fedora, or rocky + --with NAME Apply a configuration; repeatable (docker, podman-rootless, selinux, snapd) --install PATH Install a .deb or .rpm package; repeatable --copy SRC:DEST Copy a regular file to an absolute guest path, preserving its host mode; repeatable @@ -48,12 +48,13 @@ preserved_file_mode() { local source_path=$1 local source_mode - if [ "$(uname -s)" = Darwin ]; then - if ! source_mode=$(stat -f '%Lp' "${source_path}"); then - echo "could not determine mode for --copy source: ${source_path}" >&2 - return 1 - fi - elif ! source_mode=$(stat -c '%a' "${source_path}"); then + # Nix supplies GNU coreutils on macOS, so choose the supported stat format + # by probing rather than relying on the host kernel name. + if source_mode=$(stat -c '%a' "${source_path}" 2>/dev/null); then + : + elif source_mode=$(stat -f '%Lp' "${source_path}" 2>/dev/null); then + : + else echo "could not determine mode for --copy source: ${source_path}" >&2 return 1 fi @@ -141,6 +142,7 @@ if [ "${list}" -eq 1 ]; then done echo "Configurations:" for entry in "${OPENSHELL_TEST_GUEST_CONFIGURATIONS}"/*; do + [ -f "${entry}" ] || continue printf ' %s\n' "${entry##*/}" done exit 0 @@ -160,9 +162,9 @@ fi # shellcheck disable=SC1090 . "${OPENSHELL_TEST_GUEST_DISTROS}/${distro}" -for item in "${configurations[@]}"; do - if [[ ! ${item} =~ ^[a-z0-9][a-z0-9-]*$ ]] || - [ ! -r "${OPENSHELL_TEST_GUEST_CONFIGURATIONS}/${item}" ]; then + for item in "${configurations[@]}"; do + if [[ ! ${item} =~ ^[a-z0-9][a-z0-9-]*$ ]] || + [ ! -f "${OPENSHELL_TEST_GUEST_CONFIGURATIONS}/${item}" ]; then echo "unknown configuration: ${item:-}" >&2 exit 2 fi From 77158a765dcf353b5da342c79b770c79aac74072 Mon Sep 17 00:00:00 2001 From: Simon Scatton <44714756+SDAChess@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:06:32 +0000 Subject: [PATCH 09/16] ci: add Fedora conformance workflow (#3086) * ci: add Fedora conformance workflow Signed-off-by: Simon Scatton * ci: enable KVM for Fedora conformance Signed-off-by: Simon Scatton * ci: run Fedora conformance with KVM Signed-off-by: Simon Scatton * ci: copy Fedora conformance script into guest Signed-off-by: Simon Scatton * ci: limit conformance VM setup to KVM Signed-off-by: Simon Scatton * ci: streamline Fedora conformance builds Signed-off-by: Simon Scatton * ci: use dev supervisor for Fedora conformance Signed-off-by: Simon Scatton * ci: pin Fedora RPM build image Signed-off-by: Simon Scatton * ci: cache RPM vendoring dependencies Signed-off-by: Simon Scatton * ci: clarify Fedora conformance job name Signed-off-by: Simon Scatton * ci: make conformance workflow manual only Signed-off-by: Simon Scatton * ci(conformance): use new podman rootless install * fix(ci): build gateway from renamed package Signed-off-by: Simon Scatton --------- Signed-off-by: Simon Scatton --- .github/workflows/build-rpm.yml | 123 +++++++++++++++++++ .github/workflows/conformance.yml | 195 ++++++++++++++++++++++++++++++ .github/workflows/rpm-package.yml | 87 ++----------- .packit.yaml | 5 - nix/test-guest/cache-seal.sh | 4 +- openshell.spec | 46 +------ 6 files changed, 338 insertions(+), 122 deletions(-) create mode 100644 .github/workflows/build-rpm.yml create mode 100644 .github/workflows/conformance.yml diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml new file mode 100644 index 0000000000..f35e1e0b4d --- /dev/null +++ b/.github/workflows/build-rpm.yml @@ -0,0 +1,123 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Build RPM + +on: + workflow_call: + inputs: + checkout-ref: + required: true + type: string + arch: + required: true + type: string + runner: + required: true + type: string + cli-target: + required: true + type: string + gateway-target: + required: true + type: string + rpm-version: + required: false + type: string + default: "" + rpm-release: + required: false + type: string + default: "" + cargo-version: + required: false + type: string + default: "" + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + build: + name: Build RPM Package (Linux ${{ inputs.arch }}) + runs-on: ${{ inputs.runner }} + timeout-minutes: 60 + container: + image: docker.io/library/fedora:44@sha256:be9d65e2344d805cc11114319c685ecaa96b6d9b4350a0a6460cdb931babbd19 + steps: + - name: Install packaging dependencies + run: | + dnf install -y \ + packit rpm-build \ + cargo cargo-rpm-macros git-core \ + pandoc python3-devel systemd-rpm-macros + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.checkout-ref }} + fetch-depth: 0 + + - name: Cache Cargo dependencies + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: rpm-vendor-${{ inputs.arch }} + cache-targets: "false" + cache-bin: "false" + cache-on-failure: "true" + + - name: Download CLI artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openshell-${{ inputs.cli-target }} + path: package-binaries/ + + - name: Download gateway artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openshell-gateway-${{ inputs.gateway-target }} + path: package-binaries/ + + - name: Configure package inputs + run: | + set -euo pipefail + chmod +x package-binaries/openshell{,-gateway} + ls -lah package-binaries + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Fetch tags + run: git fetch --tags --force + + - name: Build RPMs via Packit + env: + OPENSHELL_RPM_VERSION: ${{ inputs.rpm-version }} + OPENSHELL_RPM_RELEASE: ${{ inputs.rpm-release }} + OPENSHELL_CARGO_VERSION: ${{ inputs.cargo-version }} + OPENSHELL_PREBUILT_BINARIES_DIR: ${{ github.workspace }}/package-binaries + run: packit build locally + + - name: Collect RPM artifacts + run: | + set -euo pipefail + mkdir -p artifacts + mapfile -t rpms < <(find "$GITHUB_WORKSPACE" -maxdepth 3 -type f -name '*.rpm' ! -name '*.src.rpm' | sort) + if [ "${#rpms[@]}" -eq 0 ]; then + echo "::error::No RPM artifacts found under $GITHUB_WORKSPACE" + find "$GITHUB_WORKSPACE" -maxdepth 3 -type f | sort + exit 1 + fi + cp "${rpms[@]}" artifacts/ + echo "=== Built RPMs ===" + ls -lah artifacts/ + + - name: Upload RPM artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: rpm-linux-${{ inputs.arch }} + path: artifacts/*.rpm + retention-days: 5 diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml new file mode 100644 index 0000000000..bae9e3d326 --- /dev/null +++ b/.github/workflows/conformance.yml @@ -0,0 +1,195 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Conformance + +on: + workflow_dispatch: {} + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + pr_metadata: + name: Resolve PR metadata + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + should_run: ${{ steps.gate.outputs.should_run }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - id: gate + uses: ./.github/actions/pr-gate + + version: + needs: pr_metadata + if: needs.pr_metadata.outputs.should_run == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + outputs: + cargo: ${{ steps.version.outputs.cargo }} + rpm_version: ${{ steps.version.outputs.rpm_version }} + rpm_release: ${{ steps.version.outputs.rpm_release }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Compute versions + id: version + run: | + cargo="$(python3 tasks/scripts/release.py get-version --cargo)" + rpm_version="$(python3 tasks/scripts/release.py get-version --rpm-version)" + rpm_release="$(python3 tasks/scripts/release.py get-version --rpm-release)" + { + echo "cargo=$cargo" + echo "rpm_version=$rpm_version" + echo "rpm_release=$rpm_release" + } >> "$GITHUB_OUTPUT" + + build-cli: + needs: version + permissions: + contents: read + uses: ./.github/workflows/build-binaries.yml + with: + package: openshell-cli + binary: openshell + triple: x86_64-unknown-linux-musl + runner: linux-amd64-cpu8 + dev-shell: .#devShells.x86_64-linux.musl + cargo-version: ${{ needs.version.outputs.cargo }} + checkout-ref: ${{ github.sha }} + secrets: inherit + + build-conformance: + needs: version + permissions: + contents: read + uses: ./.github/workflows/build-binaries.yml + with: + package: openshell-conformance-cli + binary: openshell-conformance + triple: x86_64-unknown-linux-musl + runner: linux-amd64-cpu8 + dev-shell: .#devShells.x86_64-linux.musl + cargo-version: ${{ needs.version.outputs.cargo }} + checkout-ref: ${{ github.sha }} + secrets: inherit + + build-gateway: + needs: version + permissions: + contents: read + uses: ./.github/workflows/build-binaries.yml + with: + package: openshell-gateway + binary: openshell-gateway + triple: x86_64-unknown-linux-gnu + runner: linux-amd64-cpu8 + dev-shell: .#devShells.x86_64-linux.glibc-2-28 + cargo-version: ${{ needs.version.outputs.cargo }} + image-tag: dev + interpreter: /lib64/ld-linux-x86-64.so.2 + checkout-ref: ${{ github.sha }} + secrets: inherit + + build-rpm: + needs: [version, build-cli, build-gateway] + permissions: + contents: read + uses: ./.github/workflows/build-rpm.yml + with: + checkout-ref: ${{ github.sha }} + arch: x86_64 + runner: linux-amd64-cpu8 + cli-target: x86_64-unknown-linux-musl + gateway-target: x86_64-unknown-linux-gnu + cargo-version: ${{ needs.version.outputs.cargo }} + rpm-version: ${{ needs.version.outputs.rpm_version }} + rpm-release: ${{ needs.version.outputs.rpm_release }} + + fedora: + name: Fedora with Rootless Podman + needs: [build-conformance, build-rpm] + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + actions: read + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Enable KVM access + run: | + set -euo pipefail + if [[ ! -c /dev/kvm ]]; then + echo "::error::The runner did not expose /dev/kvm" + exit 1 + fi + sudo chmod 0666 /dev/kvm + exec 3<>/dev/kvm + exec 3>&- + + - uses: ./.github/actions/setup-nix + + - name: Download RPM artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: rpm-linux-x86_64 + path: rpm-input + + - name: Download conformance CLI + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openshell-conformance-x86_64-unknown-linux-musl + path: conformance-input + + - name: Install RPMs and check status + shell: bash + run: | + set -euo pipefail + chmod +x conformance-input/openshell-conformance + guest_script="$RUNNER_TEMP/conformance.sh" + cat > "$guest_script" <<'EOF' + set -euo pipefail + + on_exit() { + rc=$? + trap - EXIT + if [ "$rc" -ne 0 ]; then + systemctl --user status openshell-gateway --no-pager || true + journalctl --user -u openshell-gateway --no-pager -n 200 || true + podman info || true + podman ps --all || true + getenforce || true + fi + exit "$rc" + } + trap on_exit EXIT + + mkdir -p "$HOME/.config/openshell" + echo 'OPENSHELL_TELEMETRY_ENABLED=false' > "$HOME/.config/openshell/gateway.env" + systemctl --user enable --now openshell-gateway + openshell gateway add --local https://127.0.0.1:17670 --name openshell + /tmp/openshell-conformance run smoke + EOF + + OPENSHELL_TEST_GUEST_CACHE_DISABLE=1 nix run .#test-guest -- \ + --distro fedora \ + --with podman-rootless \ + --with selinux \ + --install rpm-input/openshell-[0-9]*.rpm \ + --install rpm-input/openshell-gateway-[0-9]*.rpm \ + --copy "$guest_script:/tmp/conformance.sh" \ + --copy conformance-input/openshell-conformance:/tmp/openshell-conformance \ + -- bash /tmp/conformance.sh diff --git a/.github/workflows/rpm-package.yml b/.github/workflows/rpm-package.yml index 8701644585..5cda9a88c3 100644 --- a/.github/workflows/rpm-package.yml +++ b/.github/workflows/rpm-package.yml @@ -37,83 +37,20 @@ jobs: matrix: include: - arch: x86_64 - artifact_arch: amd64 runner: linux-amd64-cpu8 cli_target: x86_64-unknown-linux-musl - gnu_target: x86_64-unknown-linux-gnu + gateway_target: x86_64-unknown-linux-gnu - arch: aarch64 - artifact_arch: arm64 runner: linux-arm64-cpu8 cli_target: aarch64-unknown-linux-musl - gnu_target: aarch64-unknown-linux-gnu - runs-on: ${{ matrix.runner }} - timeout-minutes: 60 - container: - image: fedora:latest - steps: - - name: Install build dependencies - run: | - dnf install -y \ - packit rpm-build \ - rust cargo gcc gcc-c++ make cmake pkg-config \ - clang-devel z3-devel systemd-rpm-macros \ - pandoc python3-devel git-core \ - cargo-rpm-macros - - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.checkout-ref }} - fetch-depth: 0 - - - name: Download CLI artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: openshell-${{ matrix.cli_target }} - path: package-binaries/ - - - name: Download gateway artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: openshell-gateway-${{ matrix.gnu_target }} - path: package-binaries/ - - - name: Configure package inputs - run: | - set -euo pipefail - chmod +x package-binaries/openshell{,-gateway} - ls -lah package-binaries - - - name: Mark workspace safe for git - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Fetch tags - run: git fetch --tags --force - - - name: Build RPMs via Packit - env: - OPENSHELL_RPM_VERSION: ${{ inputs['rpm-version'] }} - OPENSHELL_RPM_RELEASE: ${{ inputs['rpm-release'] }} - OPENSHELL_CARGO_VERSION: ${{ inputs['cargo-version'] }} - OPENSHELL_PREBUILT_BINARIES_DIR: ${{ github.workspace }}/package-binaries - run: packit build locally - - - name: Collect RPM artifacts - run: | - set -euo pipefail - mkdir -p artifacts - mapfile -t rpms < <(find "$GITHUB_WORKSPACE" -maxdepth 3 -type f -name '*.rpm' ! -name '*.src.rpm' | sort) - if [ "${#rpms[@]}" -eq 0 ]; then - echo "::error::No RPM artifacts found under $GITHUB_WORKSPACE" - find "$GITHUB_WORKSPACE" -maxdepth 3 -type f | sort - exit 1 - fi - cp "${rpms[@]}" artifacts/ - echo "=== Built RPMs ===" - ls -lah artifacts/ - - - name: Upload RPM artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: rpm-linux-${{ matrix.arch }} - path: artifacts/*.rpm - retention-days: 5 + gateway_target: aarch64-unknown-linux-gnu + uses: ./.github/workflows/build-rpm.yml + with: + checkout-ref: ${{ inputs.checkout-ref }} + arch: ${{ matrix.arch }} + runner: ${{ matrix.runner }} + cli-target: ${{ matrix.cli_target }} + gateway-target: ${{ matrix.gateway_target }} + rpm-version: ${{ inputs.rpm-version }} + rpm-release: ${{ inputs.rpm-release }} + cargo-version: ${{ inputs.cargo-version }} diff --git a/.packit.yaml b/.packit.yaml index 6379d8db83..d3b92eafae 100644 --- a/.packit.yaml +++ b/.packit.yaml @@ -11,7 +11,6 @@ specfile_path: openshell.spec # Packages needed in the SRPM build environment to create vendor tarball srpm_build_deps: - - rust - cargo - git-core @@ -45,10 +44,6 @@ actions: # dist-info stays at the RPM Version; dev build identity is carried by # Release so Fedora's Python RPM post-processing can normalize metadata. - 'bash -c "if [ -n \"${OPENSHELL_CARGO_VERSION:-}\" ]; then sed -i -r \"s/^%global openshell_cargo_version .*/%global openshell_cargo_version ${OPENSHELL_CARGO_VERSION}/\" openshell.spec; fi"' - # Override image_tag to 'latest' for tagged stable releases. - # For PR and commit-to-main builds the spec default ('dev') is kept, - # matching the :dev images pushed by release-dev.yml. - - 'bash -c "if git describe --exact-match --tags HEAD 2>/dev/null | grep -qE ''^v[0-9]+\.[0-9]+\.[0-9]+$''; then sed -i ''s/^%global image_tag.*/%global image_tag latest/'' openshell.spec; fi"' jobs: # Build on every pull request targeting main for CI validation diff --git a/nix/test-guest/cache-seal.sh b/nix/test-guest/cache-seal.sh index 9f0da8564d..04cf8dfc0c 100755 --- a/nix/test-guest/cache-seal.sh +++ b/nix/test-guest/cache-seal.sh @@ -49,7 +49,9 @@ sync # Deleted credentials can remain in allocated blocks. Fill free space with # zeroes so qemu-img convert can safely omit those blocks from the cache disk. zero_file=/var/tmp/openshell-cache-zero -dd if=/dev/zero of="${zero_file}" bs=64M status=none 2>/dev/null || true +echo "==> Cache sealing: zeroing free disk space" +dd if=/dev/zero of="${zero_file}" bs=64M status=progress || true +echo "==> Cache sealing: free disk space zeroed" rm -f "${zero_file}" sync diff --git a/openshell.spec b/openshell.spec index 3200659d73..ac57d29ee9 100644 --- a/openshell.spec +++ b/openshell.spec @@ -12,12 +12,6 @@ # in the format redhat-rpm-config expects (especially on EPEL). %global debug_package %{nil} -# Default container image tag for supervisor and sandbox images. -# Overridden to 'latest' by Packit's fix-spec-file action for tagged stable -# releases (via git describe --exact-match). PR and commit-to-main builds -# keep the default 'dev' so they track the development image stream. -%global image_tag dev - Name: openshell Version: %{openshell_version} Release: 1.20260518180028805757.podman.toml.gateway.listener.11.g8c0cb7c8%{?dist} @@ -30,21 +24,9 @@ Source1: openshell-%{openshell_version}-vendor.tar.xz ExclusiveArch: x86_64 aarch64 -# Rust build dependencies -# NOTE: MSRV is 1.88 (Rust edition 2024). As of mid-2025, this requires -# Fedora Rawhide or newer. Stable Fedora and EPEL-10 may ship older Rust; -# adjust targets in .packit.yaml accordingly or provide a supplementary -# Rust toolchain via additional_repos in the COPR build config. -BuildRequires: rust >= 1.88 +# Cargo metadata generation BuildRequires: cargo BuildRequires: cargo-rpm-macros >= 25 -BuildRequires: gcc -BuildRequires: gcc-c++ -BuildRequires: make -BuildRequires: cmake -BuildRequires: pkg-config -BuildRequires: clang-devel -BuildRequires: z3-devel BuildRequires: systemd-rpm-macros # Man page generation @@ -103,18 +85,8 @@ sed -i 's/^version = "0.0.0"/version = "%{openshell_cargo_version}"/' Cargo.toml grep -q 'version = "%{openshell_cargo_version}"' Cargo.toml || (echo "ERROR: Cargo.toml version patch failed" && exit 1) %build -# Build the CLI and gateway binaries unless the release workflow supplied the -# same prebuilt artifacts used for tarballs and Debian packages. -export CARGO_BUILD_JOBS=%{_smp_build_ncpus} -# Set the default container image tag so compiled-in image refs point at -# real tags in the ghcr.io/nvidia/openshell registry. -export OPENSHELL_IMAGE_TAG=%{image_tag} -if [ -n "${OPENSHELL_PREBUILT_BINARIES_DIR:-}" ]; then - test -x "${OPENSHELL_PREBUILT_BINARIES_DIR}/openshell" - test -x "${OPENSHELL_PREBUILT_BINARIES_DIR}/openshell-gateway" -else - cargo build --release --bin openshell --bin openshell-gateway -fi +test -x "${OPENSHELL_PREBUILT_BINARIES_DIR}/openshell" +test -x "${OPENSHELL_PREBUILT_BINARIES_DIR}/openshell-gateway" # Generate vendored crate manifest and license metadata. # cargo-vendor.txt is consumed by an RPM generator (from cargo-rpm-macros) @@ -129,18 +101,10 @@ pandoc -s -t man deploy/man/openshell-gateway.8.md -o openshell-gateway.8 %install # --- CLI binary --- -if [ -n "${OPENSHELL_PREBUILT_BINARIES_DIR:-}" ]; then - install -Dpm 0755 "${OPENSHELL_PREBUILT_BINARIES_DIR}/%{name}" %{buildroot}%{_bindir}/%{name} -else - install -Dpm 0755 target/release/%{name} %{buildroot}%{_bindir}/%{name} -fi +install -Dpm 0755 "${OPENSHELL_PREBUILT_BINARIES_DIR}/%{name}" %{buildroot}%{_bindir}/%{name} # --- Gateway binary --- -if [ -n "${OPENSHELL_PREBUILT_BINARIES_DIR:-}" ]; then - install -Dpm 0755 "${OPENSHELL_PREBUILT_BINARIES_DIR}/%{name}-gateway" %{buildroot}%{_bindir}/%{name}-gateway -else - install -Dpm 0755 target/release/%{name}-gateway %{buildroot}%{_bindir}/%{name}-gateway -fi +install -Dpm 0755 "${OPENSHELL_PREBUILT_BINARIES_DIR}/%{name}-gateway" %{buildroot}%{_bindir}/%{name}-gateway # --- Default gateway TOML config template --- # Shipped as a read-only reference in %{_datadir}. The systemd unit seeds a From d5a04ad70d9113ebbf4b4fcce740b557e9b25fbb Mon Sep 17 00:00:00 2001 From: Simon Scatton <44714756+SDAChess@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:51:42 +0200 Subject: [PATCH 10/16] fix(ci): use multi-arch Fedora image for RPM builds (#3130) Signed-off-by: Simon Scatton --- .github/workflows/build-rpm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index f35e1e0b4d..71aa04dddf 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -47,7 +47,7 @@ jobs: runs-on: ${{ inputs.runner }} timeout-minutes: 60 container: - image: docker.io/library/fedora:44@sha256:be9d65e2344d805cc11114319c685ecaa96b6d9b4350a0a6460cdb931babbd19 + image: docker.io/library/fedora:44@sha256:43b29f65a41eb9c35e1cd5323e3bdf3b655c2357a9f4f1ff2f9c2798e5045d80 steps: - name: Install packaging dependencies run: | From 710ce26ceb8ec50c620f545c8a96eda7f8a336ca Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 2 Sep 2026 14:56:08 +0000 Subject: [PATCH 11/16] feat(vm): support corporate HTTP forward proxy egress for microVM sandboxes (#3090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(vm): support corporate HTTP forward proxy egress for microVM sandboxes The corporate forward proxy machinery from #1792 is driver-agnostic and already merged: openshell-supervisor-network implements CONNECT chaining, NO_PROXY matching, credentials, https:// proxies and corporate CA trust, and openshell-sandbox exposes it as six argv-only flags. Podman gained the driver half in #2245/#2512 and Kubernetes in #2633; the VM driver had none of it, so VM sandboxes on proxy-only networks could not reach any destination requiring the proxy even when policy allowed it. The blocking piece was not proxy logic but delivery: the VM guest init script runs as PID 1 and execs a fixed supervisor command line, and libkrun's krun_set_exec receives an empty argv, so there was no channel for driver-owned supervisor arguments. The supervisor's proxy flags deliberately have no environment fallback, and build_guest_environment merges user-supplied environment, so the guest env is not a safe transport either. Add a driver-authored argument file, mirroring the existing init.d manifest: the driver writes /opt/openshell/supervisor-args into the overlay upperdir on every launch and the guest reads it verbatim, one argument per line, appending it to every supervisor exec. It is written even when empty, which is what makes the channel unforgeable -- the upperdir always shadows the read-only image layer, so an image can neither supply its own arguments nor disable the operator's by omitting the file. Because both launch backends exec the same init script, this covers libkrun and QEMU without touching either. A microVM has no bind mounts or container secrets, so the credential and CA bundle are staged into the per-sandbox overlay the way the gateway JWT already is: credential root-only at 0600, CA at 0644, both rewritten every launch so a removed setting clears prior material, and both deleted with the sandbox state directory. This places the credential at rest in the overlay image on the gateway host, which differs from the Podman secret model and is documented as an explicit security consideration. Validation is fail-closed and shared: a new openshell_core::driver_utils::validate_upstream_proxy_settings holds the pairing rules the Podman driver established, and both the gateway and the driver call it so an invalid table names the offending key instead of surfacing as an opaque driver-readiness timeout. Guest egress leaves through gvproxy, so a proxy on the gateway host's loopback is reachable only through host.openshell.internal; the guest to gateway callback is unaffected. Closes #3088 Signed-off-by: Philippe Martin * fix(vm): bound the proxy CA read and scope the host-loopback recipe Two review findings on the corporate forward proxy support for microVM sandboxes. The driver read the operator's proxy_ca_bundle with an unbounded fs::read and accepted it on a substring match for the PEM BEGIN CERTIFICATE marker. A special file such as /dev/zero therefore grew driver memory without bound on every authorized sandbox create, and a PEM block holding invalid DER passed the host check but contributes no trust anchor in the guest, so every supervisor would fail after boot with an error attributed to the sandbox rather than to the setting. Move the read into openshell-core as read_upstream_proxy_ca_bundle_file: it reuses the credential reader's bounded-read path (non-regular files rejected on fstat, size capped, read bounded even if the file grows), then requires at least one anchor that RootCertStore::add_parsable_certificates accepts. The supervisor's own reader now delegates to it, so host acceptance and guest acceptance are the same function and cannot drift. The published host-loopback recipe was written for libkrun only. gvproxy NATs host.openshell.internal to the gateway host's 127.0.0.1, but GPU sandboxes run on the QEMU/TAP backend where that name resolves to the TAP host address and the driver's own nftables input chain accepts only the gateway port from the guest — no proxy on the gateway host is reachable there at any bind address, so an operator following the generic recipe lost all proxy-required egress while configuration validation succeeded. Scope the recipe to libkrun in every reference and reject a gateway-host proxy URL when a launch plan resolves to QEMU, naming the reason, instead of booting a sandbox whose policy-approved CONNECTs all time out. Signed-off-by: Philippe Martin * fix(vm): match the QEMU proxy preflight to the selected TAP host The gateway-host proxy guard added for the QEMU/TAP backend classified the wrong set of addresses in both directions. It ran at the top of configure_qemu_launch_plan, before the subnet allocation that settles plan.host_ip, so it could not compare against the address the guest actually reaches the host on. An operator pointing https_proxy at the sandbox's own TAP host address, such as 10.0.128.1, passed the check, and the driver's nftables input chain — which accepts only the gateway port from the guest — then dropped every policy-approved CONNECT, which is exactly the silent timeout the guard exists to prevent. In the other direction it rejected 192.168.127.254 unconditionally. That address is special only to libkrun/gvproxy; on QEMU/TAP it is an ordinary address that may be routable through the guest's masqueraded egress, so the guard refused a working configuration. Run the check after the launch plan's network allocation, on both the freshly-allocated and already-complete paths, and compare IP literals with that sandbox's selected TAP host. Loopback literals, localhost, and the documented host aliases that write_host_gateway_aliases seeds to the TAP host still classify as the gateway host, and the failure names the address. The gvproxy host-loopback constant returns to being a documentation anchor. Signed-off-by: Philippe Martin --------- Signed-off-by: Philippe Martin --- .../skills/debug-openshell-cluster/SKILL.md | 52 + Cargo.lock | 3 + architecture/sandbox.md | 29 + crates/openshell-core/Cargo.toml | 3 + crates/openshell-core/src/container_paths.rs | 29 + crates/openshell-core/src/driver_utils.rs | 535 ++++++++++- crates/openshell-driver-vm/README.md | 8 + .../scripts/openshell-vm-sandbox-init.sh | 73 +- crates/openshell-driver-vm/src/driver.rs | 904 +++++++++++++++++- crates/openshell-driver-vm/src/main.rs | 87 ++ crates/openshell-gateway/src/vm.rs | 176 +++- .../src/upstream_proxy.rs | 31 +- docs/reference/gateway-config.mdx | 44 + docs/reference/sandbox-compute-drivers.mdx | 12 + e2e/rust/Cargo.toml | 5 + e2e/rust/e2e-vm.sh | 1 + e2e/rust/src/harness/host_process.rs | 103 ++ e2e/rust/src/harness/mod.rs | 1 + e2e/rust/tests/vm_corporate_proxy.rs | 888 +++++++++++++++++ examples/governance-interceptor/Cargo.lock | 11 + 20 files changed, 2923 insertions(+), 72 deletions(-) create mode 100644 e2e/rust/src/harness/host_process.rs create mode 100644 e2e/rust/tests/vm_corporate_proxy.rs diff --git a/.agents/skills/debug-openshell-cluster/SKILL.md b/.agents/skills/debug-openshell-cluster/SKILL.md index 47588b1aeb..14e88286cb 100644 --- a/.agents/skills/debug-openshell-cluster/SKILL.md +++ b/.agents/skills/debug-openshell-cluster/SKILL.md @@ -630,6 +630,58 @@ openshell status openshell logs ``` +#### Corporate upstream proxy + +When VM sandbox egress routes through a corporate HTTP forward proxy, the +operator-owned settings live under `[openshell.drivers.vm]` and the gateway +forwards them to the `openshell-driver-vm` subprocess as `--https-proxy`, +`--no-proxy`, `--proxy-auth-file`, `--proxy-auth-allow-insecure`, +`--proxy-connect-by-hostname`, and `--proxy-ca-bundle`. Both the gateway and +the driver validate them at startup, so any present-but-invalid value fails +closed with an error naming the key rather than reverting to a direct dial. +Confirm the configuration and the resulting driver argv first: + +```bash +grep -A20 '^\[openshell.drivers.vm\]' | grep -E 'https_proxy|no_proxy|proxy_auth_file|proxy_auth_allow_insecure|proxy_connect_by_hostname|proxy_ca_bundle' +ps -o args= -p "$(pgrep -f openshell-driver-vm | head -n1)" | tr ' ' '\n' | grep -A1 -- '--proxy\|--https-proxy\|--no-proxy' +``` + +Reachability is the most common failure, and it depends on the VM backend. +On libkrun (non-GPU sandboxes) guest egress leaves through gvproxy, so a proxy +bound to the gateway host's loopback is **not** reachable at `127.0.0.1` from +inside the guest: it must be addressed as +`http://host.openshell.internal:`, which gvproxy NATs from +`192.168.127.254` to the host's `127.0.0.1`. A `https_proxy` pointing at a +loopback URL produces policy-approved CONNECT attempts that time out while +public destinations still work. + +GPU sandboxes run on QEMU/TAP, where no gateway-host proxy is reachable at +all: `host.openshell.internal` resolves to the TAP host address, and the +driver's nftables `input` chain accepts only the gateway port from the guest. +The driver rejects such a configuration at launch — a create failing with +`https_proxy ... addresses the gateway host, which a QEMU/TAP sandbox ... +cannot reach` means the proxy must move to an address routable from the +guest's masqueraded egress (or the sandbox must run without a GPU). + +The settings reach the supervisor through a driver-written argument file in +the per-sandbox overlay, not through the guest environment. The credential and +CA bundle are staged into the same overlay at fixed guest paths. Inspect the +guest side from the VM console log, which records how many driver-supplied +arguments the init script read: + +```bash +grep -E 'supervisor arguments from driver|supervisor argument list' /sandboxes//rootfs-console.log +grep -Ei 'upstream|connect|proxy' /sandboxes//rootfs-console.log | tail -n 40 +``` + +`FATAL: supervisor argument list ... is not readable` or `FATAL: empty entry in +supervisor argument list` means the overlay is broken or was tampered with, and +the guest deliberately aborts rather than starting a supervisor with a +truncated egress configuration. If the guest logs no driver arguments at all +while `gateway.toml` sets `https_proxy`, the running driver predates the +configuration — check that the gateway spawned the driver binary you expect +(`[openshell.drivers.vm].driver_dir`). + ## Common Failure Patterns | Symptom | Likely cause | Check | diff --git a/Cargo.lock b/Cargo.lock index 90240d6b3a..dc24adc6db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3861,8 +3861,11 @@ dependencies = [ "prost", "prost-types", "protoc-bin-vendored", + "rcgen", "reqwest 0.12.28", "rustix 1.1.4", + "rustls", + "rustls-pemfile", "serde", "serde_json", "tar", diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 6e4e020536..dd9621a09d 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -300,6 +300,35 @@ file and builds the `Proxy-Authorization: Basic` header; a credential that is empty, contains control characters, or is not in `user:pass` form is fatal on both sides. +The VM driver has no argv seam of its own: its guest init script runs as PID 1 +and execs a fixed supervisor command line, and the libkrun and QEMU launch +backends both reach the supervisor through that script. Driver-owned +supervisor arguments therefore travel in a per-sandbox file the driver writes +into the overlay upperdir at a fixed guest path, one argument per line, which +the guest reads verbatim (no word splitting or globbing) and appends to every +supervisor exec. The file is written on **every** launch, including an empty +file when there is nothing to pass: the upperdir copy always shadows the +read-only image layer, so a sandbox image can neither supply its own +supervisor arguments by baking a file at that path nor disable the operator's +by omitting one. This mirrors the driver-authored `init.d` manifest, which +solves the same trust problem for guest init drop-ins. + +A microVM has no bind mounts or container secrets, so the VM driver stages the +credential and the CA bundle into the per-sandbox overlay disk instead — the +credential root-only, the CA world-readable, both at fixed `/opt/openshell` +paths and both removed with the sandbox state directory. The consequence, +which differs from the Podman secret model, is that the credential is at rest +inside that overlay image on the gateway host; the per-sandbox gateway JWT +already travels the same path. Proxy reachability differs by VM backend. libkrun-backed +sandboxes egress through gvproxy, so a proxy on the gateway host's loopback is +reachable through the host alias `host.openshell.internal`, which gvproxy NATs +to the host's `127.0.0.1`. QEMU/TAP sandboxes (GPU) have no equivalent: that +alias resolves to the TAP host address, and the driver's nftables `input` +chain accepts only the gateway port from the guest, so no gateway-host proxy +is reachable. The driver rejects a gateway-host proxy URL on the QEMU path at +launch rather than producing CONNECT timeouts. The guest's gateway callback is +unaffected in both backends and never traverses the proxy. + For Kubernetes sandboxes, the operator configures a Secret name and key rather than a gateway-host file path. Kubernetes projects that Secret only into the container that runs network supervision. Proxy credential Secrets require the diff --git a/crates/openshell-core/Cargo.toml b/crates/openshell-core/Cargo.toml index d8483bd2cb..c96d536f07 100644 --- a/crates/openshell-core/Cargo.toml +++ b/crates/openshell-core/Cargo.toml @@ -27,6 +27,8 @@ serde_json = { workspace = true } tracing = { workspace = true } url = { workspace = true } ipnet = "2" +rustls = { workspace = true } +rustls-pemfile = { workspace = true } base64 = { workspace = true } chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } reqwest = { workspace = true, features = ["blocking", "rustls-tls-native-roots"], optional = true } @@ -53,6 +55,7 @@ protoc-bin-vendored = { workspace = true } [dev-dependencies] tempfile = "3" +rcgen = { workspace = true } [lints] workspace = true diff --git a/crates/openshell-core/src/container_paths.rs b/crates/openshell-core/src/container_paths.rs index c63e4bcdd8..e26ea53f7a 100644 --- a/crates/openshell-core/src/container_paths.rs +++ b/crates/openshell-core/src/container_paths.rs @@ -65,6 +65,32 @@ pub const VM_GUEST_TLS_KEY_PATH: &str = "/opt/openshell/tls/tls.key"; pub const VM_GUEST_SANDBOX_TOKEN_PATH: &str = "/opt/openshell/auth/sandbox.jwt"; pub const VM_GUEST_INIT_DROPIN_DIR: &str = "/opt/openshell/init.d"; pub const VM_GUEST_INIT_DROPIN_MANIFEST: &str = "/opt/openshell/init.d.manifest"; + +/// Guest path for the corporate upstream-proxy credential in VM sandboxes. +/// +/// The VM driver stages the `user:pass` credential here (mode `0600`, +/// root-only) inside the per-sandbox overlay upperdir, and passes only this +/// path on the supervisor's argv. A microVM has no bind mounts or container +/// secrets, so this is the same delivery the per-sandbox JWT already uses. +pub const VM_GUEST_UPSTREAM_PROXY_AUTH_PATH: &str = "/opt/openshell/auth/upstream-proxy"; + +/// Guest path for the corporate proxy CA bundle in VM sandboxes. +/// +/// A CA certificate is not secret, so unlike the credential this is staged +/// world-readable. The supervisor trusts it for the handshake with an +/// `https://` proxy and for server certificates re-signed by a +/// TLS-intercepting proxy. +pub const VM_GUEST_PROXY_CA_PATH: &str = "/opt/openshell/tls/proxy-ca.pem"; + +/// Guest path for the driver-authored supervisor argument list in VM sandboxes. +/// +/// Podman and Kubernetes build the supervisor's command line directly; the VM +/// guest init script execs a fixed argv, so driver-owned arguments travel +/// through this file instead. The driver writes it into the overlay upperdir +/// on every launch — empty when it has no arguments to pass — so a sandbox +/// image can neither forge entries nor shadow the driver's copy, and the +/// guest appends exactly what it finds there and nothing else. +pub const VM_GUEST_SUPERVISOR_ARGS_PATH: &str = "/opt/openshell/supervisor-args"; pub const VM_UMOCI_PATH: &str = "/opt/openshell/bin/umoci"; pub const VM_SANDBOX_OWNER_NORMALIZED_MARKER: &str = "/opt/openshell/.sandbox-owner-normalized"; @@ -103,6 +129,9 @@ mod tests { VM_GUEST_SANDBOX_TOKEN_PATH, VM_GUEST_INIT_DROPIN_DIR, VM_GUEST_INIT_DROPIN_MANIFEST, + VM_GUEST_UPSTREAM_PROXY_AUTH_PATH, + VM_GUEST_PROXY_CA_PATH, + VM_GUEST_SUPERVISOR_ARGS_PATH, VM_UMOCI_PATH, VM_SANDBOX_OWNER_NORMALIZED_MARKER, ]; diff --git a/crates/openshell-core/src/driver_utils.rs b/crates/openshell-core/src/driver_utils.rs index c8be114ebd..74871751da 100644 --- a/crates/openshell-core/src/driver_utils.rs +++ b/crates/openshell-core/src/driver_utils.rs @@ -375,6 +375,123 @@ pub const MAX_UPSTREAM_PROXY_CREDENTIAL_BYTES: u64 = 4096; /// cannot be opened or stat'd, is not a regular file, or exceeds the size /// bound. pub fn read_upstream_proxy_credential_file(path: &str) -> Result { + read_regular_file_bounded(path, MAX_UPSTREAM_PROXY_CREDENTIAL_BYTES).map_err(|err| match err { + BoundedReadError::Open(e) => format!("failed to open proxy auth file '{path}': {e}"), + BoundedReadError::Stat(e) => format!("failed to stat proxy auth file '{path}': {e}"), + BoundedReadError::NotRegular => format!("proxy auth file '{path}' is not a regular file"), + BoundedReadError::TooLarge => format!( + "proxy auth file '{path}' exceeds the {MAX_UPSTREAM_PROXY_CREDENTIAL_BYTES}-byte limit" + ), + BoundedReadError::Read(e) => format!("failed to read proxy auth file '{path}': {e}"), + }) +} + +/// Hard upper bound on the size of a corporate proxy CA bundle file. +/// +/// A CA bundle holding every corporate trust anchor is a few tens of +/// kilobytes; this cap only exists so a hostile or misconfigured path (a huge +/// file, or a special file such as `/dev/zero`) cannot exhaust gateway, +/// driver, or supervisor memory during a bounded read. +pub const MAX_UPSTREAM_PROXY_CA_BUNDLE_BYTES: u64 = 1024 * 1024; + +/// Read and validate an operator corporate proxy CA bundle PEM file. +/// +/// Rejects non-regular files (e.g. `/dev/zero`, directories, FIFOs) and files +/// larger than [`MAX_UPSTREAM_PROXY_CA_BUNDLE_BYTES`], then requires the +/// bundle to contribute at least one trust anchor rustls actually accepts — +/// see [`validate_upstream_proxy_ca_bundle_pem`]. Returns the PEM contents. +/// +/// Shared by the compute driver (at sandbox-create time, so the operator gets +/// an error naming the setting) and the in-container supervisor (at startup), +/// so a bundle accepted on the host is never rejected inside the sandbox and +/// vice versa. This is a blocking read; async callers should wrap it (e.g. +/// `tokio::task::spawn_blocking`). +/// +/// `label` names the operator-facing setting (`proxy_ca_bundle`, or the +/// supervisor's argument name) and prefixes every error. +/// +/// # Errors +/// +/// Returns a descriptive error (never containing file contents) when the path +/// cannot be read, is not a regular file, exceeds the size bound, or holds no +/// usable certificate. +pub fn read_upstream_proxy_ca_bundle_file(path: &str, label: &str) -> Result { + let pem = read_regular_file_bounded(path, MAX_UPSTREAM_PROXY_CA_BUNDLE_BYTES).map_err( + |err| match err { + BoundedReadError::Open(e) | BoundedReadError::Stat(e) | BoundedReadError::Read(e) => { + format!("{label} '{path}' could not be read: {e}") + } + BoundedReadError::NotRegular => { + format!("{label} '{path}' is not a regular file") + } + BoundedReadError::TooLarge => format!( + "{label} '{path}' exceeds the {MAX_UPSTREAM_PROXY_CA_BUNDLE_BYTES}-byte limit" + ), + }, + )?; + validate_upstream_proxy_ca_bundle_pem(&pem, path, label)?; + Ok(pem) +} + +/// Require a CA bundle PEM to contribute at least one usable trust anchor. +/// +/// Fail-closed to match the rest of the operator-owned proxy configuration: +/// the operator explicitly pointed at this file, so a bundle with no usable +/// certificate is an error rather than a silent fall-back to the built-in +/// roots that would quietly weaken the trust boundary. +/// +/// Validating that rustls accepts an anchor — rather than only that PEM +/// framing base64-decodes — is what makes the host-side check equivalent to +/// the guest-side one: a PEM block holding invalid DER passes +/// `rustls_pemfile::certs` but is silently dropped by +/// `RootCertStore::add_parsable_certificates`, so counting PEM blocks alone +/// would accept on the host a bundle that contributes zero anchors at runtime. +/// +/// # Errors +/// +/// Returns a descriptive error, prefixed with `label` and naming `path`, when +/// the PEM holds no certificate block or no block contains valid X.509 DER. +pub fn validate_upstream_proxy_ca_bundle_pem( + pem: &str, + path: &str, + label: &str, +) -> Result<(), String> { + let certs: Vec<_> = rustls_pemfile::certs(&mut pem.as_bytes()) + .flatten() + .collect(); + if certs.is_empty() { + return Err(format!( + "{label} '{path}' contains no PEM certificate blocks" + )); + } + let mut store = rustls::RootCertStore::empty(); + let (added, _ignored) = store.add_parsable_certificates(certs); + if added == 0 { + return Err(format!( + "{label} '{path}' contains no usable trust anchors \ + (PEM blocks were found but none contain valid X.509 DER)" + )); + } + Ok(()) +} + +/// Failure modes of [`read_regular_file_bounded`], so each caller can phrase +/// them in terms of the operator setting it is reading. +enum BoundedReadError { + Open(std::io::Error), + Stat(std::io::Error), + NotRegular, + TooLarge, + Read(std::io::Error), +} + +/// Read a regular file into a `String`, rejecting anything larger than +/// `max_bytes` and anything that is not a regular file. +/// +/// Backs the operator-supplied proxy file readers, which must never let a +/// hostile or misconfigured path (`/dev/zero`, a FIFO, a directory, a huge +/// file) exhaust memory or block the caller. +fn read_regular_file_bounded(path: &str, max_bytes: u64) -> Result { use std::io::Read as _; // Windows rejects opening a directory before a file handle is available, @@ -384,10 +501,9 @@ pub fn read_upstream_proxy_credential_file(path: &str) -> Result // window if the path is replaced between these operations. #[cfg(target_os = "windows")] { - let path_metadata = std::fs::metadata(path) - .map_err(|e| format!("failed to open proxy auth file '{path}': {e}"))?; + let path_metadata = std::fs::metadata(path).map_err(BoundedReadError::Open)?; if !path_metadata.is_file() { - return Err(format!("proxy auth file '{path}' is not a regular file")); + return Err(BoundedReadError::NotRegular); } } @@ -405,31 +521,145 @@ pub fn read_upstream_proxy_credential_file(path: &str) -> Result #[cfg(not(unix))] let open_result = std::fs::File::open(path); - let file = open_result.map_err(|e| format!("failed to open proxy auth file '{path}': {e}"))?; - let metadata = file - .metadata() - .map_err(|e| format!("failed to stat proxy auth file '{path}': {e}"))?; + let file = open_result.map_err(BoundedReadError::Open)?; + let metadata = file.metadata().map_err(BoundedReadError::Stat)?; if !metadata.is_file() { - return Err(format!("proxy auth file '{path}' is not a regular file")); + return Err(BoundedReadError::NotRegular); } - if metadata.len() > MAX_UPSTREAM_PROXY_CREDENTIAL_BYTES { - return Err(format!( - "proxy auth file '{path}' exceeds the {MAX_UPSTREAM_PROXY_CREDENTIAL_BYTES}-byte limit" - )); + if metadata.len() > max_bytes { + return Err(BoundedReadError::TooLarge); } // Bound the read even if the file grows between stat and read. let mut buf = String::new(); - file.take(MAX_UPSTREAM_PROXY_CREDENTIAL_BYTES + 1) + file.take(max_bytes + 1) .read_to_string(&mut buf) - .map_err(|e| format!("failed to read proxy auth file '{path}': {e}"))?; - if buf.len() as u64 > MAX_UPSTREAM_PROXY_CREDENTIAL_BYTES { - return Err(format!( - "proxy auth file '{path}' exceeds the {MAX_UPSTREAM_PROXY_CREDENTIAL_BYTES}-byte limit" - )); + .map_err(BoundedReadError::Read)?; + if buf.len() as u64 > max_bytes { + return Err(BoundedReadError::TooLarge); } Ok(buf) } +/// Operator-supplied corporate upstream-proxy settings, as a borrowed view. +/// +/// Compute drivers store these keys under their own +/// `[openshell.drivers.]` table; this type exists so the pairing rules +/// between them live in one place instead of being restated per driver. +/// Field names map 1:1 onto the documented TOML keys `https_proxy`, +/// `no_proxy`, `proxy_auth_file`, `proxy_auth_allow_insecure`, +/// `proxy_connect_by_hostname`, and `proxy_ca_bundle`. +#[derive(Debug, Clone, Copy, Default)] +pub struct UpstreamProxySettings<'a> { + /// `https_proxy`: the corporate forward proxy URL. + pub url: Option<&'a str>, + /// `no_proxy`: comma-separated bypass list. + pub no_proxy: Option<&'a str>, + /// `proxy_auth_file`: host path to a `user:pass` credential file. + pub auth_file: Option<&'a str>, + /// `proxy_auth_allow_insecure`: acknowledgement that Basic auth to an + /// `http://` proxy travels in cleartext. + pub auth_allow_insecure: Option, + /// `proxy_connect_by_hostname`: send hostnames rather than validated IPs + /// in CONNECT requests. + pub connect_by_hostname: Option, + /// `proxy_ca_bundle`: host path to a PEM CA bundle trusted for the proxy. + pub ca_bundle: Option<&'a str>, +} + +/// Validate operator-supplied corporate upstream-proxy settings, fail-closed. +/// +/// Shares URL semantics with the in-container supervisor through +/// [`parse_upstream_proxy_url`], so a value accepted here can never be +/// rejected by the supervisor at sandbox startup (or vice versa). Every +/// auxiliary setting is only meaningful relative to a proxy boundary the +/// operator believed was in effect, so a stray one is rejected rather than +/// silently accepted while all egress dials directly. +/// +/// A present-but-empty string is rejected everywhere: the supervisor treats +/// an empty driver-supplied argument as a fatal misconfiguration, so a driver +/// must never accept (and later pass) one. +/// +/// # Errors +/// +/// Returns a message naming the offending key. +pub fn validate_upstream_proxy_settings( + settings: &UpstreamProxySettings<'_>, +) -> Result<(), String> { + let proxy_secure = if let Some(url) = settings.url { + let addr = parse_upstream_proxy_url(url).map_err(|err| match err { + UpstreamProxyUrlError::Empty => "https_proxy must not be empty when set".to_string(), + UpstreamProxyUrlError::InlineCredentials => { + "https_proxy must not embed credentials in the URL; supply them via \ + proxy_auth_file so they are not stored in config or sandbox metadata" + .to_string() + } + err => format!("https_proxy {err}"), + })?; + addr.secure + } else { + false + }; + + if let Some(list) = settings.no_proxy { + if list.trim().is_empty() { + return Err("no_proxy must not be empty when set; omit it instead".to_string()); + } + if settings.url.is_none() { + return Err("no_proxy is set but no https_proxy is configured".to_string()); + } + } + + if let Some(path) = settings.auth_file { + if path.trim().is_empty() { + return Err("proxy_auth_file must not be empty when set".to_string()); + } + if settings.url.is_none() { + return Err("proxy_auth_file is set but no https_proxy is configured".to_string()); + } + // Basic auth over the plain-TCP proxy connection is readable by + // anyone on the network path; sending it requires an explicit + // operator acknowledgement rather than being an implicit side effect + // of configuring credentials. For an https:// proxy the credential is + // inside the verified TLS session, so the acknowledgement is + // unnecessary (but tolerated). + if settings.auth_allow_insecure != Some(true) && !proxy_secure { + return Err( + "proxy_auth_file sends the credential as cleartext Basic auth over the \ + plain-TCP connection to the http:// proxy; set proxy_auth_allow_insecure \ + = true to accept that exposure, or remove proxy_auth_file" + .to_string(), + ); + } + } else if settings.auth_allow_insecure.is_some() { + // The acknowledgement without credentials means the operator believed + // an auth file was configured; surface the mismatch. + return Err( + "proxy_auth_allow_insecure is set but no proxy_auth_file is configured".to_string(), + ); + } + + if settings.connect_by_hostname.is_some() && settings.url.is_none() { + return Err( + "proxy_connect_by_hostname is set but no https_proxy is configured".to_string(), + ); + } + + // A CA bundle only makes sense relative to a proxy boundary (an https:// + // proxy handshake, or a TLS-intercepting proxy's re-sign CA). The file's + // readability and certificate content are checked at sandbox-create time + // by the driver and fail closed again in the supervisor. + if let Some(path) = settings.ca_bundle { + if path.trim().is_empty() { + return Err("proxy_ca_bundle must not be empty when set".to_string()); + } + if settings.url.is_none() { + return Err("proxy_ca_bundle is set but no https_proxy is configured".to_string()); + } + } + + Ok(()) +} + /// Container-side directory where the provider SPIFFE Workload API socket is mounted. pub const PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR: &str = "/spiffe-workload-api"; @@ -888,4 +1118,273 @@ mod tests { "reading a FIFO must not block" ); } + + /// Build settings with only the fields a case cares about. + #[test] + fn ca_bundle_file_accepts_a_real_certificate() { + // The positive case that pins host acceptance to guest acceptance: + // what the driver stages is exactly what rustls will trust. + let cert = rcgen::generate_simple_self_signed(vec!["proxy.corp.example".to_string()]) + .expect("test CA"); + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("proxy-ca.pem"); + std::fs::write(&path, cert.cert.pem()).unwrap(); + + let pem = + read_upstream_proxy_ca_bundle_file(path.to_str().unwrap(), "proxy_ca_bundle").unwrap(); + assert!(pem.contains("BEGIN CERTIFICATE")); + } + + #[test] + fn ca_bundle_file_rejects_non_regular_and_oversized_paths() { + // /dev/zero is the case that matters: an unbounded read of it would + // exhaust gateway or driver memory on any authorized sandbox create. + let dir = tempfile::tempdir().unwrap(); + let err = + read_upstream_proxy_ca_bundle_file(dir.path().to_str().unwrap(), "proxy_ca_bundle") + .unwrap_err(); + assert!(err.contains("regular file"), "{err}"); + assert!(err.contains("proxy_ca_bundle"), "{err}"); + + if Path::new("/dev/zero").exists() { + let err = + read_upstream_proxy_ca_bundle_file("/dev/zero", "proxy_ca_bundle").unwrap_err(); + assert!(err.contains("regular file"), "{err}"); + } + + let oversized = dir.path().join("oversized.pem"); + std::fs::write( + &oversized, + vec![b'x'; usize::try_from(MAX_UPSTREAM_PROXY_CA_BUNDLE_BYTES).unwrap() + 1], + ) + .unwrap(); + let err = + read_upstream_proxy_ca_bundle_file(oversized.to_str().unwrap(), "proxy_ca_bundle") + .unwrap_err(); + assert!(err.contains("exceeds"), "{err}"); + } + + #[test] + fn ca_bundle_file_missing_path_is_an_error() { + let err = + read_upstream_proxy_ca_bundle_file("/nonexistent/proxy-ca.pem", "proxy_ca_bundle") + .unwrap_err(); + assert!(err.contains("could not be read"), "{err}"); + } + + #[test] + fn ca_bundle_rejects_a_file_without_certificate_blocks() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("proxy-ca.pem"); + std::fs::write(&path, "this is not a certificate\n").unwrap(); + let err = read_upstream_proxy_ca_bundle_file(path.to_str().unwrap(), "proxy_ca_bundle") + .unwrap_err(); + assert!(err.contains("no PEM certificate blocks"), "{err}"); + + std::fs::write(&path, "").unwrap(); + let err = read_upstream_proxy_ca_bundle_file(path.to_str().unwrap(), "proxy_ca_bundle") + .unwrap_err(); + assert!(err.contains("no PEM certificate blocks"), "{err}"); + } + + #[test] + fn ca_bundle_rejects_pem_blocks_holding_invalid_der() { + // Passes `rustls_pemfile::certs` but contributes no trust anchor, so + // accepting it on the host would break every guest after boot. + let err = validate_upstream_proxy_ca_bundle_pem( + "-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----\n", + "/etc/openshell/tls/proxy-ca.pem", + "proxy_ca_bundle", + ) + .unwrap_err(); + assert!(err.contains("no usable trust anchors"), "{err}"); + } + + fn proxy_settings(url: Option<&str>) -> UpstreamProxySettings<'_> { + UpstreamProxySettings { + url, + ..UpstreamProxySettings::default() + } + } + + #[test] + fn upstream_proxy_settings_accept_a_bare_proxy_url() { + validate_upstream_proxy_settings(&proxy_settings(Some("http://proxy.corp.com:3128"))) + .expect("a lone proxy URL is a complete configuration"); + } + + #[test] + fn upstream_proxy_settings_accept_an_empty_configuration() { + validate_upstream_proxy_settings(&UpstreamProxySettings::default()) + .expect("no proxy configured at all is valid"); + } + + #[test] + fn upstream_proxy_settings_reject_an_unsupported_scheme() { + let err = validate_upstream_proxy_settings(&proxy_settings(Some("socks5://proxy:1080"))) + .expect_err("only http:// and https:// proxies are supported"); + assert!(err.starts_with("https_proxy "), "{err}"); + assert!(err.contains("unsupported proxy scheme"), "{err}"); + } + + #[test] + fn upstream_proxy_settings_reject_inline_credentials_by_naming_the_auth_file() { + let err = validate_upstream_proxy_settings(&proxy_settings(Some("http://u:p@proxy:3128"))) + .expect_err("inline credentials would be stored in gateway config"); + assert!(err.contains("proxy_auth_file"), "{err}"); + } + + #[test] + fn upstream_proxy_settings_reject_an_empty_proxy_url() { + let err = validate_upstream_proxy_settings(&proxy_settings(Some(" "))) + .expect_err("present-but-empty is a misconfiguration, not 'unset'"); + assert_eq!(err, "https_proxy must not be empty when set"); + } + + #[test] + fn upstream_proxy_settings_reject_auxiliary_keys_without_a_proxy_url() { + // Each auxiliary key implies a proxy boundary the operator believed + // was in effect; accepting one while every dial goes direct would + // hide a fail-open state. + for (settings, key) in [ + ( + UpstreamProxySettings { + no_proxy: Some("10.0.0.0/8"), + ..UpstreamProxySettings::default() + }, + "no_proxy", + ), + ( + UpstreamProxySettings { + auth_file: Some("/etc/openshell/secrets/proxy-auth"), + ..UpstreamProxySettings::default() + }, + "proxy_auth_file", + ), + ( + UpstreamProxySettings { + connect_by_hostname: Some(true), + ..UpstreamProxySettings::default() + }, + "proxy_connect_by_hostname", + ), + ( + UpstreamProxySettings { + ca_bundle: Some("/etc/openshell/tls/proxy-ca.pem"), + ..UpstreamProxySettings::default() + }, + "proxy_ca_bundle", + ), + ] { + let err = validate_upstream_proxy_settings(&settings) + .expect_err("an auxiliary key without a proxy URL must fail closed"); + assert_eq!( + err, + format!("{key} is set but no https_proxy is configured") + ); + } + } + + #[test] + fn upstream_proxy_settings_reject_empty_auxiliary_values() { + for (settings, expected) in [ + ( + UpstreamProxySettings { + url: Some("http://proxy:3128"), + no_proxy: Some(" "), + ..UpstreamProxySettings::default() + }, + "no_proxy must not be empty when set; omit it instead", + ), + ( + UpstreamProxySettings { + url: Some("http://proxy:3128"), + auth_file: Some(""), + ..UpstreamProxySettings::default() + }, + "proxy_auth_file must not be empty when set", + ), + ( + UpstreamProxySettings { + url: Some("http://proxy:3128"), + ca_bundle: Some(""), + ..UpstreamProxySettings::default() + }, + "proxy_ca_bundle must not be empty when set", + ), + ] { + let err = validate_upstream_proxy_settings(&settings) + .expect_err("present-but-empty must never be treated as unset"); + assert_eq!(err, expected); + } + } + + #[test] + fn upstream_proxy_credentials_require_the_cleartext_acknowledgement() { + let err = validate_upstream_proxy_settings(&UpstreamProxySettings { + url: Some("http://proxy:3128"), + auth_file: Some("/etc/openshell/secrets/proxy-auth"), + ..UpstreamProxySettings::default() + }) + .expect_err("Basic auth to an http:// proxy is cleartext on the wire"); + assert!(err.contains("proxy_auth_allow_insecure"), "{err}"); + + validate_upstream_proxy_settings(&UpstreamProxySettings { + url: Some("http://proxy:3128"), + auth_file: Some("/etc/openshell/secrets/proxy-auth"), + auth_allow_insecure: Some(true), + ..UpstreamProxySettings::default() + }) + .expect("the explicit acknowledgement makes the exposure an operator decision"); + } + + #[test] + fn upstream_proxy_credentials_need_no_acknowledgement_for_an_https_proxy() { + // The credential travels inside the verified TLS session to the proxy. + validate_upstream_proxy_settings(&UpstreamProxySettings { + url: Some("https://proxy:3130"), + auth_file: Some("/etc/openshell/secrets/proxy-auth"), + ..UpstreamProxySettings::default() + }) + .expect("an https:// proxy does not expose the credential on the wire"); + + // ... but setting it anyway is tolerated rather than an error. + validate_upstream_proxy_settings(&UpstreamProxySettings { + url: Some("https://proxy:3130"), + auth_file: Some("/etc/openshell/secrets/proxy-auth"), + auth_allow_insecure: Some(true), + ..UpstreamProxySettings::default() + }) + .expect("a redundant acknowledgement is tolerated"); + } + + #[test] + fn upstream_proxy_acknowledgement_without_credentials_is_rejected() { + // Including `= false`: the operator believed an auth file was + // configured, so the mismatch is surfaced rather than ignored. + for ack in [Some(true), Some(false)] { + let err = validate_upstream_proxy_settings(&UpstreamProxySettings { + url: Some("http://proxy:3128"), + auth_allow_insecure: ack, + ..UpstreamProxySettings::default() + }) + .expect_err("the acknowledgement is meaningless without a credential"); + assert_eq!( + err, + "proxy_auth_allow_insecure is set but no proxy_auth_file is configured" + ); + } + } + + #[test] + fn upstream_proxy_ca_bundle_is_valid_with_a_plain_http_proxy() { + // A TLS-intercepting proxy can be reached over plain HTTP while still + // re-signing tunneled server certificates with its own CA. + validate_upstream_proxy_settings(&UpstreamProxySettings { + url: Some("http://proxy:3128"), + ca_bundle: Some("/etc/openshell/tls/proxy-ca.pem"), + ..UpstreamProxySettings::default() + }) + .expect("an intercepting proxy's CA is meaningful without an https:// proxy URL"); + } } diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index 4fc9ace415..5c61ae1823 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -154,6 +154,14 @@ Select the VM driver with `--drivers vm`, `OPENSHELL_DRIVERS=vm`, or `compute_dr | `guest_tls_ca` | unset | CA cert for the guest's mTLS client bundle. Required when `grpc_endpoint` uses `https://`. | | `guest_tls_cert` | unset | Guest client certificate. | | `guest_tls_key` | unset | Guest client private key. | +| `https_proxy` | unset | Corporate forward proxy (`http://host:port` or `https://host:port`) the in-guest supervisor chains policy-approved TLS CONNECT egress through. On the libkrun backend a proxy on the gateway host's loopback must be addressed as `http://host.openshell.internal:` — guest egress leaves through gvproxy, which NATs `192.168.127.254` to the host's `127.0.0.1`. The QEMU/TAP backend (GPU sandboxes) has no such NAT and its nftables rules expose only the gateway port to the guest, so a gateway-host proxy URL is rejected at launch there; use an address routable from the guest's masqueraded egress. | +| `no_proxy` | unset | Comma-separated bypass list for the corporate proxy only. OpenShell policy evaluation still applies. | +| `proxy_auth_file` | unset | Gateway-host path to a `user:pass` credential file. Staged root-only into the per-sandbox overlay and removed with the sandbox. | +| `proxy_auth_allow_insecure` | unset | Required with `proxy_auth_file` against an `http://` proxy: acknowledges that Basic auth is cleartext on the connection to the proxy. | +| `proxy_connect_by_hostname` | unset | Send hostnames rather than validated IPs in CONNECT. Last resort for proxies whose ACLs reject IP targets. | +| `proxy_ca_bundle` | unset | Gateway-host path to a PEM CA bundle trusted for an `https://` proxy and for certificates a TLS-intercepting proxy re-signs. | + +The proxy settings are operator-owned and deployment-level: they are not accepted through `template.driver_config.vm`, and they reach the supervisor on its command line through a per-sandbox argument file the driver writes into the overlay upperdir on every launch, so a sandbox image cannot forge or shadow them. Every present-but-invalid value is fatal at gateway or sandbox startup rather than degrading to a direct dial. See [`openshell-gateway --help`](../openshell-server/src/cli.rs) for the gateway process flag surface. diff --git a/crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh b/crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh index 14dbc0466b..32d6ed1dff 100644 --- a/crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh +++ b/crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh @@ -192,6 +192,67 @@ prepare_guest_image_rootfs() { rm -rf "$payload_dir" } +# Driver-owned arguments appended to the supervisor's command line. +# +# The VM driver cannot build the supervisor's argv the way the container +# drivers do, so it writes the arguments it chose into the overlay upperdir +# and this script appends them verbatim. Populated by +# read_supervisor_extra_args; empty until then. +SUPERVISOR_EXTRA_ARGS=() + +# Upper bound on driver-supplied supervisor arguments. +# +# The corporate proxy settings are the only producer today and top out at ten +# entries. The cap exists so a corrupt or oversized file cannot expand into an +# unbounded command line. +SUPERVISOR_EXTRA_ARGS_MAX=32 + +read_supervisor_extra_args() { + # Read the driver-authored supervisor argument list, one argument per + # line, verbatim -- no word splitting, globbing, or expansion, so values + # containing spaces (e.g. a NO_PROXY list) survive intact. + # + # Security: this is the operator-owned egress boundary. The driver writes + # this file into the overlay upperdir on every launch, including an empty + # file when it has no arguments to pass, so the upperdir copy always + # shadows the read-only image layer. A sandbox image can therefore neither + # supply its own supervisor arguments by baking a file at this path nor + # disable the operator's by omitting one. A missing file means the driver + # passed nothing; a file it cannot read means the overlay is broken, and + # we fail closed rather than start a supervisor with a silently truncated + # egress configuration. + local args_file + args_file="$(root_path /opt/openshell/supervisor-args)" + + SUPERVISOR_EXTRA_ARGS=() + if [ ! -f "$args_file" ]; then + return 0 + fi + if [ ! -r "$args_file" ]; then + ts "FATAL: supervisor argument list ${args_file} is not readable" + exit 1 + fi + + local arg + while IFS= read -r arg; do + # render_guest_supervisor_args never emits a blank line, so one means + # the file was truncated or tampered with after the driver wrote it. + if [ -z "$arg" ]; then + ts "FATAL: empty entry in supervisor argument list" + exit 1 + fi + if [ "${#SUPERVISOR_EXTRA_ARGS[@]}" -ge "$SUPERVISOR_EXTRA_ARGS_MAX" ]; then + ts "FATAL: supervisor argument list exceeds ${SUPERVISOR_EXTRA_ARGS_MAX} entries" + exit 1 + fi + SUPERVISOR_EXTRA_ARGS+=("$arg") + done < "$args_file" + + if [ "${#SUPERVISOR_EXTRA_ARGS[@]}" -gt 0 ]; then + ts "supervisor arguments from driver: ${#SUPERVISOR_EXTRA_ARGS[@]} entries" + fi +} + exec_supervisor_in_newroot() { local chroot_bin local bootstrap="/.openshell-bootstrap" @@ -214,14 +275,16 @@ exec_supervisor_in_newroot() { "${bootstrap}/lib64/ld-linux-aarch64.so.1"; do if [ -x "/newroot${loader}" ]; then lib_path="${bootstrap}/lib:${bootstrap}/lib64:${bootstrap}/usr/lib:${bootstrap}/usr/lib64:${bootstrap}/lib/aarch64-linux-gnu:${bootstrap}/lib/x86_64-linux-gnu:${bootstrap}/usr/lib/aarch64-linux-gnu:${bootstrap}/usr/lib/x86_64-linux-gnu" - exec "$chroot_bin" /newroot "$loader" --library-path "$lib_path" "$supervisor" --workdir /sandbox + exec "$chroot_bin" /newroot "$loader" --library-path "$lib_path" \ + "$supervisor" --workdir /sandbox "${SUPERVISOR_EXTRA_ARGS[@]+"${SUPERVISOR_EXTRA_ARGS[@]}"}" fi done - exec "$chroot_bin" /newroot "$supervisor" --workdir /sandbox + exec "$chroot_bin" /newroot "$supervisor" --workdir /sandbox "${SUPERVISOR_EXTRA_ARGS[@]+"${SUPERVISOR_EXTRA_ARGS[@]}"}" fi if [ -x /newroot/opt/openshell/bin/openshell-sandbox ]; then - exec "$chroot_bin" /newroot /opt/openshell/bin/openshell-sandbox --workdir /sandbox + exec "$chroot_bin" /newroot /opt/openshell/bin/openshell-sandbox \ + --workdir /sandbox "${SUPERVISOR_EXTRA_ARGS[@]+"${SUPERVISOR_EXTRA_ARGS[@]}"}" fi done @@ -833,11 +896,13 @@ if [ -n "${OPENSHELL_SANDBOX_ID:-}" ]; then ts "OPENSHELL_SANDBOX_ID=${OPENSHELL_SANDBOX_ID}" fi +read_supervisor_extra_args + ts "starting openshell-sandbox supervisor" if [ "${ROOT_PREFIX:-}" = "/newroot" ]; then exec_supervisor_in_newroot fi -exec /opt/openshell/bin/openshell-sandbox --workdir /sandbox +exec /opt/openshell/bin/openshell-sandbox --workdir /sandbox "${SUPERVISOR_EXTRA_ARGS[@]+"${SUPERVISOR_EXTRA_ARGS[@]}"}" } if [ "${1:-}" != "--post-overlay" ]; then diff --git a/crates/openshell-driver-vm/src/driver.rs b/crates/openshell-driver-vm/src/driver.rs index 8adcc79f92..e70a6a48d3 100644 --- a/crates/openshell-driver-vm/src/driver.rs +++ b/crates/openshell-driver-vm/src/driver.rs @@ -60,7 +60,7 @@ use sha2::{Digest, Sha256}; use std::collections::{HashMap, HashSet}; use std::fs; use std::io::Read; -use std::net::Ipv4Addr; +use std::net::{IpAddr, Ipv4Addr}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::{Component, Path, PathBuf}; @@ -132,7 +132,7 @@ impl VmSandboxDriverConfig { /// Code paths route via `GVPROXY_HOST_LOOPBACK_ALIAS` (DNS / /etc/hosts) /// instead so logs stay readable; this constant is kept for documentation /// and parity with the guest init script. -#[allow(dead_code)] +#[allow(dead_code)] // Documentation/parity anchor; all routing goes via the alias. const GVPROXY_HOST_LOOPBACK_IP: &str = "192.168.127.254"; const OPENSHELL_HOST_GATEWAY_ALIAS: &str = "host.openshell.internal"; /// Hostname gvproxy resolves (via its embedded DNS) to the host-loopback IP. @@ -162,6 +162,19 @@ const GUEST_INIT_DROPIN_DIR: &str = openshell_core::container_paths::VM_GUEST_IN /// upperdir on every launch, so the image cannot forge or shadow it. const GUEST_INIT_DROPIN_MANIFEST: &str = openshell_core::container_paths::VM_GUEST_INIT_DROPIN_MANIFEST; +/// Guest path of the root-only corporate proxy credential staged by the driver. +const GUEST_UPSTREAM_PROXY_AUTH_PATH: &str = + openshell_core::container_paths::VM_GUEST_UPSTREAM_PROXY_AUTH_PATH; +/// Guest path of the corporate proxy CA bundle staged by the driver. +const GUEST_PROXY_CA_PATH: &str = openshell_core::container_paths::VM_GUEST_PROXY_CA_PATH; +/// Guest path of the driver-authored supervisor argument list. +/// +/// The counterpart of [`GUEST_INIT_DROPIN_MANIFEST`] for the supervisor's own +/// command line: written into the overlay upperdir on every launch (empty +/// when there is nothing to pass) so the guest appends exactly the arguments +/// the driver chose and a sandbox image cannot forge or shadow them. +const GUEST_SUPERVISOR_ARGS_PATH: &str = + openshell_core::container_paths::VM_GUEST_SUPERVISOR_ARGS_PATH; const IMAGE_CACHE_ROOT_DIR: &str = "images"; const IMAGE_CACHE_ROOTFS_IMAGE: &str = "rootfs.ext4"; const OVERLAY_TEMPLATE_CACHE_DIR: &str = "overlay-templates"; @@ -217,7 +230,7 @@ enum GuestImagePayloadSource { LocalDocker { rootfs_archive: PathBuf }, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct VmDriverConfig { pub openshell_endpoint: String, pub state_dir: PathBuf, @@ -243,6 +256,104 @@ pub struct VmDriverConfig { /// When empty, defaults to the resolved UID. #[serde(default, skip_serializing_if = "Option::is_none")] pub sandbox_gid: Option, + + /// Corporate forward proxy URL (`http://host:port` or `https://host:port`) + /// passed to the in-guest supervisor. + /// + /// The supervisor chains policy-approved TLS tunnels through this proxy + /// with HTTP CONNECT instead of dialing destinations directly. This is an + /// operator-owned egress boundary: it travels on the supervisor's argv, + /// which sandbox spec/template environment and image `ENV` cannot + /// influence. A proxy on the gateway host's loopback is reachable from the + /// guest only through the gvproxy host alias + /// (`host.openshell.internal`). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub https_proxy: Option, + + /// Comma-separated `NO_PROXY` list passed alongside the proxy URL. + /// + /// Matching destinations are dialed directly instead of through the + /// corporate proxy. This bypasses only the corporate proxy, never + /// `OpenShell` policy evaluation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub no_proxy: Option, + + /// Path (on the gateway host) to a file containing the corporate proxy + /// credential in `user:pass` form. + /// + /// The driver validates it at sandbox-create time and stages it into the + /// per-sandbox overlay at [`GUEST_UPSTREAM_PROXY_AUTH_PATH`], root-only. + /// Credentials are never embedded in the proxy URL and never reach the + /// guest environment. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub proxy_auth_file: Option, + + /// Explicit acknowledgement that proxy credentials are sent in cleartext. + /// + /// `Proxy-Authorization: Basic` over the plain-TCP connection to an + /// `http://` proxy is recoverable by anyone on the network path, so + /// [`Self::proxy_auth_file`] requires this acknowledgement. An `https://` + /// proxy carries the credential inside the verified TLS session and does + /// not need it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub proxy_auth_allow_insecure: Option, + + /// Send the destination hostname in CONNECT requests instead of a + /// validated IP. + /// + /// The default binds the tunnel to an address that passed the sandbox's + /// SSRF and `allowed_ips` validation. Set this only when the proxy's ACLs + /// filter on hostnames and reject IP CONNECT targets: the proxy then + /// resolves the name itself and its own ACLs become the effective egress + /// control for proxied TLS. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub proxy_connect_by_hostname: Option, + + /// Path (on the gateway host) to a PEM CA bundle trusted for the + /// corporate proxy. + /// + /// The driver stages it into the per-sandbox overlay at + /// [`GUEST_PROXY_CA_PATH`] and passes that path via + /// `--upstream-proxy-ca-bundle`. It is trusted both for the handshake + /// with an `https://` proxy and for server certificates re-signed by a + /// TLS-intercepting proxy. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub proxy_ca_bundle: Option, +} + +/// Redacting `Debug` so a proxy URL or credential path never reaches a log. +/// +/// A validated proxy URL cannot embed credentials, but `Debug` can be emitted +/// before validation runs, so presence is logged rather than the value. +impl std::fmt::Debug for VmDriverConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VmDriverConfig") + .field("openshell_endpoint", &self.openshell_endpoint) + .field("state_dir", &self.state_dir) + .field("launcher_bin", &self.launcher_bin) + .field("default_image", &self.default_image) + .field("bootstrap_image", &self.bootstrap_image) + .field("log_level", &self.log_level) + .field("krun_log_level", &self.krun_log_level) + .field("vcpus", &self.vcpus) + .field("mem_mib", &self.mem_mib) + .field("overlay_disk_mib", &self.overlay_disk_mib) + .field("guest_tls_ca", &self.guest_tls_ca) + .field("guest_tls_cert", &self.guest_tls_cert) + .field("guest_tls_key", &self.guest_tls_key) + .field("gpu_enabled", &self.gpu_enabled) + .field("gpu_mem_mib", &self.gpu_mem_mib) + .field("gpu_vcpus", &self.gpu_vcpus) + .field("sandbox_uid", &self.sandbox_uid) + .field("sandbox_gid", &self.sandbox_gid) + .field("https_proxy", &self.https_proxy.is_some()) + .field("no_proxy", &self.no_proxy) + .field("proxy_auth_file", &self.proxy_auth_file.is_some()) + .field("proxy_auth_allow_insecure", &self.proxy_auth_allow_insecure) + .field("proxy_connect_by_hostname", &self.proxy_connect_by_hostname) + .field("proxy_ca_bundle", &self.proxy_ca_bundle) + .finish() + } } /// Default sandbox UID used by the VM driver when no config value is set. @@ -269,6 +380,12 @@ impl Default for VmDriverConfig { gpu_vcpus: 4, sandbox_uid: None, sandbox_gid: None, + https_proxy: None, + no_proxy: None, + proxy_auth_file: None, + proxy_auth_allow_insecure: None, + proxy_connect_by_hostname: None, + proxy_ca_bundle: None, } } } @@ -307,6 +424,29 @@ impl VmDriverConfig { Ok(()) } + /// Validate the operator's corporate upstream-proxy settings, fail-closed. + /// + /// Delegates to the validator shared with the Podman and Kubernetes + /// drivers and with the in-guest supervisor, so a value accepted here is + /// never rejected inside the guest — and no misconfiguration can silently + /// degrade to a direct dial. + /// + /// # Errors + /// + /// Returns a message naming the offending key. + pub fn validate_proxy_config(&self) -> Result<(), String> { + openshell_core::driver_utils::validate_upstream_proxy_settings( + &openshell_core::driver_utils::UpstreamProxySettings { + url: self.https_proxy.as_deref(), + no_proxy: self.no_proxy.as_deref(), + auth_file: self.proxy_auth_file.as_deref(), + auth_allow_insecure: self.proxy_auth_allow_insecure, + connect_by_hostname: self.proxy_connect_by_hostname, + ca_bundle: self.proxy_ca_bundle.as_deref(), + }, + ) + } + fn requires_tls_materials(&self) -> bool { self.openshell_endpoint.starts_with("https://") } @@ -447,6 +587,7 @@ impl VmDriver { .validate() .map_err(|err| err.message().to_string())?; config.validate_sandbox_identity()?; + config.validate_proxy_config()?; if config.openshell_endpoint.trim().is_empty() { return Err("openshell endpoint is required".to_string()); } @@ -908,6 +1049,16 @@ impl VmDriver { return Err(err); } + // Staged on every launch, including a restart onto a preserved + // overlay, so the driver's copy always shadows the image layer. + if let Err(err) = inject_guest_upstream_proxy(&overlay_disk, &self.config).await { + self.lifecycle_extensions + .after_launch_failed(&sandbox, &state_dir, LaunchAbortReason::GuestPrepareFailed) + .await; + self.release_gpu_and_subnet(&sandbox.id); + return Err(err); + } + let endpoint_override = if plan.backend == VmBackend::Qemu { plan.host_ip.as_deref().map(|host_ip| { guest_visible_openshell_endpoint_for_tap(&self.config.openshell_endpoint, host_ip) @@ -1694,26 +1845,44 @@ impl VmDriver { if plan.gpu_bdf.is_none() { plan.gpu_bdf = gpu_bdf; } - if has_complete_qemu_network(plan) { - return Ok(()); + if !has_complete_qemu_network(plan) { + let subnet = self + .subnet_allocator + .lock() + .map_err(|e| Status::internal(format!("subnet allocator lock poisoned: {e}")))? + .allocate(sandbox_id) + .map_err(Status::failed_precondition)?; + let mac = mac_from_sandbox_id(sandbox_id); + plan.tap_device = Some(tap_device_name(sandbox_id)); + plan.guest_ip = Some(subnet.guest_ip.to_string()); + plan.host_ip = Some(subnet.host_ip.to_string()); + plan.vsock_cid = Some(allocate_vsock_cid()); + plan.guest_mac = Some(format!( + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] + )); + plan.gateway_port = gateway_port_from_endpoint(&self.config.openshell_endpoint); + } + + // The corporate-proxy host-loopback recipe is a libkrun/gvproxy + // property and has no QEMU/TAP equivalent (see + // `proxy_url_targets_gateway_host`). Run it here, after the subnet + // allocation above has settled `plan.host_ip`, because the address to + // compare against is this sandbox's own TAP host address. Fail the + // create with the reason rather than boot a sandbox whose + // policy-approved CONNECTs all time out against an unreachable proxy. + if let Some(url) = self.config.https_proxy.as_deref() + && proxy_url_targets_gateway_host(url, plan.host_ip.as_deref()) + { + let tap_host = plan.host_ip.as_deref().unwrap_or("the TAP host address"); + return Err(Status::failed_precondition(format!( + "https_proxy '{url}' addresses the gateway host, which a QEMU/TAP sandbox \ + (GPU sandboxes) cannot reach: host.openshell.internal resolves to this \ + sandbox's TAP host address {tap_host} and the driver's nftables rules allow \ + only the gateway port from the guest. Configure a proxy address routable \ + from the guest's masqueraded egress, or run this sandbox without a GPU" + ))); } - - let subnet = self - .subnet_allocator - .lock() - .map_err(|e| Status::internal(format!("subnet allocator lock poisoned: {e}")))? - .allocate(sandbox_id) - .map_err(Status::failed_precondition)?; - let mac = mac_from_sandbox_id(sandbox_id); - plan.tap_device = Some(tap_device_name(sandbox_id)); - plan.guest_ip = Some(subnet.guest_ip.to_string()); - plan.host_ip = Some(subnet.host_ip.to_string()); - plan.vsock_cid = Some(allocate_vsock_cid()); - plan.guest_mac = Some(format!( - "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] - )); - plan.gateway_port = gateway_port_from_endpoint(&self.config.openshell_endpoint); Ok(()) } @@ -4437,6 +4606,53 @@ fn guest_visible_openshell_endpoint(endpoint: &str) -> String { endpoint.to_string() } +/// Whether a corporate proxy URL points at the gateway host itself, as seen +/// from a QEMU/TAP guest whose TAP host address is `tap_host_ip`. +/// +/// On the libkrun backend gvproxy NATs the host-loopback alias +/// `host.openshell.internal` (and any loopback URL, which the driver rewrites +/// to that alias) to the gateway host's `127.0.0.1`, so a proxy bound to host +/// loopback is reachable from the guest. The QEMU/TAP backend used for GPU +/// sandboxes has no equivalent: `host.openshell.internal` resolves to the TAP +/// host address, and the driver's own nftables `input` chain accepts only the +/// gateway port from the guest and drops the rest, so no proxy on the gateway +/// host is reachable regardless of the address it binds. +/// +/// The gateway host is therefore reached from a QEMU guest under exactly three +/// spellings: the guest's own loopback (never the host's, but a configuration +/// that plainly means the host), the documented host aliases that +/// `write_host_gateway_aliases` seeds to the TAP host address, and that TAP +/// host address written literally. `tap_host_ip` is this sandbox's allocated +/// address, so the comparison must be made after the launch plan's subnet +/// allocation; `None` means the plan carries no TAP host and only the +/// address-independent spellings are classified. +/// +/// gvproxy's `GVPROXY_HOST_LOOPBACK_IP` is deliberately **not** matched here. +/// It is special only to libkrun; on QEMU/TAP it is an ordinary address that +/// may well be routable through the guest's masqueraded egress, and rejecting +/// it would refuse a working configuration. +/// +/// Used to reject an unreachable configuration up front on the QEMU path +/// instead of letting every policy-approved CONNECT time out. +fn proxy_url_targets_gateway_host(url: &str, tap_host_ip: Option<&str>) -> bool { + let Ok(parsed) = Url::parse(url) else { + // Unparseable URLs are rejected by shared validation before launch. + return false; + }; + let tap_host = tap_host_ip.and_then(|ip| ip.parse::().ok()); + match parsed.host() { + Some(Host::Ipv4(ip)) => ip.is_loopback() || tap_host == Some(IpAddr::V4(ip)), + Some(Host::Ipv6(ip)) => ip.is_loopback() || tap_host == Some(IpAddr::V6(ip)), + Some(Host::Domain(host)) => { + host.eq_ignore_ascii_case("localhost") + || host.eq_ignore_ascii_case(OPENSHELL_HOST_GATEWAY_ALIAS) + || host.eq_ignore_ascii_case("host.containers.internal") + || host.eq_ignore_ascii_case("host.docker.internal") + } + None => false, + } +} + fn gateway_port_from_endpoint(endpoint: &str) -> Option { Url::parse(endpoint).ok().and_then(|url| url.port()) } @@ -5130,6 +5346,182 @@ fn inject_guest_init_dropins( span_status.finish(Ok(())) } +/// Build the corporate upstream-proxy arguments passed to the guest supervisor. +/// +/// This operator-owned egress boundary travels on the supervisor's argv, +/// which sandbox spec/template environment and image `ENV` cannot influence. +/// Credentials are never on argv — only the root-only guest path is passed; +/// the supervisor reads the credential from that file. +fn upstream_proxy_cli_args(config: &VmDriverConfig) -> Vec { + let mut args = Vec::new(); + if let Some(url) = &config.https_proxy { + args.push("--upstream-proxy".to_string()); + args.push(url.clone()); + } + if let Some(list) = &config.no_proxy { + args.push("--upstream-no-proxy".to_string()); + args.push(list.clone()); + } + if config.proxy_auth_file.is_some() { + args.push("--upstream-proxy-auth-file".to_string()); + // The guest path, never the gateway-host path the operator configured. + args.push(GUEST_UPSTREAM_PROXY_AUTH_PATH.to_string()); + } + // Config validation guarantees the acknowledgement is `true` whenever an + // auth file is configured against an http:// proxy; the supervisor + // independently refuses credentials without it. + if config.proxy_auth_allow_insecure == Some(true) { + args.push("--upstream-proxy-auth-allow-insecure".to_string()); + } + // Absent means the default validated-IP CONNECT binding; only the + // explicit hostname opt-in is passed through. + if config.proxy_connect_by_hostname == Some(true) { + args.push("--upstream-proxy-connect-by-hostname".to_string()); + } + if config.proxy_ca_bundle.is_some() { + args.push("--upstream-proxy-ca-bundle".to_string()); + args.push(GUEST_PROXY_CA_PATH.to_string()); + } + args +} + +/// Render the supervisor argument list as newline-separated arguments. +/// +/// One argument per line, verbatim: the guest reads the lines into an array +/// without word splitting or globbing, so values containing spaces survive +/// intact. An empty list renders an empty file, which the guest reads as "no +/// extra arguments". +fn render_guest_supervisor_args(args: &[String]) -> Vec { + let mut body = args.join("\n"); + if !body.is_empty() { + body.push('\n'); + } + body.into_bytes() +} + +/// Reject argument values the newline-delimited guest file cannot represent. +/// +/// Every value here is operator-supplied config, so this is a guard against +/// misconfiguration rather than an attack: a stray newline would otherwise +/// split one value into two arguments in the guest. +fn validate_guest_supervisor_args(args: &[String]) -> Result<(), String> { + for arg in args { + if arg.contains('\n') || arg.contains('\r') || arg.contains('\0') { + return Err( + "corporate proxy settings must not contain newline or NUL characters".to_string(), + ); + } + } + Ok(()) +} + +/// Read and validate the corporate proxy credential from the gateway host. +/// +/// Uses the validators shared with the supervisor, so a credential accepted +/// here is never rejected inside the guest. The error never carries the file +/// contents. +async fn read_sandbox_proxy_credential(path: &str) -> Result { + let path_owned = path.to_string(); + let raw = tokio::task::spawn_blocking(move || { + openshell_core::driver_utils::read_upstream_proxy_credential_file(&path_owned) + }) + .await + .map_err(|err| Status::internal(format!("proxy_auth_file read task failed: {err}")))? + .map_err(Status::invalid_argument)?; + let credential = openshell_core::driver_utils::parse_upstream_proxy_credential(&raw) + .map_err(|err| Status::invalid_argument(format!("proxy_auth_file '{path}': {err}")))?; + Ok(credential.to_string()) +} + +/// Read and validate the corporate proxy CA bundle from the gateway host. +/// +/// Uses the reader shared with the supervisor, so the bundle is bounded and +/// non-regular files are rejected (an operator path such as `/dev/zero` can +/// otherwise exhaust driver memory), and a bundle accepted here contributes at +/// least one trust anchor rustls accepts rather than merely looking like PEM. +/// Checked here rather than only in the guest so the operator gets an error +/// attributable to `proxy_ca_bundle` instead of an opaque supervisor startup +/// failure inside every sandbox. The error never carries the file contents. +async fn read_sandbox_proxy_ca_bundle(path: &str) -> Result, Status> { + let path_owned = path.to_string(); + let pem = tokio::task::spawn_blocking(move || { + openshell_core::driver_utils::read_upstream_proxy_ca_bundle_file( + &path_owned, + "proxy_ca_bundle", + ) + }) + .await + .map_err(|err| Status::internal(format!("proxy_ca_bundle read task failed: {err}")))? + .map_err(Status::invalid_argument)?; + Ok(pem.into_bytes()) +} + +/// Stage the corporate upstream-proxy configuration into the guest overlay. +/// +/// Writes three files into the overlay upperdir the driver owns: +/// +/// * the credential at [`GUEST_UPSTREAM_PROXY_AUTH_PATH`], mode `0600`; +/// * the CA bundle at [`GUEST_PROXY_CA_PATH`], mode `0644` (a CA certificate +/// is not secret); +/// * the supervisor argument list at [`GUEST_SUPERVISOR_ARGS_PATH`], mode +/// `0644`. +/// +/// All three are written on every launch, empty when the corresponding +/// setting is absent. Writing rather than skipping is what makes the channel +/// unforgeable: the upperdir copy always shadows the read-only image layer, so +/// a sandbox image cannot supply its own arguments or credential by baking a +/// file at these paths, and cannot disable the operator's by omitting one. It +/// also clears material a previous launch staged into a preserved overlay +/// after the operator removed the setting. +/// +/// A microVM has no bind mounts or container secrets, so the credential lives +/// at rest inside the per-sandbox overlay disk on the host — the same +/// delivery the per-sandbox gateway JWT already uses. It is removed with the +/// sandbox when the state directory is deleted. +#[allow(clippy::result_large_err)] +async fn inject_guest_upstream_proxy( + overlay_disk: &Path, + config: &VmDriverConfig, +) -> Result<(), Status> { + // Written whether or not they are configured. Writing empty files when + // the operator removed a setting clears material a previous launch staged + // into a preserved overlay, and shadows anything an image baked at these + // paths, so a staged file is only ever the one this launch produced. + let credential = match config.proxy_auth_file.as_deref() { + Some(path) => format!("{}\n", read_sandbox_proxy_credential(path).await?).into_bytes(), + None => Vec::new(), + }; + let credential_path = overlay_upper_path(GUEST_UPSTREAM_PROXY_AUTH_PATH); + write_rootfs_image_file(overlay_disk, &credential_path, &credential) + .map_err(|err| Status::internal(format!("write VM guest proxy credential: {err}")))?; + set_rootfs_image_file_mode(overlay_disk, &credential_path, 0o600) + .map_err(|err| Status::internal(format!("set VM guest proxy credential mode: {err}")))?; + + let ca_bundle = match config.proxy_ca_bundle.as_deref() { + Some(path) => read_sandbox_proxy_ca_bundle(path).await?, + None => Vec::new(), + }; + let ca_path = overlay_upper_path(GUEST_PROXY_CA_PATH); + write_rootfs_image_file(overlay_disk, &ca_path, &ca_bundle) + .map_err(|err| Status::internal(format!("write VM guest proxy CA bundle: {err}")))?; + set_rootfs_image_file_mode(overlay_disk, &ca_path, 0o644) + .map_err(|err| Status::internal(format!("set VM guest proxy CA bundle mode: {err}")))?; + + let args = upstream_proxy_cli_args(config); + validate_guest_supervisor_args(&args).map_err(Status::failed_precondition)?; + let guest_path = overlay_upper_path(GUEST_SUPERVISOR_ARGS_PATH); + write_rootfs_image_file( + overlay_disk, + &guest_path, + &render_guest_supervisor_args(&args), + ) + .map_err(|err| Status::internal(format!("write VM guest supervisor arguments: {err}")))?; + set_rootfs_image_file_mode(overlay_disk, &guest_path, 0o644).map_err(|err| { + Status::internal(format!("set VM guest supervisor arguments mode: {err}")) + })?; + Ok(()) +} + /// Render the drop-in allow-list as newline-separated, ASCII-sorted, /// de-duplicated names. Names are already validated to be path-safe by /// [`validate_guest_init_dropins`]. @@ -8074,6 +8466,12 @@ mod tests { } } + fn test_driver_with_proxy(https_proxy: &str) -> VmDriver { + let mut driver = test_driver_with_extensions(LifecycleExtensionRegistry::new()); + driver.config.https_proxy = Some(https_proxy.to_string()); + driver + } + #[derive(Debug)] struct QemuRequiringExtension { name: String, @@ -8403,4 +8801,466 @@ mod tests { assert!(err.is_resource_exhausted()); assert_eq!(err.message(), "pool empty"); } + + /// A driver config carrying only corporate proxy settings. + fn proxy_config( + https_proxy: Option<&str>, + auth_file: Option<&str>, + ca_bundle: Option<&str>, + ) -> VmDriverConfig { + VmDriverConfig { + openshell_endpoint: "http://127.0.0.1:8080".to_string(), + https_proxy: https_proxy.map(ToString::to_string), + proxy_auth_file: auth_file.map(ToString::to_string), + proxy_auth_allow_insecure: auth_file.map(|_| true), + proxy_ca_bundle: ca_bundle.map(ToString::to_string), + ..Default::default() + } + } + + #[test] + fn driver_config_debug_redacts_the_proxy_url_and_credential_path() { + // `Debug` can be emitted before validation runs, and an unvalidated + // proxy URL may still carry inline `user:pass@` credentials. + let rendered = format!( + "{:?}", + proxy_config( + Some("http://user:secret@proxy.corp.test:3128"), + Some("/etc/openshell/secrets/proxy-auth"), + Some("/etc/openshell/tls/corp-ca.pem"), + ) + ); + assert!( + !rendered.contains("secret") && !rendered.contains("proxy.corp.test"), + "the proxy URL must be logged as presence only: {rendered}" + ); + assert!( + !rendered.contains("/etc/openshell/secrets/proxy-auth"), + "the credential path must be logged as presence only: {rendered}" + ); + assert!( + rendered.contains("https_proxy: true") && rendered.contains("proxy_auth_file: true"), + "presence of each must still be visible for debugging: {rendered}" + ); + // A CA path is not sensitive and stays readable. + assert!( + rendered.contains("corp-ca.pem"), + "the CA bundle path is not a secret and should stay legible: {rendered}" + ); + } + + #[test] + fn proxy_material_is_staged_inside_the_per_sandbox_overlay() { + // Everything the driver stages lands in the overlay upperdir, which + // lives in the sandbox's own state directory. That is what makes the + // credential removable with the sandbox (remove_sandbox_state_dir + // deletes the whole directory) and unforgeable by the guest image + // (the upperdir shadows the read-only image layer). + for guest_path in [ + GUEST_UPSTREAM_PROXY_AUTH_PATH, + GUEST_PROXY_CA_PATH, + GUEST_SUPERVISOR_ARGS_PATH, + ] { + assert!( + guest_path.starts_with("/opt/openshell/"), + "{guest_path} must be under the reserved guest control root" + ); + assert_eq!( + overlay_upper_path(guest_path), + format!("/upper{guest_path}"), + "{guest_path} must be staged into the overlay upperdir" + ); + } + } + + #[test] + fn upstream_proxy_args_are_empty_without_a_configured_proxy() { + assert!(upstream_proxy_cli_args(&VmDriverConfig::default()).is_empty()); + // The file is still written, empty, so the guest cannot fall back to + // an image-baked argument list. + assert!(render_guest_supervisor_args(&[]).is_empty()); + } + + #[test] + fn upstream_proxy_args_pass_guest_paths_not_host_paths() { + let config = proxy_config( + Some("http://proxy.corp.test:3128"), + Some("/etc/openshell/secrets/proxy-auth"), + Some("/etc/openshell/tls/corp-ca.pem"), + ); + let args = upstream_proxy_cli_args(&config); + + // The credential and CA live at fixed guest paths; the gateway-host + // paths the operator configured must never reach the guest argv. + let auth = args + .iter() + .position(|arg| arg == "--upstream-proxy-auth-file") + .map(|i| args[i + 1].as_str()); + assert_eq!(auth, Some(GUEST_UPSTREAM_PROXY_AUTH_PATH)); + let ca = args + .iter() + .position(|arg| arg == "--upstream-proxy-ca-bundle") + .map(|i| args[i + 1].as_str()); + assert_eq!(ca, Some(GUEST_PROXY_CA_PATH)); + assert!( + !args + .iter() + .any(|arg| arg.contains("/etc/openshell/secrets") || arg.contains("corp-ca.pem")), + "host paths leaked into the guest argv: {args:?}" + ); + } + + #[test] + fn upstream_proxy_args_pass_only_explicit_opt_ins() { + let mut config = proxy_config(Some("https://proxy.corp.test:3130"), None, None); + config.no_proxy = Some("10.0.0.0/8,.svc.cluster.local".to_string()); + let args = upstream_proxy_cli_args(&config); + assert_eq!( + args, + vec![ + "--upstream-proxy".to_string(), + "https://proxy.corp.test:3130".to_string(), + "--upstream-no-proxy".to_string(), + "10.0.0.0/8,.svc.cluster.local".to_string(), + ] + ); + + // `Some(false)` must not be passed as the presence flag it is on the + // supervisor side. + config.proxy_connect_by_hostname = Some(false); + assert!( + !upstream_proxy_cli_args(&config) + .iter() + .any(|arg| arg == "--upstream-proxy-connect-by-hostname") + ); + config.proxy_connect_by_hostname = Some(true); + assert!( + upstream_proxy_cli_args(&config) + .iter() + .any(|arg| arg == "--upstream-proxy-connect-by-hostname") + ); + } + + #[test] + fn guest_supervisor_args_render_one_argument_per_line() { + let args = vec![ + "--upstream-proxy".to_string(), + "http://proxy.corp.test:3128".to_string(), + "--upstream-no-proxy".to_string(), + "a.example, b.example".to_string(), + ]; + // A value containing a space stays one line, so the guest reads it + // back as a single argument rather than word-splitting it. + assert_eq!( + String::from_utf8(render_guest_supervisor_args(&args)).unwrap(), + "--upstream-proxy\nhttp://proxy.corp.test:3128\n--upstream-no-proxy\na.example, b.example\n" + ); + } + + #[test] + fn guest_supervisor_args_reject_line_breaking_values() { + // A newline would split one operator value into two guest arguments. + for bad in ["a\nb", "a\rb", "a\0b"] { + assert!( + validate_guest_supervisor_args(&[bad.to_string()]).is_err(), + "{bad:?} must be rejected" + ); + } + validate_guest_supervisor_args(&["--upstream-proxy".to_string()]) + .expect("ordinary arguments are accepted"); + } + + #[test] + fn proxy_config_validation_rejects_settings_without_a_proxy_url() { + let config = VmDriverConfig { + no_proxy: Some("10.0.0.0/8".to_string()), + ..Default::default() + }; + let err = config + .validate_proxy_config() + .expect_err("a bypass list without a proxy would hide a fail-open state"); + assert!(err.contains("no_proxy"), "{err}"); + + let config = proxy_config(Some("http://proxy.corp.test:3128"), None, None); + config + .validate_proxy_config() + .expect("a lone proxy URL is a complete configuration"); + } + + #[test] + fn proxy_config_validation_requires_the_cleartext_acknowledgement() { + let mut config = proxy_config( + Some("http://proxy.corp.test:3128"), + Some("/etc/openshell/secrets/proxy-auth"), + None, + ); + config.proxy_auth_allow_insecure = None; + let err = config + .validate_proxy_config() + .expect_err("Basic auth to an http:// proxy is cleartext on the wire"); + assert!(err.contains("proxy_auth_allow_insecure"), "{err}"); + } + + #[tokio::test] + async fn proxy_ca_bundle_without_a_certificate_fails_the_sandbox() { + let dir = std::env::temp_dir().join(format!("openshell-vm-ca-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("not-a-ca.pem"); + std::fs::write(&path, b"this is not a certificate\n").unwrap(); + + let err = read_sandbox_proxy_ca_bundle(path.to_str().unwrap()) + .await + .expect_err("a certificate-free bundle must fail closed"); + assert_eq!(err.code(), Code::InvalidArgument); + assert!(err.message().contains("no PEM certificate"), "{err}"); + + std::fs::write(&path, b"").unwrap(); + let err = read_sandbox_proxy_ca_bundle(path.to_str().unwrap()) + .await + .expect_err("an empty bundle must fail closed"); + assert!(err.message().contains("no PEM certificate"), "{err}"); + + // PEM framing that base64-decodes but is not X.509 DER: accepted by + // `rustls_pemfile` alone, contributes zero trust anchors at runtime, + // and so would make every guest supervisor fail after boot. + std::fs::write( + &path, + b"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----\n", + ) + .unwrap(); + let err = read_sandbox_proxy_ca_bundle(path.to_str().unwrap()) + .await + .expect_err("a bundle with invalid DER must fail closed"); + assert_eq!(err.code(), Code::InvalidArgument); + assert!(err.message().contains("no usable trust anchors"), "{err}"); + + let err = read_sandbox_proxy_ca_bundle(dir.join("missing.pem").to_str().unwrap()) + .await + .expect_err("an unreadable bundle must fail closed"); + assert!(err.message().contains("could not be read"), "{err}"); + + // A special file must be rejected on its type, not read: an + // unbounded read of /dev/zero would exhaust driver memory. + #[cfg(unix)] + { + let err = read_sandbox_proxy_ca_bundle("/dev/zero") + .await + .expect_err("a non-regular bundle path must fail closed"); + assert_eq!(err.code(), Code::InvalidArgument); + assert!(err.message().contains("not a regular file"), "{err}"); + } + + // Oversized regular file: rejected on the stat'd length, again + // without reading it whole. + let oversized = dir.join("oversized.pem"); + let bound = openshell_core::driver_utils::MAX_UPSTREAM_PROXY_CA_BUNDLE_BYTES; + std::fs::write(&oversized, vec![b'x'; usize::try_from(bound).unwrap() + 1]).unwrap(); + let err = read_sandbox_proxy_ca_bundle(oversized.to_str().unwrap()) + .await + .expect_err("an oversized bundle must fail closed"); + assert_eq!(err.code(), Code::InvalidArgument); + assert!(err.message().contains("exceeds"), "{err}"); + + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn qemu_backend_rejects_a_gateway_host_proxy() { + // gvproxy's host-loopback NAT has no QEMU/TAP equivalent, so a proxy + // on the gateway host is unreachable from a GPU sandbox and must be + // rejected rather than time out on every CONNECT. The address that + // reaches the gateway host from a QEMU guest is this sandbox's own + // TAP host address, so the classifier is parameterized by it. + let tap_host = Some("10.0.128.1"); + for url in [ + "http://host.openshell.internal:8080", + "http://host.containers.internal:8080", + "http://host.docker.internal:8080", + "http://127.0.0.1:8080", + "http://localhost:8080", + "https://[::1]:8080", + // The address the aliases above resolve to inside the guest. + "http://10.0.128.1:8080", + ] { + assert!(proxy_url_targets_gateway_host(url, tap_host), "{url}"); + } + for url in [ + "http://proxy.corp.example:8080", + "https://10.1.2.3:3128", + // Special only to libkrun/gvproxy. On QEMU/TAP it is an ordinary + // address that may be routable through the guest's masqueraded + // egress, so rejecting it would refuse a working configuration. + "http://192.168.127.254:8080", + // Another sandbox's TAP host, not this one's. + "http://10.0.128.5:8080", + "not a url", + ] { + assert!(!proxy_url_targets_gateway_host(url, tap_host), "{url}"); + } + + // Without an allocated TAP host only the address-independent + // spellings classify; the loopback and alias guards still hold. + assert!(proxy_url_targets_gateway_host( + "http://127.0.0.1:8080", + None + )); + assert!(proxy_url_targets_gateway_host( + "http://host.openshell.internal:8080", + None + )); + assert!(!proxy_url_targets_gateway_host( + "http://10.0.128.1:8080", + None + )); + } + + #[test] + fn qemu_launch_plan_rejects_a_proxy_at_the_allocated_tap_host() { + // The preflight has to run against the address this sandbox actually + // got, which only exists once the launch plan's subnet is allocated. + // A proxy there is what `host.openshell.internal` resolves to in the + // guest, and the driver's own nftables input chain drops the port. + let probe = test_driver_with_extensions(LifecycleExtensionRegistry::new()); + let tap_host = probe + .build_vm_launch_plan("sandbox-proxy-tap", true, true, None) + .expect("gpu plan should build") + .host_ip + .expect("a QEMU plan carries a TAP host address"); + probe.release_subnet("sandbox-proxy-tap"); + + let driver = test_driver_with_proxy(&format!("http://{tap_host}:8080")); + let mut plan = driver + .build_vm_launch_plan("sandbox-proxy-tap", true, true, None) + .expect("gpu plan should build"); + assert_eq!(plan.host_ip.as_deref(), Some(tap_host.as_str())); + + let err = driver + .resolve_launch_plan_backend("sandbox-proxy-tap", true, None, &mut plan) + .expect_err("a proxy at the TAP host address is unreachable from the guest"); + assert_eq!(err.code(), Code::FailedPrecondition); + assert!(err.message().contains(&tap_host), "{err}"); + + driver.release_subnet("sandbox-proxy-tap"); + } + + #[test] + fn qemu_launch_plan_allows_a_proxy_at_the_gvproxy_host_loopback_address() { + // 192.168.127.254 carries no meaning on QEMU/TAP, so a launch must + // proceed rather than be refused for a libkrun-only reason. + let driver = test_driver_with_proxy(&format!("http://{GVPROXY_HOST_LOOPBACK_IP}:8080")); + let mut plan = driver + .build_vm_launch_plan("sandbox-proxy-gvproxy", true, true, None) + .expect("gpu plan should build"); + assert_ne!(plan.host_ip.as_deref(), Some(GVPROXY_HOST_LOOPBACK_IP)); + + driver + .resolve_launch_plan_backend("sandbox-proxy-gvproxy", true, None, &mut plan) + .expect("a routable proxy address must not block a GPU launch"); + assert_eq!(plan.backend, VmBackend::Qemu); + + driver.release_subnet("sandbox-proxy-gvproxy"); + } + + #[tokio::test] + async fn proxy_credential_is_validated_against_the_supervisor_rules() { + let dir = std::env::temp_dir().join(format!("openshell-vm-cred-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("proxy-auth"); + + std::fs::write(&path, "proxyuser:proxypass\n").unwrap(); + assert_eq!( + read_sandbox_proxy_credential(path.to_str().unwrap()) + .await + .expect("a well-formed credential is accepted"), + "proxyuser:proxypass" + ); + + // Rejected here rather than inside every sandbox's supervisor. + std::fs::write(&path, "no-separator\n").unwrap(); + let err = read_sandbox_proxy_credential(path.to_str().unwrap()) + .await + .expect_err("a malformed credential must fail closed"); + assert_eq!(err.code(), Code::InvalidArgument); + assert!( + !err.message().contains("no-separator"), + "the error must not echo credential file contents: {err}" + ); + + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn guest_environment_carries_no_corporate_proxy_settings() { + // The egress boundary is argv-only: `build_guest_environment` merges + // user-supplied environment, so anything it emitted here would be + // attacker-influenced. + let config = proxy_config( + Some("http://proxy.corp.test:3128"), + Some("/etc/openshell/secrets/proxy-auth"), + Some("/etc/openshell/tls/corp-ca.pem"), + ); + let sandbox = Sandbox { + id: "sb-proxy".to_string(), + name: "proxy".to_string(), + spec: Some(SandboxSpec { + environment: [ + ( + "HTTPS_PROXY".to_string(), + "http://attacker:3128".to_string(), + ), + ("NO_PROXY".to_string(), "*".to_string()), + ] + .into_iter() + .collect(), + ..Default::default() + }), + ..Default::default() + }; + + let env = build_guest_environment(&sandbox, &config, None); + assert!( + !env.iter().any(|entry| entry.starts_with("--upstream")), + "driver environment must never carry supervisor arguments: {env:?}" + ); + // A sandbox may still set the conventional variables for its own + // workload, but the supervisor ignores them on this path -- what + // matters is that the driver never derives the boundary from them. + assert!( + !env.iter() + .any(|entry| entry.contains("proxy.corp.test") || entry.contains("proxy-auth")), + "operator proxy settings must not reach the guest environment: {env:?}" + ); + } + + #[test] + fn sandbox_driver_config_cannot_carry_proxy_settings() { + // The upstream proxy is host network topology, not a per-sandbox + // setting: the caller-supplied envelope must reject it outright + // rather than silently ignoring it. + for key in [ + "https_proxy", + "no_proxy", + "proxy_auth_file", + "proxy_auth_allow_insecure", + "proxy_connect_by_hostname", + "proxy_ca_bundle", + ] { + let template = SandboxTemplate { + driver_config: Some(Struct { + fields: std::iter::once(( + key.to_string(), + Value { + kind: Some(Kind::StringValue("http://attacker:3128".to_string())), + }, + )) + .collect(), + }), + ..Default::default() + }; + assert!( + VmSandboxDriverConfig::from_template(&template).is_err(), + "template.driver_config.vm must reject '{key}'" + ); + } + } } diff --git a/crates/openshell-driver-vm/src/main.rs b/crates/openshell-driver-vm/src/main.rs index 95ebf0f8b2..2546cb2606 100644 --- a/crates/openshell-driver-vm/src/main.rs +++ b/crates/openshell-driver-vm/src/main.rs @@ -146,6 +146,30 @@ struct Args { #[arg(long, env = "OPENSHELL_VM_SANDBOX_GID")] sandbox_gid: Option, + // Corporate forward proxy for sandbox egress. Operator-owned: these reach + // the guest supervisor on its argv, which the sandbox image and the + // user-supplied environment cannot influence. + #[arg(long, env = "OPENSHELL_VM_HTTPS_PROXY")] + https_proxy: Option, + + #[arg(long, env = "OPENSHELL_VM_NO_PROXY")] + no_proxy: Option, + + #[arg(long, env = "OPENSHELL_VM_PROXY_AUTH_FILE")] + proxy_auth_file: Option, + + // Value-taking rather than a presence flag so an explicit `false` in + // `[openshell.drivers.vm]` survives the gateway -> driver hop and still + // trips the "acknowledgement without a credential" check. + #[arg(long, env = "OPENSHELL_VM_PROXY_AUTH_ALLOW_INSECURE")] + proxy_auth_allow_insecure: Option, + + #[arg(long, env = "OPENSHELL_VM_PROXY_CONNECT_BY_HOSTNAME")] + proxy_connect_by_hostname: Option, + + #[arg(long, env = "OPENSHELL_VM_PROXY_CA_BUNDLE")] + proxy_ca_bundle: Option, + #[arg(long, hide = true)] vm_backend: Option, @@ -243,6 +267,12 @@ async fn main() -> Result<()> { gpu_vcpus: args.gpu_vcpus, sandbox_uid: args.sandbox_uid, sandbox_gid: args.sandbox_gid, + https_proxy: args.https_proxy.clone(), + no_proxy: args.no_proxy.clone(), + proxy_auth_file: args.proxy_auth_file.clone(), + proxy_auth_allow_insecure: args.proxy_auth_allow_insecure, + proxy_connect_by_hostname: args.proxy_connect_by_hostname, + proxy_ca_bundle: args.proxy_ca_bundle.clone(), }) .await .map_err(|err| miette::miette!("{err}"))?; @@ -620,6 +650,63 @@ mod tests { use clap::Parser; use std::path::PathBuf; + #[test] + fn corporate_proxy_flags_parse_into_driver_settings() { + let args = Args::parse_from([ + "openshell-driver-vm", + "--openshell-endpoint", + "https://host.openshell.internal:17670", + "--https-proxy", + "http://proxy.corp.com:8080", + "--no-proxy", + "10.0.0.0/8,.svc.cluster.local", + "--proxy-auth-file", + "/etc/openshell/secrets/proxy-auth", + "--proxy-auth-allow-insecure", + "true", + "--proxy-connect-by-hostname", + "false", + "--proxy-ca-bundle", + "/etc/openshell/tls/proxy-ca.pem", + ]); + + assert_eq!( + args.https_proxy.as_deref(), + Some("http://proxy.corp.com:8080") + ); + assert_eq!( + args.no_proxy.as_deref(), + Some("10.0.0.0/8,.svc.cluster.local") + ); + assert_eq!( + args.proxy_auth_file.as_deref(), + Some("/etc/openshell/secrets/proxy-auth") + ); + assert_eq!(args.proxy_auth_allow_insecure, Some(true)); + // Value-taking rather than a presence flag, so the gateway can + // forward an explicit `false` from `[openshell.drivers.vm]`. + assert_eq!(args.proxy_connect_by_hostname, Some(false)); + assert_eq!( + args.proxy_ca_bundle.as_deref(), + Some("/etc/openshell/tls/proxy-ca.pem") + ); + } + + #[test] + fn corporate_proxy_settings_default_to_unset() { + let args = Args::parse_from([ + "openshell-driver-vm", + "--openshell-endpoint", + "https://host.openshell.internal:17670", + ]); + assert!(args.https_proxy.is_none()); + assert!(args.no_proxy.is_none()); + assert!(args.proxy_auth_file.is_none()); + assert!(args.proxy_auth_allow_insecure.is_none()); + assert!(args.proxy_connect_by_hostname.is_none()); + assert!(args.proxy_ca_bundle.is_none()); + } + #[test] fn peer_authorization_accepts_matching_uid_and_pid() { authorize_peer_credentials( diff --git a/crates/openshell-gateway/src/vm.rs b/crates/openshell-gateway/src/vm.rs index e86de28c12..f52bd9ddfa 100644 --- a/crates/openshell-gateway/src/vm.rs +++ b/crates/openshell-gateway/src/vm.rs @@ -99,6 +99,34 @@ pub struct VmComputeConfig { /// Host-side private key for the guest's mTLS client bundle. pub guest_tls_key: Option, + + /// Corporate forward proxy URL (`http://host:port` or `https://host:port`) + /// for policy-approved TLS egress from VM sandboxes. + /// + /// Deployment-level configuration, not a per-sandbox setting: it is passed + /// to the driver, which puts it on the guest supervisor's argv. A proxy on + /// this host's loopback is reachable from a guest only through the gvproxy + /// host alias `host.openshell.internal`. + pub https_proxy: Option, + + /// Comma-separated `NO_PROXY` list. Bypasses only the corporate proxy, + /// never `OpenShell` policy evaluation. + pub no_proxy: Option, + + /// Path on this host to a `user:pass` corporate proxy credential file. + pub proxy_auth_file: Option, + + /// Acknowledgement that Basic auth to an `http://` proxy is cleartext. + /// Required alongside `proxy_auth_file` unless the proxy is `https://`. + pub proxy_auth_allow_insecure: Option, + + /// Send hostnames rather than validated IPs in CONNECT requests. Last + /// resort for proxies whose ACLs reject IP CONNECT targets. + pub proxy_connect_by_hostname: Option, + + /// Path on this host to a PEM CA bundle trusted for the corporate proxy + /// and for server certificates a TLS-intercepting proxy re-signs. + pub proxy_ca_bundle: Option, } impl VmComputeConfig { @@ -135,6 +163,29 @@ impl VmComputeConfig { 4096 } + /// Validate the corporate upstream-proxy settings, fail-closed. + /// + /// Runs in the gateway as well as in the driver so an invalid + /// `[openshell.drivers.vm]` table reports the offending key instead of + /// surfacing as an opaque driver-startup timeout. + /// + /// # Errors + /// + /// Returns a [`Error::config`] naming the offending key. + pub fn validate_proxy_config(&self) -> Result<()> { + openshell_core::driver_utils::validate_upstream_proxy_settings( + &openshell_core::driver_utils::UpstreamProxySettings { + url: self.https_proxy.as_deref(), + no_proxy: self.no_proxy.as_deref(), + auth_file: self.proxy_auth_file.as_deref(), + auth_allow_insecure: self.proxy_auth_allow_insecure, + connect_by_hostname: self.proxy_connect_by_hostname, + ca_bundle: self.proxy_ca_bundle.as_deref(), + }, + ) + .map_err(Error::config) + } + #[must_use] fn default_driver_search_dirs(home: Option) -> Vec { let mut dirs = Vec::new(); @@ -163,6 +214,12 @@ impl Default for VmComputeConfig { guest_tls_ca: None, guest_tls_cert: None, guest_tls_key: None, + https_proxy: None, + no_proxy: None, + proxy_auth_file: None, + proxy_auth_allow_insecure: None, + proxy_connect_by_hostname: None, + proxy_ca_bundle: None, } } } @@ -460,6 +517,8 @@ pub async fn spawn( )); } + vm_config.validate_proxy_config()?; + let driver_bin = resolve_compute_driver_bin(vm_config)?; let socket_path = compute_driver_socket_path(vm_config); let guest_tls_paths = compute_driver_guest_tls_paths(vm_config)?; @@ -501,6 +560,7 @@ pub async fn spawn( command.arg("--guest-tls-cert").arg(tls.cert); command.arg("--guest-tls-key").arg(tls.key); } + append_upstream_proxy_args(&mut command, vm_config); let mut child = command.spawn().map_err(|e| { Error::execution(format!( @@ -515,6 +575,38 @@ pub async fn spawn( )) } +/// Forward the operator's corporate proxy settings to the driver subprocess. +/// +/// Only keys the operator actually set are passed, so the driver keeps the +/// same "omitted means no proxy" contract the supervisor enforces. The +/// booleans travel as explicit values rather than presence flags so an +/// explicit `false` still trips the driver's pairing checks. +#[cfg(unix)] +fn append_upstream_proxy_args(command: &mut Command, vm_config: &VmComputeConfig) { + if let Some(url) = &vm_config.https_proxy { + command.arg("--https-proxy").arg(url); + } + if let Some(list) = &vm_config.no_proxy { + command.arg("--no-proxy").arg(list); + } + if let Some(path) = &vm_config.proxy_auth_file { + command.arg("--proxy-auth-file").arg(path); + } + if let Some(allow) = vm_config.proxy_auth_allow_insecure { + command + .arg("--proxy-auth-allow-insecure") + .arg(allow.to_string()); + } + if let Some(by_hostname) = vm_config.proxy_connect_by_hostname { + command + .arg("--proxy-connect-by-hostname") + .arg(by_hostname.to_string()); + } + if let Some(path) = &vm_config.proxy_ca_bundle { + command.arg("--proxy-ca-bundle").arg(path); + } +} + #[cfg(unix)] fn append_otlp_args(command: &mut Command, otlp_config: Option<&OtlpConfig>, gateway_name: &str) { if let Some(config) = otlp_config { @@ -606,9 +698,10 @@ async fn connect_compute_driver(socket_path: &Path) -> Result { #[cfg(all(test, unix))] mod tests { use super::{ - VmComputeConfig, append_otlp_args, compute_driver_guest_tls_paths, - compute_driver_socket_path, current_euid, prepare_compute_driver_socket_path, - prepare_vm_state_dir, resolve_compute_driver_bin, resolve_driver_search_dirs, + VmComputeConfig, append_otlp_args, append_upstream_proxy_args, + compute_driver_guest_tls_paths, compute_driver_socket_path, current_euid, + prepare_compute_driver_socket_path, prepare_vm_state_dir, resolve_compute_driver_bin, + resolve_driver_search_dirs, }; use openshell_server::config_file::OtlpConfig; use std::os::unix::fs::PermissionsExt; @@ -644,6 +737,83 @@ mod tests { ); } + #[test] + fn vm_driver_command_forwards_corporate_proxy_settings() { + let mut command = tokio::process::Command::new("openshell-driver-vm"); + append_upstream_proxy_args( + &mut command, + &VmComputeConfig { + https_proxy: Some("http://proxy.corp.com:8080".to_string()), + no_proxy: Some("10.0.0.0/8".to_string()), + proxy_auth_file: Some("/etc/openshell/secrets/proxy-auth".to_string()), + proxy_auth_allow_insecure: Some(true), + proxy_connect_by_hostname: Some(false), + proxy_ca_bundle: Some("/etc/openshell/tls/proxy-ca.pem".to_string()), + ..VmComputeConfig::default() + }, + ); + + let args = command + .as_std() + .get_args() + .map(|arg| arg.to_string_lossy().into_owned()) + .collect::>(); + assert_eq!( + args, + [ + "--https-proxy", + "http://proxy.corp.com:8080", + "--no-proxy", + "10.0.0.0/8", + "--proxy-auth-file", + "/etc/openshell/secrets/proxy-auth", + "--proxy-auth-allow-insecure", + "true", + // Passed as an explicit value, not a presence flag, so the + // driver still sees the operator's `false`. + "--proxy-connect-by-hostname", + "false", + "--proxy-ca-bundle", + "/etc/openshell/tls/proxy-ca.pem", + ] + ); + } + + #[test] + fn vm_driver_command_omits_unset_corporate_proxy_settings() { + let mut command = tokio::process::Command::new("openshell-driver-vm"); + append_upstream_proxy_args(&mut command, &VmComputeConfig::default()); + assert_eq!(command.as_std().get_args().count(), 0); + } + + #[test] + fn invalid_corporate_proxy_config_is_rejected_before_the_driver_starts() { + // Without this the operator would see an opaque driver-readiness + // timeout instead of an error naming the offending key. + let err = VmComputeConfig { + https_proxy: Some("socks5://proxy.corp.com:1080".to_string()), + ..VmComputeConfig::default() + } + .validate_proxy_config() + .expect_err("only http:// and https:// proxies are supported"); + assert!(err.to_string().contains("https_proxy"), "{err}"); + + let err = VmComputeConfig { + proxy_ca_bundle: Some("/etc/openshell/tls/proxy-ca.pem".to_string()), + ..VmComputeConfig::default() + } + .validate_proxy_config() + .expect_err("a CA bundle without a proxy URL would hide a fail-open state"); + assert!(err.to_string().contains("proxy_ca_bundle"), "{err}"); + + VmComputeConfig { + https_proxy: Some("http://proxy.corp.com:8080".to_string()), + ..VmComputeConfig::default() + } + .validate_proxy_config() + .expect("a lone proxy URL is a complete configuration"); + } + #[test] fn resolve_driver_bin_uses_driver_dir_when_binary_present() { let dir = tempdir().unwrap(); diff --git a/crates/openshell-supervisor-network/src/upstream_proxy.rs b/crates/openshell-supervisor-network/src/upstream_proxy.rs index 253fa52263..f95c5a3e81 100644 --- a/crates/openshell-supervisor-network/src/upstream_proxy.rs +++ b/crates/openshell-supervisor-network/src/upstream_proxy.rs @@ -612,31 +612,12 @@ fn parse_proxy_url(raw: &str, var_name: &str) -> Result<(ProxyEndpoint, bool), S /// fall-back to the built-in roots that would quietly weaken the trust /// boundary. The error names `var_name` so the operator can locate the setting. pub(crate) fn read_proxy_ca_bundle(path: &str, var_name: &str) -> Result { - let pem = std::fs::read_to_string(path) - .map_err(|err| format!("{var_name} '{path}' could not be read: {err}"))?; - // Validate that the bundle contributes at least one trust anchor that - // rustls actually accepts, not just that PEM framing base64-decodes. - // A PEM block with invalid DER passes `rustls_pemfile::certs` but is - // silently rejected by `RootCertStore::add_parsable_certificates`; - // counting only PEM blocks would let such a bundle satisfy the check - // while contributing zero usable anchors at runtime. - let certs: Vec<_> = rustls_pemfile::certs(&mut pem.as_bytes()) - .flatten() - .collect(); - if certs.is_empty() { - return Err(format!( - "{var_name} '{path}' contains no PEM certificate blocks" - )); - } - let mut store = rustls::RootCertStore::empty(); - let (added, _ignored) = store.add_parsable_certificates(certs); - if added == 0 { - return Err(format!( - "{var_name} '{path}' contains no usable trust anchors \ - (PEM blocks were found but none contain valid X.509 DER)" - )); - } - Ok(pem) + // Shared with the compute driver, which validates the same file on the + // gateway host before staging it, so host acceptance and guest acceptance + // cannot diverge. It also bounds the read: the file arrives from the + // driver, but a bundle the size of the sandbox disk should fail rather + // than be loaded whole. + openshell_core::driver_utils::read_upstream_proxy_ca_bundle_file(path, var_name) } /// Build the TLS client config used to connect to an `https://` corporate diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index c04c0040d0..7553e7deb4 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -796,6 +796,50 @@ guest_tls_key = "/var/lib/openshell/guest-tls/client-key.pem" # Defaults to 10001 when unset; matching GID is used if sandbox_gid is empty. # Any non-root Linux UID/GID is valid. # sandbox_uid = 20001 +# Corporate forward proxy for sandbox egress. The keys, their semantics, and +# the fail-closed contract are identical to the Podman driver above: only TLS +# (CONNECT) egress is chained, plain-HTTP destination requests always dial +# directly, credentials must come from proxy_auth_file rather than the URL, +# an http:// proxy with credentials requires proxy_auth_allow_insecure, and +# any present-but-invalid value is rejected at gateway startup rather than +# degrading to a direct dial. proxy_auth_file and proxy_ca_bundle are paths on +# the gateway host. +# +# The sandbox cannot select or override these settings. They reach the guest +# supervisor on its command line through a per-sandbox file the driver writes +# into the overlay upperdir on every launch, so a sandbox image cannot supply +# its own values or disable the operator's by baking a file at that path. +# +# Reachability: a proxy on the corporate network needs no special address and +# works on every VM sandbox. The guest's callback to the gateway is unaffected +# and never traverses the proxy. +# +# A proxy on the gateway host itself is reachable only from libkrun-backed +# (non-GPU) sandboxes: their egress leaves through gvproxy, which NATs +# 192.168.127.254 to the host's 127.0.0.1, so address it as +# http://host.openshell.internal: rather than http://127.0.0.1:. +# GPU sandboxes run on the QEMU/TAP backend, which has no such NAT — +# host.openshell.internal resolves to the TAP host address, and the driver's +# nftables rules let the guest reach only the gateway port on the host. A +# gateway-host proxy URL is therefore rejected when the sandbox launches on +# QEMU, rather than timing out on every CONNECT; give GPU sandboxes a proxy +# address routable from the guest's masqueraded egress. +# +# Because a microVM has no bind mounts or container secrets, the driver stages +# the credential and the CA into the per-sandbox overlay disk: the credential +# root-only inside the guest, and both removed with the sandbox. The +# credential is therefore at rest in that overlay image on the gateway host — +# the same delivery the per-sandbox gateway token already uses, and a +# difference from the Podman secret model worth noting when choosing where to +# keep proxy credentials. +# https_proxy = "http://host.openshell.internal:8080" +# no_proxy = "10.0.0.0/8,.internal.example" +# proxy_auth_file = "/etc/openshell/secrets/proxy-auth" +# proxy_auth_allow_insecure = true +# Last resort for hostname-filtering proxy ACLs; see the Podman section above. +# proxy_connect_by_hostname = true +# Corporate CA trusted for an https:// proxy and TLS-intercepting proxies. +# proxy_ca_bundle = "/etc/openshell/tls/proxy-ca.pem" ``` ### Extension Driver diff --git a/docs/reference/sandbox-compute-drivers.mdx b/docs/reference/sandbox-compute-drivers.mdx index f00c49afd6..987e66b0d9 100644 --- a/docs/reference/sandbox-compute-drivers.mdx +++ b/docs/reference/sandbox-compute-drivers.mdx @@ -360,6 +360,18 @@ The VM driver creates nftables rules on the host for each sandbox VM's TAP netwo On hosts with restrictive firewalls (e.g. firewalld), the host firewall may additionally block VM traffic that the driver's rules accept. If VM sandboxes cannot reach the network, verify that the host firewall allows forwarding and input for `vmtap-*` interfaces. See the [VM driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-vm/README.md#host-side-nftables-rules) for details. +### Corporate Proxy Egress + +For proxy-required networks, the VM driver accepts the same corporate egress proxy keys as the Podman driver: `https_proxy`, `no_proxy`, `proxy_auth_file`, `proxy_auth_allow_insecure`, `proxy_connect_by_hostname`, and `proxy_ca_bundle`. The in-guest supervisor chains policy-approved TLS tunnels through the proxy with HTTP CONNECT instead of dialing destinations directly. + +The settings reach the guest supervisor on its command line through a per-sandbox argument file the driver writes into the overlay upperdir on every launch, so a sandbox cannot select, alter, or disable the proxy from inside the guest — including through image `ENV`, the sandbox environment, or files baked into the image at the paths the driver uses. + +A proxy on the corporate network needs no special address and works on every VM sandbox. The guest's callback to the gateway never traverses the proxy. + +A proxy on the gateway host itself works only for libkrun-backed (non-GPU) sandboxes, whose egress leaves through gvproxy: configure `https_proxy = "http://host.openshell.internal:"` rather than a `127.0.0.1` URL, because gvproxy NATs that alias to the host's `127.0.0.1`. GPU sandboxes use the QEMU/TAP backend, where `host.openshell.internal` resolves to the TAP host address and the driver's [host firewall rules](#host-firewall) allow the guest to reach only the gateway port on the host. The driver rejects a gateway-host proxy URL when a sandbox launches on QEMU instead of letting every CONNECT time out, so give GPU sandboxes a proxy address routable from the guest's masqueraded egress. + +Because a microVM has no bind mounts or container secrets, the driver stages the credential (root-only) and the CA bundle into the per-sandbox overlay disk and removes them with the sandbox. See the [Gateway Configuration File](./gateway-config) reference for the full contract, including the cleartext-credential acknowledgement and the validated-IP CONNECT behavior. + ## Kubernetes Driver Kubernetes-backed sandboxes run as pods in the configured sandbox namespace. Use Kubernetes for shared clusters, remote compute, GPU scheduling, and operator-managed environments. diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 6dbf46a392..18556d0f7b 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -108,6 +108,11 @@ name = "vm_gateway_start" path = "tests/vm_gateway_start.rs" required-features = ["e2e-vm"] +[[test]] +name = "vm_corporate_proxy" +path = "tests/vm_corporate_proxy.rs" +required-features = ["e2e-vm"] + [[test]] name = "provider_token_exchange" path = "tests/provider_token_exchange.rs" diff --git a/e2e/rust/e2e-vm.sh b/e2e/rust/e2e-vm.sh index 9acc633d65..96da2a879f 100755 --- a/e2e/rust/e2e-vm.sh +++ b/e2e/rust/e2e-vm.sh @@ -409,4 +409,5 @@ else run_e2e_test host_gateway_alias run_e2e_test vm_overlay run_e2e_test vm_gateway_start + run_e2e_test vm_corporate_proxy fi diff --git a/e2e/rust/src/harness/host_process.rs b/e2e/rust/src/harness/host_process.rs new file mode 100644 index 0000000000..f9fefd75a6 --- /dev/null +++ b/e2e/rust/src/harness/host_process.rs @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Host-process TCP fixtures for e2e tests. +//! +//! [`HostSupportContainer`](super::container::HostSupportContainer) publishes +//! the same shape of fixture through a container engine. VM sandboxes reach +//! the host through gvproxy's `host.openshell.internal` alias and the VM e2e +//! lane has no container runtime of its own, so this variant runs the fixture +//! as a plain host process instead — keeping the lane free of a container +//! dependency it does not otherwise need. + +use std::io::Read as _; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; + +use super::port::wait_for_port; + +/// A `python3` fixture listening on a host TCP port. +/// +/// Output is captured to a temp file rather than a pipe: these fixtures log +/// every request they serve, and a full pipe buffer would block the process +/// mid-test. [`logs`](Self::logs) reads the file, which is where a test finds +/// its evidence (the CONNECT targets a proxy saw, for example). +pub struct HostPythonFixture { + /// Host port the fixture listens on. + pub port: u16, + child: Child, + log_path: PathBuf, +} + +impl HostPythonFixture { + /// Start `python3 -c